cotomy 0.3.15 → 0.3.17

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/README.md CHANGED
@@ -38,6 +38,7 @@ The View layer provides thin wrappers around DOM elements and window events.
38
38
  - Scoped CSS
39
39
  - `scopeId: string` - Returns the value stored in the element's `data-cotomy-scopeid` attribute
40
40
  - `[scope]` placeholder in provided CSS is replaced by `[data-cotomy-scopeid="..."]`
41
+ - Scoped CSS text is kept on the instance; if the `<style id="css-${scopeId}">` is missing when the element is attached, it will be re-generated automatically
41
42
  - `stylable: boolean` - False for tags like `script`, `style`, `link`, `meta`
42
43
  - Static helpers
43
44
  - `CotomyElement.encodeHtml(text)`
@@ -77,7 +78,7 @@ The View layer provides thin wrappers around DOM elements and window events.
77
78
  - `insertBefore(sibling): this` / `insertAfter(sibling): this`
78
79
  - `appendTo(target): this` / `prependTo(target): this`
79
80
  - `comesBefore(target): boolean` / `comesAfter(target): boolean` — Checks DOM order (returns `false` for the same element or disconnected nodes)
80
- - `clone(type?): CotomyElement` - Returns a deep-cloned element, optionally typed, and reassigns new `data-cotomy-instance`/`data-cotomy-scopeid` values (and strips `data-cotomy-moving`). Cloning an invalidated element (`data-cotomy-invalidated`) throws.
81
+ - `clone(type?): CotomyElement` - Returns a deep-cloned element, optionally typed, and reassigns a new `data-cotomy-instance` while preserving the `data-cotomy-scopeid` for scoped CSS sharing (strips `data-cotomy-moving`). Cloning an invalidated element (`data-cotomy-invalidated`) throws.
81
82
  - `clear(): this` — Removes all descendants and text
82
83
  - `remove(): void` — Explicitly non-chainable after removal
83
84
  - Geometry & visibility
@@ -94,6 +95,8 @@ The View layer provides thin wrappers around DOM elements and window events.
94
95
  - `screenPosition(): { top, left }`
95
96
  - `rect(): { top, left, width, height }`
96
97
  - `innerRect()` — Subtracts padding
98
+ - `overlaps(target: CotomyElement): boolean` — True if the two elements' `rect` values overlap (AABB)
99
+ - `overlapElements: CotomyElement[]` — Returns other CotomyElements (by `data-cotomy-instance`) that overlap this element
97
100
  - Events
98
101
  - Generic: `on(eventOrEvents, handler, options?)`, `off(eventOrEvents, handler?, options?)`, `once(eventOrEvents, handler, options?)`, `trigger(event[, Event])` — `eventOrEvents` accepts either a single event name or an array for batch registration/removal. `trigger` emits bubbling events by default and can be customized by passing an `Event`.
99
102
  - Delegation: `onSubTree(eventOrEvents, selector, handler, options?)` — `eventOrEvents` can also be an array for listening to multiple delegated events at once.
@@ -105,6 +108,7 @@ The View layer provides thin wrappers around DOM elements and window events.
105
108
  - Layout (custom): `resize`, `scroll`, `changelayout` — requires `listenLayoutEvents()` on the element
106
109
  - Move lifecycle: `cotomy:transitstart`, `cotomy:transitend` — emitted automatically by `append`, `prepend`, `insertBefore/After`, `appendTo`, and `prependTo`. While moving, the element (and its descendants) receive a temporary `data-cotomy-moving` attribute so removal observers know the node is still in transit.
107
110
  - Removal: `removed` — fired when an element actually leaves the DOM (MutationObserver-backed). Because `cotomy:transitstart`/`transitend` manage the `data-cotomy-moving` flag, `removed` only runs for true detachments, making it safe for cleanup.
111
+ - Registry isolation: イベントレジストリは `data-cotomy-instance`(インスタンスID)単位で管理され、クローンは新しいインスタンスIDを持つため、同じ `scopeId` を共有していてもリスナーが混線しません。
108
112
  - File: `filedrop(handler: (files: File[]) => void)`
109
113
 
110
114
  Example (scoped CSS and events):
@@ -126,18 +130,20 @@ document.body.appendChild(panel.element);
126
130
 
127
131
  ## Testing
128
132
 
129
- The scoped CSS replacement and scope-id isolation logic are covered by `tests/view.spec.ts`. Run the focused specs below to verify the behavior:
133
+ Scoped CSS sharing/re-hydrationとインスタンス単位のイベント管理は `tests/view.spec.ts` に含まれています。主に確認したい場合は以下のコマンドで個別に実行できます:
130
134
 
