mirror of https://github.com/freeCodeCamp/devdocs
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.
491 lines
14 KiB
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 = {
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": ''',
|
|
'/': '/'
|
|
};
|
|
|
|
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;
|
|
} |