LICENSE 0000644 00000002047 15073227372 0005564 0 ustar 00 Copyright (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.sass 0000644 00000000123 15073227372 0010315 0 ustar 00 @import helpers/tether
$themePrefix: "tether"
+tether($themePrefix: $themePrefix) src/css/tether-theme-arrows.sass 0000644 00000000617 15073227372 0012740 0 ustar 00 @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.sass 0000644 00000000135 15073227372 0013043 0 ustar 00 @mixin pie-clearfix
*zoom: 1
&:after
content: ""
display: table
clear: both
src/css/mixins/_inline-block.sass 0000644 00000000173 15073227372 0013043 0 ustar 00 @mixin inline-block
display: inline-block
vertical-align: middle
*vertical-align: auto
*zoom: 1
*display: inline
src/css/helpers/_tether-theme-arrows.sass 0000644 00000016164 15073227372 0014545 0 ustar 00 =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.sass 0000644 00000001022 15073227372 0014274 0 ustar 00 =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.sass 0000644 00000000454 15073227372 0012125 0 ustar 00 =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.sass 0000644 00000000622 15073227372 0013653 0 ustar 00 @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.sass 0000644 00000000455 15073227372 0012504 0 ustar 00 @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.js 0000644 00000001277 15073227372 0007441 0 ustar 00 /* 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.js 0000644 00000002060 15073227372 0011256 0 ustar 00 /* 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.js 0000644 00000055715 15073227372 0007625 0 ustar 00 /* 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.js 0000644 00000003117 15073227372 0010136 0 ustar 00 /* 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.js 0000644 00000021363 15073227372 0007462 0 ustar 00 let 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