]> BookStack Code Mirror - bookstack/blob - resources/js/services/util.js
Page JS: Improved block jumping and highlighting
[bookstack] / resources / js / services / util.js
1 import {elem} from './dom';
2
3 /**
4  * Returns a function, that, as long as it continues to be invoked, will not
5  * be triggered. The function will be called after it stops being called for
6  * N milliseconds. If `immediate` is passed, trigger the function on the
7  * leading edge, instead of the trailing.
8  * @attribution https://davidwalsh.name/javascript-debounce-function
9  * @param {Function} func
10  * @param {Number} waitMs
11  * @param {Boolean} immediate
12  * @returns {Function}
13  */
14 export function debounce(func, waitMs, immediate) {
15     let timeout;
16     return function debouncedWrapper(...args) {
17         const context = this;
18         const later = function debouncedTimeout() {
19             timeout = null;
20             if (!immediate) func.apply(context, args);
21         };
22         const callNow = immediate && !timeout;
23         clearTimeout(timeout);
24         timeout = setTimeout(later, waitMs);
25         if (callNow) func.apply(context, args);
26     };
27 }
28
29 /**
30  * Scroll and highlight an element.
31  * @param {HTMLElement} element
32  */
33 export function scrollAndHighlightElement(element) {
34     if (!element) return;
35
36     const parentDetails = element.closest('details');
37     if (parentDetails && !parentDetails.open) {
38         parentDetails.open = true;
39     }
40
41     element.scrollIntoView({behavior: 'smooth'});
42
43     const highlight = getComputedStyle(document.body).getPropertyValue('--color-link');
44     element.style.outline = `2px dashed ${highlight}`;
45     element.style.outlineOffset = '5px';
46     element.style.transition = null;
47     setTimeout(() => {
48         element.style.transition = 'outline linear 3s';
49         element.style.outline = '2px dashed rgba(0, 0, 0, 0)';
50         const listener = () => {
51             element.removeEventListener('transitionend', listener);
52             element.style.transition = null;
53             element.style.outline = null;
54             element.style.outlineOffset = null;
55         };
56         element.addEventListener('transitionend', listener);
57     }, 1000);
58 }
59
60 /**
61  * Escape any HTML in the given 'unsafe' string.
62  * Take from https://stackoverflow.com/a/6234804.
63  * @param {String} unsafe
64  * @returns {string}
65  */
66 export function escapeHtml(unsafe) {
67     return unsafe
68         .replace(/&/g, '&')
69         .replace(/</g, '&lt;')
70         .replace(/>/g, '&gt;')
71         .replace(/"/g, '&quot;')
72         .replace(/'/g, '&#039;');
73 }
74
75 /**
76  * Generate a random unique ID.
77  *
78  * @returns {string}
79  */
80 export function uniqueId() {
81     // eslint-disable-next-line no-bitwise
82     const S4 = () => (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
83     return (`${S4() + S4()}-${S4()}-${S4()}-${S4()}-${S4()}${S4()}${S4()}`);
84 }
85
86 /**
87  * Create a promise that resolves after the given time.
88  * @param {int} timeMs
89  * @returns {Promise}
90  */
91 export function wait(timeMs) {
92     return new Promise(res => {
93         setTimeout(res, timeMs);
94     });
95 }