2 * Returns a function, that, as long as it continues to be invoked, will not
3 * be triggered. The function will be called after it stops being called for
4 * N milliseconds. If `immediate` is passed, trigger the function on the
5 * leading edge, instead of the trailing.
6 * @attribution https://davidwalsh.name/javascript-debounce-function
8 export function debounce(func: Function, waitMs: number, immediate: boolean): Function {
9 let timeout: number|null = null;
10 return function debouncedWrapper(this: any, ...args: any[]) {
11 const context: any = this;
12 const later = function debouncedTimeout() {
14 if (!immediate) func.apply(context, args);
16 const callNow = immediate && !timeout;
18 clearTimeout(timeout);
20 timeout = window.setTimeout(later, waitMs);
21 if (callNow) func.apply(context, args);
25 function isDetailsElement(element: HTMLElement): element is HTMLDetailsElement {
26 return element.nodeName === 'DETAILS';
30 * Scroll-to and highlight an element.
32 export function scrollAndHighlightElement(element: HTMLElement): void {
35 // Open up parent <details> elements if within
37 while (parent.parentElement) {
38 parent = parent.parentElement;
39 if (isDetailsElement(parent) && !parent.open) {
44 element.scrollIntoView({behavior: 'smooth'});
46 const highlight = getComputedStyle(document.body).getPropertyValue('--color-link');
47 element.style.outline = `2px dashed ${highlight}`;
48 element.style.outlineOffset = '5px';
49 element.style.removeProperty('transition');
51 element.style.transition = 'outline linear 3s';
52 element.style.outline = '2px dashed rgba(0, 0, 0, 0)';
53 const listener = () => {
54 element.removeEventListener('transitionend', listener);
55 element.style.removeProperty('transition');
56 element.style.removeProperty('outline');
57 element.style.removeProperty('outlineOffset');
59 element.addEventListener('transitionend', listener);
64 * Escape any HTML in the given 'unsafe' string.
65 * Take from https://stackoverflow.com/a/6234804.
67 export function escapeHtml(unsafe: string): string {
69 .replace(/&/g, '&')
70 .replace(/</g, '<')
71 .replace(/>/g, '>')
72 .replace(/"/g, '"')
73 .replace(/'/g, ''');
77 * Generate a random unique ID.
79 export function uniqueId(): string {
80 // eslint-disable-next-line no-bitwise
81 const S4 = () => (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
82 return (`${S4() + S4()}-${S4()}-${S4()}-${S4()}-${S4()}${S4()}${S4()}`);
86 * Generate a random smaller unique ID.
88 export function uniqueIdSmall(): string {
89 // eslint-disable-next-line no-bitwise
90 const S4 = () => (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
95 * Create a promise that resolves after the given time.
97 export function wait(timeMs: number): Promise<any> {
98 return new Promise(res => {
99 setTimeout(res, timeMs);
104 * Generate a full URL from the given relative URL, using a base
105 * URL defined in the head of the page.
107 export function baseUrl(path: string): string {
108 let targetPath = path;
109 const baseUrlMeta = document.querySelector('meta[name="base-url"]');
111 throw new Error('Could not find expected base-url meta tag in document');
114 let basePath = baseUrlMeta.getAttribute('content') || '';
115 if (basePath[basePath.length - 1] === '/') {
116 basePath = basePath.slice(0, basePath.length - 1);
119 if (targetPath[0] === '/') {
120 targetPath = targetPath.slice(1);
123 return `${basePath}/${targetPath}`;
127 * Get the current version of BookStack in use.
128 * Grabs this from the version query used on app assets.
130 function getVersion(): string {
131 const styleLink = document.querySelector('link[href*="/dist/styles.css?version="]');
133 throw new Error('Could not find expected style link in document for version use');
136 const href = (styleLink.getAttribute('href') || '');
137 return href.split('?version=').pop() || '';
141 * Perform a module import, Ensuring the import is fetched with the current
142 * app version as a cache-breaker.
144 export function importVersioned(moduleName: string): Promise<object> {
145 const importPath = window.baseUrl(`dist/${moduleName}.js?version=${getVersion()}`);
146 return import(importPath);
150 cyrb53 (c) 2018 bryc (github.com/bryc)
151 License: Public domain (or MIT if needed). Attribution appreciated.
152 A fast and simple 53-bit string hash function with decent collision resistance.
153 Largely inspired by MurmurHash2/3, but with a focus on speed/simplicity.
154 Taken from: https://github.com/bryc/code/blob/master/jshash/experimental/cyrb53.js
156 export function cyrb53(str: string, seed: number = 0): string {
157 let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
158 for(let i = 0, ch; i < str.length; i++) {
159 ch = str.charCodeAt(i);
160 h1 = Math.imul(h1 ^ ch, 2654435761);
161 h2 = Math.imul(h2 ^ ch, 1597334677);
163 h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
164 h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
165 h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
166 h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
167 return String((4294967296 * (2097151 & h2) + (h1 >>> 0)));