@jsenv/dom 0.5.3 → 0.6.1

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/jsenv_dom.js CHANGED
@@ -1,6 +1,107 @@
1
1
  import { signal, effect } from "@preact/signals";
2
2
  import { useState, useLayoutEffect } from "preact/hooks";
3
3
 
4
+ /**
5
+ * Generates a unique signature for various types of elements that can be used for identification in logs.
6
+ *
7
+ * This function handles different types of elements and returns an appropriate identifier:
8
+ * - For DOM elements: Creates a CSS selector using tag name, data-ui-name, ID, classes, or parent hierarchy
9
+ * - For React/Preact elements (JSX): Returns JSX-like representation with type and props
10
+ * - For functions: Returns function name and optional underlying element reference in brackets
11
+ * - For null/undefined: Returns the string representation
12
+ *
13
+ * The returned signature for DOM elements is a valid CSS selector that can be copy-pasted
14
+ * into browser dev tools to locate the element in the DOM.
15
+ *
16
+ * @param {HTMLElement|Object|Function|null|undefined} element - The element to generate a signature for
17
+ * @returns {string} A unique identifier string in various formats depending on element type
18
+ *
19
+ * @example
20
+ * // For DOM element with data-ui-name
21
+ * // <div data-ui-name="header">
22
+ * getElementSignature(element) // Returns: `div[data-ui-name="header"]`
23
+ *
24
+ * @example
25
+ * // For DOM element with ID
26
+ * // <div id="main" class="container active">
27
+ * getElementSignature(element) // Returns: "div#main"
28
+ *
29
+ * @example
30
+ * // For DOM element with classes only
31
+ * // <button class="btn primary">
32
+ * getElementSignature(element) // Returns: "button.btn.primary"
33
+ *
34
+ * @example
35
+ * // For DOM element without distinguishing features (uses parent hierarchy)
36
+ * // <p> inside <section id="content">
37
+ * getElementSignature(element) // Returns: "section#content > p"
38
+ *
39
+ * @example
40
+ * // For React/Preact element with props
41
+ * // <MyComponent id="widget" />
42
+ * getElementSignature(element) // Returns: `<MyComponent id="widget" />`
43
+ *
44
+ * @example
45
+ * // For named function with underlying element reference
46
+ * const MyComponent = () => {}; MyComponent.underlyingElementId = "div#main";
47
+ * getElementSignature(MyComponent) // Returns: "[function MyComponent for div#main]"
48
+ *
49
+ * @example
50
+ * // For anonymous function without underlying element
51
+ * const anonymousFunc = () => {};
52
+ * getElementSignature(anonymousFunc) // Returns: "[function]"
53
+ *
54
+ * @example
55
+ * // For named function without underlying element
56
+ * function namedHandler() {}
57
+ * getElementSignature(namedHandler) // Returns: "[function namedHandler]"
58
+ *
59
+ * @example
60
+ * // For null/undefined
61
+ * getElementSignature(null) // Returns: "null"
62
+ */
63
+ const getElementSignature = (element) => {
64
+ if (!element) {
65
+ return String(element);
66
+ }
67
+ if (typeof element === "function") {
68
+ const functionName = element.name;
69
+ const functionLabel = functionName
70
+ ? `function ${functionName}`
71
+ : "function";
72
+ const underlyingElementId = element.underlyingElementId;
73
+ if (underlyingElementId) {
74
+ return `[${functionLabel} for ${underlyingElementId}]`;
75
+ }
76
+ return `[${functionLabel}]`;
77
+ }
78
+ if (element.props) {
79
+ const type = element.type;
80
+ const id = element.props.id;
81
+ if (id) {
82
+ return `<${type} id="${id}" />`;
83
+ }
84
+ return `<${type} />`;
85
+ }
86
+
87
+ const tagName = element.tagName.toLowerCase();
88
+ const dataUIName = element.getAttribute("data-ui-name");
89
+ if (dataUIName) {
90
+ return `${tagName}[data-ui-name="${dataUIName}"]`;
91
+ }
92
+ const elementId = element.id;
93
+ if (elementId) {
94
+ return `${tagName}#${elementId}`;
95
+ }
96
+ const className = element.className;
97
+ if (className) {
98
+ return `${tagName}.${className.split(" ").join(".")}`;
99
+ }
100
+
101
+ const parentSignature = getElementSignature(element.parentElement);
102
+ return `${parentSignature} > ${tagName}`;
103
+ };
104
+
4
105
  const createIterableWeakSet = () => {
5
106
  const objectWeakRefSet = new Set();
6
107
 
@@ -102,22 +203,26 @@ const createPubSub = (clearOnPublish = false) => {
102
203
 
103
204
  const createValueEffect = (value) => {
104
205
  const callbackSet = new Set();
105
- const previousValueCleanupSet = new Set();
206
+ const valueCleanupSet = new Set();
207
+
208
+ const cleanup = () => {
209
+ for (const valueCleanup of valueCleanupSet) {
210
+ valueCleanup();
211
+ }
212
+ valueCleanupSet.clear();
213
+ };
106
214
 
107
215
  const updateValue = (newValue) => {
108
216
  if (newValue === value) {
109
217
  return;
110
218
  }
111
- for (const cleanup of previousValueCleanupSet) {
112
- cleanup();
113
- }
114
- previousValueCleanupSet.clear();
219
+ cleanup();
115
220
  const oldValue = value;
116
221
  value = newValue;
117
222
  for (const callback of callbackSet) {
118
223
  const returnValue = callback(newValue, oldValue);
119
224
  if (typeof returnValue === "function") {
120
- previousValueCleanupSet.add(returnValue);
225
+ valueCleanupSet.add(returnValue);
121
226
  }
122
227
  }
123
228
  };
@@ -129,7 +234,7 @@ const createValueEffect = (value) => {
129
234
  };
130
235
  };
131
236
 
132
- return [updateValue, addEffect];
237
+ return [updateValue, addEffect, cleanup];
133
238
  };
134
239
 
135
240
  // https://github.com/davidtheclark/tabbable/blob/master/index.js
@@ -433,6 +538,9 @@ const normalizeNumber = (value, context, unit, propertyName) => {
433
538
 
434
539
  // Normalize styles for DOM application
435
540
  const normalizeStyles = (styles, context = "js") => {
541
+ if (!styles) {
542
+ return {};
543
+ }
436
544
  if (typeof styles === "string") {
437
545
  styles = parseStyleString(styles);
438
546
  return styles;
@@ -681,6 +789,29 @@ const mergeStyles = (stylesA, stylesB, context = "js") => {
681
789
  return result;
682
790
  };
683
791
 
792
+ const appendStyles = (
793
+ stylesAObject,
794
+ stylesBNormalized,
795
+ context = "js",
796
+ ) => {
797
+ const aKeys = Object.keys(stylesAObject);
798
+ const bKeys = Object.keys(stylesBNormalized);
799
+ for (const bKey of bKeys) {
800
+ const aHasKey = aKeys.includes(bKey);
801
+ if (aHasKey) {
802
+ stylesAObject[bKey] = mergeOneStyle(
803
+ stylesAObject[bKey],
804
+ stylesBNormalized[bKey],
805
+ bKey,
806
+ context,
807
+ );
808
+ } else {
809
+ stylesAObject[bKey] = stylesBNormalized[bKey];
810
+ }
811
+ }
812
+ return stylesAObject;
813
+ };
814
+
684
815
  // Merge a single style property value with an existing value
685
816
  const mergeOneStyle = (
686
817
  existingValue,
@@ -4211,391 +4342,9 @@ const getDragCoordinates = (
4211
4342
  return [leftRelativeToScrollContainer, topRelativeToScrollContainer];
4212
4343
  };
4213
4344
 
4214
- /* eslint-disable */
4215
- // construct-style-sheets-polyfill@3.1.0
4216
- // to keep in sync with https://github.com/calebdwilliams/construct-style-sheets
4217
- // copy pasted into jsenv codebase to inject this code with more ease
4218
- (function () {
4219
-
4220
- if (typeof document === "undefined" || "adoptedStyleSheets" in document) {
4221
- return;
4222
- }
4223
-
4224
- var hasShadyCss = "ShadyCSS" in window && !ShadyCSS.nativeShadow;
4225
- var bootstrapper = document.implementation.createHTMLDocument("");
4226
- var closedShadowRootRegistry = new WeakMap();
4227
- var _DOMException = typeof DOMException === "object" ? Error : DOMException;
4228
- var defineProperty = Object.defineProperty;
4229
- var forEach = Array.prototype.forEach;
4230
-
4231
- var importPattern = /@import.+?;?$/gm;
4232
- function rejectImports(contents) {
4233
- var _contents = contents.replace(importPattern, "");
4234
- if (_contents !== contents) {
4235
- console.warn(
4236
- "@import rules are not allowed here. See https://github.com/WICG/construct-stylesheets/issues/119#issuecomment-588352418",
4237
- );
4238
- }
4239
- return _contents.trim();
4240
- }
4241
- function isElementConnected(element) {
4242
- return "isConnected" in element
4243
- ? element.isConnected
4244
- : document.contains(element);
4245
- }
4246
- function unique(arr) {
4247
- return arr.filter(function (value, index) {
4248
- return arr.indexOf(value) === index;
4249
- });
4250
- }
4251
- function diff(arr1, arr2) {
4252
- return arr1.filter(function (value) {
4253
- return arr2.indexOf(value) === -1;
4254
- });
4255
- }
4256
- function removeNode(node) {
4257
- node.parentNode.removeChild(node);
4258
- }
4259
- function getShadowRoot(element) {
4260
- return element.shadowRoot || closedShadowRootRegistry.get(element);
4261
- }
4262
-
4263
- var cssStyleSheetMethods = [
4264
- "addRule",
4265
- "deleteRule",
4266
- "insertRule",
4267
- "removeRule",
4268
- ];
4269
- var NonConstructedStyleSheet = CSSStyleSheet;
4270
- var nonConstructedProto = NonConstructedStyleSheet.prototype;
4271
- nonConstructedProto.replace = function () {
4272
- return Promise.reject(
4273
- new _DOMException(
4274
- "Can't call replace on non-constructed CSSStyleSheets.",
4275
- ),
4276
- );
4277
- };
4278
- nonConstructedProto.replaceSync = function () {
4279
- throw new _DOMException(
4280
- "Failed to execute 'replaceSync' on 'CSSStyleSheet': Can't call replaceSync on non-constructed CSSStyleSheets.",
4281
- );
4282
- };
4283
- function isCSSStyleSheetInstance(instance) {
4284
- return typeof instance === "object"
4285
- ? proto$1.isPrototypeOf(instance) ||
4286
- nonConstructedProto.isPrototypeOf(instance)
4287
- : false;
4288
- }
4289
- function isNonConstructedStyleSheetInstance(instance) {
4290
- return typeof instance === "object"
4291
- ? nonConstructedProto.isPrototypeOf(instance)
4292
- : false;
4293
- }
4294
- var $basicStyleElement = new WeakMap();
4295
- var $locations = new WeakMap();
4296
- var $adoptersByLocation = new WeakMap();
4297
- var $appliedMethods = new WeakMap();
4298
- function addAdopterLocation(sheet, location) {
4299
- var adopter = document.createElement("style");
4300
- $adoptersByLocation.get(sheet).set(location, adopter);
4301
- $locations.get(sheet).push(location);
4302
- return adopter;
4303
- }
4304
- function getAdopterByLocation(sheet, location) {
4305
- return $adoptersByLocation.get(sheet).get(location);
4306
- }
4307
- function removeAdopterLocation(sheet, location) {
4308
- $adoptersByLocation.get(sheet).delete(location);
4309
- $locations.set(
4310
- sheet,
4311
- $locations.get(sheet).filter(function (_location) {
4312
- return _location !== location;
4313
- }),
4314
- );
4315
- }
4316
- function restyleAdopter(sheet, adopter) {
4317
- requestAnimationFrame(function () {
4318
- adopter.textContent = $basicStyleElement.get(sheet).textContent;
4319
- $appliedMethods.get(sheet).forEach(function (command) {
4320
- return adopter.sheet[command.method].apply(adopter.sheet, command.args);
4321
- });
4322
- });
4323
- }
4324
- function checkInvocationCorrectness(self) {
4325
- if (!$basicStyleElement.has(self)) {
4326
- throw new TypeError("Illegal invocation");
4327
- }
4328
- }
4329
- function ConstructedStyleSheet() {
4330
- var self = this;
4331
- var style = document.createElement("style");
4332
- bootstrapper.body.appendChild(style);
4333
- $basicStyleElement.set(self, style);
4334
- $locations.set(self, []);
4335
- $adoptersByLocation.set(self, new WeakMap());
4336
- $appliedMethods.set(self, []);
4337
- }
4338
- var proto$1 = ConstructedStyleSheet.prototype;
4339
- proto$1.replace = function replace(contents) {
4340
- try {
4341
- this.replaceSync(contents);
4342
- return Promise.resolve(this);
4343
- } catch (e) {
4344
- return Promise.reject(e);
4345
- }
4346
- };
4347
- proto$1.replaceSync = function replaceSync(contents) {
4348
- checkInvocationCorrectness(this);
4349
- if (typeof contents === "string") {
4350
- var self_1 = this;
4351
- $basicStyleElement.get(self_1).textContent = rejectImports(contents);
4352
- $appliedMethods.set(self_1, []);
4353
- $locations.get(self_1).forEach(function (location) {
4354
- if (location.isConnected()) {
4355
- restyleAdopter(self_1, getAdopterByLocation(self_1, location));
4356
- }
4357
- });
4358
- }
4359
- };
4360
- defineProperty(proto$1, "cssRules", {
4361
- configurable: true,
4362
- enumerable: true,
4363
- get: function cssRules() {
4364
- checkInvocationCorrectness(this);
4365
- return $basicStyleElement.get(this).sheet.cssRules;
4366
- },
4367
- });
4368
- defineProperty(proto$1, "media", {
4369
- configurable: true,
4370
- enumerable: true,
4371
- get: function media() {
4372
- checkInvocationCorrectness(this);
4373
- return $basicStyleElement.get(this).sheet.media;
4374
- },
4375
- });
4376
- cssStyleSheetMethods.forEach(function (method) {
4377
- proto$1[method] = function () {
4378
- var self = this;
4379
- checkInvocationCorrectness(self);
4380
- var args = arguments;
4381
- $appliedMethods.get(self).push({ method: method, args: args });
4382
- $locations.get(self).forEach(function (location) {
4383
- if (location.isConnected()) {
4384
- var sheet = getAdopterByLocation(self, location).sheet;
4385
- sheet[method].apply(sheet, args);
4386
- }
4387
- });
4388
- var basicSheet = $basicStyleElement.get(self).sheet;
4389
- return basicSheet[method].apply(basicSheet, args);
4390
- };
4391
- });
4392
- defineProperty(ConstructedStyleSheet, Symbol.hasInstance, {
4393
- configurable: true,
4394
- value: isCSSStyleSheetInstance,
4395
- });
4396
-
4397
- var defaultObserverOptions = {
4398
- childList: true,
4399
- subtree: true,
4400
- };
4401
- var locations = new WeakMap();
4402
- function getAssociatedLocation(element) {
4403
- var location = locations.get(element);
4404
- if (!location) {
4405
- location = new Location(element);
4406
- locations.set(element, location);
4407
- }
4408
- return location;
4409
- }
4410
- function attachAdoptedStyleSheetProperty(constructor) {
4411
- defineProperty(constructor.prototype, "adoptedStyleSheets", {
4412
- configurable: true,
4413
- enumerable: true,
4414
- get: function () {
4415
- return getAssociatedLocation(this).sheets;
4416
- },
4417
- set: function (sheets) {
4418
- getAssociatedLocation(this).update(sheets);
4419
- },
4420
- });
4421
- }
4422
- function traverseWebComponents(node, callback) {
4423
- var iter = document.createNodeIterator(
4424
- node,
4425
- NodeFilter.SHOW_ELEMENT,
4426
- function (foundNode) {
4427
- return getShadowRoot(foundNode)
4428
- ? NodeFilter.FILTER_ACCEPT
4429
- : NodeFilter.FILTER_REJECT;
4430
- },
4431
- null,
4432
- false,
4433
- );
4434
- for (var next = void 0; (next = iter.nextNode()); ) {
4435
- callback(getShadowRoot(next));
4436
- }
4437
- }
4438
- var $element = new WeakMap();
4439
- var $uniqueSheets = new WeakMap();
4440
- var $observer = new WeakMap();
4441
- function isExistingAdopter(self, element) {
4442
- return (
4443
- element instanceof HTMLStyleElement &&
4444
- $uniqueSheets.get(self).some(function (sheet) {
4445
- return getAdopterByLocation(sheet, self);
4446
- })
4447
- );
4448
- }
4449
- function getAdopterContainer(self) {
4450
- var element = $element.get(self);
4451
- return element instanceof Document ? element.body : element;
4452
- }
4453
- function adopt(self) {
4454
- var styleList = document.createDocumentFragment();
4455
- var sheets = $uniqueSheets.get(self);
4456
- var observer = $observer.get(self);
4457
- var container = getAdopterContainer(self);
4458
- observer.disconnect();
4459
- sheets.forEach(function (sheet) {
4460
- styleList.appendChild(
4461
- getAdopterByLocation(sheet, self) || addAdopterLocation(sheet, self),
4462
- );
4463
- });
4464
- container.insertBefore(styleList, null);
4465
- observer.observe(container, defaultObserverOptions);
4466
- sheets.forEach(function (sheet) {
4467
- restyleAdopter(sheet, getAdopterByLocation(sheet, self));
4468
- });
4469
- }
4470
- function Location(element) {
4471
- var self = this;
4472
- self.sheets = [];
4473
- $element.set(self, element);
4474
- $uniqueSheets.set(self, []);
4475
- $observer.set(
4476
- self,
4477
- new MutationObserver(function (mutations, observer) {
4478
- if (!document) {
4479
- observer.disconnect();
4480
- return;
4481
- }
4482
- mutations.forEach(function (mutation) {
4483
- if (!hasShadyCss) {
4484
- forEach.call(mutation.addedNodes, function (node) {
4485
- if (!(node instanceof Element)) {
4486
- return;
4487
- }
4488
- traverseWebComponents(node, function (root) {
4489
- getAssociatedLocation(root).connect();
4490
- });
4491
- });
4492
- }
4493
- forEach.call(mutation.removedNodes, function (node) {
4494
- if (!(node instanceof Element)) {
4495
- return;
4496
- }
4497
- if (isExistingAdopter(self, node)) {
4498
- adopt(self);
4499
- }
4500
- if (!hasShadyCss) {
4501
- traverseWebComponents(node, function (root) {
4502
- getAssociatedLocation(root).disconnect();
4503
- });
4504
- }
4505
- });
4506
- });
4507
- }),
4508
- );
4509
- }
4510
- Location.prototype = {
4511
- isConnected: function () {
4512
- var element = $element.get(this);
4513
- return element instanceof Document
4514
- ? element.readyState !== "loading"
4515
- : isElementConnected(element.host);
4516
- },
4517
- connect: function () {
4518
- var container = getAdopterContainer(this);
4519
- $observer.get(this).observe(container, defaultObserverOptions);
4520
- if ($uniqueSheets.get(this).length > 0) {
4521
- adopt(this);
4522
- }
4523
- traverseWebComponents(container, function (root) {
4524
- getAssociatedLocation(root).connect();
4525
- });
4526
- },
4527
- disconnect: function () {
4528
- $observer.get(this).disconnect();
4529
- },
4530
- update: function (sheets) {
4531
- var self = this;
4532
- var locationType =
4533
- $element.get(self) === document ? "Document" : "ShadowRoot";
4534
- if (!Array.isArray(sheets)) {
4535
- throw new TypeError(
4536
- "Failed to set the 'adoptedStyleSheets' property on " +
4537
- locationType +
4538
- ": Iterator getter is not callable.",
4539
- );
4540
- }
4541
- if (!sheets.every(isCSSStyleSheetInstance)) {
4542
- throw new TypeError(
4543
- "Failed to set the 'adoptedStyleSheets' property on " +
4544
- locationType +
4545
- ": Failed to convert value to 'CSSStyleSheet'",
4546
- );
4547
- }
4548
- if (sheets.some(isNonConstructedStyleSheetInstance)) {
4549
- throw new TypeError(
4550
- "Failed to set the 'adoptedStyleSheets' property on " +
4551
- locationType +
4552
- ": Can't adopt non-constructed stylesheets",
4553
- );
4554
- }
4555
- self.sheets = sheets;
4556
- var oldUniqueSheets = $uniqueSheets.get(self);
4557
- var uniqueSheets = unique(sheets);
4558
- var removedSheets = diff(oldUniqueSheets, uniqueSheets);
4559
- removedSheets.forEach(function (sheet) {
4560
- removeNode(getAdopterByLocation(sheet, self));
4561
- removeAdopterLocation(sheet, self);
4562
- });
4563
- $uniqueSheets.set(self, uniqueSheets);
4564
- if (self.isConnected() && uniqueSheets.length > 0) {
4565
- adopt(self);
4566
- }
4567
- },
4568
- };
4569
-
4570
- window.CSSStyleSheet = ConstructedStyleSheet;
4571
- attachAdoptedStyleSheetProperty(Document);
4572
- if ("ShadowRoot" in window) {
4573
- attachAdoptedStyleSheetProperty(ShadowRoot);
4574
- var proto = Element.prototype;
4575
- var attach_1 = proto.attachShadow;
4576
- proto.attachShadow = function attachShadow(init) {
4577
- var root = attach_1.call(this, init);
4578
- if (init.mode === "closed") {
4579
- closedShadowRootRegistry.set(this, root);
4580
- }
4581
- return root;
4582
- };
4583
- }
4584
- var documentLocation = getAssociatedLocation(document);
4585
- if (documentLocation.isConnected()) {
4586
- documentLocation.connect();
4587
- } else {
4588
- document.addEventListener(
4589
- "DOMContentLoaded",
4590
- documentLocation.connect.bind(documentLocation),
4591
- );
4592
- }
4593
- })();
4345
+ const installImportMetaCss = (importMeta) => {
4346
+ const stylesheet = new CSSStyleSheet({ baseUrl: importMeta.url });
4594
4347
 
4595
- const installImportMetaCss = importMeta => {
4596
- const stylesheet = new CSSStyleSheet({
4597
- baseUrl: importMeta.url
4598
- });
4599
4348
  let called = false;
4600
4349
  // eslint-disable-next-line accessor-pairs
4601
4350
  Object.defineProperty(importMeta, "css", {
@@ -4606,8 +4355,11 @@ const installImportMetaCss = importMeta => {
4606
4355
  }
4607
4356
  called = true;
4608
4357
  stylesheet.replaceSync(value);
4609
- document.adoptedStyleSheets = [...document.adoptedStyleSheets, stylesheet];
4610
- }
4358
+ document.adoptedStyleSheets = [
4359
+ ...document.adoptedStyleSheets,
4360
+ stylesheet,
4361
+ ];
4362
+ },
4611
4363
  });
4612
4364
  };
4613
4365
 
@@ -5666,15 +5418,6 @@ const getScrollport = (scrollBox, scrollContainer) => {
5666
5418
  };
5667
5419
  };
5668
5420
 
5669
- const getElementSelector = (element) => {
5670
- const tagName = element.tagName.toLowerCase();
5671
- const id = element.id ? `#${element.id}` : "";
5672
- const className = element.className
5673
- ? `.${element.className.split(" ").join(".")}`
5674
- : "";
5675
- return `${tagName}${id}${className}`;
5676
- };
5677
-
5678
5421
  installImportMetaCss(import.meta);const setupConstraintFeedbackLine = () => {
5679
5422
  const constraintFeedbackLine = createConstraintFeedbackLine();
5680
5423
 
@@ -6700,7 +6443,7 @@ const createObstacleConstraintsFromQuerySelector = (
6700
6443
 
6701
6444
  // obstacleBounds are already in scrollable-relative coordinates, no conversion needed
6702
6445
  const obstacleObject = createObstacleContraint(obstacleBounds, {
6703
- name: `${obstacleBounds.isSticky ? "sticky " : ""}obstacle (${getElementSelector(obstacle)})`,
6446
+ name: `${obstacleBounds.isSticky ? "sticky " : ""}obstacle (${getElementSignature(obstacle)})`,
6704
6447
  element: obstacle,
6705
6448
  });
6706
6449
  return obstacleObject;
@@ -6978,9 +6721,9 @@ const createStickyFrontierOnAxis = (
6978
6721
  const hasOpposite = frontier.hasAttribute(oppositeAttrName);
6979
6722
  // Check if element has both sides (invalid)
6980
6723
  if (hasPrimary && hasOpposite) {
6981
- const elementSelector = getElementSelector(frontier);
6724
+ const elementSignature = getElementSignature(frontier);
6982
6725
  console.warn(
6983
- `Sticky frontier element (${elementSelector}) has both ${primarySide} and ${oppositeSide} attributes.
6726
+ `Sticky frontier element (${elementSignature}) has both ${primarySide} and ${oppositeSide} attributes.
6984
6727
  A sticky frontier should only have one side attribute.`,
6985
6728
  );
6986
6729
  continue;
@@ -7003,7 +6746,7 @@ const createStickyFrontierOnAxis = (
7003
6746
  element: frontier,
7004
6747
  side: hasPrimary ? primarySide : oppositeSide,
7005
6748
  bounds: frontierBounds,
7006
- name: `sticky_frontier_${hasPrimary ? primarySide : oppositeSide} (${getElementSelector(frontier)})`,
6749
+ name: `sticky_frontier_${hasPrimary ? primarySide : oppositeSide} (${getElementSignature(frontier)})`,
7007
6750
  };
7008
6751
  matchingStickyFrontiers.push(stickyFrontierObject);
7009
6752
  }
@@ -10544,34 +10287,21 @@ const useResizeStatus = (elementRef, { as = "number" } = {}) => {
10544
10287
 
10545
10288
  installImportMetaCss(import.meta);
10546
10289
  import.meta.css = /* css */ `
10547
- .ui_transition_container {
10548
- position: relative;
10549
- display: inline-flex;
10550
- flex: 1;
10551
- }
10552
-
10553
- .ui_transition_outer_wrapper {
10554
- display: inline-flex;
10555
- flex: 1;
10556
- }
10557
-
10558
- .ui_transition_measure_wrapper {
10290
+ .ui_transition_container,
10291
+ .ui_transition_outer_wrapper,
10292
+ .ui_transition_measure_wrapper,
10293
+ .ui_transition_slot {
10559
10294
  display: inline-flex;
10560
- flex: 1;
10295
+ width: fit-content;
10296
+ height: fit-content;
10561
10297
  }
10562
10298
 
10299
+ .ui_transition_container,
10563
10300
  .ui_transition_slot {
10564
10301
  position: relative;
10565
- display: inline-flex;
10566
- flex: 1;
10567
- }
10568
-
10569
- .ui_transition_phase_overlay {
10570
- position: absolute;
10571
- inset: 0;
10572
- pointer-events: none;
10573
10302
  }
10574
10303
 
10304
+ .ui_transition_phase_overlay,
10575
10305
  .ui_transition_content_overlay {
10576
10306
  position: absolute;
10577
10307
  inset: 0;
@@ -10948,7 +10678,7 @@ const initUITransition = (container) => {
10948
10678
  debug(
10949
10679
  "transition",
10950
10680
  `Cloned previous child for ${isPhaseTransition ? "phase" : "content"} transition:`,
10951
- previousChild.getAttribute("data-ui-name") || "unnamed",
10681
+ getElementSignature(previousChild),
10952
10682
  );
10953
10683
  cleanup = () => oldChild.remove();
10954
10684
  } else {
@@ -11000,7 +10730,7 @@ const initUITransition = (container) => {
11000
10730
  if (localDebug.transition) {
11001
10731
  const updateLabel =
11002
10732
  childUIName ||
11003
- (firstChild ? "data-ui-name not specified" : "cleared/empty");
10733
+ (firstChild ? getElementSignature(firstChild) : "cleared/empty");
11004
10734
  console.group(`UI Update: ${updateLabel} (reason: ${reason})`);
11005
10735
  }
11006
10736
 
@@ -11604,7 +11334,7 @@ const initUITransition = (container) => {
11604
11334
  debug(
11605
11335
  "transition",
11606
11336
  `Attribute change detected: ${attributeName} on`,
11607
- target.getAttribute("data-ui-name") || "element",
11337
+ getElementSignature(target),
11608
11338
  );
11609
11339
  }
11610
11340
  }
@@ -11976,4 +11706,4 @@ const crossFade = {
11976
11706
  },
11977
11707
  };
11978
11708
 
11979
- export { EASING, activeElementSignal, addActiveElementEffect, addAttributeEffect, addWillChange, allowWheelThrough, canInterceptKeys, captureScrollState, createDragGestureController, createDragToMoveGestureController, createHeightTransition, createIterableWeakSet, createOpacityTransition, createPubSub, createStyleController, createTimelineTransition, createTransition, createTranslateXTransition, createValueEffect, createWidthTransition, cubicBezier, dragAfterThreshold, elementIsFocusable, elementIsVisibleForFocus, elementIsVisuallyVisible, findAfter, findAncestor, findBefore, findDescendant, findFocusable, getAvailableHeight, getAvailableWidth, getBorderSizes, getContrastRatio, getDefaultStyles, getDragCoordinates, getDropTargetInfo, getFirstVisuallyVisibleAncestor, getFocusVisibilityInfo, getHeight, getInnerHeight, getInnerWidth, getMarginSizes, getMaxHeight, getMaxWidth, getMinHeight, getMinWidth, getPaddingSizes, getPositionedParent, getPreferedColorScheme, getScrollContainer, getScrollContainerSet, getScrollRelativeRect, getSelfAndAncestorScrolls, getStyle, getVisuallyVisibleInfo, getWidth, initFlexDetailsSet, initFocusGroup, initPositionSticky, initUITransition, isScrollable, mergeStyles, normalizeStyles, parseCSSColor, pickLightOrDark, pickPositionRelativeTo, prefersDarkColors, prefersLightColors, preventFocusNav, preventFocusNavViaKeyboard, resolveCSSColor, resolveCSSSize, setAttribute, setAttributes, setStyles, startDragToResizeGesture, stickyAsRelativeCoords, stringifyCSSColor, trapFocusInside, trapScrollInside, useActiveElement, useAvailableHeight, useAvailableWidth, useMaxHeight, useMaxWidth, useResizeStatus, visibleRectEffect };
11709
+ export { EASING, activeElementSignal, addActiveElementEffect, addAttributeEffect, addWillChange, allowWheelThrough, appendStyles, canInterceptKeys, captureScrollState, createDragGestureController, createDragToMoveGestureController, createHeightTransition, createIterableWeakSet, createOpacityTransition, createPubSub, createStyleController, createTimelineTransition, createTransition, createTranslateXTransition, createValueEffect, createWidthTransition, cubicBezier, dragAfterThreshold, elementIsFocusable, elementIsVisibleForFocus, elementIsVisuallyVisible, findAfter, findAncestor, findBefore, findDescendant, findFocusable, getAvailableHeight, getAvailableWidth, getBorderSizes, getContrastRatio, getDefaultStyles, getDragCoordinates, getDropTargetInfo, getElementSignature, getFirstVisuallyVisibleAncestor, getFocusVisibilityInfo, getHeight, getInnerHeight, getInnerWidth, getMarginSizes, getMaxHeight, getMaxWidth, getMinHeight, getMinWidth, getPaddingSizes, getPositionedParent, getPreferedColorScheme, getScrollContainer, getScrollContainerSet, getScrollRelativeRect, getSelfAndAncestorScrolls, getStyle, getVisuallyVisibleInfo, getWidth, initFlexDetailsSet, initFocusGroup, initPositionSticky, initUITransition, isScrollable, mergeStyles, normalizeStyles, parseCSSColor, pickLightOrDark, pickPositionRelativeTo, prefersDarkColors, prefersLightColors, preventFocusNav, preventFocusNavViaKeyboard, resolveCSSColor, resolveCSSSize, setAttribute, setAttributes, setStyles, startDragToResizeGesture, stickyAsRelativeCoords, stringifyCSSColor, trapFocusInside, trapScrollInside, useActiveElement, useAvailableHeight, useAvailableWidth, useMaxHeight, useMaxWidth, useResizeStatus, visibleRectEffect };
package/index.js CHANGED
@@ -1,3 +1,5 @@
1
+ export { getElementSignature } from "./src/element_signature.js";
2
+
1
3
  // state management
2
4
  export { createIterableWeakSet } from "./src/iterable_weak_set.js";
3
5
  export { createPubSub } from "./src/pub_sub.js";
@@ -5,7 +7,7 @@ export { createValueEffect } from "./src/value_effect.js";
5
7
 
6
8
  // style
7
9
  export { addWillChange, getStyle, setStyles } from "./src/style/dom_styles.js";
8
- export { mergeStyles } from "./src/style/style_composition.js";
10
+ export { appendStyles, mergeStyles } from "./src/style/style_composition.js";
9
11
  export { createStyleController } from "./src/style/style_controller.js";
10
12
  export { getDefaultStyles } from "./src/style/style_default.js";
11
13
  export { normalizeStyles } from "./src/style/style_parsing.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/dom",
3
- "version": "0.5.3",
3
+ "version": "0.6.1",
4
4
  "description": "DOM utilities for writing frontend code",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Generates a unique signature for various types of elements that can be used for identification in logs.
3
+ *
4
+ * This function handles different types of elements and returns an appropriate identifier:
5
+ * - For DOM elements: Creates a CSS selector using tag name, data-ui-name, ID, classes, or parent hierarchy
6
+ * - For React/Preact elements (JSX): Returns JSX-like representation with type and props
7
+ * - For functions: Returns function name and optional underlying element reference in brackets
8
+ * - For null/undefined: Returns the string representation
9
+ *
10
+ * The returned signature for DOM elements is a valid CSS selector that can be copy-pasted
11
+ * into browser dev tools to locate the element in the DOM.
12
+ *
13
+ * @param {HTMLElement|Object|Function|null|undefined} element - The element to generate a signature for
14
+ * @returns {string} A unique identifier string in various formats depending on element type
15
+ *
16
+ * @example
17
+ * // For DOM element with data-ui-name
18
+ * // <div data-ui-name="header">
19
+ * getElementSignature(element) // Returns: `div[data-ui-name="header"]`
20
+ *
21
+ * @example
22
+ * // For DOM element with ID
23
+ * // <div id="main" class="container active">
24
+ * getElementSignature(element) // Returns: "div#main"
25
+ *
26
+ * @example
27
+ * // For DOM element with classes only
28
+ * // <button class="btn primary">
29
+ * getElementSignature(element) // Returns: "button.btn.primary"
30
+ *
31
+ * @example
32
+ * // For DOM element without distinguishing features (uses parent hierarchy)
33
+ * // <p> inside <section id="content">
34
+ * getElementSignature(element) // Returns: "section#content > p"
35
+ *
36
+ * @example
37
+ * // For React/Preact element with props
38
+ * // <MyComponent id="widget" />
39
+ * getElementSignature(element) // Returns: `<MyComponent id="widget" />`
40
+ *
41
+ * @example
42
+ * // For named function with underlying element reference
43
+ * const MyComponent = () => {}; MyComponent.underlyingElementId = "div#main";
44
+ * getElementSignature(MyComponent) // Returns: "[function MyComponent for div#main]"
45
+ *
46
+ * @example
47
+ * // For anonymous function without underlying element
48
+ * const anonymousFunc = () => {};
49
+ * getElementSignature(anonymousFunc) // Returns: "[function]"
50
+ *
51
+ * @example
52
+ * // For named function without underlying element
53
+ * function namedHandler() {}
54
+ * getElementSignature(namedHandler) // Returns: "[function namedHandler]"
55
+ *
56
+ * @example
57
+ * // For null/undefined
58
+ * getElementSignature(null) // Returns: "null"
59
+ */
60
+ export const getElementSignature = (element) => {
61
+ if (!element) {
62
+ return String(element);
63
+ }
64
+ if (typeof element === "function") {
65
+ const functionName = element.name;
66
+ const functionLabel = functionName
67
+ ? `function ${functionName}`
68
+ : "function";
69
+ const underlyingElementId = element.underlyingElementId;
70
+ if (underlyingElementId) {
71
+ return `[${functionLabel} for ${underlyingElementId}]`;
72
+ }
73
+ return `[${functionLabel}]`;
74
+ }
75
+ if (element.props) {
76
+ const type = element.type;
77
+ const id = element.props.id;
78
+ if (id) {
79
+ return `<${type} id="${id}" />`;
80
+ }
81
+ return `<${type} />`;
82
+ }
83
+
84
+ const tagName = element.tagName.toLowerCase();
85
+ const dataUIName = element.getAttribute("data-ui-name");
86
+ if (dataUIName) {
87
+ return `${tagName}[data-ui-name="${dataUIName}"]`;
88
+ }
89
+ const elementId = element.id;
90
+ if (elementId) {
91
+ return `${tagName}#${elementId}`;
92
+ }
93
+ const className = element.className;
94
+ if (className) {
95
+ return `${tagName}.${className.split(" ").join(".")}`;
96
+ }
97
+
98
+ const parentSignature = getElementSignature(element.parentElement);
99
+ return `${parentSignature} > ${tagName}`;
100
+ };
@@ -1,8 +1,8 @@
1
+ import { getElementSignature } from "../../element_signature.js";
1
2
  import {
2
3
  addScrollToRect,
3
4
  getScrollRelativeRect,
4
5
  } from "../../position/dom_coords.js";
5
- import { getElementSelector } from "../element_log.js";
6
6
  import { setupConstraintFeedbackLine } from "./constraint_feedback_line.js";
7
7
  import { setupDragDebugMarkers } from "./drag_debug_markers.js";
8
8
 
@@ -333,7 +333,7 @@ const createObstacleConstraintsFromQuerySelector = (
333
333
 
334
334
  // obstacleBounds are already in scrollable-relative coordinates, no conversion needed
335
335
  const obstacleObject = createObstacleContraint(obstacleBounds, {
336
- name: `${obstacleBounds.isSticky ? "sticky " : ""}obstacle (${getElementSelector(obstacle)})`,
336
+ name: `${obstacleBounds.isSticky ? "sticky " : ""}obstacle (${getElementSignature(obstacle)})`,
337
337
  element: obstacle,
338
338
  });
339
339
  return obstacleObject;
@@ -1,5 +1,5 @@
1
+ import { getElementSignature } from "../../element_signature.js";
1
2
  import { getScrollRelativeRect } from "../../position/dom_coords.js";
2
- import { getElementSelector } from "../element_log.js";
3
3
 
4
4
  export const applyStickyFrontiersToAutoScrollArea = (
5
5
  autoScrollArea,
@@ -127,9 +127,9 @@ const createStickyFrontierOnAxis = (
127
127
  const hasOpposite = frontier.hasAttribute(oppositeAttrName);
128
128
  // Check if element has both sides (invalid)
129
129
  if (hasPrimary && hasOpposite) {
130
- const elementSelector = getElementSelector(frontier);
130
+ const elementSignature = getElementSignature(frontier);
131
131
  console.warn(
132
- `Sticky frontier element (${elementSelector}) has both ${primarySide} and ${oppositeSide} attributes.
132
+ `Sticky frontier element (${elementSignature}) has both ${primarySide} and ${oppositeSide} attributes.
133
133
  A sticky frontier should only have one side attribute.`,
134
134
  );
135
135
  continue;
@@ -152,7 +152,7 @@ const createStickyFrontierOnAxis = (
152
152
  element: frontier,
153
153
  side: hasPrimary ? primarySide : oppositeSide,
154
154
  bounds: frontierBounds,
155
- name: `sticky_frontier_${hasPrimary ? primarySide : oppositeSide} (${getElementSelector(frontier)})`,
155
+ name: `sticky_frontier_${hasPrimary ? primarySide : oppositeSide} (${getElementSignature(frontier)})`,
156
156
  };
157
157
  matchingStickyFrontiers.push(stickyFrontierObject);
158
158
  }
@@ -33,6 +33,29 @@ export const mergeStyles = (stylesA, stylesB, context = "js") => {
33
33
  return result;
34
34
  };
35
35
 
36
+ export const appendStyles = (
37
+ stylesAObject,
38
+ stylesBNormalized,
39
+ context = "js",
40
+ ) => {
41
+ const aKeys = Object.keys(stylesAObject);
42
+ const bKeys = Object.keys(stylesBNormalized);
43
+ for (const bKey of bKeys) {
44
+ const aHasKey = aKeys.includes(bKey);
45
+ if (aHasKey) {
46
+ stylesAObject[bKey] = mergeOneStyle(
47
+ stylesAObject[bKey],
48
+ stylesBNormalized[bKey],
49
+ bKey,
50
+ context,
51
+ );
52
+ } else {
53
+ stylesAObject[bKey] = stylesBNormalized[bKey];
54
+ }
55
+ }
56
+ return stylesAObject;
57
+ };
58
+
36
59
  // Merge a single style property value with an existing value
37
60
  export const mergeOneStyle = (
38
61
  existingValue,
@@ -151,6 +151,9 @@ const normalizeNumber = (value, context, unit, propertyName) => {
151
151
 
152
152
  // Normalize styles for DOM application
153
153
  export const normalizeStyles = (styles, context = "js") => {
154
+ if (!styles) {
155
+ return {};
156
+ }
154
157
  if (typeof styles === "string") {
155
158
  styles = parseStyleString(styles);
156
159
  return styles;
@@ -1,34 +1,26 @@
1
1
  /**
2
2
  * Required HTML structure for UI transitions with smooth size and phase/content animations:
3
3
  *
4
- * <div class="ui_transition_container"
5
- * data-size-transition <!-- Optional: enable size animations -->
6
- * data-size-transition-duration <!-- Optional: size transition duration, default 300ms -->
7
- * data-content-transition <!-- Content transition type: cross-fade, slide-left -->
8
- * data-content-transition-duration <!-- Content transition duration -->
9
- * data-phase-transition <!-- Phase transition type: cross-fade only -->
10
- * data-phase-transition-duration <!-- Phase transition duration -->
4
+ * <div
5
+ * class="ui_transition_container" <!-- Main container with relative positioning and overflow hidden -->
6
+ * data-size-transition <!-- Optional: enable size animations -->
7
+ * data-size-transition-duration <!-- Optional: size transition duration, default 300ms -->
8
+ * data-content-transition <!-- Content transition type: cross-fade, slide-left -->
9
+ * data-content-transition-duration <!-- Content transition duration -->
10
+ * data-phase-transition <!-- Phase transition type: cross-fade only -->
11
+ * data-phase-transition-duration <!-- Phase transition duration -->
11
12
  * >
12
- * <!-- Main container with relative positioning and overflow hidden -->
13
- *
14
- * <div class="ui_transition_outer_wrapper">
15
- * <!-- Size animation target: width/height constraints are applied here during transitions -->
16
- *
17
- * <div class="ui_transition_measure_wrapper">
18
- * <!-- Content measurement layer: ResizeObserver watches this to detect natural content size changes -->
19
- *
20
- * <div class="ui_transition_slot" data-content-key>
21
- * <!-- Content slot: actual content is inserted here via children -->
22
- * </div>
23
- *
24
- * <div class="ui_transition_phase_overlay">
25
- * <!-- Phase transition overlay: clone old content phase is positioned here for content phase transitions (loading/error) -->
26
- * </div>
13
+ * <div class="ui_transition_outer_wrapper"> <!-- Size animation target: width/height constraints are applied here during transitions -->
14
+ * <div class="ui_transition_measure_wrapper"> <!-- Content measurement layer: ResizeObserver watches this to detect natural content size changes -->
15
+ * <div class="ui_transition_slot" data-content-key></div> <!-- Content slot: actual content is here -->
16
+ * <div class="ui_transition_phase_overlay"> <!-- Used to transition to new phase: crossfade to new phase -->
17
+ * <!-- Clone of ".ui_transition_slot" children for phase transition -->
18
+ * </div>
27
19
  * </div>
28
20
  * </div>
29
21
  *
30
- * <div class="ui_transition_content_overlay">
31
- * <!-- Content transition overlay: cloned old content is positioned here for slide/fade animations -->
22
+ * <div class="ui_transition_content_overlay"> <!-- Used to transition to new content: crossfade/slide to new content -->
23
+ * <!-- Clone of ".ui_transition_slot" children for content transition -->
32
24
  * </div>
33
25
  * </div>
34
26
  *
@@ -41,6 +33,7 @@
41
33
  * - Independent content updates in the slot without affecting ongoing animations
42
34
  */
43
35
 
36
+ import { getElementSignature } from "../element_signature.js";
44
37
  import { getHeight } from "../size/get_height.js";
45
38
  import { getInnerWidth } from "../size/get_inner_width.js";
46
39
  import { getWidth } from "../size/get_width.js";
@@ -57,34 +50,21 @@ import {
57
50
  import { createGroupTransitionController } from "../transition/group_transition.js";
58
51
 
59
52
  import.meta.css = /* css */ `
60
- .ui_transition_container {
61
- position: relative;
62
- display: inline-flex;
63
- flex: 1;
64
- }
65
-
66
- .ui_transition_outer_wrapper {
67
- display: inline-flex;
68
- flex: 1;
69
- }
70
-
71
- .ui_transition_measure_wrapper {
53
+ .ui_transition_container,
54
+ .ui_transition_outer_wrapper,
55
+ .ui_transition_measure_wrapper,
56
+ .ui_transition_slot {
72
57
  display: inline-flex;
73
- flex: 1;
58
+ width: fit-content;
59
+ height: fit-content;
74
60
  }
75
61
 
62
+ .ui_transition_container,
76
63
  .ui_transition_slot {
77
64
  position: relative;
78
- display: inline-flex;
79
- flex: 1;
80
- }
81
-
82
- .ui_transition_phase_overlay {
83
- position: absolute;
84
- inset: 0;
85
- pointer-events: none;
86
65
  }
87
66
 
67
+ .ui_transition_phase_overlay,
88
68
  .ui_transition_content_overlay {
89
69
  position: absolute;
90
70
  inset: 0;
@@ -461,7 +441,7 @@ export const initUITransition = (container) => {
461
441
  debug(
462
442
  "transition",
463
443
  `Cloned previous child for ${isPhaseTransition ? "phase" : "content"} transition:`,
464
- previousChild.getAttribute("data-ui-name") || "unnamed",
444
+ getElementSignature(previousChild),
465
445
  );
466
446
  cleanup = () => oldChild.remove();
467
447
  } else {
@@ -513,7 +493,7 @@ export const initUITransition = (container) => {
513
493
  if (localDebug.transition) {
514
494
  const updateLabel =
515
495
  childUIName ||
516
- (firstChild ? "data-ui-name not specified" : "cleared/empty");
496
+ (firstChild ? getElementSignature(firstChild) : "cleared/empty");
517
497
  console.group(`UI Update: ${updateLabel} (reason: ${reason})`);
518
498
  }
519
499
 
@@ -1117,7 +1097,7 @@ export const initUITransition = (container) => {
1117
1097
  debug(
1118
1098
  "transition",
1119
1099
  `Attribute change detected: ${attributeName} on`,
1120
- target.getAttribute("data-ui-name") || "element",
1100
+ getElementSignature(target),
1121
1101
  );
1122
1102
  }
1123
1103
  }
@@ -1,21 +1,25 @@
1
1
  export const createValueEffect = (value) => {
2
2
  const callbackSet = new Set();
3
- const previousValueCleanupSet = new Set();
3
+ const valueCleanupSet = new Set();
4
+
5
+ const cleanup = () => {
6
+ for (const valueCleanup of valueCleanupSet) {
7
+ valueCleanup();
8
+ }
9
+ valueCleanupSet.clear();
10
+ };
4
11
 
5
12
  const updateValue = (newValue) => {
6
13
  if (newValue === value) {
7
14
  return;
8
15
  }
9
- for (const cleanup of previousValueCleanupSet) {
10
- cleanup();
11
- }
12
- previousValueCleanupSet.clear();
16
+ cleanup();
13
17
  const oldValue = value;
14
18
  value = newValue;
15
19
  for (const callback of callbackSet) {
16
20
  const returnValue = callback(newValue, oldValue);
17
21
  if (typeof returnValue === "function") {
18
- previousValueCleanupSet.add(returnValue);
22
+ valueCleanupSet.add(returnValue);
19
23
  }
20
24
  }
21
25
  };
@@ -27,5 +31,5 @@ export const createValueEffect = (value) => {
27
31
  };
28
32
  };
29
33
 
30
- return [updateValue, addEffect];
34
+ return [updateValue, addEffect, cleanup];
31
35
  };
@@ -1,8 +0,0 @@
1
- export const getElementSelector = (element) => {
2
- const tagName = element.tagName.toLowerCase();
3
- const id = element.id ? `#${element.id}` : "";
4
- const className = element.className
5
- ? `.${element.className.split(" ").join(".")}`
6
- : "";
7
- return `${tagName}${id}${className}`;
8
- };