Source: DOM.js

/**
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 }