cotomy 0.3.13 → 0.3.15
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 +4 -0
- package/dist/browser/cotomy.js +148 -3
- 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/api.js +10 -3
- package/dist/esm/api.js.map +1 -1
- package/dist/esm/view.js +138 -0
- package/dist/esm/view.js.map +1 -1
- package/dist/types/view.d.ts +15 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -87,6 +87,8 @@ 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 }`
|
|
@@ -151,6 +153,7 @@ The first command ensures `[scope]` expands to `[data-cotomy-scopeid="..."]` in
|
|
|
151
153
|
- DOM helpers
|
|
152
154
|
- `body: CotomyElement`
|
|
153
155
|
- `append(element: CotomyElement): this`
|
|
156
|
+
- `scrollTo(target, options?: CotomyScrollOptions | Partial<CotomyScrollOptions>): this` — Scrolls to reveal a target (`selector | CotomyElement | HTMLElement`)
|
|
154
157
|
- `moveNext(focused: CotomyElement, shift = false)` — Move focus to next/previous focusable
|
|
155
158
|
- Window events
|
|
156
159
|
- `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.
|
|
@@ -243,6 +246,7 @@ The Form layer builds on `CotomyElement` for common form flows.
|
|
|
243
246
|
|
|
244
247
|
- `mail`, `tel`, `url` — Wrap the value in a corresponding anchor tag.
|
|
245
248
|
- `number` — Uses `Intl.NumberFormat` with `data-cotomy-locale`/`data-cotomy-currency` inheritance.
|
|
249
|
+
- `data-cotomy-fraction-digits="2"` — Forces fixed fraction digits (sets both `minimumFractionDigits` and `maximumFractionDigits`). Works with or without `data-cotomy-currency` (e.g. `0` → `0.00`).
|
|
246
250
|
- `utc` — Treats the value as UTC (or appends `Z` when missing) and formats with `data-cotomy-format` (default `YYYY/MM/DD HH:mm`).
|
|
247
251
|
- `date` — Renders local dates with `data-cotomy-format` (default `YYYY/MM/DD`) when the input is a valid `Date` value.
|
|
248
252
|
|
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");
|
|
@@ -1288,6 +1312,107 @@ class CotomyElement {
|
|
|
1288
1312
|
get isRightViewport() {
|
|
1289
1313
|
return this.element.getBoundingClientRect().left > window.innerWidth;
|
|
1290
1314
|
}
|
|
1315
|
+
static isScrollableOverflow(value) {
|
|
1316
|
+
const overflow = (value ?? "").toLowerCase();
|
|
1317
|
+
return overflow === "auto" || overflow === "scroll";
|
|
1318
|
+
}
|
|
1319
|
+
static findNearestScrollableAncestor(element) {
|
|
1320
|
+
let current = element.parentElement;
|
|
1321
|
+
while (current) {
|
|
1322
|
+
if (current === document.body || current === document.documentElement) {
|
|
1323
|
+
return null;
|
|
1324
|
+
}
|
|
1325
|
+
const style = window.getComputedStyle(current);
|
|
1326
|
+
const overflowY = style.overflowY;
|
|
1327
|
+
const overflowX = style.overflowX;
|
|
1328
|
+
const canScrollY = CotomyElement.isScrollableOverflow(overflowY) && current.scrollHeight > current.clientHeight;
|
|
1329
|
+
const canScrollX = CotomyElement.isScrollableOverflow(overflowX) && current.scrollWidth > current.clientWidth;
|
|
1330
|
+
if (canScrollY || canScrollX) {
|
|
1331
|
+
return current;
|
|
1332
|
+
}
|
|
1333
|
+
current = current.parentElement;
|
|
1334
|
+
}
|
|
1335
|
+
return null;
|
|
1336
|
+
}
|
|
1337
|
+
static computeAlignedScroll(viewStart, viewSize, elementStart, elementEnd, align, onlyIfNeeded) {
|
|
1338
|
+
const viewEnd = viewStart + viewSize;
|
|
1339
|
+
if (onlyIfNeeded && elementStart >= viewStart && elementEnd <= viewEnd) {
|
|
1340
|
+
return null;
|
|
1341
|
+
}
|
|
1342
|
+
if (align === "start") {
|
|
1343
|
+
return elementStart;
|
|
1344
|
+
}
|
|
1345
|
+
if (align === "end") {
|
|
1346
|
+
return elementEnd - viewSize;
|
|
1347
|
+
}
|
|
1348
|
+
if (align === "center") {
|
|
1349
|
+
const elementSize = elementEnd - elementStart;
|
|
1350
|
+
return elementStart - (viewSize - elementSize) / 2;
|
|
1351
|
+
}
|
|
1352
|
+
if (elementStart < viewStart) {
|
|
1353
|
+
return elementStart;
|
|
1354
|
+
}
|
|
1355
|
+
if (elementEnd > viewEnd) {
|
|
1356
|
+
return elementEnd - viewSize;
|
|
1357
|
+
}
|
|
1358
|
+
return null;
|
|
1359
|
+
}
|
|
1360
|
+
scrollIn(options = {}) {
|
|
1361
|
+
if (!this.attached)
|
|
1362
|
+
return this;
|
|
1363
|
+
const resolved = CotomyScrollOptions.from(options);
|
|
1364
|
+
const behavior = resolved.resolveBehavior();
|
|
1365
|
+
const onlyIfNeeded = resolved.onlyIfNeeded;
|
|
1366
|
+
const block = resolved.block;
|
|
1367
|
+
const inline = resolved.inline;
|
|
1368
|
+
const scrollable = CotomyElement.findNearestScrollableAncestor(this.element);
|
|
1369
|
+
if (scrollable) {
|
|
1370
|
+
const elementRect = this.element.getBoundingClientRect();
|
|
1371
|
+
const containerRect = scrollable.getBoundingClientRect();
|
|
1372
|
+
const elementTopInContainer = elementRect.top - containerRect.top + scrollable.scrollTop;
|
|
1373
|
+
const elementBottomInContainer = elementRect.bottom - containerRect.top + scrollable.scrollTop;
|
|
1374
|
+
const elementLeftInContainer = elementRect.left - containerRect.left + scrollable.scrollLeft;
|
|
1375
|
+
const elementRightInContainer = elementRect.right - containerRect.left + scrollable.scrollLeft;
|
|
1376
|
+
const targetTop = CotomyElement.computeAlignedScroll(scrollable.scrollTop, scrollable.clientHeight, elementTopInContainer, elementBottomInContainer, block, onlyIfNeeded);
|
|
1377
|
+
const targetLeft = CotomyElement.computeAlignedScroll(scrollable.scrollLeft, scrollable.clientWidth, elementLeftInContainer, elementRightInContainer, inline, onlyIfNeeded);
|
|
1378
|
+
if (!onlyIfNeeded || targetTop !== null || targetLeft !== null) {
|
|
1379
|
+
const nextTop = targetTop ?? scrollable.scrollTop;
|
|
1380
|
+
const nextLeft = targetLeft ?? scrollable.scrollLeft;
|
|
1381
|
+
scrollable.scrollTo?.({ top: nextTop, left: nextLeft, behavior });
|
|
1382
|
+
if (!scrollable.scrollTo) {
|
|
1383
|
+
scrollable.scrollTop = nextTop;
|
|
1384
|
+
scrollable.scrollLeft = nextLeft;
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
return this;
|
|
1388
|
+
}
|
|
1389
|
+
const rect = this.element.getBoundingClientRect();
|
|
1390
|
+
const currentTop = window.scrollY || document.documentElement.scrollTop;
|
|
1391
|
+
const currentLeft = window.scrollX || document.documentElement.scrollLeft;
|
|
1392
|
+
const elementTop = currentTop + rect.top;
|
|
1393
|
+
const elementBottom = currentTop + rect.bottom;
|
|
1394
|
+
const elementLeft = currentLeft + rect.left;
|
|
1395
|
+
const elementRight = currentLeft + rect.right;
|
|
1396
|
+
const targetTop = CotomyElement.computeAlignedScroll(currentTop, window.innerHeight, elementTop, elementBottom, block, onlyIfNeeded);
|
|
1397
|
+
const targetLeft = CotomyElement.computeAlignedScroll(currentLeft, window.innerWidth, elementLeft, elementRight, inline, onlyIfNeeded);
|
|
1398
|
+
if (!onlyIfNeeded || targetTop !== null || targetLeft !== null) {
|
|
1399
|
+
window.scrollTo({ top: targetTop ?? currentTop, left: targetLeft ?? currentLeft, behavior });
|
|
1400
|
+
}
|
|
1401
|
+
return this;
|
|
1402
|
+
}
|
|
1403
|
+
scrollTo(target, options = {}) {
|
|
1404
|
+
if (typeof target === "string") {
|
|
1405
|
+
const element = this.first(target);
|
|
1406
|
+
element?.scrollIn(options);
|
|
1407
|
+
return this;
|
|
1408
|
+
}
|
|
1409
|
+
if (target instanceof CotomyElement) {
|
|
1410
|
+
target.scrollIn(options);
|
|
1411
|
+
return this;
|
|
1412
|
+
}
|
|
1413
|
+
new CotomyElement(target).scrollIn(options);
|
|
1414
|
+
return this;
|
|
1415
|
+
}
|
|
1291
1416
|
comesBefore(target) {
|
|
1292
1417
|
const pos = this.element.compareDocumentPosition(target.element);
|
|
1293
1418
|
if (pos & Node.DOCUMENT_POSITION_DISCONNECTED)
|
|
@@ -1993,6 +2118,19 @@ class CotomyWindow {
|
|
|
1993
2118
|
return this.trigger("scroll");
|
|
1994
2119
|
}
|
|
1995
2120
|
}
|
|
2121
|
+
scrollTo(target, options = {}) {
|
|
2122
|
+
if (typeof target === "string") {
|
|
2123
|
+
const element = CotomyElement.first(target);
|
|
2124
|
+
element?.scrollIn(options);
|
|
2125
|
+
return this;
|
|
2126
|
+
}
|
|
2127
|
+
if (target instanceof CotomyElement) {
|
|
2128
|
+
target.scrollIn(options);
|
|
2129
|
+
return this;
|
|
2130
|
+
}
|
|
2131
|
+
new CotomyElement(target).scrollIn(options);
|
|
2132
|
+
return this;
|
|
2133
|
+
}
|
|
1996
2134
|
changeLayout(handle) {
|
|
1997
2135
|
if (handle) {
|
|
1998
2136
|
return this.on("cotomy:changelayout", handle);
|
|
@@ -2276,9 +2414,16 @@ class CotomyViewRenderer {
|
|
|
2276
2414
|
if (value !== undefined && value !== null) {
|
|
2277
2415
|
const currency = element.attribute("data-cotomy-currency")
|
|
2278
2416
|
|| element.closest("[data-cotomy-currency]")?.attribute("data-cotomy-currency");
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2417
|
+
const fractionDigitsAttribute = element.attribute("data-cotomy-fraction-digits")
|
|
2418
|
+
|| element.closest("[data-cotomy-fraction-digits]")?.attribute("data-cotomy-fraction-digits");
|
|
2419
|
+
const fractionDigits = fractionDigitsAttribute ? Number.parseInt(fractionDigitsAttribute, 10) : undefined;
|
|
2420
|
+
const hasFractionDigits = Number.isFinite(fractionDigits)
|
|
2421
|
+
&& fractionDigits && fractionDigits >= 0 && fractionDigits <= 20;
|
|
2422
|
+
const options = {
|
|
2423
|
+
...(currency ? { style: "currency", currency } : {}),
|
|
2424
|
+
...(hasFractionDigits ? { minimumFractionDigits: fractionDigits, maximumFractionDigits: fractionDigits } : {}),
|
|
2425
|
+
};
|
|
2426
|
+
element.text = new Intl.NumberFormat(this.locale, options).format(value);
|
|
2282
2427
|
}
|
|
2283
2428
|
});
|
|
2284
2429
|
this.renderer("utc", (element, value) => {
|