2 * Check if the given param is a HTMLElement
4 export function isHTMLElement(el: any): el is HTMLElement {
5 return el instanceof HTMLElement;
9 * Create a new element with the given attrs and children.
10 * Children can be a string for text nodes or other elements.
12 export function elem(tagName: string, attrs: Record<string, string> = {}, children: Element[]|string[] = []): HTMLElement {
13 const el = document.createElement(tagName);
15 for (const [key, val] of Object.entries(attrs)) {
17 el.removeAttribute(key);
19 el.setAttribute(key, val);
23 for (const child of children) {
24 if (typeof child === 'string') {
25 el.append(document.createTextNode(child));
35 * Run the given callback against each element that matches the given selector.
37 export function forEach(selector: string, callback: (el: Element) => any) {
38 const elements = document.querySelectorAll(selector);
39 for (const element of elements) {
45 * Helper to listen to multiple DOM events
47 export function onEvents(listenerElement: Element|null, events: string[], callback: (e: Event) => any): void {
48 if (listenerElement) {
49 for (const eventName of events) {
50 listenerElement.addEventListener(eventName, callback);
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.
59 export function onSelect(elements: HTMLElement|HTMLElement[], callback: (e: Event) => any): void {
60 if (!Array.isArray(elements)) {
61 elements = [elements];
64 for (const listenerElement of elements) {
65 listenerElement.addEventListener('click', callback);
66 listenerElement.addEventListener('keydown', event => {
67 if (event.key === 'Enter' || event.key === ' ') {
68 event.preventDefault();
76 * Listen to key press on the given element(s).
78 function onKeyPress(key: string, elements: HTMLElement|HTMLElement[], callback: (e: KeyboardEvent) => any): void {
79 if (!Array.isArray(elements)) {
80 elements = [elements];
83 const listener = (event: KeyboardEvent) => {
84 if (event.key === key) {
89 elements.forEach(e => e.addEventListener('keydown', listener));
93 * Listen to enter press on the given element(s).
95 export function onEnterPress(elements: HTMLElement|HTMLElement[], callback: (e: KeyboardEvent) => any): void {
96 onKeyPress('Enter', elements, callback);
100 * Listen to escape press on the given element(s).
102 export function onEscapePress(elements: HTMLElement|HTMLElement[], callback: (e: KeyboardEvent) => any): void {
103 onKeyPress('Escape', elements, callback);
107 * Set a listener on an element for an event emitted by a child
108 * matching the given childSelector param.
109 * Used in a similar fashion to jQuery's $('listener').on('eventName', 'childSelector', callback)
111 export function onChildEvent(
112 listenerElement: HTMLElement,
113 childSelector: string,
115 callback: (this: HTMLElement, e: Event, child: HTMLElement) => any
117 listenerElement.addEventListener(eventName, (event: Event) => {
118 const matchingChild = (event.target as HTMLElement|null)?.closest(childSelector) as HTMLElement;
120 callback.call(matchingChild, event, matchingChild);
126 * Look for elements that match the given selector and contain the given text.
127 * Is case-insensitive and returns the first result or null if nothing is found.
129 export function findText(selector: string, text: string): Element|null {
130 const elements = document.querySelectorAll(selector);
131 text = text.toLowerCase();
132 for (const element of elements) {
133 if ((element.textContent || '').toLowerCase().includes(text) && isHTMLElement(element)) {
141 * Show a loading indicator in the given element.
142 * This will effectively clear the element.
144 export function showLoading(element: HTMLElement): void {
145 element.innerHTML = '<div class="loading-container"><div></div><div></div><div></div></div>';
149 * Get a loading element indicator element.
151 export function getLoading(): HTMLElement {
152 const wrap = document.createElement('div');
153 wrap.classList.add('loading-container');
154 wrap.innerHTML = '<div></div><div></div><div></div>';
159 * Remove any loading indicators within the given element.
161 export function removeLoading(element: HTMLElement): void {
162 const loadingEls = element.querySelectorAll('.loading-container');
163 for (const el of loadingEls) {
169 * Convert the given html data into a live DOM element.
170 * Initiates any components defined in the data.
172 export function htmlToDom(html: string): HTMLElement {
173 const wrap = document.createElement('div');
174 wrap.innerHTML = html;
175 window.$components.init(wrap);
176 const firstChild = wrap.children[0];
177 if (!isHTMLElement(firstChild)) {
178 throw new Error('Could not find child HTMLElement when creating DOM element from HTML');
184 export function normalizeNodeTextOffsetToParent(node: Node, offset: number, parentElement: HTMLElement): number {
185 if (!parentElement.contains(node)) {
186 throw new Error('ParentElement must be a prent of element');
189 let normalizedOffset = offset;
190 let currentNode: Node|null = node.nodeType === Node.TEXT_NODE ?
191 node : node.childNodes[offset];
193 while (currentNode !== parentElement && currentNode) {
194 if (currentNode.previousSibling) {
195 currentNode = currentNode.previousSibling;
196 normalizedOffset += (currentNode.textContent?.length || 0);
198 currentNode = currentNode.parentNode;
202 return normalizedOffset;