@jsenv/dom 0.5.2 → 0.6.0

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
@@ -418,26 +523,24 @@ const normalizeNumber = (value, context, unit, propertyName) => {
418
523
  return value;
419
524
  }
420
525
  if (typeof value === "string") {
421
- if (value === "auto") {
422
- return "auto";
423
- }
424
- if (value === "none") {
425
- return "none";
426
- }
427
- const numericValue = parseFloat(value);
428
- if (isNaN(numericValue)) {
429
- console.warn(
430
- `"${propertyName}": ${value} cannot be converted to number, returning value as-is.`,
431
- );
432
- return value;
526
+ // For js context, only convert px values to numbers
527
+ if (unit === "px" && value.endsWith("px")) {
528
+ const numericValue = parseFloat(value);
529
+ if (!isNaN(numericValue)) {
530
+ return numericValue;
531
+ }
433
532
  }
434
- return numericValue;
533
+ // Keep all other strings as-is (including %, em, rem, auto, none, etc.)
534
+ return value;
435
535
  }
436
536
  return value;
437
537
  };
438
538
 
439
539
  // Normalize styles for DOM application
440
540
  const normalizeStyles = (styles, context = "js") => {
541
+ if (!styles) {
542
+ return {};
543
+ }
441
544
  if (typeof styles === "string") {
442
545
  styles = parseStyleString(styles);
443
546
  return styles;
@@ -686,6 +789,29 @@ const mergeStyles = (stylesA, stylesB, context = "js") => {
686
789
  return result;
687
790
  };
688
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
+
689
815
  // Merge a single style property value with an existing value
690
816
  const mergeOneStyle = (
691
817
  existingValue,
@@ -4216,391 +4342,9 @@ const getDragCoordinates = (
4216
4342
  return [leftRelativeToScrollContainer, topRelativeToScrollContainer];
4217
4343
  };
4218
4344
 
4219
- /* eslint-disable */
4220
- // construct-style-sheets-polyfill@3.1.0
4221
- // to keep in sync with https://github.com/calebdwilliams/construct-style-sheets
4222
- // copy pasted into jsenv codebase to inject this code with more ease
4223
- (function () {
4224
-
4225
- if (typeof document === "undefined" || "adoptedStyleSheets" in document) {
4226
- return;
4227
- }
4228
-
4229
- var hasShadyCss = "ShadyCSS" in window && !ShadyCSS.nativeShadow;
4230
- var bootstrapper = document.implementation.createHTMLDocument("");
4231
- var closedShadowRootRegistry = new WeakMap();
4232
- var _DOMException = typeof DOMException === "object" ? Error : DOMException;
4233
- var defineProperty = Object.defineProperty;
4234
- var forEach = Array.prototype.forEach;
4235
-
4236
- var importPattern = /@import.+?;?$/gm;
4237
- function rejectImports(contents) {
4238
- var _contents = contents.replace(importPattern, "");
4239
- if (_contents !== contents) {
4240
- console.warn(
4241
- "@import rules are not allowed here. See https://github.com/WICG/construct-stylesheets/issues/119#issuecomment-588352418",
4242
- );
4243
- }
4244
- return _contents.trim();
4245
- }
4246
- function isElementConnected(element) {
4247
- return "isConnected" in element
4248
- ? element.isConnected
4249
- : document.contains(element);
4250
- }
4251
- function unique(arr) {
4252
- return arr.filter(function (value, index) {
4253
- return arr.indexOf(value) === index;
4254
- });
4255
- }
4256
- function diff(arr1, arr2) {
4257
- return arr1.filter(function (value) {
4258
- return arr2.indexOf(value) === -1;
4259
- });
4260
- }
4261
- function removeNode(node) {
4262
- node.parentNode.removeChild(node);
4263
- }
4264
- function getShadowRoot(element) {
4265
- return element.shadowRoot || closedShadowRootRegistry.get(element);
4266
- }
4345
+ const installImportMetaCss = (importMeta) => {
4346
+ const stylesheet = new CSSStyleSheet({ baseUrl: importMeta.url });
4267
4347
 
4268
- var cssStyleSheetMethods = [
4269
- "addRule",
4270
- "deleteRule",
4271
- "insertRule",
4272
- "removeRule",
4273
- ];
4274
- var NonConstructedStyleSheet = CSSStyleSheet;
4275
- var nonConstructedProto = NonConstructedStyleSheet.prototype;
4276
- nonConstructedProto.replace = function () {
4277
- return Promise.reject(
4278
- new _DOMException(
4279
- "Can't call replace on non-constructed CSSStyleSheets.",
4280
- ),
4281
- );
4282
- };
4283
- nonConstructedProto.replaceSync = function () {
4284
- throw new _DOMException(
4285
- "Failed to execute 'replaceSync' on 'CSSStyleSheet': Can't call replaceSync on non-constructed CSSStyleSheets.",
4286
- );
4287
- };
4288
- function isCSSStyleSheetInstance(instance) {
4289
- return typeof instance === "object"
4290
- ? proto$1.isPrototypeOf(instance) ||
4291
- nonConstructedProto.isPrototypeOf(instance)
4292
- : false;
4293
- }
4294
- function isNonConstructedStyleSheetInstance(instance) {
4295
- return typeof instance === "object"
4296
- ? nonConstructedProto.isPrototypeOf(instance)
4297
- : false;
4298
- }
4299
- var $basicStyleElement = new WeakMap();
4300
- var $locations = new WeakMap();
4301
- var $adoptersByLocation = new WeakMap();
4302
- var $appliedMethods = new WeakMap();
4303
- function addAdopterLocation(sheet, location) {
4304
- var adopter = document.createElement("style");
4305
- $adoptersByLocation.get(sheet).set(location, adopter);
4306
- $locations.get(sheet).push(location);
4307
- return adopter;
4308
- }
4309
- function getAdopterByLocation(sheet, location) {
4310
- return $adoptersByLocation.get(sheet).get(location);
4311
- }
4312
- function removeAdopterLocation(sheet, location) {
4313
- $adoptersByLocation.get(sheet).delete(location);
4314
- $locations.set(
4315
- sheet,
4316
- $locations.get(sheet).filter(function (_location) {
4317
- return _location !== location;
4318
- }),
4319
- );
4320
- }
4321
- function restyleAdopter(sheet, adopter) {
4322
- requestAnimationFrame(function () {
4323
- adopter.textContent = $basicStyleElement.get(sheet).textContent;
4324
- $appliedMethods.get(sheet).forEach(function (command) {
4325
- return adopter.sheet[command.method].apply(adopter.sheet, command.args);
4326
- });
4327
- });
4328
- }
4329
- function checkInvocationCorrectness(self) {
4330
- if (!$basicStyleElement.has(self)) {
4331
- throw new TypeError("Illegal invocation");
4332
- }
4333
- }
4334
- function ConstructedStyleSheet() {
4335
- var self = this;
4336
- var style = document.createElement("style");
4337
- bootstrapper.body.appendChild(style);
4338
- $basicStyleElement.set(self, style);
4339
- $locations.set(self, []);
4340
- $adoptersByLocation.set(self, new WeakMap());
4341
- $appliedMethods.set(self, []);
4342
- }
4343
- var proto$1 = ConstructedStyleSheet.prototype;
4344
- proto$1.replace = function replace(contents) {
4345
- try {
4346
- this.replaceSync(contents);
4347
- return Promise.resolve(this);
4348
- } catch (e) {
4349
- return Promise.reject(e);
4350
- }
4351
- };
4352
- proto$1.replaceSync = function replaceSync(contents) {
4353
- checkInvocationCorrectness(this);
4354
- if (typeof contents === "string") {
4355
- var self_1 = this;
4356
- $basicStyleElement.get(self_1).textContent = rejectImports(contents);
4357
- $appliedMethods.set(self_1, []);
4358
- $locations.get(self_1).forEach(function (location) {
4359
- if (location.isConnected()) {
4360
- restyleAdopter(self_1, getAdopterByLocation(self_1, location));
4361
- }
4362
- });
4363
- }
4364
- };
4365
- defineProperty(proto$1, "cssRules", {
4366
- configurable: true,
4367
- enumerable: true,
4368
- get: function cssRules() {
4369
- checkInvocationCorrectness(this);
4370
- return $basicStyleElement.get(this).sheet.cssRules;
4371
- },
4372
- });
4373
- defineProperty(proto$1, "media", {
4374
- configurable: true,
4375
- enumerable: true,
4376
- get: function media() {
4377
- checkInvocationCorrectness(this);
4378
- return $basicStyleElement.get(this).sheet.media;
4379
- },
4380
- });
4381
- cssStyleSheetMethods.forEach(function (method) {
4382
- proto$1[method] = function () {
4383
- var self = this;
4384
- checkInvocationCorrectness(self);
4385
- var args = arguments;
4386
- $appliedMethods.get(self).push({ method: method, args: args });
4387
- $locations.get(self).forEach(function (location) {
4388
- if (location.isConnected()) {
4389
- var sheet = getAdopterByLocation(self, location).sheet;
4390
- sheet[method].apply(sheet, args);
4391
- }
4392
- });
4393
- var basicSheet = $basicStyleElement.get(self).sheet;
4394
- return basicSheet[method].apply(basicSheet, args);
4395
- };
4396
- });
4397
- defineProperty(ConstructedStyleSheet, Symbol.hasInstance, {
4398
- configurable: true,
4399
- value: isCSSStyleSheetInstance,
4400
- });
4401
-
4402
- var defaultObserverOptions = {
4403
- childList: true,
4404
- subtree: true,
4405
- };
4406
- var locations = new WeakMap();
4407
- function getAssociatedLocation(element) {
4408
- var location = locations.get(element);
4409
- if (!location) {
4410
- location = new Location(element);
4411
- locations.set(element, location);
4412
- }
4413
- return location;
4414
- }
4415
- function attachAdoptedStyleSheetProperty(constructor) {
4416
- defineProperty(constructor.prototype, "adoptedStyleSheets", {
4417
- configurable: true,
4418
- enumerable: true,
4419
- get: function () {
4420
- return getAssociatedLocation(this).sheets;
4421
- },
4422
- set: function (sheets) {
4423
- getAssociatedLocation(this).update(sheets);
4424
- },
4425
- });
4426
- }
4427
- function traverseWebComponents(node, callback) {
4428
- var iter = document.createNodeIterator(
4429
- node,
4430
- NodeFilter.SHOW_ELEMENT,
4431
- function (foundNode) {
4432
- return getShadowRoot(foundNode)
4433
- ? NodeFilter.FILTER_ACCEPT
4434
- : NodeFilter.FILTER_REJECT;
4435
- },
4436
- null,
4437
- false,
4438
- );
4439
- for (var next = void 0; (next = iter.nextNode()); ) {
4440
- callback(getShadowRoot(next));
4441
- }
4442
- }
4443
- var $element = new WeakMap();
4444
- var $uniqueSheets = new WeakMap();
4445
- var $observer = new WeakMap();
4446
- function isExistingAdopter(self, element) {
4447
- return (
4448
- element instanceof HTMLStyleElement &&
4449
- $uniqueSheets.get(self).some(function (sheet) {
4450
- return getAdopterByLocation(sheet, self);
4451
- })
4452
- );
4453
- }
4454
- function getAdopterContainer(self) {
4455
- var element = $element.get(self);
4456
- return element instanceof Document ? element.body : element;
4457
- }
4458
- function adopt(self) {
4459
- var styleList = document.createDocumentFragment();
4460
- var sheets = $uniqueSheets.get(self);
4461
- var observer = $observer.get(self);
4462
- var container = getAdopterContainer(self);
4463
- observer.disconnect();
4464
- sheets.forEach(function (sheet) {
4465
- styleList.appendChild(
4466
- getAdopterByLocation(sheet, self) || addAdopterLocation(sheet, self),
4467
- );
4468
- });
4469
- container.insertBefore(styleList, null);
4470
- observer.observe(container, defaultObserverOptions);
4471
- sheets.forEach(function (sheet) {
4472
- restyleAdopter(sheet, getAdopterByLocation(sheet, self));
4473
- });
4474
- }
4475
- function Location(element) {
4476
- var self = this;
4477
- self.sheets = [];
4478
- $element.set(self, element);
4479
- $uniqueSheets.set(self, []);
4480
- $observer.set(
4481
- self,
4482
- new MutationObserver(function (mutations, observer) {
4483
- if (!document) {
4484
- observer.disconnect();
4485
- return;
4486
- }
4487
- mutations.forEach(function (mutation) {
4488
- if (!hasShadyCss) {
4489
- forEach.call(mutation.addedNodes, function (node) {
4490
- if (!(node instanceof Element)) {
4491
- return;
4492
- }
4493
- traverseWebComponents(node, function (root) {
4494
- getAssociatedLocation(root).connect();
4495
- });
4496
- });
4497
- }
4498
- forEach.call(mutation.removedNodes, function (node) {
4499
- if (!(node instanceof Element)) {
4500
- return;
4501
- }
4502
- if (isExistingAdopter(self, node)) {
4503
- adopt(self);
4504
- }
4505
- if (!hasShadyCss) {
4506
- traverseWebComponents(node, function (root) {
4507
- getAssociatedLocation(root).disconnect();
4508
- });
4509
- }
4510
- });
4511
- });
4512
- }),
4513
- );
4514
- }
4515
- Location.prototype = {
4516
- isConnected: function () {
4517
- var element = $element.get(this);
4518
- return element instanceof Document
4519
- ? element.readyState !== "loading"
4520
- : isElementConnected(element.host);
4521
- },
4522
- connect: function () {
4523
- var container = getAdopterContainer(this);
4524
- $observer.get(this).observe(container, defaultObserverOptions);
4525
- if ($uniqueSheets.get(this).length > 0) {
4526
- adopt(this);
4527
- }
4528
- traverseWebComponents(container, function (root) {
4529
- getAssociatedLocation(root).connect();
4530
- });
4531
- },
4532
- disconnect: function () {
4533
- $observer.get(this).disconnect();
4534
- },
4535
- update: function (sheets) {
4536
- var self = this;
4537
- var locationType =
4538
- $element.get(self) === document ? "Document" : "ShadowRoot";
4539
- if (!Array.isArray(sheets)) {
4540
- throw new TypeError(
4541
- "Failed to set the 'adoptedStyleSheets' property on " +
4542
- locationType +
4543
- ": Iterator getter is not callable.",
4544
- );
4545
- }
4546
- if (!sheets.every(isCSSStyleSheetInstance)) {
4547
- throw new TypeError(
4548
- "Failed to set the 'adoptedStyleSheets' property on " +
4549
- locationType +
4550
- ": Failed to convert value to 'CSSStyleSheet'",
4551
- );
4552
- }
4553
- if (sheets.some(isNonConstructedStyleSheetInstance)) {
4554
- throw new TypeError(
4555
- "Failed to set the 'adoptedStyleSheets' property on " +
4556
- locationType +
4557
- ": Can't adopt non-constructed stylesheets",
4558
- );
4559
- }
4560
- self.sheets = sheets;
4561
- var oldUniqueSheets = $uniqueSheets.get(self);
4562
- var uniqueSheets = unique(sheets);
4563
- var removedSheets = diff(oldUniqueSheets, uniqueSheets);
4564
- removedSheets.forEach(function (sheet) {
4565
- removeNode(getAdopterByLocation(sheet, self));
4566
- removeAdopterLocation(sheet, self);
4567
- });
4568
- $uniqueSheets.set(self, uniqueSheets);
4569
- if (self.isConnected() && uniqueSheets.length > 0) {
4570
- adopt(self);
4571
- }
4572
- },
4573
- };
4574
-
4575
- window.CSSStyleSheet = ConstructedStyleSheet;
4576
- attachAdoptedStyleSheetProperty(Document);
4577
- if ("ShadowRoot" in window) {
4578
- attachAdoptedStyleSheetProperty(ShadowRoot);
4579
- var proto = Element.prototype;
4580
- var attach_1 = proto.attachShadow;
4581
- proto.attachShadow = function attachShadow(init) {
4582
- var root = attach_1.call(this, init);
4583
- if (init.mode === "closed") {
4584
- closedShadowRootRegistry.set(this, root);
4585
- }
4586
- return root;
4587
- };
4588
- }
4589
- var documentLocation = getAssociatedLocation(document);
4590
- if (documentLocation.isConnected()) {
4591
- documentLocation.connect();
4592
- } else {
4593
- document.addEventListener(
4594
- "DOMContentLoaded",
4595
- documentLocation.connect.bind(documentLocation),
4596
- );
4597
- }
4598
- })();
4599
-
4600
- const installImportMetaCss = importMeta => {
4601
- const stylesheet = new CSSStyleSheet({
4602
- baseUrl: importMeta.url
4603
- });
4604
4348
  let called = false;
4605
4349
  // eslint-disable-next-line accessor-pairs
4606
4350
  Object.defineProperty(importMeta, "css", {
@@ -4611,8 +4355,11 @@ const installImportMetaCss = importMeta => {
4611
4355
  }
4612
4356
  called = true;
4613
4357
  stylesheet.replaceSync(value);
4614
- document.adoptedStyleSheets = [...document.adoptedStyleSheets, stylesheet];
4615
- }
4358
+ document.adoptedStyleSheets = [
4359
+ ...document.adoptedStyleSheets,
4360
+ stylesheet,
4361
+ ];
4362
+ },
4616
4363
  });
4617
4364
  };
4618
4365
 
@@ -5671,15 +5418,6 @@ const getScrollport = (scrollBox, scrollContainer) => {
5671
5418
  };
5672
5419
  };
5673
5420
 
5674
- const getElementSelector = (element) => {
5675
- const tagName = element.tagName.toLowerCase();
5676
- const id = element.id ? `#${element.id}` : "";
5677
- const className = element.className
5678
- ? `.${element.className.split(" ").join(".")}`
5679
- : "";
5680
- return `${tagName}${id}${className}`;
5681
- };
5682
-
5683
5421
  installImportMetaCss(import.meta);const setupConstraintFeedbackLine = () => {
5684
5422
  const constraintFeedbackLine = createConstraintFeedbackLine();
5685
5423
 
@@ -6705,7 +6443,7 @@ const createObstacleConstraintsFromQuerySelector = (
6705
6443
 
6706
6444
  // obstacleBounds are already in scrollable-relative coordinates, no conversion needed
6707
6445
  const obstacleObject = createObstacleContraint(obstacleBounds, {
6708
- name: `${obstacleBounds.isSticky ? "sticky " : ""}obstacle (${getElementSelector(obstacle)})`,
6446
+ name: `${obstacleBounds.isSticky ? "sticky " : ""}obstacle (${getElementSignature(obstacle)})`,
6709
6447
  element: obstacle,
6710
6448
  });
6711
6449
  return obstacleObject;
@@ -6983,9 +6721,9 @@ const createStickyFrontierOnAxis = (
6983
6721
  const hasOpposite = frontier.hasAttribute(oppositeAttrName);
6984
6722
  // Check if element has both sides (invalid)
6985
6723
  if (hasPrimary && hasOpposite) {
6986
- const elementSelector = getElementSelector(frontier);
6724
+ const elementSignature = getElementSignature(frontier);
6987
6725
  console.warn(
6988
- `Sticky frontier element (${elementSelector}) has both ${primarySide} and ${oppositeSide} attributes.
6726
+ `Sticky frontier element (${elementSignature}) has both ${primarySide} and ${oppositeSide} attributes.
6989
6727
  A sticky frontier should only have one side attribute.`,
6990
6728
  );
6991
6729
  continue;
@@ -7008,7 +6746,7 @@ const createStickyFrontierOnAxis = (
7008
6746
  element: frontier,
7009
6747
  side: hasPrimary ? primarySide : oppositeSide,
7010
6748
  bounds: frontierBounds,
7011
- name: `sticky_frontier_${hasPrimary ? primarySide : oppositeSide} (${getElementSelector(frontier)})`,
6749
+ name: `sticky_frontier_${hasPrimary ? primarySide : oppositeSide} (${getElementSignature(frontier)})`,
7012
6750
  };
7013
6751
  matchingStickyFrontiers.push(stickyFrontierObject);
7014
6752
  }
@@ -10953,7 +10691,7 @@ const initUITransition = (container) => {
10953
10691
  debug(
10954
10692
  "transition",
10955
10693
  `Cloned previous child for ${isPhaseTransition ? "phase" : "content"} transition:`,
10956
- previousChild.getAttribute("data-ui-name") || "unnamed",
10694
+ getElementSignature(previousChild),
10957
10695
  );
10958
10696
  cleanup = () => oldChild.remove();
10959
10697
  } else {
@@ -11005,7 +10743,7 @@ const initUITransition = (container) => {
11005
10743
  if (localDebug.transition) {
11006
10744
  const updateLabel =
11007
10745
  childUIName ||
11008
- (firstChild ? "data-ui-name not specified" : "cleared/empty");
10746
+ (firstChild ? getElementSignature(firstChild) : "cleared/empty");
11009
10747
  console.group(`UI Update: ${updateLabel} (reason: ${reason})`);
11010
10748
  }
11011
10749
 
@@ -11609,7 +11347,7 @@ const initUITransition = (container) => {
11609
11347
  debug(
11610
11348
  "transition",
11611
11349
  `Attribute change detected: ${attributeName} on`,
11612
- target.getAttribute("data-ui-name") || "element",
11350
+ getElementSignature(target),
11613
11351
  );
11614
11352
  }
11615
11353
  }
@@ -11981,4 +11719,4 @@ const crossFade = {
11981
11719
  },
11982
11720
  };
11983
11721
 
11984
- 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 };
11722
+ 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.2",
3
+ "version": "0.6.0",
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,
@@ -136,26 +136,24 @@ const normalizeNumber = (value, context, unit, propertyName) => {
136
136
  return value;
137
137
  }
138
138
  if (typeof value === "string") {
139
- if (value === "auto") {
140
- return "auto";
141
- }
142
- if (value === "none") {
143
- return "none";
144
- }
145
- const numericValue = parseFloat(value);
146
- if (isNaN(numericValue)) {
147
- console.warn(
148
- `"${propertyName}": ${value} cannot be converted to number, returning value as-is.`,
149
- );
150
- return value;
139
+ // For js context, only convert px values to numbers
140
+ if (unit === "px" && value.endsWith("px")) {
141
+ const numericValue = parseFloat(value);
142
+ if (!isNaN(numericValue)) {
143
+ return numericValue;
144
+ }
151
145
  }
152
- return numericValue;
146
+ // Keep all other strings as-is (including %, em, rem, auto, none, etc.)
147
+ return value;
153
148
  }
154
149
  return value;
155
150
  };
156
151
 
157
152
  // Normalize styles for DOM application
158
153
  export const normalizeStyles = (styles, context = "js") => {
154
+ if (!styles) {
155
+ return {};
156
+ }
159
157
  if (typeof styles === "string") {
160
158
  styles = parseStyleString(styles);
161
159
  return styles;
@@ -41,6 +41,7 @@
41
41
  * - Independent content updates in the slot without affecting ongoing animations
42
42
  */
43
43
 
44
+ import { getElementSignature } from "../element_signature.js";
44
45
  import { getHeight } from "../size/get_height.js";
45
46
  import { getInnerWidth } from "../size/get_inner_width.js";
46
47
  import { getWidth } from "../size/get_width.js";
@@ -461,7 +462,7 @@ export const initUITransition = (container) => {
461
462
  debug(
462
463
  "transition",
463
464
  `Cloned previous child for ${isPhaseTransition ? "phase" : "content"} transition:`,
464
- previousChild.getAttribute("data-ui-name") || "unnamed",
465
+ getElementSignature(previousChild),
465
466
  );
466
467
  cleanup = () => oldChild.remove();
467
468
  } else {
@@ -513,7 +514,7 @@ export const initUITransition = (container) => {
513
514
  if (localDebug.transition) {
514
515
  const updateLabel =
515
516
  childUIName ||
516
- (firstChild ? "data-ui-name not specified" : "cleared/empty");
517
+ (firstChild ? getElementSignature(firstChild) : "cleared/empty");
517
518
  console.group(`UI Update: ${updateLabel} (reason: ${reason})`);
518
519
  }
519
520
 
@@ -1117,7 +1118,7 @@ export const initUITransition = (container) => {
1117
1118
  debug(
1118
1119
  "transition",
1119
1120
  `Attribute change detected: ${attributeName} on`,
1120
- target.getAttribute("data-ui-name") || "element",
1121
+ getElementSignature(target),
1121
1122
  );
1122
1123
  }
1123
1124
  }
@@ -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
- };