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

601 lines
13 KiB

//
// Traversing
//
let smoothDistance, smoothDuration, smoothEnd, smoothStart;
1 year ago
this.$ = function (selector, el) {
if (el == null) {
el = document;
}
try {
return el.querySelector(selector);
} catch (error) {}
};
1 year ago
this.$$ = function (selector, el) {
if (el == null) {
el = document;
}
try {
return el.querySelectorAll(selector);
} catch (error) {}
};
1 year ago
$.id = (id) => document.getElementById(id);
1 year ago
$.hasChild = function (parent, el) {
if (!parent) {
return;
}
while (el) {
1 year ago
if (el === parent) {
return true;
}
if (el === document.body) {
return;
}
el = el.parentNode;
}
};
1 year ago
$.closestLink = function (el, parent) {
if (parent == null) {
parent = document.body;
}
while (el) {
1 year ago
if (el.tagName === "A") {
return el;
}
if (el === parent) {
return;
}
el = el.parentNode;
}
};
//
// Events
//
1 year ago
$.on = function (el, event, callback, useCapture) {
if (useCapture == null) {
useCapture = false;
}
if (event.includes(" ")) {
for (var name of event.split(" ")) {
1 year ago
$.on(el, name, callback);
}
} else {
el.addEventListener(event, callback, useCapture);
}
};
1 year ago
$.off = function (el, event, callback, useCapture) {
if (useCapture == null) {
useCapture = false;
}
if (event.includes(" ")) {
for (var name of event.split(" ")) {
1 year ago
$.off(el, name, callback);
}
} else {
el.removeEventListener(event, callback, useCapture);
}
};
1 year ago
$.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);
};
1 year ago
$.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);
};
1 year ago
$.stopEvent = function (event) {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
};
1 year ago
$.eventTarget = (event) => event.target.correspondingUseElement || event.target;
//
// Manipulation
//
1 year ago
const buildFragment = function (value) {
const fragment = document.createDocumentFragment();
if ($.isCollection(value)) {
for (var child of $.makeArray(value)) {
1 year ago
fragment.appendChild(child);
}
} else {
fragment.innerHTML = value;
}
return fragment;
};
1 year ago
$.append = function (el, value) {
if (typeof value === "string") {
el.insertAdjacentHTML("beforeend", value);
} else {
1 year ago
if ($.isCollection(value)) {
value = buildFragment(value);
}
el.appendChild(value);
}
};
1 year ago
$.prepend = function (el, value) {
if (!el.firstChild) {
$.append(value);
1 year ago
} else if (typeof value === "string") {
el.insertAdjacentHTML("afterbegin", value);
} else {
1 year ago
if ($.isCollection(value)) {
value = buildFragment(value);
}
el.insertBefore(value, el.firstChild);
}
};
1 year ago
$.before = function (el, value) {
if (typeof value === "string" || $.isCollection(value)) {
value = buildFragment(value);
}
el.parentNode.insertBefore(value, el);
};
1 year ago
$.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);
}
};
1 year ago
$.remove = function (value) {
if ($.isCollection(value)) {
for (var el of $.makeArray(value)) {
1 year ago
if (el.parentNode != null) {
el.parentNode.removeChild(el);
}
}
} else {
if (value.parentNode != null) {
value.parentNode.removeChild(value);
}
}
};
1 year ago
$.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.
1 year ago
$.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
//
1 year ago
$.rect = (el) => el.getBoundingClientRect();
1 year ago
$.offset = function (el, container) {
if (container == null) {
container = document.body;
}
let top = 0;
let left = 0;
1 year ago
while (el && el !== container) {
top += el.offsetTop;
left += el.offsetLeft;
el = el.offsetParent;
}
return {
top,
1 year ago
left,
};
};
1 year ago
$.scrollParent = function (el) {
while ((el = el.parentNode) && el.nodeType === 1) {
if (el.scrollTop > 0) {
break;
}
if (["auto", "scroll"].includes(getComputedStyle(el)?.overflowY ?? "")) {
1 year ago
break;
}
}
return el;
};
1 year ago
$.scrollTo = function (el, parent, position, options) {
if (position == null) {
position = "center";
}
if (options == null) {
options = {};
}
if (!el) {
return;
}
1 year ago
if (parent == null) {
parent = $.scrollParent(el);
}
if (!parent) {
return;
}
const parentHeight = parent.clientHeight;
const parentScrollHeight = parent.scrollHeight;
1 year ago
if (!(parentScrollHeight > parentHeight)) {
return;
}
1 year ago
const { top } = $.offset(el, parent);
const { offsetTop } = parent.firstElementChild;
switch (position) {
1 year ago
case "top":
parent.scrollTop = top - offsetTop - (options.margin || 0);
break;
1 year ago
case "center":
parent.scrollTop =
top - Math.round(parentHeight / 2 - el.offsetHeight / 2);
break;
1 year ago
case "continuous":
var { scrollTop } = parent;
var height = el.offsetHeight;
1 year ago
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.
1 year ago
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;
}
};
1 year ago
$.scrollToWithImageLock = function (el, parent, ...args) {
if (parent == null) {
parent = $.scrollParent(el);
}
if (!parent) {
return;
}
$.scrollTo(el, parent, ...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 parent.getElementsByTagName("img")) {
if (!image.complete) {
1 year ago
(function () {
let timeout;
1 year ago
const onLoad = function (event) {
clearTimeout(timeout);
unbind(event.target);
return $.scrollTo(el, parent, ...args);
};
1 year ago
var unbind = (target) => $.off(target, "load", onLoad);
1 year ago
$.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.
1 year ago
$.lockScroll = function (el, fn) {
let parent;
1 year ago
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();
}
};
1 year ago
let smoothScroll =
(smoothStart =
smoothEnd =
smoothDistance =
smoothDuration =
null);
1 year ago
$.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();
1 year ago
smoothScroll = function () {
const p = Math.min(1, (Date.now() - startTime) / smoothDuration);
1 year ago
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) {
1 year ago
return (smoothScroll = null);
} else {
return requestAnimationFrame(smoothScroll);
}
};
return requestAnimationFrame(smoothScroll);
};
//
// Utilities
//
1 year ago
$.extend = function (target, ...objects) {
for (var object of objects) {
if (object) {
for (var key in object) {
var value = object[key];
target[key] = value;
}
}
}
return target;
};
1 year ago
$.makeArray = function (object) {
if (Array.isArray(object)) {
return object;
} else {
return Array.prototype.slice.apply(object);
}
};
1 year ago
$.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.
1 year ago
$.isCollection = (object) =>
Array.isArray(object) || typeof object?.item === "function";
const ESCAPE_HTML_MAP = {
1 year ago
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#x27;",
"/": "&#x2F;",
};
const ESCAPE_HTML_REGEXP = /[&<>"'\/]/g;
1 year ago
$.escape = (string) =>
string.replace(ESCAPE_HTML_REGEXP, (match) => ESCAPE_HTML_MAP[match]);
const ESCAPE_REGEXP = /([.*+?^=!:${}()|\[\]\/\\])/g;
1 year ago
$.escapeRegexp = (string) => string.replace(ESCAPE_REGEXP, "\\$1");
1 year ago
$.urlDecode = (string) => decodeURIComponent(string.replace(/\+/g, "%20"));
1 year ago
$.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);
}
1 year ago
return string.join("");
};
1 year ago
$.framify = function (fn, obj) {
if (window.requestAnimationFrame) {
return (...args) => requestAnimationFrame(fn.bind(obj, ...args));
} else {
return fn;
}
};
1 year ago
$.requestAnimationFrame = function (fn) {
if (window.requestAnimationFrame) {
requestAnimationFrame(fn);
} else {
setTimeout(fn, 0);
}
};
//
// Miscellaneous
//
1 year ago
$.noop = function () {};
1 year ago
$.popup = function (value) {
try {
const win = window.open();
1 year ago
if (win.opener) {
win.opener = null;
}
win.location = value.href || value;
} catch (error) {
1 year ago
window.open(value.href || value, "_blank");
}
};
let isMac = null;
1 year ago
$.isMac = () =>
isMac != null ? isMac : (isMac = navigator.userAgent.includes("Mac"));
let isIE = null;
1 year ago
$.isIE = () =>
isIE != null
? isIE
: (isIE =
navigator.userAgent.includes("MSIE") ||
navigator.userAgent.includes("rv:11.0"));
let isChromeForAndroid = null;
1 year ago
$.isChromeForAndroid = () =>
isChromeForAndroid != null
? isChromeForAndroid
: (isChromeForAndroid =
navigator.userAgent.includes("Android") &&
1 year ago
/Chrome\/([.0-9])+ Mobile/.test(navigator.userAgent));
let isAndroid = null;
1 year ago
$.isAndroid = () =>
isAndroid != null
? isAndroid
: (isAndroid = navigator.userAgent.includes("Android"));
let isIOS = null;
1 year ago
$.isIOS = () =>
isIOS != null
? isIOS
: (isIOS =
navigator.userAgent.includes("iPhone") ||
navigator.userAgent.includes("iPad"));
1 year ago
$.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 = {
1 year ago
className: "highlight",
delay: 1000,
};
1 year ago
$.highlight = function (el, options) {
if (options == null) {
options = {};
}
options = $.extend({}, HIGHLIGHT_DEFAULTS, options);
el.classList.add(options.className);
1 year ago
setTimeout(() => el.classList.remove(options.className), options.delay);
};
1 year ago
$.copyToClipboard = function (string) {
let result;
1 year ago
const textarea = document.createElement("textarea");
textarea.style.position = "fixed";
textarea.style.opacity = 0;
textarea.value = string;
document.body.appendChild(textarea);
try {
textarea.select();
1 year ago
result = !!document.execCommand("copy");
} catch (error) {
result = false;
1 year ago
} finally {
document.body.removeChild(textarea);
}
return result;
};