]> BookStack Code Mirror - bookstack/blob - resources/js/services/dom.js
Merge pull request #4193 from BookStackApp/custom_dropzone
[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         el.setAttribute(key, val);
14     }
15
16     for (const child of children) {
17         if (typeof child === 'string') {
18             el.append(document.createTextNode(child));
19         } else {
20             el.append(child);
21         }
22     }
23
24     return el;
25 }
26
27 /**
28  * Run the given callback against each element that matches the given selector.
29  * @param {String} selector
30  * @param {Function<Element>} callback
31  */
32 export function forEach(selector, callback) {
33     const elements = document.querySelectorAll(selector);
34     for (const element of elements) {
35         callback(element);
36     }
37 }
38
39 /**
40  * Helper to listen to multiple DOM events
41  * @param {Element} listenerElement
42  * @param {Array<String>} events
43  * @param {Function<Event>} callback
44  */
45 export function onEvents(listenerElement, events, callback) {
46     for (const eventName of events) {
47         listenerElement.addEventListener(eventName, callback);
48     }
49 }
50
51 /**
52  * Helper to run an action when an element is selected.
53  * A "select" is made to be accessible, So can be a click, space-press or enter-press.
54  * @param {HTMLElement|Array} elements
55  * @param {function} callback
56  */
57 export function onSelect(elements, callback) {
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 enter press on the given element(s).
75  * @param {HTMLElement|Array} elements
76  * @param {function} callback
77  */
78 export function onEnterPress(elements, callback) {
79     if (!Array.isArray(elements)) {
80         elements = [elements];
81     }
82
83     const listener = event => {
84         if (event.key === 'Enter') {
85             callback(event);
86         }
87     };
88
89     elements.forEach(e => e.addEventListener('keypress', listener));
90 }
91
92 /**
93  * Set a listener on an element for an event emitted by a child
94  * matching the given childSelector param.
95  * Used in a similar fashion to jQuery's $('listener').on('eventName', 'childSelector', callback)
96  * @param {Element} listenerElement
97  * @param {String} childSelector
98  * @param {String} eventName
99  * @param {Function} callback
100  */
101 export function onChildEvent(listenerElement, childSelector, eventName, callback) {
102     listenerElement.addEventListener(eventName, event => {
103         const matchingChild = event.target.closest(childSelector);
104         if (matchingChild) {
105             callback.call(matchingChild, event, matchingChild);
106         }
107     });
108 }
109
110 /**
111  * Look for elements that match the given selector and contain the given text.
112  * Is case insensitive and returns the first result or null if nothing is found.
113  * @param {String} selector
114  * @param {String} text
115  * @returns {Element}
116  */
117 export function findText(selector, text) {
118     const elements = document.querySelectorAll(selector);
119     text = text.toLowerCase();
120     for (const element of elements) {
121         if (element.textContent.toLowerCase().includes(text)) {
122             return element;
123         }
124     }
125     return null;
126 }
127
128 /**
129  * Show a loading indicator in the given element.
130  * This will effectively clear the element.
131  * @param {Element} element
132  */
133 export function showLoading(element) {
134     element.innerHTML = '<div class="loading-container"><div></div><div></div><div></div></div>';
135 }
136
137 /**
138  * Get a loading element indicator element.
139  * @returns {Element}
140  */
141 export function getLoading() {
142     const wrap = document.createElement('div');
143     wrap.classList.add('loading-container');
144     wrap.innerHTML = '<div></div><div></div><div></div>';
145     return wrap;
146 }
147
148 /**
149  * Remove any loading indicators within the given element.
150  * @param {Element} element
151  */
152 export function removeLoading(element) {
153     const loadingEls = element.querySelectorAll('.loading-container');
154     for (const el of loadingEls) {
155         el.remove();
156     }
157 }
158
159 /**
160  * Convert the given html data into a live DOM element.
161  * Initiates any components defined in the data.
162  * @param {String} html
163  * @returns {Element}
164  */
165 export function htmlToDom(html) {
166     const wrap = document.createElement('div');
167     wrap.innerHTML = html;
168     window.$components.init(wrap);
169     return wrap.children[0];
170 }