├── README.md ├── esm ├── document-persistent-fragment.js ├── element.js ├── index.js ├── node.js ├── poly.js └── utils.js ├── index.html ├── package.json └── test.js /README.md: -------------------------------------------------------------------------------- 1 | # DocumentPersistentFragment 2 | 3 | - - - 4 | 5 | ## ⚠️ Deprecated 6 | 7 | Current [discussion](https://github.com/whatwg/dom/issues/736#issuecomment-2764636514) is pivoting toward this [GroupNodes](https://github.com/WebReflection/group-nodes#readme) variant/alternative which looks and feels way easier to reason about and implement, it's just a "*special fragment*" weakly bound to its surrounding comments. 8 | 9 | - - - 10 | 11 | Currently a polyfill for the [WHATWG proposal](https://github.com/whatwg/dom/issues/736). 12 | 13 | [Live test](https://webreflection.github.io/document-persistent-fragment/) with 100% code coverage and a global `dpf` usable at the end of the test. 14 | 15 | ### Warning 16 | 17 | This is most likely going to change, don't use this in prod. 18 | -------------------------------------------------------------------------------- /esm/document-persistent-fragment.js: -------------------------------------------------------------------------------- 1 | import poly from './poly.js'; 2 | 3 | const {freeze, setPrototypeOf} = Object; 4 | const childNodes = new WeakMap; 5 | const getText = node => node.textContent; 6 | const isElement = node => node instanceof Element; 7 | const isLive = node => node.isConnected; 8 | const isVisible = node => { 9 | switch (node.nodeType) { 10 | case Node.ELEMENT_NODE: 11 | case Node.TEXT_NODE: 12 | case Node.DOCUMENT_FRAGMENT_NODE: 13 | case Node.DOCUMENT_PERSISTENT_FRAGMENT_NODE: 14 | return true; 15 | } 16 | return false; 17 | }; 18 | 19 | class Bug extends DocumentFragment {} 20 | const shenanigans = !(new Bug instanceof Bug); 21 | 22 | export default class DocumentPersistentFragment extends DocumentFragment { 23 | 24 | // DocumentFragment overrides 25 | constructor() { 26 | super(); 27 | childNodes.set(this, []); 28 | if (shenanigans) 29 | return setPrototypeOf(this, DocumentPersistentFragment.prototype); 30 | } 31 | get children() { 32 | return childNodes.get(this).filter(isElement); 33 | } 34 | get firstElementChild() { 35 | const {children} = this; 36 | const {length} = children; 37 | return length < 1 ? null : children[0]; 38 | } 39 | get lastElementChild() { 40 | const {children} = this; 41 | const {length} = children; 42 | return length < 1 ? null : children[length - 1]; 43 | } 44 | get childElementCount() { 45 | return this.children.length; 46 | } 47 | prepend(...nodes) { 48 | nodes.forEach(removeChild, this); 49 | childNodes.get(this).unshift(...nodes); 50 | return super.prepend(...nodes); 51 | } 52 | append(...nodes) { 53 | nodes.forEach(appendChild, this); 54 | return super.append(...nodes); 55 | } 56 | getElementById(id) { 57 | return this.querySelector(`#${id}`); 58 | } 59 | querySelector(css) { 60 | return this.isConnected ? 61 | this.parentNode.querySelector(css) : 62 | super.querySelector(css); 63 | } 64 | querySelectorAll(css) { 65 | return this.isConnected ? 66 | this.parentNode.querySelectorAll(css) : 67 | super.querySelectorAll(css); 68 | } 69 | 70 | // Node overrides 71 | get nodeType() { 72 | return Node.DOCUMENT_PERSISTENT_FRAGMENT_NODE; 73 | } 74 | get nodeName() { 75 | return "#document-persistent-fragment"; 76 | } 77 | get isConnected() { 78 | return childNodes.get(this).some(isLive); 79 | } 80 | get parentNode() { 81 | const node = childNodes.get(this).find(isLive); 82 | return node.parentNode; 83 | } 84 | get parentElement() { 85 | return this.parentNode; 86 | } 87 | get childNodes() { 88 | return freeze(childNodes.get(this).slice(0)); 89 | } 90 | get firstChild() { 91 | const nodes = childNodes.get(this); 92 | const {length} = nodes; 93 | return length < 1 ? null : nodes[0]; 94 | } 95 | get lastChild() { 96 | const nodes = childNodes.get(this); 97 | const {length} = nodes; 98 | return length < 1 ? null : nodes[length - 1]; 99 | } 100 | get previousSibling() { 101 | const {firstChild} = this; 102 | return firstChild && firstChild.previousSibling; 103 | } 104 | get nextSibling() { 105 | const {lastChild} = this; 106 | return lastChild && lastChild.nextSibling; 107 | } 108 | get textContent() { 109 | return childNodes.get(this).filter(isVisible).map(getText).join(''); 110 | } 111 | hasChildNodes() { 112 | return 0 < childNodes.get(this).length; 113 | } 114 | cloneNode(...args) { 115 | const pf = new DocumentPersistentFragment; 116 | pf.append(...childNodes.get(this).map(getClone, args)); 117 | return pf; 118 | } 119 | compareDocumentPosition(node) { 120 | const {firstChild} = this; 121 | return firstChild ? 122 | firstChild.compareDocumentPosition(node) : 123 | super.compareDocumentPosition(node); 124 | } 125 | contains(node) { 126 | return childNodes.get(this).includes(node); 127 | } 128 | insertBefore(before, node) { 129 | const nodes = childNodes.get(this); 130 | const i = nodes.indexOf(node); 131 | if (-1 < i) 132 | nodes.splice(i, 0, before); 133 | return super.insertBefore(before, node); 134 | } 135 | appendChild(node) { 136 | if (this.isConnected) 137 | this.parentNode.insertBefore(node, this.nextSibling); 138 | else 139 | super.appendChild(node); 140 | appendChild.call(this, node); 141 | return node; 142 | } 143 | replaceChild(replace, node) { 144 | const nodes = childNodes.get(this); 145 | const i = nodes.indexOf(node); 146 | if (-1 < i) 147 | nodes[i] = replace; 148 | return this.isConnected ? 149 | this.parentNode.replaceChild(replace, node) : 150 | super.replaceChild(replace, node); 151 | } 152 | removeChild(node) { 153 | removeChild.call(this, node); 154 | return this.isConnected ? 155 | this.parentNode.removeChild(node) : 156 | super.removeChild(node); 157 | } 158 | remove() { 159 | this.append(...childNodes.get(this)); 160 | } 161 | valueOf() { 162 | this.remove(); 163 | return this; 164 | } 165 | } 166 | 167 | if (poly) 168 | window.DocumentPersistentFragment = DocumentPersistentFragment; 169 | 170 | function getClone(node) { 171 | return node.cloneNode(...this); 172 | } 173 | 174 | function appendChild(node) { 175 | removeChild.call(this, node); 176 | childNodes.get(this).push(node); 177 | } 178 | 179 | function removeChild(node) { 180 | const nodes = childNodes.get(this); 181 | const i = nodes.indexOf(node); 182 | if (-1 < i) 183 | nodes.splice(i, 1); 184 | } 185 | -------------------------------------------------------------------------------- /esm/element.js: -------------------------------------------------------------------------------- 1 | import poly from './poly.js'; 2 | import {asDPF} from './utils.js'; 3 | 4 | const proto = Element.prototype; 5 | const { 6 | after, before, 7 | append, prepend, 8 | insertAdjacentElement, replaceWith 9 | } = proto; 10 | 11 | if (poly) { 12 | Object.assign( 13 | proto, 14 | { 15 | after(...nodes) { 16 | return after.apply(this, nodes.map(asDPF)); 17 | }, 18 | before(...nodes) { 19 | return before.apply(this, nodes.map(asDPF)); 20 | }, 21 | insertAdjacentElement(position, node) { 22 | return insertAdjacentElement.call(this, position, asDPF(node)); 23 | }, 24 | append(...nodes) { 25 | return append.apply(this, nodes.map(asDPF)); 26 | }, 27 | prepend(...nodes) { 28 | return prepend.apply(this, nodes.map(asDPF)); 29 | }, 30 | replaceWith(node) { 31 | return replaceWith.call(this, asDPF(node)); 32 | } 33 | } 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /esm/index.js: -------------------------------------------------------------------------------- 1 | import './node.js'; 2 | import './element.js'; 3 | import DocumentPersistentFragment from './document-persistent-fragment.js'; 4 | export default DocumentPersistentFragment; 5 | -------------------------------------------------------------------------------- /esm/node.js: -------------------------------------------------------------------------------- 1 | import poly from './poly.js'; 2 | import {asDPF, isDPF} from './utils.js'; 3 | 4 | const proto = Node.prototype; 5 | const { 6 | appendChild, removeChild, 7 | insertBefore, replaceChild 8 | } = proto; 9 | 10 | if (poly) { 11 | Node.DOCUMENT_PERSISTENT_FRAGMENT_NODE = 18; 12 | Object.assign( 13 | proto, 14 | { 15 | appendChild(node) { 16 | return appendChild.call(this, asDPF(node)); 17 | }, 18 | removeChild(node) { 19 | if (isDPF(node) && node.parentNode === this) { 20 | node.remove(); 21 | } else { 22 | removeChild.call(this, node); 23 | } 24 | return node; 25 | }, 26 | insertBefore(before, node) { 27 | return insertBefore.call(this, asDPF(before), node); 28 | }, 29 | replaceChild(replace, node) { 30 | return replaceChild.call(this, asDPF(replace), node); 31 | } 32 | } 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /esm/poly.js: -------------------------------------------------------------------------------- 1 | export default !('DocumentPersistentFragment' in window); 2 | -------------------------------------------------------------------------------- /esm/utils.js: -------------------------------------------------------------------------------- 1 | export const asDPF = node => (isDPF(node) ? node.valueOf() : node); 2 | 3 | // This is fully based on window/global patching side effect. 4 | // Do not import DocumentPersistentFragment upfront or shenanigans happen. 5 | export const isDPF = node => node instanceof DocumentPersistentFragment; 6 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 |'); 66 | console.assert(dpf.isConnected === false, 'not connected'); 67 | console.assert(dpf.previousSibling === null, 'no previousSibling'); 68 | console.assert(dpf.nextSibling === null, 'no nextSibling'); 69 | // LIVE 70 | console.assert(document.body.appendChild(dpf) === dpf, 'Element can append a DPF'); 71 | console.assert(dpf.isConnected === true, 'connected'); 72 | console.assert(dpf.childElementCount === 4, '4 childElementCount'); 73 | console.assert(dpf.childNodes.length === 5, '5 childNodes.length'); 74 | console.assert(dpf.getElementById(id) === nodes[2], 'getElementById is OK'); 75 | console.assert(dpf.getElementById('nope') === null, 'getElementById returns null if not found'); 76 | console.assert(dpf.querySelector(`#${id}`) === nodes[2], 'querySelector is OK'); 77 | console.assert(dpf.querySelectorAll(`p`).length === 3, 'querySelectorAll returns 3
'); 78 | console.assert(dpf.parentElement === document.body, 'correct parentElement/Node'); 79 | console.assert(dpf.previousSibling === previous, 'correct previousSibling'); 80 | document.body.appendChild(next); 81 | console.assert(dpf.compareDocumentPosition(previous) === 2, 'correct previous compareDocumentPosition'); 82 | console.assert(dpf.compareDocumentPosition(next) === 4, 'correct next compareDocumentPosition'); 83 | console.assert(dpf.nextSibling === next, 'correct nextSibling'); 84 | console.assert(dpf.textContent === '12345', 'correct textContent'); 85 | const clone = dpf.cloneNode(true); 86 | console.assert(clone.isConnected === false, 'clone not connected'); 87 | console.assert(dpf.textContent === clone.textContent, 'clone textContent'); 88 | document.body.insertBefore(clone, dpf.nextSibling); 89 | console.assert(clone.isConnected === true, 'clone is connected'); 90 | console.assert(document.body.textContent.trim() === 'P1234512345N', 'correct doubled body text'); 91 | clone.remove(); 92 | console.assert(document.body.textContent.trim() === 'P12345N', 'correct body text'); 93 | console.assert(clone.isConnected === false, 'clone is disconnected'); 94 | dpf.removeChild(nodes[2]); 95 | dpf.removeChild(nodes[3]); 96 | console.assert(document.body.textContent.trim() === 'P125N', 'smaller body text'); 97 | dpf.appendChild(nodes[2]); 98 | console.assert(document.body.textContent.trim() === 'P1253N', 'bigger body text'); 99 | dpf.replaceChild(nodes[3], nodes[1]); 100 | console.assert(document.body.textContent.trim() === 'P1453N', 'different body text'); 101 | document.body.removeChild(dpf); 102 | console.assert(document.body.textContent.trim() === 'PN', 'tiny body text'); 103 | dpf.append(...nodes); 104 | console.assert(dpf.textContent === '12345', 'correct dpf text'); 105 | dpf.removeChild(nodes[2]); 106 | dpf.removeChild(nodes[3]); 107 | console.assert(dpf.textContent === '125', 'smaller dpf text'); 108 | dpf.appendChild(nodes[2]); 109 | console.assert(dpf.textContent === '1253', 'bigger dpf text'); 110 | dpf.replaceChild(nodes[3], nodes[1]); 111 | console.assert(dpf.textContent === '1453', 'different dpf text'); 112 | document.body.lastChild.replaceWith(dpf); 113 | console.assert(document.body.textContent.trim() === 'P1453', 'final body text'); 114 | dpf.append(...nodes); 115 | document.body.textContent = 'OK'; 116 | }, 117 | {once: true} 118 | ); 119 | --------------------------------------------------------------------------------