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