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 +5 -0
- package/dist/browser/cotomy.js +168 -0
- package/dist/browser/cotomy.js.map +1 -1
- package/dist/browser/cotomy.min.js +1 -1
- package/dist/browser/cotomy.min.js.map +1 -1
- package/dist/cjs/index.cjs +1 -1
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/esm/view.js +168 -0
- package/dist/esm/view.js.map +1 -1
- package/dist/types/view.d.ts +18 -0
- package/package.json +1 -1
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.
|
package/dist/browser/cotomy.js
CHANGED
|
@@ -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);
|