diff options
| author | schneefux <schneefux+commit@schneefux.xyz> | 2017-03-22 19:19:26 +0100 |
|---|---|---|
| committer | schneefux <schneefux+commit@schneefux.xyz> | 2017-03-22 19:19:26 +0100 |
| commit | d42c8b528eb595101954f478e0024e3f079f6fbf (patch) | |
| tree | 61a528cf94f1f6941f4683ae8736a4a0135c035e /templates/assets/tether/src/js/tether.js | |
| download | minionlivesmatter-d42c8b528eb595101954f478e0024e3f079f6fbf.tar.gz minionlivesmatter-d42c8b528eb595101954f478e0024e3f079f6fbf.zip | |
first implementation of fun stats site
Diffstat (limited to 'templates/assets/tether/src/js/tether.js')
| -rw-r--r-- | templates/assets/tether/src/js/tether.js | 809 |
1 files changed, 809 insertions, 0 deletions
diff --git a/templates/assets/tether/src/js/tether.js b/templates/assets/tether/src/js/tether.js new file mode 100644 index 0000000..5ebc84f --- /dev/null +++ b/templates/assets/tether/src/js/tether.js @@ -0,0 +1,809 @@ +/* globals TetherBase, performance */ + +if (typeof TetherBase === 'undefined') { + throw new Error('You must include the utils.js file before tether.js'); +} + +const { + getScrollParents, + getBounds, + getOffsetParent, + extend, + addClass, + removeClass, + updateClasses, + defer, + flush, + getScrollBarSize, + removeUtilElements +} = TetherBase.Utils; + +function within(a, b, diff=1) { + return (a + diff >= b && b >= a - diff); +} + +const transformKey = (() => { + if(typeof document === 'undefined') { + return ''; + } + const el = document.createElement('div'); + + const transforms = ['transform', 'WebkitTransform', 'OTransform', 'MozTransform', 'msTransform']; + for (let i = 0; i < transforms.length; ++i) { + const key = transforms[i]; + if (el.style[key] !== undefined) { + return key; + } + } +})(); + +const tethers = []; + +const position = () => { + tethers.forEach(tether => { + tether.position(false); + }); + flush(); +}; + +function now() { + if (typeof performance !== 'undefined' && typeof performance.now !== 'undefined') { + return performance.now(); + } + return +new Date; +} + +(() => { + let lastCall = null; + let lastDuration = null; + let pendingTimeout = null; + + const tick = () => { + if (typeof lastDuration !== 'undefined' && lastDuration > 16) { + // We voluntarily throttle ourselves if we can't manage 60fps + lastDuration = Math.min(lastDuration - 16, 250); + + // Just in case this is the last event, remember to position just once more + pendingTimeout = setTimeout(tick, 250); + return; + } + + if (typeof lastCall !== 'undefined' && (now() - lastCall) < 10) { + // Some browsers call events a little too frequently, refuse to run more than is reasonable + return; + } + + if (pendingTimeout != null) { + clearTimeout(pendingTimeout); + pendingTimeout = null; + } + + lastCall = now(); + position(); + lastDuration = now() - lastCall; + }; + + if(typeof window !== 'undefined' && typeof window.addEventListener !== 'undefined') { + ['resize', 'scroll', 'touchmove'].forEach(event => { + window.addEventListener(event, tick); + }); + } +})(); + +const MIRROR_LR = { + center: 'center', + left: 'right', + right: 'left' +}; + +const MIRROR_TB = { + middle: 'middle', + top: 'bottom', + bottom: 'top' +}; + +const OFFSET_MAP = { + top: 0, + left: 0, + middle: '50%', + center: '50%', + bottom: '100%', + right: '100%' +}; + +const autoToFixedAttachment = (attachment, relativeToAttachment) => { + let {left, top} = attachment; + + if (left === 'auto') { + left = MIRROR_LR[relativeToAttachment.left]; + } + + if (top === 'auto') { + top = MIRROR_TB[relativeToAttachment.top]; + } + + return {left, top}; +}; + +const attachmentToOffset = (attachment) => { + let left = attachment.left; + let top = attachment.top; + + if (typeof OFFSET_MAP[attachment.left] !== 'undefined') { + left = OFFSET_MAP[attachment.left]; + } + + if (typeof OFFSET_MAP[attachment.top] !== 'undefined') { + top = OFFSET_MAP[attachment.top]; + } + + return {left, top}; +}; + +function addOffset(...offsets) { + const out = {top: 0, left: 0}; + + offsets.forEach(({top, left}) => { + if (typeof top === 'string') { + top = parseFloat(top, 10); + } + if (typeof left === 'string') { + left = parseFloat(left, 10); + } + + out.top += top; + out.left += left; + }); + + return out; +} + +function offsetToPx(offset, size) { + if (typeof offset.left === 'string' && offset.left.indexOf('%') !== -1) { + offset.left = parseFloat(offset.left, 10) / 100 * size.width; + } + if (typeof offset.top === 'string' && offset.top.indexOf('%') !== -1) { + offset.top = parseFloat(offset.top, 10) / 100 * size.height; + } + + return offset; +} + +const parseOffset = (value) => { + const [top, left] = value.split(' '); + return {top, left}; +}; +const parseAttachment = parseOffset; + +class TetherClass extends Evented { + + constructor(options) { + super(); + this.position = this.position.bind(this); + + tethers.push(this); + + this.history = []; + + this.setOptions(options, false); + + TetherBase.modules.forEach(module => { + if (typeof module.initialize !== 'undefined') { + module.initialize.call(this); + } + }); + + this.position(); + } + + getClass(key='') { + const {classes} = this.options; + if (typeof classes !== 'undefined' && classes[key]) { + return this.options.classes[key]; + } else if (this.options.classPrefix) { + return `${ this.options.classPrefix }-${ key }`; + } else { + return key; + } + } + + setOptions(options, pos=true) { + const defaults = { + offset: '0 0', + targetOffset: '0 0', + targetAttachment: 'auto auto', + classPrefix: 'tether' + }; + + this.options = extend(defaults, options); + + let {element, target, targetModifier} = this.options; + this.element = element; + this.target = target; + this.targetModifier = targetModifier; + + if (this.target === 'viewport') { + this.target = document.body; + this.targetModifier = 'visible'; + } else if (this.target === 'scroll-handle') { + this.target = document.body; + this.targetModifier = 'scroll-handle'; + } + + ['element', 'target'].forEach(key => { + if (typeof this[key] === 'undefined') { + throw new Error('Tether Error: Both element and target must be defined'); + } + + if (typeof this[key].jquery !== 'undefined') { + this[key] = this[key][0]; + } else if (typeof this[key] === 'string') { + this[key] = document.querySelector(this[key]); + } + }); + + addClass(this.element, this.getClass('element')); + if (!(this.options.addTargetClasses === false)) { + addClass(this.target, this.getClass('target')); + } + + if (!this.options.attachment) { + throw new Error('Tether Error: You must provide an attachment'); + } + + this.targetAttachment = parseAttachment(this.options.targetAttachment); + this.attachment = parseAttachment(this.options.attachment); + this.offset = parseOffset(this.options.offset); + this.targetOffset = parseOffset(this.options.targetOffset); + + if (typeof this.scrollParents !== 'undefined') { + this.disable(); + } + + if (this.targetModifier === 'scroll-handle') { + this.scrollParents = [this.target]; + } else { + this.scrollParents = getScrollParents(this.target); + } + + if(!(this.options.enabled === false)) { + this.enable(pos); + } + } + + getTargetBounds() { + if (typeof this.targetModifier !== 'undefined') { + if (this.targetModifier === 'visible') { + if (this.target === document.body) { + return {top: pageYOffset, left: pageXOffset, height: innerHeight, width: innerWidth}; + } else { + const bounds = getBounds(this.target); + + const out = { + height: bounds.height, + width: bounds.width, + top: bounds.top, + left: bounds.left + }; + + out.height = Math.min(out.height, bounds.height - (pageYOffset - bounds.top)); + out.height = Math.min(out.height, bounds.height - ((bounds.top + bounds.height) - (pageYOffset + innerHeight))); + out.height = Math.min(innerHeight, out.height); + out.height -= 2; + + out.width = Math.min(out.width, bounds.width - (pageXOffset - bounds.left)); + out.width = Math.min(out.width, bounds.width - ((bounds.left + bounds.width) - (pageXOffset + innerWidth))); + out.width = Math.min(innerWidth, out.width); + out.width -= 2; + + if (out.top < pageYOffset) { + out.top = pageYOffset; + } + if (out.left < pageXOffset) { + out.left = pageXOffset; + } + + return out; + } + } else if (this.targetModifier === 'scroll-handle') { + let bounds; + let target = this.target; + if (target === document.body) { + target = document.documentElement; + + bounds = { + left: pageXOffset, + top: pageYOffset, + height: innerHeight, + width: innerWidth + }; + } else { + bounds = getBounds(target); + } + + const style = getComputedStyle(target); + + const hasBottomScroll = ( + target.scrollWidth > target.clientWidth || + [style.overflow, style.overflowX].indexOf('scroll') >= 0 || + this.target !== document.body + ); + + let scrollBottom = 0; + if (hasBottomScroll) { + scrollBottom = 15; + } + + const height = bounds.height - parseFloat(style.borderTopWidth) - parseFloat(style.borderBottomWidth) - scrollBottom; + + const out = { + width: 15, + height: height * 0.975 * (height / target.scrollHeight), + left: bounds.left + bounds.width - parseFloat(style.borderLeftWidth) - 15 + }; + + let fitAdj = 0; + if (height < 408 && this.target === document.body) { + fitAdj = -0.00011 * Math.pow(height, 2) - 0.00727 * height + 22.58; + } + + if (this.target !== document.body) { + out.height = Math.max(out.height, 24); + } + + const scrollPercentage = this.target.scrollTop / (target.scrollHeight - height); + out.top = scrollPercentage * (height - out.height - fitAdj) + bounds.top + parseFloat(style.borderTopWidth); + + if (this.target === document.body) { + out.height = Math.max(out.height, 24); + } + + return out; + } + } else { + return getBounds(this.target); + } + } + + clearCache() { + this._cache = {}; + } + + cache(k, getter) { + // More than one module will often need the same DOM info, so + // we keep a cache which is cleared on each position call + if (typeof this._cache === 'undefined') { + this._cache = {}; + } + + if (typeof this._cache[k] === 'undefined') { + this._cache[k] = getter.call(this); + } + + return this._cache[k]; + } + + enable(pos=true) { + if (!(this.options.addTargetClasses === false)) { + addClass(this.target, this.getClass('enabled')); + } + addClass(this.element, this.getClass('enabled')); + this.enabled = true; + + this.scrollParents.forEach((parent) => { + if (parent !== this.target.ownerDocument) { + parent.addEventListener('scroll', this.position); + } + }) + + if (pos) { + this.position(); + } + } + + disable() { + removeClass(this.target, this.getClass('enabled')); + removeClass(this.element, this.getClass('enabled')); + this.enabled = false; + + if (typeof this.scrollParents !== 'undefined') { + this.scrollParents.forEach((parent) => { + parent.removeEventListener('scroll', this.position); + }) + } + } + + destroy() { + this.disable(); + + tethers.forEach((tether, i) => { + if (tether === this) { + tethers.splice(i, 1); + } + }); + + // Remove any elements we were using for convenience from the DOM + if (tethers.length === 0) { + removeUtilElements(); + } + } + + updateAttachClasses(elementAttach, targetAttach) { + elementAttach = elementAttach || this.attachment; + targetAttach = targetAttach || this.targetAttachment; + const sides = ['left', 'top', 'bottom', 'right', 'middle', 'center']; + + if (typeof this._addAttachClasses !== 'undefined' && this._addAttachClasses.length) { + // updateAttachClasses can be called more than once in a position call, so + // we need to clean up after ourselves such that when the last defer gets + // ran it doesn't add any extra classes from previous calls. + this._addAttachClasses.splice(0, this._addAttachClasses.length); + } + + if (typeof this._addAttachClasses === 'undefined') { + this._addAttachClasses = []; + } + const add = this._addAttachClasses; + + if (elementAttach.top) { + add.push(`${ this.getClass('element-attached') }-${ elementAttach.top }`); + } + if (elementAttach.left) { + add.push(`${ this.getClass('element-attached') }-${ elementAttach.left }`); + } + if (targetAttach.top) { + add.push(`${ this.getClass('target-attached') }-${ targetAttach.top }`); + } + if (targetAttach.left) { + add.push(`${ this.getClass('target-attached') }-${ targetAttach.left }`); + } + + const all = []; + sides.forEach(side => { + all.push(`${ this.getClass('element-attached') }-${ side }`); + all.push(`${ this.getClass('target-attached') }-${ side }`); + }); + + defer(() => { + if (!(typeof this._addAttachClasses !== 'undefined')) { + return; + } + + updateClasses(this.element, this._addAttachClasses, all); + if (!(this.options.addTargetClasses === false)) { + updateClasses(this.target, this._addAttachClasses, all); + } + + delete this._addAttachClasses; + }); + } + + position(flushChanges=true) { + // flushChanges commits the changes immediately, leave true unless you are positioning multiple + // tethers (in which case call Tether.Utils.flush yourself when you're done) + + if (!this.enabled) { + return; + } + + this.clearCache(); + + // Turn 'auto' attachments into the appropriate corner or edge + const targetAttachment = autoToFixedAttachment(this.targetAttachment, this.attachment); + + this.updateAttachClasses(this.attachment, targetAttachment); + + const elementPos = this.cache('element-bounds', () => { + return getBounds(this.element); + }); + + let {width, height} = elementPos; + + if (width === 0 && height === 0 && typeof this.lastSize !== 'undefined') { + // We cache the height and width to make it possible to position elements that are + // getting hidden. + ({width, height} = this.lastSize); + } else { + this.lastSize = {width, height}; + } + + const targetPos = this.cache('target-bounds', () => { + return this.getTargetBounds(); + }); + const targetSize = targetPos; + + // Get an actual px offset from the attachment + let offset = offsetToPx(attachmentToOffset(this.attachment), {width, height}); + let targetOffset = offsetToPx(attachmentToOffset(targetAttachment), targetSize); + + const manualOffset = offsetToPx(this.offset, {width, height}); + const manualTargetOffset = offsetToPx(this.targetOffset, targetSize); + + // Add the manually provided offset + offset = addOffset(offset, manualOffset); + targetOffset = addOffset(targetOffset, manualTargetOffset); + + // It's now our goal to make (element position + offset) == (target position + target offset) + let left = targetPos.left + targetOffset.left - offset.left; + let top = targetPos.top + targetOffset.top - offset.top; + + for (let i = 0; i < TetherBase.modules.length; ++i) { + const module = TetherBase.modules[i]; + const ret = module.position.call(this, { + left, + top, + targetAttachment, + targetPos, + elementPos, + offset, + targetOffset, + manualOffset, + manualTargetOffset, + scrollbarSize, + attachment: this.attachment + }); + + if (ret === false) { + return false; + } else if (typeof ret === 'undefined' || typeof ret !== 'object') { + continue; + } else { + ({top, left} = ret); + } + } + + // We describe the position three different ways to give the optimizer + // a chance to decide the best possible way to position the element + // with the fewest repaints. + const next = { + // It's position relative to the page (absolute positioning when + // the element is a child of the body) + page: { + top: top, + left: left + }, + + // It's position relative to the viewport (fixed positioning) + viewport: { + top: top - pageYOffset, + bottom: pageYOffset - top - height + innerHeight, + left: left - pageXOffset, + right: pageXOffset - left - width + innerWidth + } + }; + + var doc = this.target.ownerDocument; + var win = doc.defaultView; + + let scrollbarSize; + if (win.innerHeight > doc.documentElement.clientHeight) { + scrollbarSize = this.cache('scrollbar-size', getScrollBarSize); + next.viewport.bottom -= scrollbarSize.height; + } + + if (win.innerWidth > doc.documentElement.clientWidth) { + scrollbarSize = this.cache('scrollbar-size', getScrollBarSize); + next.viewport.right -= scrollbarSize.width; + } + + if (['', 'static'].indexOf(doc.body.style.position) === -1 || + ['', 'static'].indexOf(doc.body.parentElement.style.position) === -1) { + // Absolute positioning in the body will be relative to the page, not the 'initial containing block' + next.page.bottom = doc.body.scrollHeight - top - height; + next.page.right = doc.body.scrollWidth - left - width; + } + + if (typeof this.options.optimizations !== 'undefined' && + this.options.optimizations.moveElement !== false && + !(typeof this.targetModifier !== 'undefined')) { + const offsetParent = this.cache('target-offsetparent', () => getOffsetParent(this.target)); + const offsetPosition = this.cache('target-offsetparent-bounds', () => getBounds(offsetParent)); + const offsetParentStyle = getComputedStyle(offsetParent); + const offsetParentSize = offsetPosition; + + const offsetBorder = {}; + ['Top', 'Left', 'Bottom', 'Right'].forEach(side => { + offsetBorder[side.toLowerCase()] = parseFloat(offsetParentStyle[`border${ side }Width`]); + }); + + offsetPosition.right = doc.body.scrollWidth - offsetPosition.left - offsetParentSize.width + offsetBorder.right; + offsetPosition.bottom = doc.body.scrollHeight - offsetPosition.top - offsetParentSize.height + offsetBorder.bottom; + + if (next.page.top >= (offsetPosition.top + offsetBorder.top) && next.page.bottom >= offsetPosition.bottom) { + if (next.page.left >= (offsetPosition.left + offsetBorder.left) && next.page.right >= offsetPosition.right) { + // We're within the visible part of the target's scroll parent + const scrollTop = offsetParent.scrollTop; + const scrollLeft = offsetParent.scrollLeft; + + // It's position relative to the target's offset parent (absolute positioning when + // the element is moved to be a child of the target's offset parent). + next.offset = { + top: next.page.top - offsetPosition.top + scrollTop - offsetBorder.top, + left: next.page.left - offsetPosition.left + scrollLeft - offsetBorder.left + }; + } + } + } + + + // We could also travel up the DOM and try each containing context, rather than only + // looking at the body, but we're gonna get diminishing returns. + + this.move(next); + + this.history.unshift(next); + + if (this.history.length > 3) { + this.history.pop(); + } + + if (flushChanges) { + flush(); + } + + return true; + } + + // THE ISSUE + move(pos) { + if (!(typeof this.element.parentNode !== 'undefined')) { + return; + } + + const same = {}; + + for (let type in pos) { + same[type] = {}; + + for (let key in pos[type]) { + let found = false; + + for (let i = 0; i < this.history.length; ++i) { + const point = this.history[i]; + if (typeof point[type] !== 'undefined' && + !within(point[type][key], pos[type][key])) { + found = true; + break; + } + + } + + if (!found) { + same[type][key] = true; + } + } + } + + let css = {top: '', left: '', right: '', bottom: ''}; + + const transcribe = (_same, _pos) => { + const hasOptimizations = typeof this.options.optimizations !== 'undefined'; + const gpu = hasOptimizations ? this.options.optimizations.gpu : null; + if (gpu !== false) { + let yPos, xPos; + if (_same.top) { + css.top = 0; + yPos = _pos.top; + } else { + css.bottom = 0; + yPos = -_pos.bottom; + } + + if (_same.left) { + css.left = 0; + xPos = _pos.left; + } else { + css.right = 0; + xPos = -_pos.right; + } + + if (window.matchMedia) { + // HubSpot/tether#207 + const retina = window.matchMedia('only screen and (min-resolution: 1.3dppx)').matches || + window.matchMedia('only screen and (-webkit-min-device-pixel-ratio: 1.3)').matches; + if (!retina) { + xPos = Math.round(xPos); + yPos = Math.round(yPos); + } + } + + css[transformKey] = `translateX(${ xPos }px) translateY(${ yPos }px)`; + + if (transformKey !== 'msTransform') { + // The Z transform will keep this in the GPU (faster, and prevents artifacts), + // but IE9 doesn't support 3d transforms and will choke. + css[transformKey] += " translateZ(0)"; + } + + } else { + if (_same.top) { + css.top = `${ _pos.top }px`; + } else { + css.bottom = `${ _pos.bottom }px`; + } + + if (_same.left) { + css.left = `${ _pos.left }px`; + } else { + css.right = `${ _pos.right }px`; + } + } + }; + + let moved = false; + if ((same.page.top || same.page.bottom) && (same.page.left || same.page.right)) { + css.position = 'absolute'; + transcribe(same.page, pos.page); + + } else if ((same.viewport.top || same.viewport.bottom) && (same.viewport.left || same.viewport.right)) { + css.position = 'fixed'; + transcribe(same.viewport, pos.viewport); + + } else if (typeof same.offset !== 'undefined' && same.offset.top && same.offset.left) { + css.position = 'absolute'; + const offsetParent = this.cache('target-offsetparent', () => getOffsetParent(this.target)); + + if (getOffsetParent(this.element) !== offsetParent) { + defer(() => { + this.element.parentNode.removeChild(this.element); + offsetParent.appendChild(this.element); + }); + } + + transcribe(same.offset, pos.offset); + moved = true; + + } else { + css.position = 'absolute'; + transcribe({top: true, left: true}, pos.page); + } + + if (!moved) { + if (this.options.bodyElement) { + this.options.bodyElement.appendChild(this.element); + } else { + let offsetParentIsBody = true; + let currentNode = this.element.parentNode; + while (currentNode && currentNode.nodeType === 1 && currentNode.tagName !== 'BODY') { + if (getComputedStyle(currentNode).position !== 'static') { + offsetParentIsBody = false; + break; + } + + currentNode = currentNode.parentNode; + } + + if (!offsetParentIsBody) { + this.element.parentNode.removeChild(this.element); + this.element.ownerDocument.body.appendChild(this.element); + } + } + } + + // Any css change will trigger a repaint, so let's avoid one if nothing changed + const writeCSS = {}; + let write = false; + for (let key in css) { + let val = css[key]; + let elVal = this.element.style[key]; + + if (elVal !== val) { + write = true; + writeCSS[key] = val; + } + } + + if (write) { + defer(() => { + extend(this.element.style, writeCSS); + this.trigger('repositioned'); + }); + } + } +} + +TetherClass.modules = []; + +TetherBase.position = position; + +let Tether = extend(TetherClass, TetherBase); |
