htmx.org 4.0.0-alpha5 → 4.0.0-alpha6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/htmx.js CHANGED
@@ -6,17 +6,17 @@ var htmx = (() => {
6
6
  #q = []
7
7
 
8
8
  issue(ctx, queueStrategy) {
9
+ ctx.queueStrategy = queueStrategy
9
10
  if (!this.#c) {
10
11
  this.#c = ctx
11
12
  return true
12
13
  } else {
13
- // Update ctx.status properly for replaced request contexts
14
- if (queueStrategy === "replace") {
14
+ // Replace strategy OR current is abortable: abort current and issue new
15
+ if (queueStrategy === "replace" || (queueStrategy !== "abort" && this.#c.queueStrategy === "abort")) {
15
16
  this.#q.map(value => value.status = "dropped");
16
17
  this.#q = []
17
- if (this.#c) {
18
- this.#c.abort();
19
- }
18
+ this.#c.request.abort();
19
+ this.#c = ctx
20
20
  return true
21
21
  } else if (queueStrategy === "queue all") {
22
22
  this.#q.push(ctx)
@@ -28,7 +28,7 @@ var htmx = (() => {
28
28
  this.#q.map(value => value.status = "dropped");
29
29
  this.#q = [ctx]
30
30
  ctx.status = "queued";
31
- } else if (this.#q.length === 0) {
31
+ } else if (this.#q.length === 0 && queueStrategy !== "abort") {
32
32
  // default queue first
33
33
  this.#q.push(ctx)
34
34
  ctx.status = "queued";
@@ -81,7 +81,8 @@ var htmx = (() => {
81
81
  createRequestContext: this.#createRequestContext.bind(this),
82
82
  collectFormData: this.#collectFormData.bind(this),
83
83
  handleHxVals: this.#handleHxVals.bind(this),
84
- insertContent: this.#insertContent.bind(this)
84
+ insertContent: this.#insertContent.bind(this),
85
+ morph: this.#morph.bind(this)
85
86
  };
86
87
  document.addEventListener("DOMContentLoaded", () => {
87
88
  this.#initHistoryHandling();
@@ -91,7 +92,7 @@ var htmx = (() => {
91
92
 
92
93
  #initHtmxConfig() {
93
94
  this.config = {
94
- version: '4.0.0-alpha5',
95
+ version: '4.0.0-alpha6',
95
96
  logAll: false,
96
97
  prefix: "",
97
98
  transitions: false,
@@ -112,6 +113,7 @@ var htmx = (() => {
112
113
  pauseInBackground: false
113
114
  },
114
115
  morphIgnore: ["data-htmx-powered"],
116
+ morphScanLimit: 10,
115
117
  noSwap: [204, 304],
116
118
  implicitInheritance: false
117
119
  }
@@ -376,20 +378,9 @@ var htmx = (() => {
376
378
  }
377
379
 
378
380
  #handleHxHeaders(elt, headers) {
379
- let result = this.#getAttributeObject(elt, "hx-headers");
380
- if (result) {
381
- if (result instanceof Promise) {
382
- return result.then(obj => {
383
- for (let key in obj) {
384
- headers[key] = String(obj[key]);
385
- }
386
- });
387
- } else {
388
- for (let key in result) {
389
- headers[key] = String(result[key]);
390
- }
391
- }
392
- }
381
+ return this.#getAttributeObject(elt, "hx-headers", obj => {
382
+ for (let key in obj) headers[key] = String(obj[key]);
383
+ });
393
384
  }
394
385
 
395
386
  #resolveTarget(elt, selector) {
@@ -457,15 +448,20 @@ var htmx = (() => {
457
448
  return
458
449
  } else if (/GET|DELETE/.test(ctx.request.method)) {
459
450
  let url = new URL(ctx.request.action, document.baseURI);
460
-
451
+
461
452
  for (let key of ctx.request.body.keys()) {
462
453
  url.searchParams.delete(key);
463
454
  }
464
455
  for (let [key, value] of ctx.request.body) {
465
456
  url.searchParams.append(key, value);
466
457
  }
467
-
468
- ctx.request.action = url.pathname + url.search;
458
+
459
+ // Keep relative if same origin, otherwise use full URL
460
+ if (url.origin === location.origin) {
461
+ ctx.request.action = url.pathname + url.search;
462
+ } else {
463
+ ctx.request.action = url.href;
464
+ }
469
465
  ctx.request.body = null;
470
466
  } else if (this.#attributeValue(elt, "hx-encoding") !== "multipart/form-data") {
471
467
  ctx.request.body = new URLSearchParams(ctx.request.body);
@@ -769,7 +765,7 @@ var htmx = (() => {
769
765
  if (syncValue && syncValue.includes(":")) {
770
766
  let strings = syncValue.split(":");
771
767
  let selector = strings[0];
772
- syncElt = this.#findExt(selector);
768
+ syncElt = this.#findExt(elt, selector, "hx-sync");
773
769
  }
774
770
  return syncElt._htmxRequestQueue ||= new ReqQ()
775
771
  }
@@ -1088,8 +1084,10 @@ var htmx = (() => {
1088
1084
  }
1089
1085
  this.#trigger(elt, "htmx:after:cleanup")
1090
1086
  }
1091
- for (let child of elt.querySelectorAll('[data-htmx-powered]')) {
1092
- this.#cleanup(child);
1087
+ if (elt.firstChild) {
1088
+ for (let child of elt.querySelectorAll('[data-htmx-powered]')) {
1089
+ this.#cleanup(child);
1090
+ }
1093
1091
  }
1094
1092
  }
1095
1093
 
@@ -1100,10 +1098,12 @@ var htmx = (() => {
1100
1098
  let newPreservedElts = fragment.querySelectorAll?.(`[${this.#prefix('hx-preserve')}]`) || [];
1101
1099
  for (let preservedElt of newPreservedElts) {
1102
1100
  let currentElt = document.getElementById(preservedElt.id);
1103
- if (pantry.moveBefore) {
1104
- pantry.moveBefore(currentElt, null);
1105
- } else {
1106
- pantry.appendChild(currentElt);
1101
+ if (currentElt) {
1102
+ if (pantry.moveBefore) {
1103
+ pantry.moveBefore(currentElt, null);
1104
+ } else {
1105
+ pantry.appendChild(currentElt);
1106
+ }
1107
1107
  }
1108
1108
  }
1109
1109
  return pantry
@@ -1112,13 +1112,15 @@ var htmx = (() => {
1112
1112
  #restorePreservedElements(pantry) {
1113
1113
  for (let preservedElt of pantry.children) {
1114
1114
  let newElt = document.getElementById(preservedElt.id);
1115
- if (newElt.parentNode.moveBefore) {
1116
- newElt.parentNode.moveBefore(preservedElt, newElt);
1117
- } else {
1118
- newElt.replaceWith(preservedElt);
1115
+ if (newElt) {
1116
+ if (newElt.parentNode.moveBefore) {
1117
+ newElt.parentNode.moveBefore(preservedElt, newElt);
1118
+ } else {
1119
+ newElt.replaceWith(preservedElt);
1120
+ }
1121
+ this.#cleanup(newElt)
1122
+ newElt.remove()
1119
1123
  }
1120
- this.#cleanup(newElt)
1121
- newElt.remove()
1122
1124
  }
1123
1125
  pantry.remove();
1124
1126
  }
@@ -1290,65 +1292,41 @@ var htmx = (() => {
1290
1292
  let partialTasks = this.#processPartials(fragment, ctx);
1291
1293
  tasks.push(...oobTasks, ...partialTasks);
1292
1294
 
1293
- // Process main swap
1295
+ // Process main swap first
1294
1296
  let mainSwap = this.#processMainSwap(ctx, fragment, partialTasks);
1295
1297
  if (mainSwap) {
1296
- tasks.push(mainSwap);
1298
+ tasks.unshift(mainSwap);
1297
1299
  }
1298
1300
 
1299
- // TODO - can we remove this and just let the function complete?
1300
- if (tasks.length === 0) return;
1301
-
1302
1301
  if(!this.#trigger(document, "htmx:before:swap", {ctx, tasks})){
1303
1302
  return
1304
1303
  }
1305
1304
 
1306
- // insert non-transition tasks immediately or with delay, collect transition tasks
1305
+ let swapPromises = [];
1307
1306
  let transitionTasks = [];
1308
1307
  for (let task of tasks) {
1309
- // OOB/partial tasks with swap delays should be non-transition (non-blocking)
1310
- let swapDelay = task.swapSpec?.swap;
1311
- if (!(task.swapSpec?.transition ?? mainSwap?.transition) || (swapDelay && task !== mainSwap)) {
1312
- if (swapDelay) {
1313
- if (task === mainSwap) {
1314
- await this.timeout(swapDelay);
1315
- } else {
1316
- setTimeout(() => this.#insertContent(task), this.parseInterval(swapDelay));
1317
- continue;
1318
- }
1319
- }
1320
- this.#insertContent(task)
1321
- } else {
1308
+ if (task.swapSpec?.transition ?? mainSwap?.transition) {
1322
1309
  transitionTasks.push(task);
1310
+ } else {
1311
+ swapPromises.push(this.#insertContent(task));
1323
1312
  }
1324
1313
  }
1325
1314
 
1326
- // insert transition tasks in the transition queue
1315
+ // submit all transition tasks in the transition queue w/no CSS transitions
1327
1316
  if (transitionTasks.length > 0) {
1328
- if (mainSwap?.transition && mainSwap?.swapSpec?.swap) {
1329
- await this.timeout(mainSwap.swapSpec.swap);
1330
- }
1331
- let tasksWrapper = ()=> {
1317
+ let tasksWrapper = async ()=> {
1332
1318
  for (let task of transitionTasks) {
1333
- this.#insertContent(task)
1319
+ await this.#insertContent(task, false)
1334
1320
  }
1335
1321
  }
1336
- await this.#submitTransitionTask(tasksWrapper);
1322
+ swapPromises.push(this.#submitTransitionTask(tasksWrapper));
1337
1323
  }
1338
1324
 
1325
+ await Promise.all(swapPromises);
1326
+
1339
1327
  this.#trigger(document, "htmx:after:swap", {ctx});
1340
1328
  if (ctx.title && !mainSwap?.swapSpec?.ignoreTitle) document.title = ctx.title;
1341
- await this.timeout(1);
1342
- // invoke restore tasks
1343
- for (let task of tasks) {
1344
- for (let restore of task.restoreTasks || []) {
1345
- restore()
1346
- }
1347
- }
1348
- this.#trigger(document, "htmx:after:restore", { ctx });
1349
1329
  this.#handleAnchorScroll(ctx);
1350
- // TODO this stuff should be an extension
1351
- // if (ctx.hx?.triggerafterswap) this.#handleTriggerHeader(ctx.hx.triggerafterswap, ctx.sourceElement);
1352
1330
  }
1353
1331
 
1354
1332
  #processMainSwap(ctx, fragment, partialTasks) {
@@ -1376,7 +1354,7 @@ var htmx = (() => {
1376
1354
  }
1377
1355
  }
1378
1356
 
1379
- #insertContent(task) {
1357
+ async #insertContent(task, cssTransition = true) {
1380
1358
  let {target, swapSpec, fragment} = task;
1381
1359
  if (typeof target === 'string') {
1382
1360
  target = document.querySelector(target);
@@ -1385,66 +1363,109 @@ var htmx = (() => {
1385
1363
  if (typeof swapSpec === 'string') {
1386
1364
  swapSpec = this.#parseSwapSpec(swapSpec);
1387
1365
  }
1366
+ if (swapSpec.style === 'none') return;
1388
1367
  if (swapSpec.strip && fragment.firstElementChild) {
1389
1368
  fragment = document.createDocumentFragment();
1390
1369
  fragment.append(...(task.fragment.firstElementChild.content || task.fragment.firstElementChild).childNodes);
1391
1370
  }
1392
1371
 
1393
- let pantry = this.#handlePreservedElements(fragment);
1394
- let parentNode = target.parentNode;
1395
- let newContent = [...fragment.childNodes]
1396
- if (swapSpec.style === 'innerHTML') {
1397
- this.#captureCSSTransitions(task, target);
1398
- for (const child of target.children) {
1399
- this.#cleanup(child)
1400
- }
1401
- target.replaceChildren(...fragment.childNodes);
1402
- } else if (swapSpec.style === 'outerHTML') {
1403
- if (parentNode) {
1404
- this.#captureCSSTransitions(task, parentNode);
1405
- this.#insertNodes(parentNode, target, fragment);
1406
- this.#cleanup(target)
1407
- parentNode.removeChild(target);
1408
- }
1409
- } else if (swapSpec.style === 'innerMorph') {
1410
- this.#morph(target, fragment, true);
1411
- } else if (swapSpec.style === 'outerMorph') {
1412
- this.#morph(target, fragment, false);
1413
- } else if (swapSpec.style === 'beforebegin') {
1414
- if (parentNode) {
1415
- this.#insertNodes(parentNode, target, fragment);
1416
- }
1417
- } else if (swapSpec.style === 'afterbegin') {
1418
- this.#insertNodes(target, target.firstChild, fragment);
1419
- } else if (swapSpec.style === 'beforeend') {
1420
- this.#insertNodes(target, null, fragment);
1421
- } else if (swapSpec.style === 'afterend') {
1422
- if (parentNode) {
1423
- this.#insertNodes(parentNode, target.nextSibling, fragment);
1424
- }
1425
- } else if (swapSpec.style === 'delete') {
1426
- if (parentNode) {
1427
- this.#cleanup(target)
1428
- parentNode.removeChild(target)
1372
+ target.classList.add("htmx-swapping")
1373
+ if (cssTransition && task.swapSpec?.swap) {
1374
+ await this.timeout(task.swapSpec?.swap)
1375
+ }
1376
+
1377
+ if (swapSpec.style === 'delete') {
1378
+ if (target.parentNode) {
1379
+ this.#cleanup(target);
1380
+ target.parentNode.removeChild(target);
1429
1381
  }
1430
1382
  return;
1431
- } else if (swapSpec.style === 'none') {
1383
+ }
1384
+
1385
+ if (swapSpec.style === 'textContent') {
1386
+ target.textContent = fragment.textContent;
1387
+ target.classList.remove("htmx-swapping")
1432
1388
  return;
1433
- } else {
1434
- let methods = this.#extMethods.get('handle_swap')
1435
- let handled = false;
1436
- for (const method of methods) {
1437
- if (method(swapSpec.style, target, fragment)) {
1438
- handled = true;
1439
- break;
1389
+ }
1390
+
1391
+ let pantry = this.#handlePreservedElements(fragment);
1392
+ let parentNode = target.parentNode;
1393
+ let newContent = [...fragment.childNodes]
1394
+ let settleTasks = []
1395
+ try {
1396
+ if (swapSpec.style === 'innerHTML') {
1397
+ settleTasks = cssTransition ? this.#startCSSTransitions(fragment, target) : []
1398
+ for (const child of target.children) {
1399
+ this.#cleanup(child)
1400
+ }
1401
+ target.replaceChildren(...fragment.childNodes);
1402
+ } else if (swapSpec.style === 'outerHTML') {
1403
+ if (parentNode) {
1404
+ settleTasks = cssTransition ? this.#startCSSTransitions(fragment, target) : []
1405
+ this.#insertNodes(parentNode, target, fragment);
1406
+ this.#cleanup(target)
1407
+ parentNode.removeChild(target);
1408
+ }
1409
+ } else if (swapSpec.style === 'innerMorph') {
1410
+ this.#morph(target, fragment, true);
1411
+ newContent = [...target.childNodes];
1412
+ } else if (swapSpec.style === 'outerMorph') {
1413
+ this.#morph(target, fragment, false);
1414
+ newContent.push(target);
1415
+ } else if (swapSpec.style === 'beforebegin') {
1416
+ if (parentNode) {
1417
+ this.#insertNodes(parentNode, target, fragment);
1418
+ }
1419
+ } else if (swapSpec.style === 'afterbegin') {
1420
+ this.#insertNodes(target, target.firstChild, fragment);
1421
+ } else if (swapSpec.style === 'beforeend') {
1422
+ this.#insertNodes(target, null, fragment);
1423
+ } else if (swapSpec.style === 'afterend') {
1424
+ if (parentNode) {
1425
+ this.#insertNodes(parentNode, target.nextSibling, fragment);
1426
+ }
1427
+ } else {
1428
+ let methods = this.#extMethods.get('handle_swap')
1429
+ let handled = false;
1430
+ for (const method of methods) {
1431
+ let result = method(swapSpec.style, target, fragment, swapSpec);
1432
+ if (result) {
1433
+ handled = true;
1434
+ if (Array.isArray(result)) {
1435
+ newContent = result;
1436
+ }
1437
+ break;
1438
+ }
1439
+ }
1440
+ if (!handled) {
1441
+ throw new Error(`Unknown swap style: ${swapSpec.style}`);
1440
1442
  }
1441
1443
  }
1442
- if (!handled) {
1443
- throw new Error(`Unknown swap style: ${swapSpec.style}`);
1444
- }
1444
+ } finally {
1445
+ target.classList.remove("htmx-swapping")
1445
1446
  }
1446
1447
  this.#restorePreservedElements(pantry);
1448
+
1449
+ this.#trigger(target, "htmx:before:settle", {task, newContent, settleTasks})
1450
+
1451
+ for (const elt of newContent) {
1452
+ elt.classList?.add?.("htmx-added")
1453
+ }
1454
+
1455
+ if (cssTransition) {
1456
+ target.classList.add("htmx-settling")
1457
+ await this.timeout(swapSpec.settle ?? 1);
1458
+ // invoke settle tasks
1459
+ for (let settleTask of settleTasks) {
1460
+ settleTask()
1461
+ }
1462
+ target.classList.remove("htmx-settling")
1463
+ }
1464
+
1465
+ this.#trigger(target, "htmx:after:settle", {task, newContent, settleTasks})
1466
+
1447
1467
  for (const elt of newContent) {
1468
+ elt.classList?.remove?.("htmx-added")
1448
1469
  this.process(elt);
1449
1470
  this.#handleAutoFocus(elt);
1450
1471
  }
@@ -1457,7 +1478,7 @@ var htmx = (() => {
1457
1478
  }
1458
1479
  on = this.#normalizeElement(on)
1459
1480
  this.#triggerExtensions(on, eventName, detail);
1460
- return this.trigger(on, eventName, detail, bubbles)
1481
+ return this.trigger(on, this.#maybeAdjustMetaCharacter(eventName), detail, bubbles)
1461
1482
  }
1462
1483
 
1463
1484
  #triggerExtensions(elt, eventName, detail = {}) {
@@ -1643,7 +1664,9 @@ var htmx = (() => {
1643
1664
  if (!path || path === 'false' || path === false) return;
1644
1665
 
1645
1666
  if (path === 'true') {
1646
- path = ctx.request.action + (ctx.request.anchor ? '#' + ctx.request.anchor : '');
1667
+ let finalUrl = response?.raw?.url || ctx.request.action;
1668
+ let url = new URL(finalUrl, location.href);
1669
+ path = url.pathname + url.search + (ctx.request.anchor ? '#' + ctx.request.anchor : '');
1647
1670
  }
1648
1671
 
1649
1672
  let type = push ? 'push' : 'replace';
@@ -1787,7 +1810,7 @@ var htmx = (() => {
1787
1810
  }
1788
1811
  }
1789
1812
 
1790
- #getAttributeObject(elt, attrName) {
1813
+ #getAttributeObject(elt, attrName, callback) {
1791
1814
  let attrValue = this.#attributeValue(elt, attrName);
1792
1815
  if (!attrValue) return null;
1793
1816
 
@@ -1798,28 +1821,19 @@ var htmx = (() => {
1798
1821
  javascriptContent = '{' + javascriptContent + '}';
1799
1822
  }
1800
1823
  // Return promise for async evaluation
1801
- return this.#executeJavaScriptAsync(elt, {}, javascriptContent, true);
1824
+ return this.#executeJavaScriptAsync(elt, {}, javascriptContent, true).then(obj => {
1825
+ callback(obj);
1826
+ });
1802
1827
  } else {
1803
1828
  // Synchronous path - return the parsed object directly
1804
- return this.#parseConfig(attrValue);
1829
+ callback(this.#parseConfig(attrValue));
1805
1830
  }
1806
1831
  }
1807
1832
 
1808
1833
  #handleHxVals(elt, body) {
1809
- let result = this.#getAttributeObject(elt, "hx-vals");
1810
- if (result) {
1811
- if (result instanceof Promise) {
1812
- return result.then(obj => {
1813
- for (let key in obj) {
1814
- body.set(key, obj[key])
1815
- }
1816
- });
1817
- } else {
1818
- for (let key in result) {
1819
- body.set(key, result[key])
1820
- }
1821
- }
1822
- }
1834
+ return this.#getAttributeObject(elt, "hx-vals", obj => {
1835
+ for (let key in obj) body.set(key, obj[key]);
1836
+ });
1823
1837
  }
1824
1838
 
1825
1839
  #stringHyperscriptStyleSelector(selector) {
@@ -2012,26 +2026,35 @@ var htmx = (() => {
2012
2026
  }
2013
2027
 
2014
2028
  #findBestMatch(ctx, node, startPoint, endPoint) {
2015
- let softMatch = null, nextSibling = node.nextSibling, siblingSoftMatchCount = 0, displaceMatchCount = 0;
2029
+ let softMatch = null, nextSibling = node.nextSibling, siblingMatchCount = 0, displaceMatchCount = 0, scanLimit = this.config.morphScanLimit;
2030
+ // Get ID count for this node to prioritize ID-based matches
2016
2031
  let newSet = ctx.idMap.get(node), nodeMatchCount = newSet?.size || 0;
2017
2032
  let cursor = startPoint;
2018
2033
  while (cursor && cursor != endPoint) {
2019
2034
  let oldSet = ctx.idMap.get(cursor);
2020
2035
  if (this.#isSoftMatch(cursor, node)) {
2036
+ // Hard match: matching IDs found in both nodes
2021
2037
  if (oldSet && newSet && [...oldSet].some(id => newSet.has(id))) return cursor;
2022
- if (softMatch === null && !oldSet) {
2023
- if (!nodeMatchCount) return cursor;
2024
- else softMatch = cursor;
2038
+ if (!oldSet) {
2039
+ // Exact match: nodes are identical
2040
+ if (scanLimit > 0 && cursor.isEqualNode(node)) return cursor;
2041
+ // Soft match: same tag/type, save as fallback
2042
+ if (!softMatch) softMatch = cursor;
2025
2043
  }
2026
2044
  }
2045
+ // Stop if too many ID elements would be displaced
2027
2046
  displaceMatchCount += oldSet?.size || 0;
2028
2047
  if (displaceMatchCount > nodeMatchCount) break;
2029
- if (softMatch === null && nextSibling && this.#isSoftMatch(cursor, nextSibling)) {
2030
- siblingSoftMatchCount++;
2048
+ // Look ahead: if next siblings match exactly, abort to let them match instead
2049
+ if (nextSibling && scanLimit > 0 && cursor.isEqualNode(nextSibling)) {
2050
+ siblingMatchCount++;
2031
2051
  nextSibling = nextSibling.nextSibling;
2032
- if (siblingSoftMatchCount >= 2) softMatch = undefined;
2052
+ if (siblingMatchCount >= 2) return null;
2033
2053
  }
2054
+ // Don't move elements containing focus
2034
2055
  if (cursor.contains(document.activeElement)) break;
2056
+ // Stop scanning if limit reached and no IDs to match
2057
+ if (--scanLimit < 1 && nodeMatchCount === 0) break;
2035
2058
  cursor = cursor.nextSibling;
2036
2059
  }
2037
2060
  return softMatch || null;
@@ -2194,21 +2217,22 @@ var htmx = (() => {
2194
2217
  }
2195
2218
  }
2196
2219
 
2197
- #captureCSSTransitions(task, root) {
2220
+ #startCSSTransitions(fragment, root) {
2198
2221
  let idElements = root.querySelectorAll("[id]");
2199
2222
  let existingElementsById = Object.fromEntries([...idElements].map(e => [e.id, e]));
2200
- let newElementsWithIds = task.fragment.querySelectorAll("[id]");
2201
- task.restoreTasks = []
2223
+ let newElementsWithIds = fragment.querySelectorAll("[id]");
2224
+ let restoreTasks = []
2202
2225
  for (let elt of newElementsWithIds) {
2203
2226
  let existing = existingElementsById[elt.id];
2204
2227
  if (existing?.tagName === elt.tagName) {
2205
2228
  let clone = elt.cloneNode(false); // shallow clone node
2206
- this.#copyAttributes(elt, existing, this.config.morphIgnore)
2207
- task.restoreTasks.push(()=>{
2208
- this.#copyAttributes(elt, clone, this.config.morphIgnore)
2229
+ this.#copyAttributes(elt, existing)
2230
+ restoreTasks.push(()=>{
2231
+ this.#copyAttributes(elt, clone)
2209
2232
  })
2210
2233
  }
2211
2234
  }
2235
+ return restoreTasks;
2212
2236
  }
2213
2237
 
2214
2238
  #normalizeElement(cssOrElement) {