Skip to content
This repository was archived by the owner on Oct 26, 2021. It is now read-only.

Commit e6e6774

Browse files
authored
Merge pull request #147 from webcomponents/insertAdjacentHTML
Add wrapper for Element#insertAdjacentHTML
2 parents 4f7072c + 4215da0 commit e6e6774

File tree

6 files changed

+381
-25
lines changed

6 files changed

+381
-25
lines changed

custom-elements.min.js

Lines changed: 23 additions & 21 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

custom-elements.min.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Patch/Element.js

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -200,14 +200,14 @@ export default function(internals) {
200200
Utilities.setPropertyUnchecked(destination, 'insertAdjacentElement',
201201
/**
202202
* @this {Element}
203-
* @param {string} where
203+
* @param {string} position
204204
* @param {!Element} element
205205
* @return {?Element}
206206
*/
207-
function(where, element) {
207+
function(position, element) {
208208
const wasConnected = Utilities.isConnected(element);
209209
const insertedElement = /** @type {!Element} */
210-
(baseMethod.call(this, where, element));
210+
(baseMethod.call(this, position, element));
211211

212212
if (wasConnected) {
213213
internals.disconnectTree(element);
@@ -229,6 +229,66 @@ export default function(internals) {
229229
}
230230

231231

232+
function patch_insertAdjacentHTML(destination, baseMethod) {
233+
/**
234+
* Patches and upgrades all nodes which are siblings between `start`
235+
* (inclusive) and `end` (exclusive). If `end` is `null`, then all siblings
236+
* following `start` will be patched and upgraded.
237+
* @param {!Node} start
238+
* @param {?Node} end
239+
*/
240+
function upgradeNodesInRange(start, end) {
241+
const nodes = [];
242+
for (let node = start; node !== end; node = node.nextSibling) {
243+
nodes.push(node);
244+
}
245+
for (let i = 0; i < nodes.length; i++) {
246+
internals.patchAndUpgradeTree(nodes[i]);
247+
}
248+
}
249+
250+
Utilities.setPropertyUnchecked(destination, 'insertAdjacentHTML',
251+
/**
252+
* @this {Element}
253+
* @param {string} position
254+
* @param {string} text
255+
*/
256+
function(position, text) {
257+
position = position.toLowerCase();
258+
259+
if (position === "beforebegin") {
260+
const marker = this.previousSibling;
261+
baseMethod.call(this, position, text);
262+
upgradeNodesInRange(
263+
/** @type {!Node} */ (marker || this.parentNode.firstChild), this);
264+
} else if (position === "afterbegin") {
265+
const marker = this.firstChild;
266+
baseMethod.call(this, position, text);
267+
upgradeNodesInRange(/** @type {!Node} */ (this.firstChild), marker);
268+
} else if (position === "beforeend") {
269+
const marker = this.lastChild;
270+
baseMethod.call(this, position, text);
271+
upgradeNodesInRange(marker || this.firstChild, null);
272+
} else if (position === "afterend") {
273+
const marker = this.nextSibling;
274+
baseMethod.call(this, position, text);
275+
upgradeNodesInRange(/** @type {!Node} */ (this.nextSibling), marker);
276+
} else {
277+
throw new SyntaxError(`The value provided (${String(position)}) is ` +
278+
"not one of 'beforebegin', 'afterbegin', 'beforeend', or 'afterend'.");
279+
}
280+
});
281+
}
282+
283+
if (Native.HTMLElement_insertAdjacentHTML) {
284+
patch_insertAdjacentHTML(HTMLElement.prototype, Native.HTMLElement_insertAdjacentHTML);
285+
} else if (Native.Element_insertAdjacentHTML) {
286+
patch_insertAdjacentHTML(Element.prototype, Native.Element_insertAdjacentHTML);
287+
} else {
288+
console.warn('Custom Elements: `Element#insertAdjacentHTML` was not patched.');
289+
}
290+
291+
232292
PatchParentNode(internals, Element.prototype, {
233293
prepend: Native.Element_prepend,
234294
append: Native.Element_append,

src/Patch/Native.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export default {
2121
Element_setAttributeNS: window.Element.prototype.setAttributeNS,
2222
Element_removeAttributeNS: window.Element.prototype.removeAttributeNS,
2323
Element_insertAdjacentElement: window.Element.prototype['insertAdjacentElement'],
24+
Element_insertAdjacentHTML: window.Element.prototype['insertAdjacentHTML'],
2425
Element_prepend: window.Element.prototype['prepend'],
2526
Element_append: window.Element.prototype['append'],
2627
Element_before: window.Element.prototype['before'],
@@ -30,4 +31,5 @@ export default {
3031
HTMLElement: window.HTMLElement,
3132
HTMLElement_innerHTML: Object.getOwnPropertyDescriptor(window.HTMLElement.prototype, 'innerHTML'),
3233
HTMLElement_insertAdjacentElement: window.HTMLElement.prototype['insertAdjacentElement'],
34+
HTMLElement_insertAdjacentHTML: window.HTMLElement.prototype['insertAdjacentHTML'],
3335
};

tests/html/Element/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@
99
'./removeAttribute.html',
1010
'./removeAttributeNS.html',
1111
'./insertAdjacentElement.html',
12+
'./insertAdjacentHTML.html',
1213
]);
1314
</script>
Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<title>Element#insertAdjacentHTML</title>
5+
<script>
6+
// Capture these before loading the custom elements polyfill so that their
7+
// behavior can be checked before running tests for `insertAdjacentHTML`.
8+
window.NATIVE = {
9+
createElement:
10+
Document.prototype.createElement,
11+
insertAdjacentHTML:
12+
HTMLElement.prototype.insertAdjacentHTML ||
13+
Element.prototype.insertAdjacentHTML,
14+
};
15+
</script>
16+
<script>
17+
(window.customElements = window.customElements || {}).forcePolyfill = true;
18+
</script>
19+
<script src="../../../../es6-promise/dist/es6-promise.auto.min.js"></script>
20+
<script src="../../../../template/template.js"></script>
21+
<script src="../../../../web-component-tester/browser.js"></script>
22+
<script src="../../../custom-elements.min.js"></script>
23+
</head>
24+
<body>
25+
<script>
26+
function generateLocalName() {
27+
return 'test-element-' + Math.random().toString(32).substring(2);
28+
}
29+
30+
function defineWithLocalName(localName, observedAttributes) {
31+
const customElementClass = class extends HTMLElement {
32+
constructor() {
33+
super();
34+
this.constructed = true;
35+
this.connectedCallbackCount = 0;
36+
this.disconnectedCallbackCount = 0;
37+
}
38+
39+
connectedCallback() {
40+
this.connectedCallbackCount++;
41+
}
42+
43+
disconnectedCallback() {
44+
this.disconnectedCallbackCount++;
45+
}
46+
};
47+
48+
customElements.define(localName, customElementClass);
49+
50+
return customElementClass;
51+
}
52+
53+
test('Calling on `this` with position "afterend" while upgrading during a ' +
54+
'clone does not upgrade other children.', function() {
55+
const log = [];
56+
57+
class LoggingElement extends HTMLElement {
58+
constructor() {
59+
super();
60+
log.push(this.localName);
61+
}
62+
}
63+
64+
const ceInsertedName = generateLocalName();
65+
const CEInserted = class extends LoggingElement {};
66+
customElements.define(ceInsertedName, CEInserted);
67+
68+
const ce1Name = generateLocalName();
69+
customElements.define(ce1Name, class extends LoggingElement {
70+
constructor() {
71+
super();
72+
this.insertAdjacentHTML('afterend', `<${ceInsertedName}></${ceInsertedName}>`);
73+
}
74+
});
75+
76+
const ce2Name = generateLocalName();
77+
customElements.define(ce2Name, class extends LoggingElement {});
78+
79+
const template = document.createElement('template');
80+
template.innerHTML = `<div><${ce1Name}><${ce2Name}></${ce2Name}></${ce1Name}></div>`;
81+
82+
// Insert a clone of the template, so that the elements are contained in a
83+
// document with a registry.
84+
85+
const div = template.content.querySelector('div').cloneNode(true);
86+
document.body.appendChild(div);
87+
88+
// Remove the <ce-inserted> created by the initial upgrade, so that the DOM
89+
// content looks like the HTML from the template.
90+
91+
const inserted = div.querySelector(ceInsertedName);
92+
assert(inserted && inserted instanceof CEInserted);
93+
div.removeChild(inserted);
94+
95+
// Clear the upgrade log and clone the div.
96+
97+
log.length = 0;
98+
const clone = div.cloneNode(true);
99+
100+
// <ce-inserted> should have been upgraded before <ce-2>, even though it
101+
// comes after <ce-2> in document order.
102+
103+
assert.deepEqual(log, [ce1Name, ceInsertedName, ce2Name]);
104+
105+
document.body.removeChild(div);
106+
});
107+
108+
suite('Inserting HTML with defined custom elements into a disconnected ' +
109+
'context element with a parent.', function() {
110+
let disconnectedElement;
111+
let localName;
112+
let customElementClass;
113+
114+
setup(function() {
115+
const parent = document.createElement('div');
116+
disconnectedElement = document.createElement('div');
117+
parent.appendChild(disconnectedElement);
118+
localName = generateLocalName();
119+
customElementClass = defineWithLocalName(localName);
120+
});
121+
122+
test('beforebegin', function() {
123+
disconnectedElement.insertAdjacentHTML('beforebegin', `<${localName}></${localName}>`);
124+
125+
const element = disconnectedElement.previousSibling;
126+
assert(element instanceof customElementClass);
127+
assert.equal(element.connectedCallbackCount, 0);
128+
assert.equal(element.disconnectedCallbackCount, 0);
129+
});
130+
131+
test('afterbegin', function() {
132+
disconnectedElement.insertAdjacentHTML('afterbegin', `<${localName}></${localName}>`);
133+
134+
const element = disconnectedElement.firstChild;
135+
assert(element instanceof customElementClass);
136+
assert.equal(element.connectedCallbackCount, 0);
137+
assert.equal(element.disconnectedCallbackCount, 0);
138+
});
139+
140+
test('beforeend', function() {
141+
disconnectedElement.insertAdjacentHTML('beforeend', `<${localName}></${localName}>`);
142+
143+
const element = disconnectedElement.lastChild;
144+
assert(element instanceof customElementClass);
145+
assert.equal(element.connectedCallbackCount, 0);
146+
assert.equal(element.disconnectedCallbackCount, 0);
147+
});
148+
149+
test('afterend', function() {
150+
disconnectedElement.insertAdjacentHTML('afterend', `<${localName}></${localName}>`);
151+
152+
const element = disconnectedElement.nextSibling;
153+
assert(element instanceof customElementClass);
154+
assert.equal(element.connectedCallbackCount, 0);
155+
assert.equal(element.disconnectedCallbackCount, 0);
156+
});
157+
});
158+
159+
suite('Inserting HTML with defined custom elements into a disconnected ' +
160+
'context element without a parent.', function() {
161+
let disconnectedElement;
162+
let localName;
163+
let customElementClass;
164+
165+
setup(function() {
166+
disconnectedElement = document.createElement('div');
167+
localName = generateLocalName();
168+
customElementClass = defineWithLocalName(localName);
169+
});
170+
171+
// Safari 9 and 10 (incorrectly) do not throw when calling
172+
// `insertAdjacentHTML` of an orphan element with position 'beforebegin'.
173+
const beforebeginErrorExpected = (() => {
174+
const div = NATIVE.createElement.call(document, 'div');
175+
try {
176+
NATIVE.insertAdjacentHTML.call(div, 'beforebegin', '<input>');
177+
return false;
178+
} catch (e) {
179+
return true;
180+
}
181+
})();
182+
183+
(beforebeginErrorExpected ? test : test.skip)('Position "beforebegin" throws.', function() {
184+
assert.throws(function() {
185+
disconnectedElement.insertAdjacentHTML('beforebegin', `<${localName}></${localName}>`);
186+
});
187+
});
188+
189+
test('afterbegin', function() {
190+
disconnectedElement.insertAdjacentHTML('afterbegin', `<${localName}></${localName}>`);
191+
192+
const element = disconnectedElement.firstChild;
193+
assert(element instanceof customElementClass);
194+
assert.equal(element.connectedCallbackCount, 0);
195+
assert.equal(element.disconnectedCallbackCount, 0);
196+
});
197+
198+
test('beforeend', function() {
199+
disconnectedElement.insertAdjacentHTML('beforeend', `<${localName}></${localName}>`);
200+
201+
const element = disconnectedElement.lastChild;
202+
assert(element instanceof customElementClass);
203+
assert.equal(element.connectedCallbackCount, 0);
204+
assert.equal(element.disconnectedCallbackCount, 0);
205+
});
206+
207+
// Safari 9 and 10 (incorrectly) do not throw if you read `nextSibling` of
208+
// an orphan element before trying to call its `insertAdjacentHTML` method
209+
// with position 'afterend'.
210+
const afterendErrorExpected = (() => {
211+
const div = NATIVE.createElement.call(document, 'div');
212+
try {
213+
div.nextSibling; // This line may prevent the next from throwing!
214+
NATIVE.insertAdjacentHTML.call(div, 'afterend', '<input>');
215+
return false;
216+
} catch (e) {
217+
return true;
218+
}
219+
})();
220+
221+
(afterendErrorExpected ? test : test.skip)('Position "afterend" throws.', function() {
222+
assert.throws(function() {
223+
disconnectedElement.insertAdjacentHTML('afterend', `<${localName}></${localName}>`);
224+
});
225+
});
226+
});
227+
228+
suite('Inserting HTML with defined custom elements into a connected context ' +
229+
'element.', function() {
230+
let connectedRoot;
231+
let connectedElement;
232+
let localName;
233+
let customElementClass;
234+
235+
setup(function() {
236+
connectedRoot = document.createElement('div');
237+
document.body.appendChild(connectedRoot);
238+
239+
connectedElement = document.createElement('div');
240+
connectedRoot.appendChild(connectedElement);
241+
242+
localName = generateLocalName();
243+
customElementClass = defineWithLocalName(localName);
244+
});
245+
246+
teardown(function() {
247+
while (connectedRoot.firstChild) {
248+
connectedRoot.removeChild(connectedRoot.firstChild);
249+
}
250+
document.body.removeChild(connectedRoot);
251+
});
252+
253+
test('beforebegin', function() {
254+
connectedElement.insertAdjacentHTML('beforebegin', `<${localName}></${localName}>`);
255+
256+
const element = connectedElement.previousSibling;
257+
assert(element instanceof customElementClass);
258+
assert.equal(element.connectedCallbackCount, 1);
259+
assert.equal(element.disconnectedCallbackCount, 0);
260+
});
261+
262+
test('afterbegin', function() {
263+
connectedElement.insertAdjacentHTML('afterbegin', `<${localName}></${localName}>`);
264+
265+
const element = connectedElement.firstChild;
266+
assert(element instanceof customElementClass);
267+
assert.equal(element.connectedCallbackCount, 1);
268+
assert.equal(element.disconnectedCallbackCount, 0);
269+
});
270+
271+
test('beforeend', function() {
272+
connectedElement.insertAdjacentHTML('beforeend', `<${localName}></${localName}>`);
273+
274+
const element = connectedElement.lastChild;
275+
assert(element instanceof customElementClass);
276+
assert.equal(element.connectedCallbackCount, 1);
277+
assert.equal(element.disconnectedCallbackCount, 0);
278+
});
279+
280+
test('afterend', function() {
281+
connectedElement.insertAdjacentHTML('afterend', `<${localName}></${localName}>`);
282+
283+
const element = connectedElement.nextSibling;
284+
assert(element instanceof customElementClass);
285+
assert.equal(element.connectedCallbackCount, 1);
286+
assert.equal(element.disconnectedCallbackCount, 0);
287+
});
288+
});
289+
</script>
290+
</body>
291+
</html>

0 commit comments

Comments
 (0)