cotomy 2.0.3 → 2.0.5

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
@@ -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"
@@ -287,11 +290,13 @@ const view = new CotomyViewRenderer(
287
290
  new CotomyElement(document.querySelector("#profile")!)
288
291
  );
289
292
 
290
- await view.applyAsync(apiResponse); // apiResponse is CotomyApiResponse from CotomyApi
293
+ await view.applyAsync(apiResponse); // CotomyApiResponse from CotomyApi, or a plain object payload
291
294
  // <span data-cotomy-bind="user.birthday" data-cotomy-bindtype="date" data-cotomy-format="MMM D, YYYY"></span>
292
295
  // → renders localized date text if the API payload contains user.birthday
293
296
  ```
294
297
 
298
+ `applyAsync` accepts either a `CotomyApiResponse` from `CotomyApi` or a plain object payload.
299
+
295
300
  #### Array binding
296
301
 
297
302
  - Both `CotomyViewRenderer.applyAsync` and `CotomyEntityFillApiForm.fillAsync` resolve array elements by index via the active `ICotomyBindNameGenerator` (dot style → `items[0].name`, bracket style → `items[0][name]`).
@@ -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 hasRoot = /\[root\]/.test(css);
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;
@@ -3284,10 +3495,15 @@ class CotomyViewRenderer {
3284
3495
  }
3285
3496
  }
3286
3497
  async applyAsync(respose) {
3287
- if (!respose.available) {
3288
- throw new Error("Response is not available.");
3498
+ if (respose instanceof CotomyApiResponse) {
3499
+ if (!respose.available) {
3500
+ throw new Error("Response is not available.");
3501
+ }
3502
+ await this.applyObjectAsync(await respose.objectAsync());
3503
+ }
3504
+ else {
3505
+ await this.applyObjectAsync(respose);
3289
3506
  }
3290
- await this.applyObjectAsync(await respose.objectAsync());
3291
3507
  return this;
3292
3508
  }
3293
3509
  }
@@ -3970,7 +4186,8 @@ class CotomyPageController {
3970
4186
  async initializeAsync() {
3971
4187
  this.initializeDateTimeElements();
3972
4188
  CotomyWindow.instance.pageshow(async (e) => {
3973
- if (e.persisted) {
4189
+ const navType = performance.getEntriesByType("navigation")[0]?.type;
4190
+ if (e.persisted || navType === "back_forward") {
3974
4191
  await this.restoreAsync();
3975
4192
  if (CotomyWindow.instance.reloading) {
3976
4193
  e.stopImmediatePropagation();