Skip to content

Commit 163d9fb

Browse files
Release v0.7.3
1 parent c344a39 commit 163d9fb

13 files changed

+483
-259
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
* Removed:
66
* Remove AMD publish target since its EOL: https://github.com/requirejs/requirejs/issues/1816#issuecomment-707503323
77

8+
## [0.7.3] - 2025-03-05
9+
810
* Fixed:
911
* Fix error when morphing elements with numeric ids (@botandrose, @ksbrooksjr)
1012
* Fix issue with outerHTML morphing an IDed node that gets moved (@botandrose, @MichaelWest22)

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,11 @@ A testimonial:
3838
3939
## Installing
4040

41-
Idiomorph is a small (3.1k min/gz'd), dependency free JavaScript library. The `/dist/idiomorph.js` file can be included
41+
Idiomorph is a small (3.2k min/gz'd), dependency free JavaScript library. The `/dist/idiomorph.js` file can be included
4242
directly in a browser:
4343

4444
```html
45-
<script src="https://unpkg.com/idiomorph@0.7.2"></script>
45+
<script src="https://unpkg.com/idiomorph@0.7.3"></script>
4646
```
4747

4848
For production systems we recommend downloading and vendoring the library.

dist/idiomorph-ext.esm.js

Lines changed: 79 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -189,14 +189,6 @@ var Idiomorph = (function () {
189189
*/
190190
function morphOuterHTML(ctx, oldNode, newNode) {
191191
const oldParent = normalizeParent(oldNode);
192-
193-
// basis for calulating which nodes were morphed
194-
// since there may be unmorphed sibling nodes
195-
let childNodes = Array.from(oldParent.childNodes);
196-
const index = childNodes.indexOf(oldNode);
197-
// how many elements are to the right of the oldNode
198-
const rightMargin = childNodes.length - (index + 1);
199-
200192
morphChildren(
201193
ctx,
202194
oldParent,
@@ -205,10 +197,8 @@ var Idiomorph = (function () {
205197
oldNode, // start point for iteration
206198
oldNode.nextSibling, // end point for iteration
207199
);
208-
209-
// return just the morphed nodes
210-
childNodes = Array.from(oldParent.childNodes);
211-
return childNodes.slice(index, childNodes.length - rightMargin);
200+
// this is safe even with siblings, because normalizeParent returns a SlicedParentNode if needed.
201+
return Array.from(oldParent.childNodes);
212202
}
213203

214204
/**
@@ -238,7 +228,7 @@ var Idiomorph = (function () {
238228
const results = fn();
239229

240230
if (activeElementId && activeElementId !== document.activeElement?.id) {
241-
activeElement = ctx.target.querySelector(`#${activeElementId}`);
231+
activeElement = ctx.target.querySelector(`[id="${activeElementId}"]`);
242232
activeElement?.focus();
243233
}
244234
if (activeElement && !activeElement.selectionEnd && selectionEnd) {
@@ -550,8 +540,9 @@ var Idiomorph = (function () {
550540
const target =
551541
/** @type {Element} - will always be found */
552542
(
553-
ctx.target.querySelector(`#${id}`) ||
554-
ctx.pantry.querySelector(`#${id}`)
543+
(ctx.target.id === id && ctx.target) ||
544+
ctx.target.querySelector(`[id="${id}"]`) ||
545+
ctx.pantry.querySelector(`[id="${id}"]`)
555546
);
556547
removeElementFromAncestorsIdMaps(target, ctx);
557548
moveBefore(parentNode, target, after);
@@ -1200,8 +1191,9 @@ var Idiomorph = (function () {
12001191
if (newContent.parentNode) {
12011192
// we can't use the parent directly because newContent may have siblings
12021193
// that we don't want in the morph, and reparenting might be expensive (TODO is it?),
1203-
// so we create a duck-typed parent node instead.
1204-
return createDuckTypedParent(newContent);
1194+
// so instead we create a fake parent node that only sees a slice of its children.
1195+
/** @type {Element} */
1196+
return /** @type {any} */ (new SlicedParentNode(newContent));
12051197
} else {
12061198
// a single node is added as a child to a dummy parent
12071199
const dummyParent = document.createElement("div");
@@ -1220,33 +1212,78 @@ var Idiomorph = (function () {
12201212
}
12211213

12221214
/**
1223-
* Creates a fake duck-typed parent element to wrap a single node, without actually reparenting it.
1215+
* A fake duck-typed parent element to wrap a single node, without actually reparenting it.
1216+
* This is useful because the node may have siblings that we don't want in the morph, and it may also be moved
1217+
* or replaced with one or more elements during the morph. This class effectively allows us a window into
1218+
* a slice of a node's children.
12241219
* "If it walks like a duck, and quacks like a duck, then it must be a duck!" -- James Whitcomb Riley (1849–1916)
1225-
*
1226-
* @param {Node} newContent
1227-
* @returns {Element}
12281220
*/
1229-
function createDuckTypedParent(newContent) {
1230-
return /** @type {Element} */ (
1231-
/** @type {unknown} */ ({
1232-
childNodes: [newContent],
1233-
/** @ts-ignore - cover your eyes for a minute, tsc */
1234-
querySelectorAll: (s) => {
1235-
/** @ts-ignore */
1236-
const elements = newContent.querySelectorAll(s);
1237-
/** @ts-ignore */
1238-
return newContent.matches(s) ? [newContent, ...elements] : elements;
1239-
},
1240-
/** @ts-ignore */
1241-
insertBefore: (n, r) => newContent.parentNode.insertBefore(n, r),
1242-
/** @ts-ignore */
1243-
moveBefore: (n, r) => newContent.parentNode.moveBefore(n, r),
1244-
// for later use with populateIdMapWithTree to halt upwards iteration
1245-
get __idiomorphRoot() {
1246-
return newContent;
1247-
},
1248-
})
1249-
);
1221+
class SlicedParentNode {
1222+
/** @param {Node} node */
1223+
constructor(node) {
1224+
this.originalNode = node;
1225+
this.realParentNode = /** @type {Element} */ (node.parentNode);
1226+
this.previousSibling = node.previousSibling;
1227+
this.nextSibling = node.nextSibling;
1228+
}
1229+
1230+
/** @returns {Node[]} */
1231+
get childNodes() {
1232+
// return slice of realParent's current childNodes, based on previousSibling and nextSibling
1233+
const nodes = [];
1234+
let cursor = this.previousSibling
1235+
? this.previousSibling.nextSibling
1236+
: this.realParentNode.firstChild;
1237+
while (cursor && cursor != this.nextSibling) {
1238+
nodes.push(cursor);
1239+
cursor = cursor.nextSibling;
1240+
}
1241+
return nodes;
1242+
}
1243+
1244+
/**
1245+
* @param {string} selector
1246+
* @returns {Element[]}
1247+
*/
1248+
querySelectorAll(selector) {
1249+
return this.childNodes.reduce((results, node) => {
1250+
if (node instanceof Element) {
1251+
if (node.matches(selector)) results.push(node);
1252+
const nodeList = node.querySelectorAll(selector);
1253+
for (let i = 0; i < nodeList.length; i++) {
1254+
results.push(nodeList[i]);
1255+
}
1256+
}
1257+
return results;
1258+
}, /** @type {Element[]} */ ([]));
1259+
}
1260+
1261+
/**
1262+
* @param {Node} node
1263+
* @param {Node} referenceNode
1264+
* @returns {Node}
1265+
*/
1266+
insertBefore(node, referenceNode) {
1267+
return this.realParentNode.insertBefore(node, referenceNode);
1268+
}
1269+
1270+
/**
1271+
* @param {Node} node
1272+
* @param {Node} referenceNode
1273+
* @returns {Node}
1274+
*/
1275+
moveBefore(node, referenceNode) {
1276+
// @ts-ignore - use new moveBefore feature
1277+
return this.realParentNode.moveBefore(node, referenceNode);
1278+
}
1279+
1280+
/**
1281+
* for later use with populateIdMapWithTree to halt upwards iteration
1282+
* @returns {Node}
1283+
*/
1284+
get __idiomorphRoot() {
1285+
return this.originalNode;
1286+
}
12501287
}
12511288

12521289
/**

dist/idiomorph-ext.js

Lines changed: 79 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -187,14 +187,6 @@ var Idiomorph = (function () {
187187
*/
188188
function morphOuterHTML(ctx, oldNode, newNode) {
189189
const oldParent = normalizeParent(oldNode);
190-
191-
// basis for calulating which nodes were morphed
192-
// since there may be unmorphed sibling nodes
193-
let childNodes = Array.from(oldParent.childNodes);
194-
const index = childNodes.indexOf(oldNode);
195-
// how many elements are to the right of the oldNode
196-
const rightMargin = childNodes.length - (index + 1);
197-
198190
morphChildren(
199191
ctx,
200192
oldParent,
@@ -203,10 +195,8 @@ var Idiomorph = (function () {
203195
oldNode, // start point for iteration
204196
oldNode.nextSibling, // end point for iteration
205197
);
206-
207-
// return just the morphed nodes
208-
childNodes = Array.from(oldParent.childNodes);
209-
return childNodes.slice(index, childNodes.length - rightMargin);
198+
// this is safe even with siblings, because normalizeParent returns a SlicedParentNode if needed.
199+
return Array.from(oldParent.childNodes);
210200
}
211201

212202
/**
@@ -236,7 +226,7 @@ var Idiomorph = (function () {
236226
const results = fn();
237227

238228
if (activeElementId && activeElementId !== document.activeElement?.id) {
239-
activeElement = ctx.target.querySelector(`#${activeElementId}`);
229+
activeElement = ctx.target.querySelector(`[id="${activeElementId}"]`);
240230
activeElement?.focus();
241231
}
242232
if (activeElement && !activeElement.selectionEnd && selectionEnd) {
@@ -548,8 +538,9 @@ var Idiomorph = (function () {
548538
const target =
549539
/** @type {Element} - will always be found */
550540
(
551-
ctx.target.querySelector(`#${id}`) ||
552-
ctx.pantry.querySelector(`#${id}`)
541+
(ctx.target.id === id && ctx.target) ||
542+
ctx.target.querySelector(`[id="${id}"]`) ||
543+
ctx.pantry.querySelector(`[id="${id}"]`)
553544
);
554545
removeElementFromAncestorsIdMaps(target, ctx);
555546
moveBefore(parentNode, target, after);
@@ -1198,8 +1189,9 @@ var Idiomorph = (function () {
11981189
if (newContent.parentNode) {
11991190
// we can't use the parent directly because newContent may have siblings
12001191
// that we don't want in the morph, and reparenting might be expensive (TODO is it?),
1201-
// so we create a duck-typed parent node instead.
1202-
return createDuckTypedParent(newContent);
1192+
// so instead we create a fake parent node that only sees a slice of its children.
1193+
/** @type {Element} */
1194+
return /** @type {any} */ (new SlicedParentNode(newContent));
12031195
} else {
12041196
// a single node is added as a child to a dummy parent
12051197
const dummyParent = document.createElement("div");
@@ -1218,33 +1210,78 @@ var Idiomorph = (function () {
12181210
}
12191211

12201212
/**
1221-
* Creates a fake duck-typed parent element to wrap a single node, without actually reparenting it.
1213+
* A fake duck-typed parent element to wrap a single node, without actually reparenting it.
1214+
* This is useful because the node may have siblings that we don't want in the morph, and it may also be moved
1215+
* or replaced with one or more elements during the morph. This class effectively allows us a window into
1216+
* a slice of a node's children.
12221217
* "If it walks like a duck, and quacks like a duck, then it must be a duck!" -- James Whitcomb Riley (1849–1916)
1223-
*
1224-
* @param {Node} newContent
1225-
* @returns {Element}
12261218
*/
1227-
function createDuckTypedParent(newContent) {
1228-
return /** @type {Element} */ (
1229-
/** @type {unknown} */ ({
1230-
childNodes: [newContent],
1231-
/** @ts-ignore - cover your eyes for a minute, tsc */
1232-
querySelectorAll: (s) => {
1233-
/** @ts-ignore */
1234-
const elements = newContent.querySelectorAll(s);
1235-
/** @ts-ignore */
1236-
return newContent.matches(s) ? [newContent, ...elements] : elements;
1237-
},
1238-
/** @ts-ignore */
1239-
insertBefore: (n, r) => newContent.parentNode.insertBefore(n, r),
1240-
/** @ts-ignore */
1241-
moveBefore: (n, r) => newContent.parentNode.moveBefore(n, r),
1242-
// for later use with populateIdMapWithTree to halt upwards iteration
1243-
get __idiomorphRoot() {
1244-
return newContent;
1245-
},
1246-
})
1247-
);
1219+
class SlicedParentNode {
1220+
/** @param {Node} node */
1221+
constructor(node) {
1222+
this.originalNode = node;
1223+
this.realParentNode = /** @type {Element} */ (node.parentNode);
1224+
this.previousSibling = node.previousSibling;
1225+
this.nextSibling = node.nextSibling;
1226+
}
1227+
1228+
/** @returns {Node[]} */
1229+
get childNodes() {
1230+
// return slice of realParent's current childNodes, based on previousSibling and nextSibling
1231+
const nodes = [];
1232+
let cursor = this.previousSibling
1233+
? this.previousSibling.nextSibling
1234+
: this.realParentNode.firstChild;
1235+
while (cursor && cursor != this.nextSibling) {
1236+
nodes.push(cursor);
1237+
cursor = cursor.nextSibling;
1238+
}
1239+
return nodes;
1240+
}
1241+
1242+
/**
1243+
* @param {string} selector
1244+
* @returns {Element[]}
1245+
*/
1246+
querySelectorAll(selector) {
1247+
return this.childNodes.reduce((results, node) => {
1248+
if (node instanceof Element) {
1249+
if (node.matches(selector)) results.push(node);
1250+
const nodeList = node.querySelectorAll(selector);
1251+
for (let i = 0; i < nodeList.length; i++) {
1252+
results.push(nodeList[i]);
1253+
}
1254+
}
1255+
return results;
1256+
}, /** @type {Element[]} */ ([]));
1257+
}
1258+
1259+
/**
1260+
* @param {Node} node
1261+
* @param {Node} referenceNode
1262+
* @returns {Node}
1263+
*/
1264+
insertBefore(node, referenceNode) {
1265+
return this.realParentNode.insertBefore(node, referenceNode);
1266+
}
1267+
1268+
/**
1269+
* @param {Node} node
1270+
* @param {Node} referenceNode
1271+
* @returns {Node}
1272+
*/
1273+
moveBefore(node, referenceNode) {
1274+
// @ts-ignore - use new moveBefore feature
1275+
return this.realParentNode.moveBefore(node, referenceNode);
1276+
}
1277+
1278+
/**
1279+
* for later use with populateIdMapWithTree to halt upwards iteration
1280+
* @returns {Node}
1281+
*/
1282+
get __idiomorphRoot() {
1283+
return this.originalNode;
1284+
}
12481285
}
12491286

12501287
/**

0 commit comments

Comments
 (0)