cotomy 0.3.14 → 0.3.16

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
@@ -87,11 +87,15 @@ The View layer provides thin wrappers around DOM elements and window events.
87
87
  - `innerWidth: number` / `innerHeight: number`
88
88
  - `outerWidth: number` / `outerHeight: number` — Includes margins
89
89
  - `scrollWidth: number` / `scrollHeight: number` / `scrollTop: number`
90
+ - `scrollIn(options?: CotomyScrollOptions | Partial<CotomyScrollOptions>): this` — Scrolls the nearest scrollable container (or window) to reveal the element
91
+ - `scrollTo(target, options?: CotomyScrollOptions | Partial<CotomyScrollOptions>): this` — Convenience wrapper; if `target` is a selector it searches descendants then calls `scrollIn()`
90
92
  - `position(): { top, left }` — Relative to viewport
91
93
  - `absolutePosition(): { top, left }` — Viewport + page scroll offset
92
94
  - `screenPosition(): { top, left }`
93
95
  - `rect(): { top, left, width, height }`
94
96
  - `innerRect()` — Subtracts padding
97
+ - `overlaps(target: CotomyElement): boolean` — True if the two elements' `rect` values overlap (AABB)
98
+ - `overlapElements: CotomyElement[]` — Returns other CotomyElements (by `data-cotomy-instance`) that overlap this element
95
99
  - Events
