You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
devdocs/assets/javascripts/lib/util.js

491 lines
14 KiB

/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS103: Rewrite code to no longer use __guard__, or convert again using --optional-chaining
* DS104: Avoid inline assignments
* DS204: Change includes calls to have a more natural evaluation order
* DS207: Consider shorter variations of null checks
* DS208: Avoid top-level this
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
//
// Traversing
//
let smoothDistance, smoothDuration, smoothEnd, smoothStart;
this.$ = function(selector, el) {
if (el == null) { el = document; }
try { return el.querySelector(selector); } catch (error) {}
};
this.$$ = function(selector, el) {
if (el == null) { el = document; }
try { return el.querySelectorAll(selector); } catch (error) {}
};
$.id = id => document.getElementById(id);
$.hasChild = function(parent, el) {
if (!parent) { return; }
while (el) {
if (el === parent) { return true; }
if (el === document.body) { return; }
el = el.parentNode;
}
};
$.closestLink = function(el, parent) {
if (parent == null) { parent = document.body; }
while (el) {
if (el.tagName === 'A') { return el; }
if (el === parent) { return; }
el = el.parentNode;
}
};
//
// Events
//
$.on = function(el, event, callback, useCapture) {
if (useCapture == null) { useCapture = false; }
if (event.indexOf(' ') >= 0) {
for (var name of Array.from(event.split(' '))) { $.on(el, name, callback); }
} else {
el.addEventListener(event, callback, useCapture);
}
};
$.off = function(el, event, callback, useCapture) {
if (useCapture == null) { useCapture = false; }
if (event.indexOf(' ') >= 0) {
for (var name of Array.from(event.split(' '))) { $.off(el, name, callback); }
} else {
el.removeEventListener(event, callback, useCapture);
}
};
$.trigger = function(el, type, canBubble, cancelable) {
if (canBubble == null) { canBubble = true; }
if (cancelable == null) { cancelable = true; }
const event = document.createEvent('Event');
event.initEvent(type, canBubble, cancelable);
el.dispatchEvent(event);
};
$.click = function(el) {
const event = document.createEvent('MouseEvent');
event.initMouseEvent('click', true, true, window, null, 0, 0, 0, 0, false, false, false, false, 0, null);
el.dispatchEvent(event);
};
$.stopEvent = function(event) {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
};
$.eventTarget = event => event.target.correspondingUseElement || event.target;
//
// Manipulation
//
const buildFragment = function(value) {
const fragment = document.createDocumentFragment();
if ($.isCollection(value)) {
for (var child of Array.from($.makeArray(value))) { fragment.appendChild(child); }
} else {
fragment.innerHTML = value;
}
return fragment;
};
$.append = function(el, value) {
if (typeof value === 'string') {
el.insertAdjacentHTML('beforeend', value);
} else {
if ($.isCollection(value)) { value = buildFragment(value); }
el.appendChild(value);
}
};
$.prepend = function(el, value) {
if (!el.firstChild) {
$.append(value);
} else if (typeof value === 'string') {
el.insertAdjacentHTML('afterbegin', value);
} else {
if ($.isCollection(value)) { value = buildFragment(value); }
el.insertBefore(value, el.firstChild);
}
};
$.before = function(el, value) {
if ((typeof value === 'string') || $.isCollection(value)) {
value = buildFragment(value);
}
el.parentNode.insertBefore(value, el);
};
$.after = function(el, value) {
if ((typeof value === 'string') || $.isCollection(value)) {
value = buildFragment(value);
}
if (el.nextSibling) {
el.parentNode.insertBefore(value, el.nextSibling);
} else {
el.parentNode.appendChild(value);
}
};
$.remove = function(value) {
if ($.isCollection(value)) {
for (var el of Array.from($.makeArray(value))) { if (el.parentNode != null) {
el.parentNode.removeChild(el);
} }
} else {
if (value.parentNode != null) {
value.parentNode.removeChild(value);
}
}
};
$.empty = function(el) {
while (el.firstChild) { el.removeChild(el.firstChild); }
};
// Calls the function while the element is off the DOM to avoid triggering
// unnecessary reflows and repaints.
$.batchUpdate = function(el, fn) {
const parent = el.parentNode;
const sibling = el.nextSibling;
parent.removeChild(el);
fn(el);
if (sibling) {
parent.insertBefore(el, sibling);
} else {
parent.appendChild(el);
}
};
//
// Offset
//
$.rect = el => el.getBoundingClientRect();
$.offset = function(el, container) {
if (container == null) { container = document.body; }
let top = 0;
let left = 0;
while (el && (el !== container)) {
top += el.offsetTop;
left += el.offsetLeft;
el = el.offsetParent;
}
return {
top,
left
};
};
$.scrollParent = function(el) {
while ((el = el.parentNode) && (el.nodeType === 1)) {
var needle;
if (el.scrollTop > 0) { break; }
if ((needle = __guard__(getComputedStyle(el), x => x.overflowY), ['auto', 'scroll'].includes(needle))) { break; }
}
return el;
};
$.scrollTo = function(el, parent, position, options) {
if (position == null) { position = 'center'; }
if (options == null) { options = {}; }
if (!el) { return; }
if (parent == null) { parent = $.scrollParent(el); }
if (!parent) { return; }
const parentHeight = parent.clientHeight;
const parentScrollHeight = parent.scrollHeight;
if (!(parentScrollHeight > parentHeight)) { return; }
const {
top
} = $.offset(el, parent);
const {
offsetTop
} = parent.firstElementChild;
switch (position) {
case 'top':
parent.scrollTop = top - offsetTop - ((options.margin != null) ? options.margin : 0);
break;
case 'center':
parent.scrollTop = top - Math.round((parentHeight / 2) - (el.offsetHeight / 2));
break;
case 'continuous':
var {
scrollTop
} = parent;
var height = el.offsetHeight;
var lastElementOffset = parent.lastElementChild.offsetTop + parent.lastElementChild.offsetHeight;
var offsetBottom = lastElementOffset > 0 ? parentScrollHeight - lastElementOffset : 0;
// If the target element is above the visible portion of its scrollable
// ancestor, move it near the top with a gap = options.topGap * target's height.
if ((top - offsetTop) <= (scrollTop + (height * (options.topGap || 1)))) {
parent.scrollTop = top - offsetTop - (height * (options.topGap || 1));
// If the target element is below the visible portion of its scrollable
// ancestor, move it near the bottom with a gap = options.bottomGap * target's height.
} else if ((top + offsetBottom) >= ((scrollTop + parentHeight) - (height * ((options.bottomGap || 1) + 1)))) {
parent.scrollTop = ((top + offsetBottom) - parentHeight) + (height * ((options.bottomGap || 1) + 1));
}
break;
}
};
$.scrollToWithImageLock = function(el, parent, ...args) {
if (parent == null) { parent = $.scrollParent(el); }
if (!parent) { return; }
$.scrollTo(el, parent, ...Array.from(args));
// Lock the scroll position on the target element for up to 3 seconds while
// nearby images are loaded and rendered.
for (var image of Array.from(parent.getElementsByTagName('img'))) {
if (!image.complete) {
(function() {
let timeout;
const onLoad = function(event) {
clearTimeout(timeout);
unbind(event.target);
return $.scrollTo(el, parent, ...Array.from(args));
};
var unbind = target => $.off(target, 'load', onLoad);
$.on(image, 'load', onLoad);
return timeout = setTimeout(unbind.bind(null, image), 3000);
})();
}
}
};
// Calls the function while locking the element's position relative to the window.
$.lockScroll = function(el, fn) {
let parent;
if (parent = $.scrollParent(el)) {
let {
top
} = $.rect(el);
if (![document.body, document.documentElement].includes(parent)) { top -= $.rect(parent).top; }
fn();
parent.scrollTop = $.offset(el, parent).top - top;
} else {
fn();
}
};
let smoothScroll = (smoothStart = (smoothEnd = (smoothDistance = (smoothDuration = null))));
$.smoothScroll = function(el, end) {
if (!window.requestAnimationFrame) {
el.scrollTop = end;
return;
}
smoothEnd = end;
if (smoothScroll) {
const newDistance = smoothEnd - smoothStart;
smoothDuration += Math.min(300, Math.abs(smoothDistance - newDistance));
smoothDistance = newDistance;
return;
}
smoothStart = el.scrollTop;
smoothDistance = smoothEnd - smoothStart;
smoothDuration = Math.min(300, Math.abs(smoothDistance));
const startTime = Date.now();
smoothScroll = function() {
const p = Math.min(1, (Date.now() - startTime) / smoothDuration);
const y = Math.max(0, Math.floor(smoothStart + (smoothDistance * (p < 0.5 ? 2 * p * p : (p * (4 - (p * 2))) - 1))));
el.scrollTop = y;
if (p === 1) {
return smoothScroll = null;
} else {
return requestAnimationFrame(smoothScroll);
}
};
return requestAnimationFrame(smoothScroll);
};
//
// Utilities
//
$.extend = function(target, ...objects) {
for (var object of Array.from(objects)) {
if (object) {
for (var key in object) {
var value = object[key];
target[key] = value;
}
}
}
return target;
};
$.makeArray = function(object) {
if (Array.isArray(object)) {
return object;
} else {
return Array.prototype.slice.apply(object);
}
};
$.arrayDelete = function(array, object) {
const index = array.indexOf(object);
if (index >= 0) {
array.splice(index, 1);
return true;
} else {
return false;
}
};
// Returns true if the object is an array or a collection of DOM elements.
$.isCollection = object => Array.isArray(object) || (typeof (object != null ? object.item : undefined) === 'function');
const ESCAPE_HTML_MAP = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#x27;',
'/': '&#x2F;'
};
const ESCAPE_HTML_REGEXP = /[&<>"'\/]/g;
$.escape = string => string.replace(ESCAPE_HTML_REGEXP, match => ESCAPE_HTML_MAP[match]);
const ESCAPE_REGEXP = /([.*+?^=!:${}()|\[\]\/\\])/g;
$.escapeRegexp = string => string.replace(ESCAPE_REGEXP, "\\$1");
$.urlDecode = string => decodeURIComponent(string.replace(/\+/g, '%20'));
$.classify = function(string) {
string = string.split('_');
for (let i = 0; i < string.length; i++) {
var substr = string[i];
string[i] = substr[0].toUpperCase() + substr.slice(1);
}
return string.join('');
};
$.framify = function(fn, obj) {
if (window.requestAnimationFrame) {
return (...args) => requestAnimationFrame(fn.bind(obj, ...Array.from(args)));
} else {
return fn;
}
};
$.requestAnimationFrame = function(fn) {
if (window.requestAnimationFrame) {
requestAnimationFrame(fn);
} else {
setTimeout(fn, 0);
}
};
//
// Miscellaneous
//
$.noop = function() {};
$.popup = function(value) {
try {
const win = window.open();
if (win.opener) { win.opener = null; }
win.location = value.href || value;
} catch (error) {
window.open(value.href || value, '_blank');
}
};
let isMac = null;
$.isMac = () => isMac != null ? isMac : (isMac = (navigator.userAgent != null ? navigator.userAgent.indexOf('Mac') : undefined) >= 0);
let isIE = null;
$.isIE = () => isIE != null ? isIE : (isIE = ((navigator.userAgent != null ? navigator.userAgent.indexOf('MSIE') : undefined) >= 0) || ((navigator.userAgent != null ? navigator.userAgent.indexOf('rv:11.0') : undefined) >= 0));
let isChromeForAndroid = null;
$.isChromeForAndroid = () => isChromeForAndroid != null ? isChromeForAndroid : (isChromeForAndroid = ((navigator.userAgent != null ? navigator.userAgent.indexOf('Android') : undefined) >= 0) && /Chrome\/([.0-9])+ Mobile/.test(navigator.userAgent));
let isAndroid = null;
$.isAndroid = () => isAndroid != null ? isAndroid : (isAndroid = (navigator.userAgent != null ? navigator.userAgent.indexOf('Android') : undefined) >= 0);
let isIOS = null;
$.isIOS = () => isIOS != null ? isIOS : (isIOS = ((navigator.userAgent != null ? navigator.userAgent.indexOf('iPhone') : undefined) >= 0) || ((navigator.userAgent != null ? navigator.userAgent.indexOf('iPad') : undefined) >= 0));
$.overlayScrollbarsEnabled = function() {
if (!$.isMac()) { return false; }
const div = document.createElement('div');
div.setAttribute('style', 'width: 100px; height: 100px; overflow: scroll; position: absolute');
document.body.appendChild(div);
const result = div.offsetWidth === div.clientWidth;
document.body.removeChild(div);
return result;
};
const HIGHLIGHT_DEFAULTS = {
className: 'highlight',
delay: 1000
};
$.highlight = function(el, options) {
if (options == null) { options = {}; }
options = $.extend({}, HIGHLIGHT_DEFAULTS, options);
el.classList.add(options.className);
setTimeout((() => el.classList.remove(options.className)), options.delay);
};
$.copyToClipboard = function(string) {
let result;
const textarea = document.createElement('textarea');
textarea.style.position = 'fixed';
textarea.style.opacity = 0;
textarea.value = string;
document.body.appendChild(textarea);
try {
textarea.select();
result = !!document.execCommand('copy');
} catch (error) {
result = false;
}
finally {
document.body.removeChild(textarea);
}
return result;
};
function __guard__(value, transform) {
return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined;
}