131
135
  ```bash
132
- npx vitest run tests/view.spec.ts -t "constructs from multiple sources and applies scoped css"
133
- npx vitest run tests/view.spec.ts -t "assigns fresh scope ids when cloning, including descendants"
134
- npx vitest run tests/view.spec.ts -t "regenerates instance ids and lifecycle hooks when cloning"
135
- npx vitest run tests/view.spec.ts -t "strips moving flags when cloning"
136
- npx vitest run tests/view.spec.ts -t "throws when cloning an invalidated element"
137
- npx vitest run tests/view.spec.ts -t "compares document order with comesBefore/comesAfter"
136
+ npm test -- --run tests/view.spec.ts -t "constructs from multiple sources and applies scoped css"
137
+ npm test -- --run tests/view.spec.ts -t "preserves scope ids when cloning, including descendants"
138
+ npm test -- --run tests/view.spec.ts -t "regenerates instance ids and lifecycle hooks when cloning"
139
+ npm test -- --run tests/view.spec.ts -t "keeps event handlers isolated by instance even when sharing scope"
140
+ npm test -- --run tests/view.spec.ts -t "rehydrates scoped css when a clone shares scope after the original was removed"
141
+ npm test -- --run tests/view.spec.ts -t "strips moving flags when cloning"
142
+ npm test -- --run tests/view.spec.ts -t "throws when cloning an invalidated element"
143
+ npm test -- --run tests/view.spec.ts -t "compares document order with comesBefore/comesAfter"
138
144
  ```
139
145
 
140
- The first command ensures `[scope]` expands to `[data-cotomy-scopeid="..."]` in injected styles, the second confirms that cloning reassigns new `data-cotomy-scopeid` attributes to the cloned tree, the third verifies fresh `data-cotomy-instance` values and lifecycle hooks, and the last two cover stripping transit flags and rejecting invalidated nodes during cloning.
146
+ 普段は `npm test` で全体を実行できます。上記のコマンドでは `[scope]` 展開、スコープID共有のクローン挙動、インスタンス単位のイベント隔離、クローン後のスコープCSS再生成、移動フラグの除去、無効化要素のクローン拒否、DOM順序判定などをピンポイントで確認できます。
141
147
 
142
148
  ### CotomyMetaElement
143
149
 
@@ -811,11 +811,11 @@ class EventRegistry {
811
811
  return this._instance ?? (this._instance = new EventRegistry());
812
812
  }
813
813
  map(target) {
814
- const scopeId = target.scopeId;
815
- let registry = this._registry.get(scopeId);
814
+ const instanceId = target.instanceId;
815
+ let registry = this._registry.get(instanceId);
816
816
  if (!registry) {
817
817
  registry = new HandlerRegistory(target);
818
- this._registry.set(scopeId, registry);
818
+ this._registry.set(instanceId, registry);
819
819
  }
820
820
  return registry;
821
821
  }
@@ -824,7 +824,7 @@ class EventRegistry {
824
824
  registry.add(event, entry);
825
825
  }
826
826
  off(event, target, entry) {
827
- const registry = this._registry.get(target.scopeId);
827
+ const registry = this._registry.get(target.instanceId);
828
828
  if (!registry)
829
829
  return;
830
830
  if (entry) {
@@ -834,11 +834,11 @@ class EventRegistry {
834
834
  registry.remove(event);
835
835
  }
836
836
  if (registry.empty) {
837
- this._registry.delete(target.scopeId);
837
+ this._registry.delete(target.instanceId);
838
838
  }
839
839
  }
840
840
  clear(target) {
841
- this._registry.delete(target.scopeId);
841
+ this._registry.delete(target.instanceId);
842
842
  }
843
843
  }