96
100
  - 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`.
97
101
  - Delegation: `onSubTree(eventOrEvents, selector, handler, options?)` — `eventOrEvents` can also be an array for listening to multiple delegated events at once.
@@ -151,6 +155,7 @@ The first command ensures `[scope]` expands to `[data-cotomy-scopeid="..."]` in
151
155
  - DOM helpers
152
156
  - `body: CotomyElement`
153
157
  - `append(element: CotomyElement): this`
158
+ - `scrollTo(target, options?: CotomyScrollOptions | Partial<CotomyScrollOptions>): this` — Scrolls to reveal a target (`selector | CotomyElement | HTMLElement`)
154
159
  - `moveNext(focused: CotomyElement, shift = false)` — Move focus to next/previous focusable
155
160
  - Window events
156
161
  - `on(eventOrEvents, handler): this` / `off(eventOrEvents, handler?): this` / `trigger(event[, Event]): this` — `eventOrEvents` accepts a single event name or an array. CotomyWindow’s `trigger` also bubbles by default and accepts an `Event` to override the behavior.
@@ -841,6 +841,30 @@ class EventRegistry {
841
841
  this._registry.delete(target.scopeId);
842
842
  }
843
843
  }
844
+ class CotomyScrollOptions {
845
+ constructor(init = {}) {
846
+ this.behavior = "smooth";
847
+ this.onlyIfNeeded = true;
848
+ this.block = "nearest";
849
+ this.inline = "nearest";
850
+ if (init.behavior !== undefined)
851
+ this.behavior = init.behavior;
852
+ if (init.onlyIfNeeded !== undefined)
853
+ this.onlyIfNeeded = init.onlyIfNeeded;
854
+ if (init.block !== undefined)
855
+ this.block = init.block;
856
+ if (init.inline !== undefined)
857
+ this.inline = init.inline;
858
+ }
859
+ resolveBehavior() {
860
+ return this.behavior;
861
+ }
862
+ static from(options) {
863
+ if (options instanceof CotomyScrollOptions)
864
+ return options;
865
+ return new CotomyScrollOptions(options ?? {});
866
+ }
867
+ }
844
868
  class CotomyElement {
845
869
  static encodeHtml(text) {
846
870
  const div = document.createElement("div");
@@ -1254,6 +1278,36 @@ class CotomyElement {
1254
1278
  height: rect.height + margin.top + margin.bottom
1255
1279
  };
1256
1280
  }
1281
+ static overlapsRect(left, right) {
1282
+ if (left.width <= 0 || left.height <= 0 || right.width <= 0 || right.height <= 0) {
1283
+ return false;
1284
+ }
1285
+ const leftRight = left.left + left.width;
1286
+ const leftBottom = left.top + left.height;
1287
+ const rightRight = right.left + right.width;
1288
+ const rightBottom = right.top + right.height;
1289
+ return left.left < rightRight
1290
+ && leftRight > right.left
1291
+ && left.top < rightBottom
1292
+ && leftBottom > right.top;
1293
+ }
1294
+ overlaps(target) {
1295
+ if (!this.attached || !target.attached) {
1296
+ return false;
1297
+ }
1298
+ if (this.element === target.element) {
1299
+ return false;
1300
+ }
1301
+ return CotomyElement.overlapsRect(this.rect, target.rect);
1302
+ }
1303
+ get overlapElements() {
1304
+ if (!this.attached) {
1305
+ return [];
1306
+ }
1307
+ return CotomyElement.find("[data-cotomy-instance]")
1308
+ .filter(e => e.element !== this.element)
1309
+ .filter(e => this.overlaps(e));
1310
+ }
1257
1311
  get padding() {
1258
1312
  const style = this.getComputedStyle();
1259
1313
  return {
@@ -1288,6 +1342,107 @@ class CotomyElement {
1288
1342
  get isRightViewport() {
1289
1343
  return this.element.getBoundingClientRect().left > window.innerWidth;
1290
1344
  }
1345
+ static isScrollableOverflow(value) {
1346
+ const overflow = (value ?? "").toLowerCase();
1347
+ return overflow === "auto" || overflow === "scroll";
1348
+ }
1349
+ static findNearestScrollableAncestor(element) {
1350
+ let current = element.parentElement;
1351
+ while (current) {
1352
+ if (current === document.body || current === document.documentElement) {
1353
+ return null;
1354
+ }
1355
+ const style = window.getComputedStyle(current);
1356
+ const overflowY = style.overflowY;
1357
+ const overflowX = style.overflowX;
1358
+ const canScrollY = CotomyElement.isScrollableOverflow(overflowY) && current.scrollHeight > current.clientHeight;
1359
+ const canScrollX = CotomyElement.isScrollableOverflow(overflowX) && current.scrollWidth > current.clientWidth;
1360
+ if (canScrollY || canScrollX) {
1361
+ return current;
1362
+ }
1363
+ current = current.parentElement;
1364
+ }
1365
+ return null;
1366
+ }
1367
+ static computeAlignedScroll(viewStart, viewSize, elementStart, elementEnd, align, onlyIfNeeded) {
1368
+ const viewEnd = viewStart + viewSize;
1369
+ if (onlyIfNeeded && elementStart >= viewStart && elementEnd <= viewEnd) {
1370
+ return null;
1371
+ }
1372
+ if (align === "start") {
1373
+ return elementStart;
1374
+ }
1375
+ if (align === "end") {
1376
+ return elementEnd - viewSize;
1377
+ }
1378
+ if (align === "center") {
1379
+ const elementSize = elementEnd - elementStart;
1380
+ return elementStart - (viewSize - elementSize) / 2;
1381
+ }
1382
+ if (elementStart < viewStart) {
1383
+ return elementStart;
1384
+ }
1385
+ if (elementEnd > viewEnd) {
1386
+ return elementEnd - viewSize;
1387
+ }
1388
+ return null;
1389
+ }
1390
+ scrollIn(options = {}) {
1391
+ if (!this.attached)
1392
+ return this;
1393
+ const resolved = CotomyScrollOptions.from(options);
1394
+ const behavior = resolved.resolveBehavior();
1395
+ const onlyIfNeeded = resolved.onlyIfNeeded;
1396
+ const block = resolved.block;
1397
+ const inline = resolved.inline;
1398
+ const scrollable = CotomyElement.findNearestScrollableAncestor(this.element);
1399
+ if (scrollable) {
1400
+ const elementRect = this.element.getBoundingClientRect();
1401
+ const containerRect = scrollable.getBoundingClientRect();
1402
+ const elementTopInContainer = elementRect.top - containerRect.top + scrollable.scrollTop;
1403
+ const elementBottomInContainer = elementRect.bottom - containerRect.top + scrollable.scrollTop;
1404
+ const elementLeftInContainer = elementRect.left - containerRect.left + scrollable.scrollLeft;
1405
+ const elementRightInContainer = elementRect.right - containerRect.left + scrollable.scrollLeft;
1406
+ const targetTop = CotomyElement.computeAlignedScroll(scrollable.scrollTop, scrollable.clientHeight, elementTopInContainer, elementBottomInContainer, block, onlyIfNeeded);
1407
+ const targetLeft = CotomyElement.computeAlignedScroll(scrollable.scrollLeft, scrollable.clientWidth, elementLeftInContainer, elementRightInContainer, inline, onlyIfNeeded);
1408
+ if (!onlyIfNeeded || targetTop !== null || targetLeft !== null) {
1409
+ const nextTop = targetTop ?? scrollable.scrollTop;
1410
+ const nextLeft = targetLeft ?? scrollable.scrollLeft;
1411
+ scrollable.scrollTo?.({ top: nextTop, left: nextLeft, behavior });
1412
+ if (!scrollable.scrollTo) {
1413
+ scrollable.scrollTop = nextTop;
1414
+ scrollable.scrollLeft = nextLeft;
1415
+ }
1416
+ }
1417
+ return this;
1418
+ }
1419
+ const rect = this.element.getBoundingClientRect();
1420
+ const currentTop = window.scrollY || document.documentElement.scrollTop;
1421
+ const currentLeft = window.scrollX || document.documentElement.scrollLeft;
1422
+ const elementTop = currentTop + rect.top;
1423
+ const elementBottom = currentTop + rect.bottom;
1424
+ const elementLeft = currentLeft + rect.left;
1425
+ const elementRight = currentLeft + rect.right;
1426
+ const targetTop = CotomyElement.computeAlignedScroll(currentTop, window.innerHeight, elementTop, elementBottom, block, onlyIfNeeded);
1427
+ const targetLeft = CotomyElement.computeAlignedScroll(currentLeft, window.innerWidth, elementLeft, elementRight, inline, onlyIfNeeded);
1428
+ if (!onlyIfNeeded || targetTop !== null || targetLeft !== null) {
1429
+ window.scrollTo({ top: targetTop ?? currentTop, left: targetLeft ?? currentLeft, behavior });
1430
+ }
1431
+ return this;
1432
+ }
1433
+ scrollTo(target, options = {}) {
1434
+ if (typeof target === "string") {
1435
+ const element = this.first(target);
1436
+ element?.scrollIn(options);
1437
+ return this;
1438
+ }
1439
+ if (target instanceof CotomyElement) {
1440
+ target.scrollIn(options);
1441
+ return this;
1442
+ }
1443
+ new CotomyElement(target).scrollIn(options);
1444
+ return this;
1445
+ }
1291
1446
  comesBefore(target) {
1292
1447
  const pos = this.element.compareDocumentPosition(target.element);
1293
1448
  if (pos & Node.DOCUMENT_POSITION_DISCONNECTED)
@@ -1993,6 +2148,19 @@ class CotomyWindow {
1993
2148
  return this.trigger("scroll");
1994
2149
  }
1995
2150
  }
2151
+ scrollTo(target, options = {}) {
2152
+ if (typeof target === "string") {
2153
+ const element = CotomyElement.first(target);
2154
+ element?.scrollIn(options);
2155
+ return this;
2156
+ }
2157
+ if (target instanceof CotomyElement) {
2158
+ target.scrollIn(options);
2159
+ return this;
2160
+ }
2161
+ new CotomyElement(target).scrollIn(options);
2162
+ return this;
2163
+ }
1996
2164
  changeLayout(handle) {
1997
2165
  if (handle) {
1998
2166
  return this.on("cotomy:changelayout", handle);