UNPKG

@transmutable/bink

Version:

A sleek and reactive framework for web pages

324 lines (300 loc) 7.56 kB
/** An object that provides helper functions that generate [HTMLElements](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement), usually for populating {@link Component.dom}. @example dom.li() // returns an `li` HTMLElement: `<li></li>` dom.div() // returns a 'div' HTMLElement: '<div></div>' dom.span(lt('Some text')) // `<span>Some text</span>` @example <caption>Text and dictionary parameters do handy things</caption> dom.a(lt('Click me'), { href: '/some-url/' }) // `<a href="/some-url/">Click me</a>` @example <caption>Any parameter can be a string, a dictionary, or another element</caption> * dom.button( * { type: 'button', class: 'my-button' }, * dom.img({ src: 'image.jpg' }), * lt('Click me') * ) * // `<button type="button" class="my-button"><img src="image.jpg" />Click me</button>` * * dom.ul( * dom.li(lt('First item')), * dom.li(lt('Second item'), { class: 'selected-item' }) // Attribute dicts can be in any parameter * ) * // <ul> * // <li>First Item</li> * // <li class: "selected-item">Second item</li> * // </ul> @example <caption>Populate a Component's UI</caption> * class MyComponent extends Component { * constructor(dataObject, options) { * super(dataObject, options) * this.dom.appendChild(dom.h1(lt('This is a heading'))) * } * } */ const dom = {} dom.enhanceElement = function (element) { // A convenience function to allow chaining like `let fooDiv = dom.div().appendTo(document.body)` element.appendTo = function (parent) { parent.appendChild(this) return this } // if element.parentElement exists, call removeChild(element) on it element.remove = function () { if (this.parentElement) { this.parentElement.removeChild(this) } return this } // A convenience function to allow appending strings, dictionaries of attributes, arrays of subchildren, or children element.append = function (child = null) { if (child === null) { return } if (typeof child === 'string') { this.appendChild(document.createTextNode(child)) } else if (Array.isArray(child)) { for (const subChild of child) { this.append(subChild) } // If it's an object but not a DOM element, consider it a dictionary of attributes } else if (typeof child === 'object' && typeof child.nodeType === 'undefined') { for (const key in child) { if (child.hasOwnProperty(key) == false) { continue } this.setAttribute(key, child[key]) } } else { this.appendChild(child) } return this } element.documentPosition = function () { return dom.documentOffset(this) } /* Sort element.children *in place* using the comparator function See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort for an explanation of the comparator function */ element.sort = function (comparator = dom.defaultComparator) { // Populate the holding array while removing children from the DOM const holdingArray = [] while (this.children.length > 0) { holdingArray.push(this.removeChild(this.children.item(0))) } holdingArray.sort(comparator) for (const child of holdingArray) { this.appendChild(child) } return this } // Sort element.children *in place* using child[attributeName] and the comparator function element.sortByAttribute = function (attributeName, comparator = dom.defaultComparator) { this.sort((el1, el2) => { return comparator(el1.getAttribute(attributeName), el2.getAttribute(attributeName)) }) return this } // Convenience functions to add and remove classes from this element without duplication element.addClass = function (...classNames) { const classAttribute = this.getAttribute('class') || '' const classes = classAttribute === '' ? [] : classAttribute.split(/\s+/) for (const className of classNames) { if (classes.indexOf(className) === -1) { classes.push(className) } } this.setAttribute('class', classes.join(' ')) return this } element.removeClass = function (...classNames) { const classAttribute = this.getAttribute('class') || '' const classes = classAttribute === '' ? [] : classAttribute.split(/\s+/) for (const className of classNames) { const index = classes.indexOf(className) if (index !== -1) { classes.splice(index, 1) } } if (classes.length === 0) { this.removeAttribute('class') } else { this.setAttribute('class', classes.join(' ')) } return this } return element } /** domElementFunction is the behind the scenes logic for the functions like dom.div(...) Below you will find the loop that uses domElementFunction */ dom.domElementFunction = function (tagName, ...params) { const element = dom.enhanceElement(document.createElement(tagName)) for (const child of params) { element.append(child) } return element } // This comparator stringifies the passed values and returns the comparison of those values dom.defaultComparator = function (el1, el2) { if (el1 === el2) return 0 const str1 = '' + el1 const str2 = '' + el2 if (str1 == str2) return 0 if (str1 < str2) return -1 return 1 } // Traverse the document tree to calculate the offset in the entire document of this element dom.documentOffset = function (element) { let left = 0 let top = 0 const findPos = function (obj) { left += obj.offsetLeft top += obj.offsetTop if (obj.offsetParent) { findPos(obj.offsetParent) } } findPos(element) return [left, top] } /** The tag names that will be used to generate all of the element generating functions like dom.div(...) and dom.button(...) These names were ovingly copied from the excellent Laconic.js @see https://github.com/joestelmach/laconic/blob/master/laconic.js */ dom.TAGS = [ 'a', 'abbr', 'address', 'area', 'article', 'aside', 'audio', 'b', 'base', 'bdo', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'cite', 'code', 'col', 'colgroup', 'command', 'datalist', 'dd', 'del', 'details', 'dfn', 'div', 'dl', 'dt', 'em', 'embed', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'iframe', 'img', 'input', 'ins', 'keygen', 'kbd', 'label', 'legend', 'li', 'link', 'main', 'map', 'mark', 'menu', 'meta', 'meter', 'nav', 'noscript', 'object', 'ol', 'optgroup', 'option', 'output', 'p', 'picture', 'param', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'script', 'section', 'select', 'small', 'source', 'span', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'time', 'title', 'tr', 'ul', 'var', 'video', 'wbr', ] // This loop generates the element generating functions like dom.div(...) for (const tag of dom.TAGS) { const innerTag = tag dom[innerTag] = function (...params) { return dom.domElementFunction(innerTag, ...params) } } const CookieValueRegularExpressionParts = ['(?:(?:^|.*;\\s*)', '\\s*\\=\\s*([^;]*).*$)|^.*$'] dom.getCookie = function (cookieName) { const cookieRegExp = new RegExp( `${CookieValueRegularExpressionParts[0]}${encodeURIComponent(cookieName)}${CookieValueRegularExpressionParts[1]}` ) return document.cookie.replace(cookieRegExp, '$1') } dom.setCookie = function (cookieName, value) { document.cookie = `${encodeURIComponent(cookieName)}=${encodeURIComponent(value)}` } dom.removeCookie = function (cookieName) { document.cookie = `${encodeURIComponent(cookieName)}=; expires=Thu, 01 Jan 1970 00:00:00 GMT` } export default dom export { dom }