Skip to content

Instantly share code, notes, and snippets.

@garth
Last active April 8, 2022 09:02
Show Gist options
  • Save garth/c63c45b107e323c561de5ad492dc123f to your computer and use it in GitHub Desktop.
Save garth/c63c45b107e323c561de5ad492dc123f to your computer and use it in GitHub Desktop.
Split DOM nodes

Simple DOM splitter

I couln't find a simple function to split DOM nodes by some child, so here is.

Split any node by a child maintaining all nested nodes across the split. The child node is not included in the split.

To see how it works and what it can do, have a look at the tests.

import { expect, describe, it } from 'vitest'
import { splitDOM } from './splitDOM'
describe('slitDOM', () => {
it('splits a paragraph in two', () => {
const doc = new DOMParser().parseFromString('<p>a<br/>b</p>', 'text/html')
const p = doc.querySelector('p') as HTMLElement
const br = doc.querySelector('br') as HTMLElement
expect(p).toBeDefined()
expect(br).toBeDefined()
const [a, b] = splitDOM(p, br)
expect(a.outerHTML).toEqual('<p>a</p>')
expect(b.outerHTML).toEqual('<p>b</p>')
})
it('splits a paragraph with marks in two', () => {
const doc = new DOMParser().parseFromString('<p><strong><em>a</em><br/>b</strong></p>', 'text/html')
const p = doc.querySelector('p') as HTMLElement
const br = doc.querySelector('br') as HTMLElement
expect(p).toBeDefined()
expect(br).toBeDefined()
const [a, b] = splitDOM(p, br)
expect(a.outerHTML).toEqual('<p><strong><em>a</em></strong></p>')
expect(b.outerHTML).toEqual('<p><strong>b</strong></p>')
})
it('maintains attributes when it splits', () => {
const doc = new DOMParser().parseFromString('<p class="big" style="color:red;">a<br/>b</p>', 'text/html')
const p = doc.querySelector('p') as HTMLElement
const br = doc.querySelector('br') as HTMLElement
expect(p).toBeDefined()
expect(br).toBeDefined()
const [a, b] = splitDOM(p, br)
expect(a.outerHTML).toEqual('<p class="big" style="color:red;">a</p>')
expect(b.outerHTML).toEqual('<p class="big" style="color:red;">b</p>')
})
})
const splitter = (chain: HTMLElement[]) => {
const baseNode = chain[0]
const a = chain[0].cloneNode(false) as HTMLElement
const b = chain[0].cloneNode(false) as HTMLElement
let currentNode = a
for (let i = 0; i < baseNode.childNodes.length; i++) {
const node = baseNode.childNodes[i]
if (chain[1] === node) {
if (chain.length > 2) {
const [childA, childB] = splitter(chain.slice(1))
a.appendChild(childA)
b.appendChild(childB)
}
currentNode = b
} else {
currentNode.appendChild(node.cloneNode(true))
}
}
return [a, b]
}
export const splitDOM = (rootNode: HTMLElement, splitNode: HTMLElement) => {
const chain: HTMLElement[] = [splitNode]
while (chain[0] !== rootNode) {
if (!chain[0].parentElement) {
throw new Error('rootNode is not a parent of splitNode')
}
chain.unshift(chain[0].parentElement)
}
if (chain.length < 2) {
throw new Error('rootNode is not a parent of splitNode')
}
return splitter(chain)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment