]> BookStack Code Mirror - bookstack/blob - resources/js/services/dom.ts
c88827bac40a1788b89295152799b62e9871425f
[bookstack] / resources / js / services / dom.ts
1 /**
2  * Check if the given param is a HTMLElement
3  */
4 export function isHTMLElement(el: any): el is HTMLElement {
5     return el instanceof HTMLElement;
6 }
7
8 /**
9  * Create a new element with the given attrs and children.
10  * Children can be a string for text nodes or other elements.
11  */
12 export function elem(tagName: string, attrs: Record<string, string> = {}, children: Element[]|string[] = []): HTMLElement {
13     const el = document.createElement(tagName);
14
15     for (const [key, val] of Object.entries(attrs)) {
16         if (val === null) {
17             el.removeAttribute(key);
18         } else {
19             el.setAttribute(key, val);
20         }
21     }
22
23     for (const child of children) {
24         if (typeof child === 'string') {
25             el.append(document.createTextNode(child));
26         } else {
27             el.append(child);
28         }
29     }
30
31     return el;
32 }
33
34 /**
35  * Run the given callback against each element that matches the given selector.
36  */
37 export function forEach(selector: string, callback: (el: Element) => any) {
38     const elements = document.querySelectorAll(selector);
39     for (const element of elements) {
40         callback(element);
41     }
42 }
43
44 /**
45  * Helper to listen to multiple DOM events
46  */
47 export function onEvents(listenerElement: Element, events: string[], callback: (e: Event) => any): void {
48     for (const eventName of events) {
49         listenerElement.addEventListener(eventName, callback);
50     }
51 }
52
53 /**
54  * Helper to run an action when an element is selected.
55  * A "select" is made to be accessible, So can be a click, space-press or enter-press.
56  */
57 export function onSelect(elements: HTMLElement|HTMLElement[], callback: (e: Event) => any): void {
58     if (!Array.isArray(elements)) {
59         elements = [elements];
60     }
61
62     for (const listenerElement of elements) {
63         listenerElement.addEventListener('click', callback);
64         listenerElement.addEventListener('keydown', event => {
65             if (event.key === 'Enter' || event.key === ' ') {
66                 event.preventDefault();
67                 callback(event);
68             }
69         });
70     }
71 }
72
73 /**
74  * Listen to key press on the given element(s).
75  */
76 function onKeyPress(key: string, elements: HTMLElement|HTMLElement[], callback: (e: KeyboardEvent) => any): void {
77     if (!Array.isArray(elements)) {
78         elements = [elements];
79     }
80
81     const listener = (event: KeyboardEvent) => {
82         if (event.key === key) {
83             callback(event);
84         }
85     };
86
87     elements.forEach(e => e.addEventListener('keydown', listener));
88 }
89
90 /**
91  * Listen to enter press on the given element(s).
92  */
93 export function onEnterPress(elements: HTMLElement|HTMLElement[], callback: (e: KeyboardEvent) => any): void {
94     onKeyPress('Enter', elements, callback);
95 }
96
97 /**
98  * Listen to escape press on the given element(s).
99  */
100 export function onEscapePress(elements: HTMLElement|HTMLElement[], callback: (e: KeyboardEvent) => any): void {
101     onKeyPress('Escape', elements, callback);
102 }
103
104 /**
105  * Set a listener on an element for an event emitted by a child
106  * matching the given childSelector param.
107  * Used in a similar fashion to jQuery's $('listener').on('eventName', 'childSelector', callback)
108  */
109 export function onChildEvent(
110     listenerElement: HTMLElement,
111     childSelector: string,
112     eventName: string,
113     callback: (this: HTMLElement, e: Event, child: HTMLElement) => any
114 ): void {
115     listenerElement.addEventListener(eventName, (event: Event) => {
116         const matchingChild = (event.target as HTMLElement|null)?.closest(childSelector) as HTMLElement;
117         if (matchingChild) {
118             callback.call(matchingChild, event, matchingChild);
119         }
120     });
121 }
122
123 /**
124  * Look for elements that match the given selector and contain the given text.
125  * Is case-insensitive and returns the first result or null if nothing is found.
126  */
127 export function findText(selector: string, text: string): Element|null {
128     const elements = document.querySelectorAll(selector);
129     text = text.toLowerCase();
130     for (const element of elements) {
131         if ((element.textContent || '').toLowerCase().includes(text) && isHTMLElement(element)) {
132             return element;
133         }
134     }
135     return null;
136 }
137
138 /**
139  * Show a loading indicator in the given element.
140  * This will effectively clear the element.
141  */
142 export function showLoading(element: HTMLElement): void {
143     element.innerHTML = '<div class="loading-container"><div></div><div></div><div></div></div>';
144 }
145
146 /**
147  * Get a loading element indicator element.
148  */
149 export function getLoading(): HTMLElement {
150     const wrap = document.createElement('div');
151     wrap.classList.add('loading-container');
152     wrap.innerHTML = '<div></div><div></div><div></div>';
153     return wrap;
154 }
155
156 /**
157  * Remove any loading indicators within the given element.
158  */
159 export function removeLoading(element: HTMLElement): void {
160     const loadingEls = element.querySelectorAll('.loading-container');
161     for (const el of loadingEls) {
162         el.remove();
163     }
164 }
165
166 /**
167  * Convert the given html data into a live DOM element.
168  * Initiates any components defined in the data.
169  */
170 export function htmlToDom(html: string): HTMLElement {
171     const wrap = document.createElement('div');
172     wrap.innerHTML = html;
173     window.$components.init(wrap);
174     const firstChild = wrap.children[0];
175     if (!isHTMLElement(firstChild)) {
176         throw new Error('Could not find child HTMLElement when creating DOM element from HTML');
177     }
178
179     return firstChild;
180 }