844
844
  class CotomyScrollOptions {
@@ -1004,6 +1004,7 @@ class CotomyElement {
1004
1004
  }
1005
1005
  useScopedCss(css) {
1006
1006
  if (css && this.stylable) {
1007
+ this._scopedCss = css;
1007
1008
  const cssid = this.scopedCssElementId;
1008
1009
  CotomyElement.find(`#${cssid}`).forEach(e => e.remove());
1009
1010
  const element = document.createElement("style");
@@ -1015,11 +1016,22 @@ class CotomyElement {
1015
1016
  || new CotomyElement({ html: `<head></head>` }).prependTo(new CotomyElement(document.documentElement));
1016
1017
  head.append(new CotomyElement(element));
1017
1018
  this.removed(() => {
1018
- CotomyElement.find(`#${cssid}`).forEach(e => e.remove());
1019
+ const hasSameScope = document.querySelector(`[data-cotomy-scopeid="${this.scopeId}"]`) !== null;
1020
+ if (!hasSameScope) {
1021
+ CotomyElement.find(`#${cssid}`).forEach(e => e.remove());
1022
+ }
1019
1023
  });
1020
1024
  }
1021
1025
  return this;
1022
1026
  }
1027
+ ensureScopedCss() {
1028
+ if (!this._scopedCss || !this.stylable)
1029
+ return;
1030
+ const cssid = this.scopedCssElementId;
1031
+ if (!document.getElementById(cssid)) {
1032
+ this.useScopedCss(this._scopedCss);
1033
+ }
1034
+ }
1023
1035
  listenLayoutEvents() {
1024
1036
  this.attribute(CotomyElement.LISTEN_LAYOUT_EVENTS_ATTRIBUTE, "");
1025
1037
  return this;
@@ -1038,7 +1050,7 @@ class CotomyElement {
1038
1050
  }
1039
1051
  clone(type) {
1040
1052
  const ctor = (type ?? CotomyElement);
1041
- const ATTRIBUTES_TO_STRIP = ["data-cotomy-instance", "data-cotomy-scopeid", "data-cotomy-moving"];
1053
+ const ATTRIBUTES_TO_STRIP = ["data-cotomy-instance", "data-cotomy-moving"];
1042
1054
  const clonedElement = this.element.cloneNode(true);
1043
1055
  if (clonedElement.hasAttribute("data-cotomy-invalidated")) {
1044
1056
  throw new Error("Cannot clone an invalidated CotomyElement.");
@@ -1048,6 +1060,7 @@ class CotomyElement {
1048
1060
  clonedElement.querySelectorAll(`[${attr}]`).forEach(el => el.removeAttribute(attr));
1049
1061
  });
1050
1062
  const cloned = new ctor(clonedElement);
1063
+ cloned._scopedCss = this._scopedCss;
1051
1064
  return cloned;
1052
1065
  }
1053
1066
  get tagname() {
@@ -1278,6 +1291,36 @@ class CotomyElement {
1278
1291
  height: rect.height + margin.top + margin.bottom
1279
1292
  };
1280
1293
  }
1294
+ static overlapsRect(left, right) {
1295
+ if (left.width <= 0 || left.height <= 0 || right.width <= 0 || right.height <= 0) {
1296
+ return false;
1297
+ }
1298
+ const leftRight = left.left + left.width;
1299
+ const leftBottom = left.top + left.height;
1300
+ const rightRight = right.left + right.width;
1301
+ const rightBottom = right.top + right.height;
1302
+ return left.left < rightRight
1303
+ && leftRight > right.left
1304
+ && left.top < rightBottom
1305
+ && leftBottom > right.top;
1306
+ }
1307
+ overlaps(target) {
1308
+ if (!this.attached || !target.attached) {
1309
+ return false;
1310
+ }
1311
+ if (this.element === target.element) {
1312
+ return false;
1313
+ }
1314
+ return CotomyElement.overlapsRect(this.rect, target.rect);
1315
+ }
1316
+ get overlapElements() {
1317
+ if (!this.attached) {
1318
+ return [];
1319
+ }
1320
+ return CotomyElement.find("[data-cotomy-instance]")
1321
+ .filter(e => e.element !== this.element)
1322
+ .filter(e => this.overlaps(e));
1323
+ }
1281
1324
  get padding() {
1282
1325
  const style = this.getComputedStyle();
1283
1326
  return {
@@ -1575,12 +1618,14 @@ class CotomyElement {
1575
1618
  CotomyElement.runWithMoveEvents(prepend, () => {
1576
1619
  this.element.prepend(prepend.element);
1577
1620
  });
1621
+ prepend.ensureScopedCss();
1578
1622
  return this;
1579
1623
  }
1580
1624
  append(target) {
1581
1625
  CotomyElement.runWithMoveEvents(target, () => {
1582
1626
  this.element.append(target.element);
1583
1627
  });
1628
+ target.ensureScopedCss();
1584
1629
  return this;
1585
1630
  }
1586
1631
  appendAll(targets) {
@@ -1591,24 +1636,28 @@ class CotomyElement {
1591
1636
  CotomyElement.runWithMoveEvents(append, () => {
1592
1637
  this.element.before(append.element);
1593
1638
  });
1639
+ append.ensureScopedCss();
1594
1640
  return this;
1595
1641
  }
1596
1642
  insertAfter(append) {
1597
1643
  CotomyElement.runWithMoveEvents(append, () => {
1598
1644
  this.element.after(append.element);
1599
1645
  });
1646
+ append.ensureScopedCss();
1600
1647
  return this;
1601
1648
  }
1602
1649
  appendTo(target) {
1603
1650
  CotomyElement.runWithMoveEvents(this, () => {
1604
1651
  target.element.append(this.element);
1605
1652
  });
1653
+ this.ensureScopedCss();
1606
1654
  return this;
1607
1655
  }
1608
1656
  prependTo(target) {
1609
1657
  CotomyElement.runWithMoveEvents(this, () => {
1610
1658
  target.element.prepend(this.element);
1611
1659
  });
1660
+ this.ensureScopedCss();
1612
1661
  return this;
1613
1662
  }
1614
1663
  trigger(event, e) {