mirror of
https://github.com/snipe/snipe-it.git
synced 2026-03-02 22:54:24 +00:00
371 lines
8.6 KiB
JavaScript
371 lines
8.6 KiB
JavaScript
let TetherBase;
|
|
if (typeof TetherBase === 'undefined') {
|
|
TetherBase = {modules: []};
|
|
}
|
|
|
|
let zeroElement = null;
|
|
|
|
// Same as native getBoundingClientRect, except it takes into account parent <frame> offsets
|
|
// if the element lies within a nested document (<frame> or <iframe>-like).
|
|
function getActualBoundingClientRect(node) {
|
|
let boundingRect = node.getBoundingClientRect();
|
|
|
|
// The original object returned by getBoundingClientRect is immutable, so we clone it
|
|
// We can't use extend because the properties are not considered part of the object by hasOwnProperty in IE9
|
|
let rect = {};
|
|
for (var k in boundingRect) {
|
|
rect[k] = boundingRect[k];
|
|
}
|
|
|
|
if (node.ownerDocument !== document) {
|
|
let frameElement = node.ownerDocument.defaultView.frameElement;
|
|
if (frameElement) {
|
|
let frameRect = getActualBoundingClientRect(frameElement);
|
|
rect.top += frameRect.top;
|
|
rect.bottom += frameRect.top;
|
|
rect.left += frameRect.left;
|
|
rect.right += frameRect.left;
|
|
}
|
|
}
|
|
|
|
return rect;
|
|
}
|
|
|
|
function getScrollParents(el) {
|
|
// In firefox if the el is inside an iframe with display: none; window.getComputedStyle() will return null;
|
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=548397
|
|
const computedStyle = getComputedStyle(el) || {};
|
|
const position = computedStyle.position;
|
|
let parents = [];
|
|
|
|
if (position === 'fixed') {
|
|
return [el];
|
|
}
|
|
|
|
let parent = el;
|
|
while ((parent = parent.parentNode) && parent && parent.nodeType === 1) {
|
|
let style;
|
|
try {
|
|
style = getComputedStyle(parent);
|
|
} catch (err) {}
|
|
|
|
if (typeof style === 'undefined' || style === null) {
|
|
parents.push(parent);
|
|
return parents;
|
|
}
|
|
|
|
const {overflow, overflowX, overflowY} = style;
|
|
if (/(auto|scroll)/.test(overflow + overflowY + overflowX)) {
|
|
if (position !== 'absolute' || ['relative', 'absolute', 'fixed'].indexOf(style.position) >= 0) {
|
|
parents.push(parent)
|
|
}
|
|
}
|
|
}
|
|
|
|
parents.push(el.ownerDocument.body);
|
|
|
|
// If the node is within a frame, account for the parent window scroll
|
|
if (el.ownerDocument !== document) {
|
|
parents.push(el.ownerDocument.defaultView);
|
|
}
|
|
|
|
return parents;
|
|
}
|
|
|
|
const uniqueId = (() => {
|
|
let id = 0;
|
|
return () => ++id;
|
|
})();
|
|
|
|
const zeroPosCache = {};
|
|
const getOrigin = () => {
|
|
// getBoundingClientRect is unfortunately too accurate. It introduces a pixel or two of
|
|
// jitter as the user scrolls that messes with our ability to detect if two positions
|
|
// are equivilant or not. We place an element at the top left of the page that will
|
|
// get the same jitter, so we can cancel the two out.
|
|
let node = zeroElement;
|
|
if (!node || !document.body.contains(node)) {
|
|
node = document.createElement('div');
|
|
node.setAttribute('data-tether-id', uniqueId());
|
|
extend(node.style, {
|
|
top: 0,
|
|
left: 0,
|
|
position: 'absolute'
|
|
});
|
|
|
|
document.body.appendChild(node);
|
|
|
|
zeroElement = node;
|
|
}
|
|
|
|
const id = node.getAttribute('data-tether-id');
|
|
if (typeof zeroPosCache[id] === 'undefined') {
|
|
zeroPosCache[id] = getActualBoundingClientRect(node);
|
|
|
|
// Clear the cache when this position call is done
|
|
defer(() => {
|
|
delete zeroPosCache[id];
|
|
});
|
|
}
|
|
|
|
return zeroPosCache[id];
|
|
};
|
|
|
|
function removeUtilElements() {
|
|
if (zeroElement) {
|
|
document.body.removeChild(zeroElement);
|
|
}
|
|
zeroElement = null;
|
|
};
|
|
|
|
function getBounds(el) {
|
|
let doc;
|
|
if (el === document) {
|
|
doc = document;
|
|
el = document.documentElement;
|
|
} else {
|
|
doc = el.ownerDocument;
|
|
}
|
|
|
|
const docEl = doc.documentElement;
|
|
|
|
const box = getActualBoundingClientRect(el);
|
|
|
|
const origin = getOrigin();
|
|
|
|
box.top -= origin.top;
|
|
box.left -= origin.left;
|
|
|
|
if (typeof box.width === 'undefined') {
|
|
box.width = document.body.scrollWidth - box.left - box.right;
|
|
}
|
|
if (typeof box.height === 'undefined') {
|
|
box.height = document.body.scrollHeight - box.top - box.bottom;
|
|
}
|
|
|
|
box.top = box.top - docEl.clientTop;
|
|
box.left = box.left - docEl.clientLeft;
|
|
box.right = doc.body.clientWidth - box.width - box.left;
|
|
box.bottom = doc.body.clientHeight - box.height - box.top;
|
|
|
|
return box;
|
|
}
|
|
|
|
function getOffsetParent(el) {
|
|
return el.offsetParent || document.documentElement;
|
|
}
|
|
|
|
let _scrollBarSize = null;
|
|
function getScrollBarSize() {
|
|
if (_scrollBarSize) {
|
|
return _scrollBarSize;
|
|
}
|
|
const inner = document.createElement('div');
|
|
inner.style.width = '100%';
|
|
inner.style.height = '200px';
|
|
|
|
const outer = document.createElement('div');
|
|
extend(outer.style, {
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
pointerEvents: 'none',
|
|
visibility: 'hidden',
|
|
width: '200px',
|
|
height: '150px',
|
|
overflow: 'hidden'
|
|
});
|
|
|
|
outer.appendChild(inner);
|
|
|
|
document.body.appendChild(outer);
|
|
|
|
const widthContained = inner.offsetWidth;
|
|
outer.style.overflow = 'scroll';
|
|
let widthScroll = inner.offsetWidth;
|
|
|
|
if (widthContained === widthScroll) {
|
|
widthScroll = outer.clientWidth;
|
|
}
|
|
|
|
document.body.removeChild(outer);
|
|
|
|
const width = widthContained - widthScroll;
|
|
|
|
_scrollBarSize = {width, height: width};
|
|
return _scrollBarSize;
|
|
}
|
|
|
|
function extend(out={}) {
|
|
const args = [];
|
|
|
|
Array.prototype.push.apply(args, arguments);
|
|
|
|
args.slice(1).forEach(obj => {
|
|
if (obj) {
|
|
for (let key in obj) {
|
|
if ({}.hasOwnProperty.call(obj, key)) {
|
|
out[key] = obj[key];
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
return out;
|
|
}
|
|
|
|
function removeClass(el, name) {
|
|
if (typeof el.classList !== 'undefined') {
|
|
name.split(' ').forEach(cls => {
|
|
if (cls.trim()) {
|
|
el.classList.remove(cls);
|
|
}
|
|
});
|
|
} else {
|
|
const regex = new RegExp(`(^| )${ name.split(' ').join('|') }( |$)`, 'gi');
|
|
const className = getClassName(el).replace(regex, ' ');
|
|
setClassName(el, className);
|
|
}
|
|
}
|
|
|
|
function addClass(el, name) {
|
|
if (typeof el.classList !== 'undefined') {
|
|
name.split(' ').forEach(cls => {
|
|
if (cls.trim()) {
|
|
el.classList.add(cls);
|
|
}
|
|
});
|
|
} else {
|
|
removeClass(el, name);
|
|
const cls = getClassName(el) + ` ${name}`;
|
|
setClassName(el, cls);
|
|
}
|
|
}
|
|
|
|
function hasClass(el, name) {
|
|
if (typeof el.classList !== 'undefined') {
|
|
return el.classList.contains(name);
|
|
}
|
|
const className = getClassName(el);
|
|
return new RegExp(`(^| )${ name }( |$)`, 'gi').test(className);
|
|
}
|
|
|
|
function getClassName(el) {
|
|
// Can't use just SVGAnimatedString here since nodes within a Frame in IE have
|
|
// completely separately SVGAnimatedString base classes
|
|
if (el.className instanceof el.ownerDocument.defaultView.SVGAnimatedString) {
|
|
return el.className.baseVal;
|
|
}
|
|
return el.className;
|
|
}
|
|
|
|
function setClassName(el, className) {
|
|
el.setAttribute('class', className);
|
|
}
|
|
|
|
|
|
function updateClasses(el, add, all) {
|
|
// Of the set of 'all' classes, we need the 'add' classes, and only the
|
|
// 'add' classes to be set.
|
|
all.forEach(cls => {
|
|
if (add.indexOf(cls) === -1 && hasClass(el, cls)) {
|
|
removeClass(el, cls);
|
|
}
|
|
});
|
|
|
|
add.forEach(cls => {
|
|
if (!hasClass(el, cls)) {
|
|
addClass(el, cls);
|
|
}
|
|
});
|
|
}
|
|
|
|
const deferred = [];
|
|
|
|
const defer = (fn) => {
|
|
deferred.push(fn);
|
|
};
|
|
|
|
const flush = () => {
|
|
let fn;
|
|
while(fn = deferred.pop()) {
|
|
fn();
|
|
}
|
|
};
|
|
|
|
class Evented {
|
|
on(event, handler, ctx, once=false) {
|
|
if (typeof this.bindings === 'undefined') {
|
|
this.bindings = {};
|
|
}
|
|
if (typeof this.bindings[event] === 'undefined') {
|
|
this.bindings[event] = [];
|
|
}
|
|
this.bindings[event].push({handler, ctx, once});
|
|
}
|
|
|
|
once(event, handler, ctx) {
|
|
this.on(event, handler, ctx, true);
|
|
}
|
|
|
|
off(event, handler) {
|
|
if (typeof this.bindings === 'undefined' ||
|
|
typeof this.bindings[event] === 'undefined') {
|
|
return;
|
|
}
|
|
|
|
if (typeof handler === 'undefined') {
|
|
delete this.bindings[event];
|
|
} else {
|
|
let i = 0;
|
|
while (i < this.bindings[event].length) {
|
|
if (this.bindings[event][i].handler === handler) {
|
|
this.bindings[event].splice(i, 1);
|
|
} else {
|
|
++i;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
trigger(event, ...args) {
|
|
if (typeof this.bindings !== 'undefined' && this.bindings[event]) {
|
|
let i = 0;
|
|
while (i < this.bindings[event].length) {
|
|
const {handler, ctx, once} = this.bindings[event][i];
|
|
|
|
let context = ctx;
|
|
if (typeof context === 'undefined') {
|
|
context = this;
|
|
}
|
|
|
|
handler.apply(context, args);
|
|
|
|
if (once) {
|
|
this.bindings[event].splice(i, 1);
|
|
} else {
|
|
++i;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
TetherBase.Utils = {
|
|
getActualBoundingClientRect,
|
|
getScrollParents,
|
|
getBounds,
|
|
getOffsetParent,
|
|
extend,
|
|
addClass,
|
|
removeClass,
|
|
hasClass,
|
|
updateClasses,
|
|
defer,
|
|
flush,
|
|
uniqueId,
|
|
Evented,
|
|
getScrollBarSize,
|
|
removeUtilElements
|
|
};
|