LICENSE000064400000002047150732273720005564 0ustar00Copyright (c) 2014-2017 HubSpot, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. src/css/tether.sass000064400000000123150732273720010315 0ustar00@import helpers/tether $themePrefix: "tether" +tether($themePrefix: $themePrefix)src/css/tether-theme-arrows.sass000064400000000617150732273720012740 0ustar00@import helpers/tether @import helpers/tether-theme-arrows $themePrefix: "tether" $themeName: "arrows" $arrowSize: 16px $backgroundColor: #fff $color: inherit $useDropShadow: true +tether($themePrefix: $themePrefix) +tether-theme-arrows($themePrefix: $themePrefix, $themeName: $themeName, $arrowSize: $arrowSize, $backgroundColor: $backgroundColor, $color: $color, $useDropShadow: $useDropShadow) src/css/mixins/_pie-clearfix.sass000064400000000135150732273720013043 0ustar00@mixin pie-clearfix *zoom: 1 &:after content: "" display: table clear: both src/css/mixins/_inline-block.sass000064400000000173150732273720013043 0ustar00@mixin inline-block display: inline-block vertical-align: middle *vertical-align: auto *zoom: 1 *display: inline src/css/helpers/_tether-theme-arrows.sass000064400000016164150732273720014545 0ustar00=tether-theme-arrows($themePrefix: "tether", $themeName: "arrows", $arrowSize: 16px, $arrowPointerEvents: null, $backgroundColor: #fff, $color: inherit, $useDropShadow: false) .#{ $themePrefix }-element.#{ $themePrefix }-theme-#{ $themeName } max-width: 100% max-height: 100% .#{ $themePrefix }-content border-radius: 5px position: relative font-family: inherit background: $backgroundColor color: $color padding: 1em font-size: 1.1em line-height: 1.5em @if $useDropShadow transform: translateZ(0) filter: drop-shadow(0 1px 4px rgba(0, 0, 0, .2)) &:before content: "" display: block position: absolute width: 0 height: 0 border-color: transparent border-width: $arrowSize border-style: solid pointer-events: $arrowPointerEvents // Centers and middles &.#{ $themePrefix }-element-attached-bottom.#{ $themePrefix }-element-attached-center .#{ $themePrefix }-content margin-bottom: $arrowSize &:before top: 100% left: 50% margin-left: - $arrowSize border-top-color: $backgroundColor border-bottom: 0 &.#{ $themePrefix }-element-attached-top.#{ $themePrefix }-element-attached-center .#{ $themePrefix }-content margin-top: $arrowSize &:before bottom: 100% left: 50% margin-left: - $arrowSize border-bottom-color: $backgroundColor border-top: 0 &.#{ $themePrefix }-element-attached-right.#{ $themePrefix }-element-attached-middle .#{ $themePrefix }-content margin-right: $arrowSize &:before left: 100% top: 50% margin-top: - $arrowSize border-left-color: $backgroundColor border-right: 0 &.#{ $themePrefix }-element-attached-left.#{ $themePrefix }-element-attached-middle .#{ $themePrefix }-content margin-left: $arrowSize &:before right: 100% top: 50% margin-top: - $arrowSize border-right-color: $backgroundColor border-left: 0 // Target middle/center, element corner &.#{ $themePrefix }-element-attached-left.#{ $themePrefix }-target-attached-center .#{ $themePrefix }-content left: - $arrowSize * 2 &.#{ $themePrefix }-element-attached-right.#{ $themePrefix }-target-attached-center .#{ $themePrefix }-content left: $arrowSize * 2 &.#{ $themePrefix }-element-attached-top.#{ $themePrefix }-element-attached-left.#{ $themePrefix }-target-attached-middle .#{ $themePrefix }-content margin-top: $arrowSize &:before bottom: 100% left: $arrowSize border-bottom-color: $backgroundColor border-top: 0 &.#{ $themePrefix }-element-attached-top.#{ $themePrefix }-element-attached-right.#{ $themePrefix }-target-attached-middle .#{ $themePrefix }-content margin-top: $arrowSize &:before bottom: 100% right: $arrowSize border-bottom-color: $backgroundColor border-top: 0 &.#{ $themePrefix }-element-attached-bottom.#{ $themePrefix }-element-attached-left.#{ $themePrefix }-target-attached-middle .#{ $themePrefix }-content margin-bottom: $arrowSize &:before top: 100% left: $arrowSize border-top-color: $backgroundColor border-bottom: 0 &.#{ $themePrefix }-element-attached-bottom.#{ $themePrefix }-element-attached-right.#{ $themePrefix }-target-attached-middle .#{ $themePrefix }-content margin-bottom: $arrowSize &:before top: 100% right: $arrowSize border-top-color: $backgroundColor border-bottom: 0 // Top and bottom corners &.#{ $themePrefix }-element-attached-top.#{ $themePrefix }-element-attached-left.#{ $themePrefix }-target-attached-bottom .#{ $themePrefix }-content margin-top: $arrowSize &:before bottom: 100% left: $arrowSize border-bottom-color: $backgroundColor border-top: 0 &.#{ $themePrefix }-element-attached-top.#{ $themePrefix }-element-attached-right.#{ $themePrefix }-target-attached-bottom .#{ $themePrefix }-content margin-top: $arrowSize &:before bottom: 100% right: $arrowSize border-bottom-color: $backgroundColor border-top: 0 &.#{ $themePrefix }-element-attached-bottom.#{ $themePrefix }-element-attached-left.#{ $themePrefix }-target-attached-top .#{ $themePrefix }-content margin-bottom: $arrowSize &:before top: 100% left: $arrowSize border-top-color: $backgroundColor border-bottom: 0 &.#{ $themePrefix }-element-attached-bottom.#{ $themePrefix }-element-attached-right.#{ $themePrefix }-target-attached-top .#{ $themePrefix }-content margin-bottom: $arrowSize &:before top: 100% right: $arrowSize border-top-color: $backgroundColor border-bottom: 0 // Side corners &.#{ $themePrefix }-element-attached-top.#{ $themePrefix }-element-attached-right.#{ $themePrefix }-target-attached-left .#{ $themePrefix }-content margin-right: $arrowSize &:before top: $arrowSize left: 100% border-left-color: $backgroundColor border-right: 0 &.#{ $themePrefix }-element-attached-top.#{ $themePrefix }-element-attached-left.#{ $themePrefix }-target-attached-right .#{ $themePrefix }-content margin-left: $arrowSize &:before top: $arrowSize right: 100% border-right-color: $backgroundColor border-left: 0 &.#{ $themePrefix }-element-attached-bottom.#{ $themePrefix }-element-attached-right.#{ $themePrefix }-target-attached-left .#{ $themePrefix }-content margin-right: $arrowSize &:before bottom: $arrowSize left: 100% border-left-color: $backgroundColor border-right: 0 &.#{ $themePrefix }-element-attached-bottom.#{ $themePrefix }-element-attached-left.#{ $themePrefix }-target-attached-right .#{ $themePrefix }-content margin-left: $arrowSize &:before bottom: $arrowSize right: 100% border-right-color: $backgroundColor border-left: 0 src/css/helpers/_tether-theme-basic.sass000064400000001022150732273720014274 0ustar00=tether-theme-basic($themePrefix: "tether", $themeName: "basic", $backgroundColor: #fff, $color: inherit) .#{ $themePrefix }-element.#{ $themePrefix }-theme-#{ $themeName } max-width: 100% max-height: 100% .#{ $themePrefix }-content border-radius: 5px box-shadow: 0 2px 8px rgba(0, 0, 0, .2) font-family: inherit background: $backgroundColor color: $color padding: 1em font-size: 1.1em line-height: 1.5em src/css/helpers/_tether.sass000064400000000454150732273720012125 0ustar00=tether($themePrefix: "tether") .#{ $themePrefix }-element, .#{ $themePrefix }-element * &, &:after, &:before box-sizing: border-box .#{ $themePrefix }-element position: absolute display: none &.#{ $themePrefix }-open display: block src/css/tether-theme-arrows-dark.sass000064400000000622150732273720013653 0ustar00@import helpers/tether @import helpers/tether-theme-arrows $themePrefix: "tether" $themeName: "arrows-dark" $arrowSize: 16px $backgroundColor: #000 $color: #fff $useDropShadow: false +tether($themePrefix: $themePrefix) +tether-theme-arrows($themePrefix: $themePrefix, $themeName: $themeName, $arrowSize: $arrowSize, $backgroundColor: $backgroundColor, $color: $color, $useDropShadow: $useDropShadow) src/css/tether-theme-basic.sass000064400000000455150732273720012504 0ustar00@import helpers/tether @import helpers/tether-theme-basic $themePrefix: "tether" $themeName: "basic" $backgroundColor: #fff $color: inherit +tether($themePrefix: $themePrefix) +tether-theme-basic($themePrefix: $themePrefix, $themeName: $themeName, $backgroundColor: $backgroundColor, $color: $color)src/js/shift.js000064400000001277150732273720007441 0ustar00/* globals TetherBase */ TetherBase.modules.push({ position({top, left}) { if (!this.options.shift) { return; } let shift = this.options.shift; if (typeof this.options.shift === 'function') { shift = this.options.shift.call(this, {top, left}); } let shiftTop, shiftLeft; if (typeof shift === 'string') { shift = shift.split(' '); shift[1] = shift[1] || shift[0]; ([shiftTop, shiftLeft] = shift); shiftTop = parseFloat(shiftTop, 10); shiftLeft = parseFloat(shiftLeft, 10); } else { ([shiftTop, shiftLeft] = [shift.top, shift.left]); } top += shiftTop; left += shiftLeft; return {top, left}; } }); src/js/markAttachment.js000064400000002060150732273720011256 0ustar00/* globals Tether */ Tether.modules.push({ initialize() { this.markers = {}; ['target', 'element'].forEach(type => { const el = document.createElement('div'); el.className = this.getClass(`${ type }-marker`); const dot = document.createElement('div'); dot.className = this.getClass('marker-dot'); el.appendChild(dot); this[type].appendChild(el); this.markers[type] = {dot, el}; }); }, position({manualOffset, manualTargetOffset}) { const offsets = { element: manualOffset, target: manualTargetOffset }; for (let type in offsets) { const offset = offsets[type]; for (let side in offset) { let val = offset[side]; const notString = typeof val !== 'string'; if (notString || val.indexOf('%') === -1 && val.indexOf('px') === -1) { val += 'px'; } if (this.markers[type].dot.style[side] !== val) { this.markers[type].dot.style[side] = val; } } } return true; } }); src/js/tether.js000064400000055715150732273720007625 0ustar00/* 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 === 'object' && typeof performance.now === 'function') { 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 (typeof window.devicePixelRatio === 'number' && devicePixelRatio % 1 === 0) { xPos = Math.round(xPos * devicePixelRatio) / devicePixelRatio; yPos = Math.round(yPos * devicePixelRatio) / devicePixelRatio; } 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) { if (this.element.parentNode !== this.options.bodyElement) { this.options.bodyElement.appendChild(this.element); } } else { let offsetParentIsBody = true; function isFullscreenElement(e) { let d = e.ownerDocument; let fe = d.fullscreenElement || d.webkitFullscreenElement || d.mozFullScreenElement || d.msFullscreenElement; return fe === e; } let currentNode = this.element.parentNode; while (currentNode && currentNode.nodeType === 1 && currentNode.tagName !== 'BODY' && !isFullscreenElement(currentNode)) { 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); src/js/abutment.js000064400000003117150732273720010136 0ustar00/* globals TetherBase */ const {getBounds, updateClasses, defer} = TetherBase.Utils; TetherBase.modules.push({ position({top, left}) { const {height, width} = this.cache('element-bounds', () => { return getBounds(this.element); }); const targetPos = this.getTargetBounds(); const bottom = top + height; const right = left + width; const abutted = []; if (top <= targetPos.bottom && bottom >= targetPos.top) { ['left', 'right'].forEach(side => { const targetPosSide = targetPos[side]; if (targetPosSide === left || targetPosSide === right) { abutted.push(side); } }); } if (left <= targetPos.right && right >= targetPos.left) { ['top', 'bottom'].forEach(side => { const targetPosSide = targetPos[side]; if (targetPosSide === top || targetPosSide === bottom) { abutted.push(side); } }); } const allClasses = []; const addClasses = []; const sides = ['left', 'top', 'right', 'bottom']; allClasses.push(this.getClass('abutted')); sides.forEach(side => { allClasses.push(`${ this.getClass('abutted') }-${ side }`); }); if (abutted.length) { addClasses.push(this.getClass('abutted')); } abutted.forEach(side => { addClasses.push(`${ this.getClass('abutted') }-${ side }`); }); defer(() => { if (!(this.options.addTargetClasses === false)) { updateClasses(this.target, addClasses, allClasses); } updateClasses(this.element, addClasses, allClasses); }); return true; } }); src/js/utils.js000064400000021363150732273720007462 0ustar00let TetherBase; if (typeof TetherBase === 'undefined') { TetherBase = {modules: []}; } let zeroElement = null; // Same as native getBoundingClientRect, except it takes into account parent offsets // if the element lies within a nested document ( or