cotomy 2.0.2 → 2.0.4
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 -1
- package/dist/browser/cotomy.js +222 -8
- 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 +8 -5
- package/dist/esm/api.js.map +1 -1
- package/dist/esm/view.js +214 -3
- package/dist/esm/view.js.map +1 -1
- package/dist/types/view.d.ts +10 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -149,6 +149,9 @@ Scoped CSS sharing/re-hydrationとインスタンス単位のイベント管理
|
|
|
149
149
|
|
|
150
150
|
```bash
|
|
151
151
|
npm test -- --run tests/view.spec.ts -t "constructs from multiple sources and applies scoped css"
|
|
152
|
+
npm test -- --run tests/view.spec.ts -t "auto-prefixes [root] when no scope placeholder is present"
|
|
153
|
+
npm test -- --run tests/view.spec.ts -t "auto-prefixes every selector without a scope placeholder"
|
|
154
|
+
npm test -- --run tests/view.spec.ts -t "scopes nested rules while preserving non-style at-rules"
|
|
152
155
|
npm test -- --run tests/view.spec.ts -t "preserves scope ids when cloning, including descendants"
|
|
153
156
|
npm test -- --run tests/view.spec.ts -t "regenerates instance ids and lifecycle hooks when cloning"
|
|
154
157
|
npm test -- --run tests/view.spec.ts -t "keeps event handlers isolated by instance even when sharing scope"
|
|
@@ -271,7 +274,7 @@ The Form layer builds on `CotomyElement` for common form flows.
|
|
|
271
274
|
- `utc` — Treats the value as UTC (or appends `Z` when missing) and formats with `data-cotomy-format` (default `YYYY/MM/DD HH:mm`). By default it renders in local time; set `data-cotomy-timezone` (element or ancestor) to render in a specific IANA timezone.
|
|
272
275
|
- `date` — Renders local dates with `data-cotomy-format` (default `YYYY/MM/DD`) when the input is a valid `Date` value. By default it renders in local time; set `data-cotomy-timezone` (element or ancestor) to render in a specific IANA timezone.
|
|
273
276
|
|
|
274
|
-
`data-cotomy-bindtype` values are matched case-insensitively and surrounding whitespace is ignored. Custom renderers registered through `renderer(type, callback)` are stored with lowercase keys. If you override the protected `renderers` map directly,
|
|
277
|
+
`data-cotomy-bindtype` values are matched case-insensitively and surrounding whitespace is ignored. Custom renderers registered through `renderer(type, callback)` are stored with lowercase keys. If you override the protected `renderers` map directly, keys are also matched case-insensitively.
|
|
275
278
|
|
|
276
279
|
### UTC Renderer
|
|
277
280
|
|
package/dist/browser/cotomy.js
CHANGED
|
@@ -1690,9 +1690,7 @@ class CotomyElement {
|
|
|
1690
1690
|
const cssid = this.scopedCssElementId;
|
|
1691
1691
|
CotomyElement.find(`#${cssid}`).forEach(e => e.remove());
|
|
1692
1692
|
const element = document.createElement("style");
|
|
1693
|
-
const
|
|
1694
|
-
const normalizedCss = hasRoot ? css : `[root] ${css}`;
|
|
1695
|
-
const writeCss = normalizedCss.replace(/\[root\]/g, `[data-cotomy-scopeid="${this.scopeId}"]`);
|
|
1693
|
+
const writeCss = this.createScopedCss(css);
|
|
1696
1694
|
const node = document.createTextNode(writeCss);
|
|
1697
1695
|
element.appendChild(node);
|
|
1698
1696
|
element.id = cssid;
|
|
@@ -1708,6 +1706,219 @@ class CotomyElement {
|
|
|
1708
1706
|
}
|
|
1709
1707
|
return this;
|
|
1710
1708
|
}
|
|
1709
|
+
createScopedCss(css) {
|
|
1710
|
+
return this.scopeCssRules(css);
|
|
1711
|
+
}
|
|
1712
|
+
scopeCssRules(css) {
|
|
1713
|
+
let scoped = "";
|
|
1714
|
+
let cursor = 0;
|
|
1715
|
+
while (cursor < css.length) {
|
|
1716
|
+
const open = this.findNextCssBlockStart(css, cursor);
|
|
1717
|
+
if (open < 0) {
|
|
1718
|
+
scoped += css.slice(cursor);
|
|
1719
|
+
break;
|
|
1720
|
+
}
|
|
1721
|
+
const prelude = css.slice(cursor, open);
|
|
1722
|
+
const statementEnd = this.findCssStatementEnd(prelude);
|
|
1723
|
+
if (prelude.trimStart().startsWith("@") && statementEnd >= 0) {
|
|
1724
|
+
scoped += css.slice(cursor, cursor + statementEnd + 1);
|
|
1725
|
+
cursor += statementEnd + 1;
|
|
1726
|
+
continue;
|
|
1727
|
+
}
|
|
1728
|
+
const close = this.findCssBlockEnd(css, open);
|
|
1729
|
+
if (close < 0) {
|
|
1730
|
+
scoped += css.slice(cursor);
|
|
1731
|
+
break;
|
|
1732
|
+
}
|
|
1733
|
+
const body = css.slice(open + 1, close);
|
|
1734
|
+
scoped += this.scopeCssPrelude(prelude) + "{";
|
|
1735
|
+
scoped += this.shouldScopeNestedCss(prelude) ? this.scopeCssRules(body) : body;
|
|
1736
|
+
scoped += "}";
|
|
1737
|
+
cursor = close + 1;
|
|
1738
|
+
}
|
|
1739
|
+
return scoped;
|
|
1740
|
+
}
|
|
1741
|
+
scopeCssPrelude(prelude) {
|
|
1742
|
+
const trimmed = prelude.trimStart();
|
|
1743
|
+
if (trimmed.startsWith("@"))
|
|
1744
|
+
return prelude;
|
|
1745
|
+
return this.scopeSelectorList(prelude);
|
|
1746
|
+
}
|
|
1747
|
+
shouldScopeNestedCss(prelude) {
|
|
1748
|
+
const atRule = prelude.trimStart().match(/^@([a-z-]+)/i)?.[1].toLowerCase();
|
|
1749
|
+
return atRule === "media"
|
|
1750
|
+
|| atRule === "supports"
|
|
1751
|
+
|| atRule === "container"
|
|
1752
|
+
|| atRule === "layer"
|
|
1753
|
+
|| atRule === "document";
|
|
1754
|
+
}
|
|
1755
|
+
scopeSelectorList(selectors) {
|
|
1756
|
+
return this.splitCssSelectorList(selectors).map(selector => {
|
|
1757
|
+
if (!selector.trim())
|
|
1758
|
+
return selector;
|
|
1759
|
+
const leading = selector.match(/^\s*/)?.[0] ?? "";
|
|
1760
|
+
const trailing = selector.match(/\s*$/)?.[0] ?? "";
|
|
1761
|
+
const body = selector.slice(leading.length, selector.length - trailing.length);
|
|
1762
|
+
if (/\[root\]/.test(body)) {
|
|
1763
|
+
return leading + body.replace(/\[root\]/g, this.scopedRootSelector) + trailing;
|
|
1764
|
+
}
|
|
1765
|
+
return `${leading}${this.scopedRootSelector} ${body}${trailing}`;
|
|
1766
|
+
}).join(",");
|
|
1767
|
+
}
|
|
1768
|
+
splitCssSelectorList(selectors) {
|
|
1769
|
+
const result = [];
|
|
1770
|
+
let start = 0;
|
|
1771
|
+
let stringQuote = null;
|
|
1772
|
+
let bracketDepth = 0;
|
|
1773
|
+
let parenthesisDepth = 0;
|
|
1774
|
+
for (let i = 0; i < selectors.length; i++) {
|
|
1775
|
+
const char = selectors[i];
|
|
1776
|
+
const next = selectors[i + 1];
|
|
1777
|
+
if (stringQuote) {
|
|
1778
|
+
if (char === "\\") {
|
|
1779
|
+
i++;
|
|
1780
|
+
}
|
|
1781
|
+
else if (char === stringQuote) {
|
|
1782
|
+
stringQuote = null;
|
|
1783
|
+
}
|
|
1784
|
+
continue;
|
|
1785
|
+
}
|
|
1786
|
+
if (char === "/" && next === "*") {
|
|
1787
|
+
i = selectors.indexOf("*/", i + 2);
|
|
1788
|
+
if (i < 0)
|
|
1789
|
+
break;
|
|
1790
|
+
i++;
|
|
1791
|
+
continue;
|
|
1792
|
+
}
|
|
1793
|
+
if (char === "\"" || char === "'") {
|
|
1794
|
+
stringQuote = char;
|
|
1795
|
+
}
|
|
1796
|
+
else if (char === "[") {
|
|
1797
|
+
bracketDepth++;
|
|
1798
|
+
}
|
|
1799
|
+
else if (char === "]") {
|
|
1800
|
+
bracketDepth = Math.max(0, bracketDepth - 1);
|
|
1801
|
+
}
|
|
1802
|
+
else if (char === "(") {
|
|
1803
|
+
parenthesisDepth++;
|
|
1804
|
+
}
|
|
1805
|
+
else if (char === ")") {
|
|
1806
|
+
parenthesisDepth = Math.max(0, parenthesisDepth - 1);
|
|
1807
|
+
}
|
|
1808
|
+
else if (char === "," && bracketDepth === 0 && parenthesisDepth === 0) {
|
|
1809
|
+
result.push(selectors.slice(start, i));
|
|
1810
|
+
start = i + 1;
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
result.push(selectors.slice(start));
|
|
1814
|
+
return result;
|
|
1815
|
+
}
|
|
1816
|
+
findCssStatementEnd(css) {
|
|
1817
|
+
let stringQuote = null;
|
|
1818
|
+
let parenthesisDepth = 0;
|
|
1819
|
+
for (let i = 0; i < css.length; i++) {
|
|
1820
|
+
const char = css[i];
|
|
1821
|
+
const next = css[i + 1];
|
|
1822
|
+
if (stringQuote) {
|
|
1823
|
+
if (char === "\\") {
|
|
1824
|
+
i++;
|
|
1825
|
+
}
|
|
1826
|
+
else if (char === stringQuote) {
|
|
1827
|
+
stringQuote = null;
|
|
1828
|
+
}
|
|
1829
|
+
continue;
|
|
1830
|
+
}
|
|
1831
|
+
if (char === "/" && next === "*") {
|
|
1832
|
+
i = css.indexOf("*/", i + 2);
|
|
1833
|
+
if (i < 0)
|
|
1834
|
+
return -1;
|
|
1835
|
+
i++;
|
|
1836
|
+
continue;
|
|
1837
|
+
}
|
|
1838
|
+
if (char === "\"" || char === "'") {
|
|
1839
|
+
stringQuote = char;
|
|
1840
|
+
}
|
|
1841
|
+
else if (char === "(") {
|
|
1842
|
+
parenthesisDepth++;
|
|
1843
|
+
}
|
|
1844
|
+
else if (char === ")") {
|
|
1845
|
+
parenthesisDepth = Math.max(0, parenthesisDepth - 1);
|
|
1846
|
+
}
|
|
1847
|
+
else if (char === ";" && parenthesisDepth === 0) {
|
|
1848
|
+
return i;
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
return -1;
|
|
1852
|
+
}
|
|
1853
|
+
findNextCssBlockStart(css, start) {
|
|
1854
|
+
let stringQuote = null;
|
|
1855
|
+
for (let i = start; i < css.length; i++) {
|
|
1856
|
+
const char = css[i];
|
|
1857
|
+
const next = css[i + 1];
|
|
1858
|
+
if (stringQuote) {
|
|
1859
|
+
if (char === "\\") {
|
|
1860
|
+
i++;
|
|
1861
|
+
}
|
|
1862
|
+
else if (char === stringQuote) {
|
|
1863
|
+
stringQuote = null;
|
|
1864
|
+
}
|
|
1865
|
+
continue;
|
|
1866
|
+
}
|
|
1867
|
+
if (char === "/" && next === "*") {
|
|
1868
|
+
i = css.indexOf("*/", i + 2);
|
|
1869
|
+
if (i < 0)
|
|
1870
|
+
return -1;
|
|
1871
|
+
i++;
|
|
1872
|
+
continue;
|
|
1873
|
+
}
|
|
1874
|
+
if (char === "\"" || char === "'") {
|
|
1875
|
+
stringQuote = char;
|
|
1876
|
+
}
|
|
1877
|
+
else if (char === "{") {
|
|
1878
|
+
return i;
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
return -1;
|
|
1882
|
+
}
|
|
1883
|
+
findCssBlockEnd(css, open) {
|
|
1884
|
+
let depth = 0;
|
|
1885
|
+
let stringQuote = null;
|
|
1886
|
+
for (let i = open; i < css.length; i++) {
|
|
1887
|
+
const char = css[i];
|
|
1888
|
+
const next = css[i + 1];
|
|
1889
|
+
if (stringQuote) {
|
|
1890
|
+
if (char === "\\") {
|
|
1891
|
+
i++;
|
|
1892
|
+
}
|
|
1893
|
+
else if (char === stringQuote) {
|
|
1894
|
+
stringQuote = null;
|
|
1895
|
+
}
|
|
1896
|
+
continue;
|
|
1897
|
+
}
|
|
1898
|
+
if (char === "/" && next === "*") {
|
|
1899
|
+
i = css.indexOf("*/", i + 2);
|
|
1900
|
+
if (i < 0)
|
|
1901
|
+
return -1;
|
|
1902
|
+
i++;
|
|
1903
|
+
continue;
|
|
1904
|
+
}
|
|
1905
|
+
if (char === "\"" || char === "'") {
|
|
1906
|
+
stringQuote = char;
|
|
1907
|
+
}
|
|
1908
|
+
else if (char === "{") {
|
|
1909
|
+
depth++;
|
|
1910
|
+
}
|
|
1911
|
+
else if (char === "}") {
|
|
1912
|
+
depth--;
|
|
1913
|
+
if (depth === 0)
|
|
1914
|
+
return i;
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
return -1;
|
|
1918
|
+
}
|
|
1919
|
+
get scopedRootSelector() {
|
|
1920
|
+
return `[data-cotomy-scopeid="${this.scopeId}"]`;
|
|
1921
|
+
}
|
|
1711
1922
|
ensureScopedCss() {
|
|
1712
1923
|
if (!this._scopedCss || !this.stylable)
|
|
1713
1924
|
return;
|
|
@@ -3235,17 +3446,20 @@ class CotomyViewRenderer {
|
|
|
3235
3446
|
return this;
|
|
3236
3447
|
}
|
|
3237
3448
|
bindPrimitiveValue(propertyName, value) {
|
|
3449
|
+
let renderers;
|
|
3238
3450
|
this.element.find(`[data-cotomy-bind="${propertyName}" i]`).forEach(element => {
|
|
3239
3451
|
if (CotomyDebugSettings.isEnabled(CotomyDebugFeature.Bind)) {
|
|
3240
3452
|
console.debug(`Binding data to element [data-cotomy-bind="${propertyName}"]:`, value);
|
|
3241
3453
|
}
|
|
3242
3454
|
const type = element.attribute("data-cotomy-bindtype")?.trim().toLowerCase();
|
|
3243
|
-
if (type
|
|
3244
|
-
this.renderers[
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3455
|
+
if (type) {
|
|
3456
|
+
renderers ?? (renderers = Object.fromEntries(Object.entries(this.renderers).map(([key, renderer]) => [key.toLowerCase(), renderer])));
|
|
3457
|
+
if (renderers[type]) {
|
|
3458
|
+
renderers[type](element, value);
|
|
3459
|
+
return;
|
|
3460
|
+
}
|
|
3248
3461
|
}
|
|
3462
|
+
element.text = String(value ?? "");
|
|
3249
3463
|
});
|
|
3250
3464
|
}
|
|
3251
3465
|
async applyArrayAsync(values, propertyName) {
|