Skip to content
This repository has been archived by the owner on Nov 17, 2021. It is now read-only.

Commit

Permalink
feat(Shared JS): Add functions for DOM manipulation
Browse files Browse the repository at this point in the history
  • Loading branch information
nokome committed Feb 19, 2020
1 parent 68d42ec commit 01d6d71
Show file tree
Hide file tree
Showing 2 changed files with 160 additions and 0 deletions.
50 changes: 50 additions & 0 deletions src/shared/js/dom.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { create, select, wrap } from './dom'

describe('create', () => {
it('works with HTML', () => {
expect(create('<div>').tagName).toEqual('DIV')

let elem = create('<span id="foo" class="bar" data-attr="baz">')
expect(elem.tagName).toEqual('SPAN')
expect(elem.id).toEqual('foo')
expect(elem.className).toEqual('bar')
expect(elem.getAttribute('data-attr')).toEqual('baz')

elem = create('<ul><li>One</li><li>Two</li></ul>')
expect(elem.tagName).toEqual('UL')
expect(elem.children.length).toEqual(2)
})

it('works with only the tag name', () => {
expect(create('div').tagName).toEqual('DIV')
expect(create('span').tagName).toEqual('SPAN')
})

it('works with only id', () => {
expect(create('#one').id).toEqual('one')
expect(create('#one').id).toEqual('one')
})

it('works with only classes', () => {
expect(create('.one').className).toEqual('one')
expect(create('.one .two').className).toEqual('one two')
})

it('works with only attributes', () => {
expect(create('[one]').getAttribute('one')).toEqual('')
expect(create('[one="a"]').getAttribute('one')).toEqual('a')
expect(create('[one="a"] [two=\'b\']').getAttributeNames()).toEqual([
'one',
'two'
])
})

it('works with a combination of selectors', () => {
let elem = create('span#foo.bar[attr="baz"]')

expect(elem.tagName).toEqual('SPAN')
expect(elem.id).toEqual('foo')
expect(elem.className).toEqual('bar')
expect(elem.getAttribute('attr')).toEqual('baz')
})
})
110 changes: 110 additions & 0 deletions src/shared/js/dom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/**
* Convenience functions for modifying the elements in the DOM.
*/

import { semanticToAttributeSelectors } from '../../selectors'

/**
* Select all DOM elements matching a CSS selector
*
* @param {string} selector The selector to match
* @param {Element} elem The element to query (defaults to the `window.document`)
* @returns {Element[]} An array of elements
*/
export function select(selector: string): Element[]
export function select(elem: Document | Element, selector: string): Element[]
export function select(...args: (string | Document | Element)[]): Element[] {
const [elem, selector] = (args.length == 1
? [document, args[0]]
: args.slice(0, 2)) as [Element, string]
return Array.from(
elem.querySelectorAll(semanticToAttributeSelectors(selector))
)
}

/**
* Create a DOM element using a fragment of HTML or a CSS selector
*
* Credit to [dom-create-element-query-selector](https://github.com/hekigan/dom-create-element-query-selector)
* for the regexes.
*
* @param {string} [spec='div'] Specification of element.
* @param {...Element[]} children Additional child elements to add
* @returns {Element}
*/
export function create(spec = 'div', ...children: Element[]): Element {
if (spec.startsWith('<')) {
const wrapper = document.createElement('div')
wrapper.innerHTML = spec
return wrapper.firstChild as Element
}

const tag = spec.match(/^[a-z0-9]+/i)?.[0] ?? 'div'
const id = spec.match(/#([a-z]+[a-z0-9-]*)/gi) ?? []
const classes = spec.match(/\.([a-z]+[a-z0-9-]*)/gi) ?? []
const attrs = spec.match(/\[([a-z][a-z-]+)(=['|"]?([^\]]*)['|"]?)?\]/gi) ?? []

const elem = document.createElement(tag)

if (id.length === 1) elem.id = id[0].slice(1)
else if (id.length > 1)
console.warn(`Got more than one id; ignoring all but first`)

if (classes.length > 0)
elem.setAttribute('class', classes.map(item => item.slice(1)).join(' '))

attrs.forEach(item => {
let [label, value] = item.slice(1, -1).split('=')
if (value !== undefined) value = value.replace(/^['"](.*)['"]$/, '$1')
elem.setAttribute(label, value || '')
})

children.forEach(item =>
elem.appendChild(
item instanceof Element ? item : document.createTextNode(`${item}`)
)
)

return elem
}

/**
* Type definition for something that can be used as a wrapper for
* HTML elements: an existing element, HTML for an element, a function
* that creates an element.
*/
type Wrapper = Element | string | ((elems: Element[]) => Element)

/**
* Wrap a DOM element in a wrapper.
*
* @param {string} within CSS selector for the elements within which wrapping happens
* @param {string} target CSS selector for the elements to be wrapped
* @param {Wrapper} wrapper The wrapper to create
*/
export function wrap(target: string, wrapper: Wrapper): void
export function wrap(within: string, target: string, wrapper: Wrapper): void
export function wrap(...args: (string | Wrapper | undefined)[]): void {
const wrapper = args.pop() ?? 'div'
const target = args.pop() as string
if (target === undefined)
throw new Error('Required argument `target` is missing')
const within = (args.pop() as string) ?? 'body'

select(document, within).forEach(parent => {
const wrapees = select(parent, target)

let wrapperElem: Element
if (wrapper instanceof Element) wrapperElem = wrapper.cloneNode() as Element
else if (typeof wrapper === 'string') wrapperElem = create(wrapper)
else if (typeof wrapper === 'function')
wrapperElem = wrapper(Array.from(wrapees))
else throw new Error(`Unhandled wrapper type: ${typeof wrapper}`)

if (wrapees.length > 0) {
const first = wrapees[0]
first?.parentNode?.insertBefore(wrapperElem, wrapees[0])
}
wrapees.forEach(wrapee => wrapperElem.appendChild(wrapee))
})
}

0 comments on commit 01d6d71

Please sign in to comment.