mirror of
https://github.com/livebook-dev/livebook.git
synced 2024-09-20 10:05:57 +08:00
338 lines
8 KiB
JavaScript
338 lines
8 KiB
JavaScript
import md5 from "crypto-js/md5";
|
|
import sha256 from "crypto-js/sha256";
|
|
import encBase64 from "crypto-js/enc-base64";
|
|
|
|
export function isMacOS() {
|
|
return /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform);
|
|
}
|
|
|
|
export function isEditableElement(element) {
|
|
return (
|
|
element.matches && element.matches("input, textarea, [contenteditable]")
|
|
);
|
|
}
|
|
|
|
export function isElementInViewport(element, proximity = 0) {
|
|
const box = element.getBoundingClientRect();
|
|
return box.bottom >= -proximity && box.top <= window.innerHeight + proximity;
|
|
}
|
|
|
|
export function isElementHidden(element) {
|
|
return element.offsetParent === null;
|
|
}
|
|
|
|
export function waitUntilVisible(element) {
|
|
let observer = null;
|
|
|
|
return new Promise((resolve, reject) => {
|
|
if (isElementHidden(element)) {
|
|
observer = new ResizeObserver((entries) => {
|
|
if (!isElementHidden(element)) {
|
|
observer.disconnect();
|
|
resolve();
|
|
}
|
|
});
|
|
observer.observe(element);
|
|
} else {
|
|
resolve();
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Returns a promise that resolves when the element enters the viewport.
|
|
*
|
|
* ## Options
|
|
*
|
|
* * `root` - a scrollable ancestor that should be used for observing
|
|
* the intersection, instead of the viewport
|
|
*
|
|
* * `proximity` - the number of pixels around `root` used to expand
|
|
* the intersection box, which effectively resolves the promise when
|
|
* `element` is in certain proximity of the viewport. Note that if
|
|
* the element is inside a scrollable ancestor, the ancestor must
|
|
* be set as `root`.
|
|
*
|
|
* > NOTE: rootMargin only applies to the intersection root itself.
|
|
* > If a target Element is clipped by an ancestor other than the
|
|
* > intersection root, that clipping is unaffected by rootMargin.
|
|
* > ~ https://w3c.github.io/IntersectionObserver
|
|
*
|
|
*/
|
|
export function waitUntilInViewport(
|
|
element,
|
|
{ root = null, proximity = 0 } = {},
|
|
) {
|
|
let observer = null;
|
|
|
|
const promise = new Promise((resolve, reject) => {
|
|
if (isElementVisibleInViewport(element, proximity)) {
|
|
resolve();
|
|
} else {
|
|
observer = new IntersectionObserver(
|
|
(entries) => {
|
|
if (entries[0].isIntersecting) {
|
|
observer.disconnect();
|
|
observer = null;
|
|
resolve();
|
|
}
|
|
},
|
|
{ root, rootMargin: `${proximity}px` },
|
|
);
|
|
observer.observe(element);
|
|
}
|
|
});
|
|
|
|
const cancel = () => {
|
|
observer && observer.disconnect();
|
|
};
|
|
|
|
return { promise, cancel };
|
|
}
|
|
|
|
export function isElementVisibleInViewport(element, proximity = 0) {
|
|
return !isElementHidden(element) && isElementInViewport(element, proximity);
|
|
}
|
|
|
|
export function clamp(n, x, y) {
|
|
return Math.min(Math.max(n, x), y);
|
|
}
|
|
|
|
export function getLineHeight(element) {
|
|
const computedStyle = window.getComputedStyle(element);
|
|
const lineHeight = parseInt(computedStyle.lineHeight, 10);
|
|
|
|
if (Number.isNaN(lineHeight)) {
|
|
const clone = element.cloneNode();
|
|
clone.innerHTML = "<br>";
|
|
element.appendChild(clone);
|
|
const singleLineHeight = clone.clientHeight;
|
|
clone.innerHTML = "<br><br>";
|
|
const doubleLineHeight = clone.clientHeight;
|
|
element.removeChild(clone);
|
|
const lineHeight = doubleLineHeight - singleLineHeight;
|
|
return lineHeight;
|
|
} else {
|
|
return lineHeight;
|
|
}
|
|
}
|
|
|
|
export function selectElementContent(element) {
|
|
const selection = window.getSelection();
|
|
const range = document.createRange();
|
|
range.selectNodeContents(element);
|
|
selection.removeAllRanges();
|
|
selection.addRange(range);
|
|
}
|
|
|
|
export function smoothlyScrollToElement(element) {
|
|
const { height } = element.getBoundingClientRect();
|
|
|
|
if (height < window.innerHeight) {
|
|
element.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
} else {
|
|
element.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
}
|
|
}
|
|
|
|
export function isScrolledToEnd(element) {
|
|
// See https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight#determine_if_an_element_has_been_totally_scrolled
|
|
return (
|
|
Math.abs(element.scrollHeight - element.clientHeight - element.scrollTop) <
|
|
1
|
|
);
|
|
}
|
|
|
|
export function scrollToEnd(element) {
|
|
element.scrollTop = element.scrollHeight;
|
|
}
|
|
|
|
/**
|
|
* Transforms a UTF8 string into base64 encoding.
|
|
*/
|
|
export function encodeBase64(string) {
|
|
return btoa(unescape(encodeURIComponent(string)));
|
|
}
|
|
|
|
/**
|
|
* Transforms base64 encoding into UTF8 string.
|
|
*/
|
|
export function decodeBase64(binary) {
|
|
return decodeURIComponent(escape(atob(binary)));
|
|
}
|
|
|
|
/**
|
|
* Generates a random string.
|
|
*/
|
|
export function randomId() {
|
|
return randomString(24);
|
|
}
|
|
|
|
/**
|
|
* Generates a random long string.
|
|
*/
|
|
export function randomToken() {
|
|
return randomString(40);
|
|
}
|
|
|
|
function randomString(byteSize) {
|
|
const array = new Uint8Array(byteSize);
|
|
crypto.getRandomValues(array);
|
|
const byteString = String.fromCharCode(...array);
|
|
return btoa(byteString);
|
|
}
|
|
|
|
/**
|
|
* Calculates MD5 of the given string and returns
|
|
* the base64 encoded binary.
|
|
*/
|
|
export function md5Base64(string) {
|
|
return md5(string).toString(encBase64);
|
|
}
|
|
|
|
/**
|
|
* Calculates SHA256 of the given string and returns
|
|
* the base64 encoded binary.
|
|
*/
|
|
export function sha256Base64(string) {
|
|
return sha256(string).toString(encBase64);
|
|
}
|
|
|
|
/**
|
|
* A simple throttle version that ensures
|
|
* the given function is called at most once
|
|
* within the given time window.
|
|
*/
|
|
export function throttle(fn, windowMs) {
|
|
let ignore = false;
|
|
|
|
return (...args) => {
|
|
if (!ignore) {
|
|
fn(...args);
|
|
ignore = true;
|
|
setTimeout(() => {
|
|
ignore = false;
|
|
}, windowMs);
|
|
}
|
|
};
|
|
}
|
|
|
|
export function setFavicon(name) {
|
|
let link = document.querySelector(`[rel="icon"]`);
|
|
|
|
if (!link) {
|
|
link = document.createElement("link");
|
|
link.rel = "icon";
|
|
document.head.appendChild(link);
|
|
}
|
|
|
|
link.href = `/${name}.svg`;
|
|
}
|
|
|
|
export function findChildOrThrow(element, selector) {
|
|
const child = element.querySelector(selector);
|
|
|
|
if (!child) {
|
|
throw new Error(
|
|
`expected a child matching ${selector}, but none was found`,
|
|
);
|
|
}
|
|
|
|
return child;
|
|
}
|
|
|
|
export function cancelEvent(event) {
|
|
// Cancel any default browser behavior.
|
|
event.preventDefault();
|
|
// Stop event propagation (e.g. so it doesn't reach the editor).
|
|
event.stopPropagation();
|
|
}
|
|
|
|
const htmlEscapes = {
|
|
"&": "&",
|
|
"<": "<",
|
|
">": ">",
|
|
'"': """,
|
|
"'": "'",
|
|
};
|
|
|
|
/**
|
|
* Transforms the given string to a HTML-safe value.
|
|
*/
|
|
export function escapeHtml(string) {
|
|
return (string || "").replace(/[&<>"']/g, (char) => htmlEscapes[char]);
|
|
}
|
|
|
|
/**
|
|
* Encodes the given binary buffer into base64 string.
|
|
*/
|
|
export function bufferToBase64(buffer) {
|
|
let binaryString = "";
|
|
const bytes = new Uint8Array(buffer);
|
|
const length = bytes.byteLength;
|
|
|
|
for (let i = 0; i < length; i++) {
|
|
binaryString += String.fromCharCode(bytes[i]);
|
|
}
|
|
|
|
return btoa(binaryString);
|
|
}
|
|
|
|
/**
|
|
* Decodes a base64 string into a binary buffer.
|
|
*/
|
|
export function base64ToBuffer(base64) {
|
|
const binaryString = atob(base64);
|
|
const bytes = new Uint8Array(binaryString.length);
|
|
const length = bytes.byteLength;
|
|
|
|
for (let i = 0; i < length; i++) {
|
|
bytes[i] = binaryString.charCodeAt(i);
|
|
}
|
|
|
|
return bytes.buffer;
|
|
}
|
|
|
|
export function isFeatureFlagEnabled(feature) {
|
|
const features = document
|
|
.querySelector("body")
|
|
.getAttribute("data-feature-flags", "");
|
|
|
|
if (features.legnth === 0) {
|
|
return false;
|
|
} else {
|
|
return features.split(",").includes(feature);
|
|
}
|
|
}
|
|
|
|
export function cookieOptions() {
|
|
if (document.body.hasAttribute("data-within-iframe")) {
|
|
return ";SameSite=None;Secure";
|
|
} else {
|
|
return ";SameSite=Lax";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Removes `key` from `object` and returns the associated value and
|
|
* the updated object.
|
|
*/
|
|
export function pop(object, key, defaultValue = undefined) {
|
|
if (object.hasOwnProperty(key)) {
|
|
const { [key]: value, ...newObject } = object;
|
|
return [value, newObject];
|
|
}
|
|
|
|
return [defaultValue, object];
|
|
}
|
|
|
|
export function wait(milliseconds) {
|
|
return new Promise((resolve, reject) => {
|
|
setTimeout(() => resolve(), milliseconds);
|
|
});
|
|
}
|
|
|
|
export function isSafari() {
|
|
return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
|
}
|