UNPKG

@scidian/osui

Version:

Lightweight JavaScript UI library.

485 lines (397 loc) 14.6 kB
/** Base Class of Osui, the Onsight Gui Library */ class Element { constructor(dom) { const self = this; this.isElement = true; this.dom = dom; // HTML Element this.name = undefined; // Object Name this.contents = function() { return self; }; // Inner Osui Element to be filled with other elements this.children = []; // Holds Osui Children (.add / .remove / .clearContents) this.parent = undefined; } /********** DESTROY **********/ /** Removes all children DOM elements from this element */ destroy() { clearChildren(this, true /* destroy event */); return this; } /********** CHILDREN **********/ /** Adds to contents() any number of Osui Elements passed as arguments */ add(/* any number of Elements to remove */) { for (let i = 0; i < arguments.length; i++) { const element = arguments[i]; addToParent(this.contents(), element); } return this; } addToSelf(/* any number of Elements to remove */) { for (let i = 0; i < arguments.length; i++) { const element = arguments[i]; addToParent(this, element); } return this; } /** Removes all children DOM elements from element's 'contents' only */ clearContents() { clearChildren(this.contents(), false /* destroy event */); return this; } /** Removes any number of Elements or Dom Nodes passed as arguments from contents() or self children */ remove(/* any number of Elements to remove */) { for (let i = 0; i < arguments.length; i++) { const element = arguments[i]; // Attempt to remove element from contents(), then try to remove from self.dom let removed = removeFromParent(this.contents(), element); if (!removed) removed = removeFromParent(this, element); if (!removed) { // // DEBUG: Could not remove element(s) // console.log(`Element.removeFromParent: Could not remove child!`); } } return this; } /********** CLASS / ID / NAME **********/ setClass(className) { this.dom.className = className; return this; } addClass(/* any number of comma seperated classes to add */) { for (let i = 0; i < arguments.length; i ++) { const argument = arguments[i]; this.dom.classList.add(argument); } return this; } hasClass(className) { return this.dom.classList.contains(className); } hasClassWithString(substring) { substring = String(substring).toLowerCase(); const classArray = [...this.dom.classList] for (let i = 0; i < classArray.length; i++) { const className = classArray[i]; if (className.toLowerCase().includes(substring)) return true; } return false; } removeClass(/* any number of comma seperated classes to remove */) { for (let i = 0; i < arguments.length; i ++) { const argument = arguments[i]; this.dom.classList.remove(argument); } return this; } setId(id) { this.dom.id = id; if (this.name === undefined) this.name = id; return this; } getId() { return this.dom.id; } setName(name) { this.name = name; return this; } getName() { return (this.name === undefined) ? 'No name' : this.name; } /********** HTML **********/ setAttribute(attrib, value) { this.dom.setAttribute(attrib, value); } setDisabled(value = true) { if (value) { this.addClass('osui-disabled'); } else { this.removeClass('osui-disabled'); } this.dom.disabled = value; return this; } /** Makes this Element Selectable / Unselectable */ selectable(allowSelection) { if (allowSelection) { this.removeClass('osui-unselectable'); } else { this.addClass('osui-unselectable'); } return this; } hide(event = true) { this.setStyle('display', 'none'); if (event) this.dom.dispatchEvent(new Event('hidden')); } display(event = true) { this.setStyle('display', ''); if (event) this.dom.dispatchEvent(new Event('displayed')); } isDisplayed() { return getComputedStyle(this.dom).display != 'none'; } isHidden() { return getComputedStyle(this.dom).display == 'none'; } /** Enable user focus */ allowFocus() { // https://developer.mozilla.org/en-US/docs/Web/Accessibility/Keyboard-navigable_JavaScript_widgets#using_tabindex // - turns on focusin / focusout events // - keyup event doesn't work without setting tabIndex this.dom.tabIndex = 0; // // OR // this.dom.setAttribute('tabindex', '0'); } focus() { this.dom.focus(); } blur() { this.dom.blur(); } // // WARNING: Setting any of the following will delete children! // // SEE: https://kellegous.com/j/2013/02/27/innertext-vs-textcontent/ // Order of content, from least to most: // textContent: All text contained by an element and all its children // innerText: All text contained by an element and all its children, affected by 'style' // innerHtml: All text, including html tags, that is contained by an element /** The textContent property represents the text content of the node and its descendants */ setTextContent(value) { if (value != undefined) this.contents().dom.textContent = value; return this; } getTextContent() { return this.contents().dom.textContent; } /** The innerHTML returns all text, including html tags, that is contained by an element */ setInnerHtml(value) { if (value === undefined || value === null) value = ''; // NOTE: Attempt to sanitize html // https://developer.mozilla.org/en-US/docs/Web/API/Element/setHTML# // https://github.com/WICG/sanitizer-api if (typeof this.contents().dom.setHTML === 'function') { this.contents().dom.setHTML(value); } else { this.contents().dom.innerHTML = value; } return this; } getInnerHtml() { return this.contents().dom.innerHTML; } /********** CSS **********/ setStyle(/* style, value, style, value, etc. */) { /***** ALL AT ONCE */ // const styles = {}; // let changed = false; // // Parse existing inline style string to object // let styleText = this.dom.getAttribute('style'); // let regex = /\s*([a-z\-]+)\s*:\s*((?:[^;]*url\(.*?\)[^;]*|[^;]*)*)\s*(?:;|$)/gi; // let match; // while (match = regex.exec(styleText)) styles[match[1]] = match[2].trim(); // // Update new values // for (let i = 0, l = arguments.length; i < l; i += 2) { // const style = arguments[i + 0].replace(/[A-Z]/g, match => `-${match.toLowerCase()}`); // const value = arguments[i + 1]; // if (styles[style] !== value) { // styles[style] = value; // changed = true; // } // } // if (!changed) return this; // // Rebuild style string // styleText = Object.entries(styles).map(([k, v]) => `${k}: ${v}`).join('; '); // this.dom.setAttribute('style', styleText); /***** ORIGINAL */ for (let i = 0, l = arguments.length; i < l; i += 2) { const style = arguments[i]; const value = arguments[i + 1]; this.dom.style[style] = value; } return this; } setContentsStyle(/* style, value, style, value, etc. */) { for (let i = 0, l = arguments.length; i < l; i += 2) { const style = arguments[i]; const value = arguments[i + 1]; this.contents().dom.style[style] = value; } return this; } /********** DOM **********/ getLeft() { // return this.dom.left; return this.dom.getBoundingClientRect().left; } getTop() { // return this.dom.top; return this.dom.getBoundingClientRect().top; } getWidth() { // return this.dom.clientWidth; // <-- does not include margin / border return this.dom.getBoundingClientRect().width; } getHeight() { // return this.dom.clientHeight; // <-- does not include margin / border return this.dom.getBoundingClientRect().height; } /********** TRAVERSE **********/ /** Applies a callback function to all Element children, recursively */ traverse(callback, applyToSelf = true) { if (applyToSelf) callback(this); if (this.children) { for (let i = 0; i < this.children.length; i++) { this.children[i].traverse(callback, true); } } } } export { Element }; /******************** ADD / REMOVE / CLEAR ********************/ function addToParent(parent, element) { if (!element) return; // Osui Element if (element.isElement) { // Add node parent.dom.appendChild(element.dom); // Add to child array if not already there let hasIt = false; for (let i = 0; i < parent.children.length; i++) { const child = parent.children[i]; if (child.dom.isSameNode(element.dom)) { hasIt = true; break; } } if (!hasIt) parent.children.push(element); // Set element parent element.parent = parent; // Html Node? } else { try { parent.dom.appendChild(element); } catch (error) { // REMOVE FAILED } } } // Clears Element Children function clearElementChildren(osui) { for (let i = 0; i < osui.children.length; i++) { const child = osui.children[i]; clearChildren(child, true /* destroy event */); } osui.children.length = 0; } // Clears Dom Element Children function clearDomChildren(dom) { if (!dom.children) return; for (let i = dom.children.length - 1; i >= 0; i--) { const child = dom.children[i]; clearChildren(child, true /* destroy event */); try { dom.removeChild(child); } catch (error) { /* FAILED TO REMOVE */ } } } /* Clears all osui children / dom children from element */ function clearChildren(element, destroy = true) { if (!element) return; // Osui Element if (element.isElement) { clearElementChildren(element); clearDomChildren(element.dom); // 'destroy' event if (destroy && element.dom && element.dom.dispatchEvent) { element.dom.dispatchEvent(new Event('destroy')); } // Html Node? } else { clearDomChildren(element); // 'destroy' event if (destroy && element && element.dispatchEvent) { element.dispatchEvent(new Event('destroy')); } } } /** Returns true if element was removed */ function removeFromParent(parent, element) { if (!parent) return; if (!element) return; // Osui Element if (element.isElement && parent.isElement) { for (let i = 0; i < parent.children.length; i++) { const child = parent.children[i]; if (child.dom.isSameNode(element.dom)) { parent.children.splice(i, 1); element.parent = undefined; } } } // Clear Children clearChildren(element); // Remove from Parent try { if (parent.isElement) { parent.dom.removeChild((element.isElement) ? element.dom : element); } else { parent.removeChild((element.isElement) ? element.dom : element); } return true; } catch (error) { return false; /* REMOVE FAILED */ } } /******************** PROPERTIES ********************/ // Hyphenated style properties can be referenced via camelCase in JavaScript // See: http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSS2Properties const properties = [ 'display', 'color', 'left', 'top', 'right', 'bottom', 'width', 'height', ]; properties.forEach(function(property) { const method = 'set' + property.substring(0, 1).toUpperCase() + property.substring(1, property.length); Element.prototype[method] = function(value) { this.setStyle(property, value); return this; }; }); Object.defineProperties(Element.prototype, { id: { get: function() { return this.getId(); }, set: function(value) { this.setId(value); } }, }); /******************** EVENTS ********************/ const events = [ 'Focus', 'Blur', 'Change', 'Input', 'Wheel', 'KeyUp', 'KeyDown', 'Click', 'DblClick', 'ContextMenu', 'PointerDown', 'PointerMove', 'PointerUp', 'PointerEnter', 'PointerLeave', 'PointerOut', 'PointerOver', 'PointerCancel', ]; events.forEach(function(event) { const method = 'on' + event; Element.prototype[method] = function(callback) { const eventName = event.toLowerCase(); if (typeof callback !== 'function') { console.warn(`${method} in ${this.name}: No callback function provided!`); return this; } const eventHandler = callback.bind(this); const dom = this.dom; dom.addEventListener(eventName, eventHandler); dom.addEventListener('destroy', () => dom.removeEventListener(eventName, eventHandler), { once: true }); return this; }; }); /******************** REFERENCE ********************/ // 'blur' Fires when element has lost focus (does not bubble, 'focusout' follows and does bubble) // 'focus' Fires when element has received focus (does not bubble, 'focusin' follows and does bubble) // 'input' Fires constantly as <input> <select> <textarea> value's are being changed. // 'change' Fires when <input> <select> <textarea> value's are done being modified. // 'contextmenu' Fires when user attempts to open context menu (typically right clicking mouse) // 'dragstart', 'dragend' // 'dragenter', 'dragover', 'dragleave' // 'drop'