@microsoft/webui-framework 0.0.6 → 0.0.7

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.
@@ -1,42 +1,121 @@
1
- const MARKER_REPEAT_START = "wr";
2
- const MARKER_REPEAT_END = "/wr";
3
- const MARKER_COND_START = "wc";
4
- const MARKER_COND_END = "/wc";
5
- const MARKER_REPEAT_ITEM = "wi";
6
- function collectItemMarkers(repeatStart) {
7
- const items = [];
8
- let end = null;
9
- let node = repeatStart.nextSibling;
10
- while (node) {
11
- if (node.nodeType === 8) {
12
- const data = node.data;
13
- if (data === MARKER_REPEAT_END) {
14
- end = node;
15
- break;
16
- }
17
- if (data === MARKER_REPEAT_ITEM) items.push(node);
1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT license.
3
+ /**
4
+ * Hydration marker utilities for zero-DOM-mutation in-place hydration.
5
+ *
6
+ * The WebUI handler plugin emits lightweight HTML comment markers around
7
+ * structural boundaries (for-loops and if-conditions). These utilities
8
+ * walk markers and locate elements so the hydration path can wire
9
+ * bindings in-place without reparenting DOM nodes.
10
+ *
11
+ * Marker format:
12
+ * <!--wr--> repeat block start
13
+ * <!--/wr--> repeat block end
14
+ * <!--wi--> repeat item boundary
15
+ * <!--wc--> conditional block start
16
+ * <!--/wc--> conditional block end
17
+ */
18
+ // Marker data constants matching the handler plugin output.
19
+ export const MARKER_REPEAT_START = 'wr';
20
+ export const MARKER_REPEAT_END = '/wr';
21
+ export const MARKER_COND_START = 'wc';
22
+ export const MARKER_COND_END = '/wc';
23
+ const MARKER_REPEAT_ITEM = 'wi';
24
+ /**
25
+ * Collect the item markers (<!--wi-->) within a repeat range.
26
+ *
27
+ * Walks siblings from the repeat start marker to the repeat end marker.
28
+ * Returns an array of <!--wi--> comment nodes that delineate items.
29
+ */
30
+ export function collectItemMarkers(repeatStart) {
31
+ const items = [];
32
+ let end = null;
33
+ let node = repeatStart.nextSibling;
34
+ while (node) {
35
+ if (node.nodeType === 8 /* COMMENT_NODE */) {
36
+ const data = node.data;
37
+ if (data === MARKER_REPEAT_END) {
38
+ end = node;
39
+ break;
40
+ }
41
+ if (data === MARKER_REPEAT_ITEM)
42
+ items.push(node);
43
+ }
44
+ node = node.nextSibling;
18
45
  }
19
- node = node.nextSibling;
20
- }
21
- return { items, end };
46
+ return { items, end };
22
47
  }
23
- function nextElement(marker) {
24
- let node = marker.nextSibling;
25
- while (node) {
26
- if (node.nodeType === 1) return node;
27
- if (node.nodeType === 8) {
28
- const data = node.data;
29
- if (data === MARKER_REPEAT_END || data === MARKER_REPEAT_ITEM) return null;
48
+ /**
49
+ * Get the next element sibling after a marker comment, skipping
50
+ * whitespace text nodes and other comments.
51
+ */
52
+ export function nextElement(marker) {
53
+ let node = marker.nextSibling;
54
+ while (node) {
55
+ if (node.nodeType === 1 /* ELEMENT_NODE */)
56
+ return node;
57
+ if (node.nodeType === 8 /* COMMENT_NODE */) {
58
+ const data = node.data;
59
+ if (data === MARKER_REPEAT_END || data === MARKER_REPEAT_ITEM)
60
+ return null;
61
+ }
62
+ node = node.nextSibling;
30
63
  }
31
- node = node.nextSibling;
32
- }
33
- return null;
64
+ return null;
65
+ }
66
+ /**
67
+ * Find the Nth child of a given nodeType, skipping structural block ranges.
68
+ *
69
+ * The compiled template static HTML (`meta.h`) does not contain conditional
70
+ * or repeat block content — those are stored as separate block metadata.
71
+ * But the SSR DOM has this content rendered inline between marker pairs
72
+ * (`<!--wc-->...<!--/wc-->` and `<!--wr-->...<!--/wr-->`).
73
+ *
74
+ * This function walks `parent.firstChild` → siblings, counting only
75
+ * children of the requested `nodeType` that are NOT inside a structural
76
+ * block range. Nested blocks of the same type are handled via depth
77
+ * tracking. Returns the child at the given `ordinal`, or null.
78
+ *
79
+ * Used by `$resolveSSR` (element ordinals) and `$findSSRText` (text
80
+ * ordinals) to keep SSR DOM ordinals aligned with template metadata.
81
+ *
82
+ * **Requires closing markers to still be in the DOM** — caller must
83
+ * not remove `<!--/wc-->` or `<!--/wr-->` before all resolution is done.
84
+ */
85
+ export function findByOrdinal(parent, nodeType, ordinal) {
86
+ let count = 0;
87
+ let child = parent.firstChild;
88
+ while (child) {
89
+ // Detect a structural block opening marker and skip the entire range.
90
+ if (child.nodeType === 8 /* COMMENT_NODE */) {
91
+ const data = child.data;
92
+ if (data === MARKER_COND_START || data === MARKER_REPEAT_START) {
93
+ const endTag = data === MARKER_COND_START ? MARKER_COND_END : MARKER_REPEAT_END;
94
+ let depth = 1;
95
+ child = child.nextSibling;
96
+ while (child && depth > 0) {
97
+ if (child.nodeType === 8 /* COMMENT_NODE */) {
98
+ const d = child.data;
99
+ if (d === data)
100
+ depth++;
101
+ else if (d === endTag)
102
+ depth--;
103
+ }
104
+ if (depth > 0)
105
+ child = child.nextSibling;
106
+ }
107
+ // Advance past the closing marker itself
108
+ if (child)
109
+ child = child.nextSibling;
110
+ continue;
111
+ }
112
+ }
113
+ if (child.nodeType === nodeType) {
114
+ if (count === ordinal)
115
+ return child;
116
+ count++;
117
+ }
118
+ child = child.nextSibling;
119
+ }
120
+ return null;
34
121
  }
35
- export {
36
- MARKER_COND_END,
37
- MARKER_COND_START,
38
- MARKER_REPEAT_END,
39
- MARKER_REPEAT_START,
40
- collectItemMarkers,
41
- nextElement
42
- };
@@ -1,7 +1,11 @@
1
1
  /**
2
- * Inject a CSS module stylesheet. Shadow DOM components get a
3
- * constructable stylesheet on their shadow root; light DOM components
4
- * get a `<style>` in the document head. Each specifier is processed
5
- * once per page.
2
+ * Adopt a CSS module stylesheet onto a shadow root, or inject into `<head>`
3
+ * for light DOM components.
4
+ *
5
+ * For shadow DOM: uses `import(specifier, { with: { type: "css" } })` to
6
+ * retrieve the browser-registered CSSStyleSheet from the module registry.
7
+ * The browser caches the sheet internally — no application-level cache needed.
8
+ *
9
+ * For light DOM: appends a `<style>` element to `<head>` (once per specifier).
6
10
  */
7
11
  export declare function injectModuleStyle(specifier: string, shadowRoot: ShadowRoot | null): void;
@@ -1,29 +1,77 @@
1
- const injectedStyles = /* @__PURE__ */ new Set();
2
- function injectModuleStyle(specifier, shadowRoot) {
3
- if (injectedStyles.has(specifier)) return;
4
- injectedStyles.add(specifier);
5
- const defs = document.querySelectorAll('style[type="module"][specifier]');
6
- let cssText = null;
7
- for (let i = 0; i < defs.length; i++) {
8
- if (defs[i].getAttribute("specifier") === specifier) {
9
- cssText = defs[i].textContent;
10
- break;
1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT license.
3
+ /**
4
+ * Stylesheet management for WebUI components.
5
+ *
6
+ * Three CSS strategies are supported:
7
+ *
8
+ * - **Link**: `<link rel="stylesheet">` tags in each component's shadow template.
9
+ * The browser deduplicates fetches by URL. No JS-side style management needed.
10
+ *
11
+ * - **Style**: Inline `<style>` tags inside each shadow template.
12
+ *
13
+ * - **Module**: Uses the Declarative CSS Module Scripts proposal. During SSR,
14
+ * `<style type="module" specifier="...">` definitions are emitted inline in
15
+ * each rendered component's light DOM. The browser registers these globally
16
+ * and automatically adopts them via `shadowrootadoptedstylesheets` on
17
+ * declarative shadow roots.
18
+ *
19
+ * During SPA navigation, the router appends new `<style type="module">`
20
+ * definitions to `<head>` via `templateStyles[]`. The framework uses
21
+ * `import(specifier, { with: { type: "css" } })` to retrieve the browser's
22
+ * registered CSSStyleSheet and adopts it onto the shadow root. This is a
23
+ * direct hash-map lookup in the browser's module registry — no DOM queries,
24
+ * no manual CSSStyleSheet construction.
25
+ *
26
+ * For light DOM components (no shadow root), Module mode injects a `<style>`
27
+ * element in `<head>`, deduplicated by `headInjected`.
28
+ */
29
+ /**
30
+ * Specifiers already injected into `<head>` via the light DOM path.
31
+ * Prevents duplicate `<style>` elements for non-shadow components.
32
+ */
33
+ const headInjected = new Set();
34
+ /**
35
+ * Adopt a CSS module stylesheet onto a shadow root, or inject into `<head>`
36
+ * for light DOM components.
37
+ *
38
+ * For shadow DOM: uses `import(specifier, { with: { type: "css" } })` to
39
+ * retrieve the browser-registered CSSStyleSheet from the module registry.
40
+ * The browser caches the sheet internally — no application-level cache needed.
41
+ *
42
+ * For light DOM: appends a `<style>` element to `<head>` (once per specifier).
43
+ */
44
+ export function injectModuleStyle(specifier, shadowRoot) {
45
+ if (shadowRoot) {
46
+ // SSR hydration: the browser already adopted the sheet from
47
+ // shadowrootadoptedstylesheets on the declarative shadow root.
48
+ if (shadowRoot.adoptedStyleSheets.length > 0)
49
+ return;
50
+ // SPA path: import the CSS module from the browser's registry.
51
+ // The <style type="module" specifier="X"> definition was either
52
+ // emitted inline during SSR or appended to <head> by the router.
53
+ // The import resolves to the same CSSStyleSheet the browser registered.
54
+ import(specifier, { with: { type: 'css' } }).then((mod) => {
55
+ shadowRoot.adoptedStyleSheets = [
56
+ ...shadowRoot.adoptedStyleSheets,
57
+ mod.default,
58
+ ];
59
+ }, () => {
60
+ // Specifier not registered — component has no CSS module definition.
61
+ // This is expected for Link/Style strategies or components without CSS.
62
+ });
63
+ }
64
+ else if (!headInjected.has(specifier)) {
65
+ headInjected.add(specifier);
66
+ import(specifier, { with: { type: 'css' } }).then((mod) => {
67
+ const style = document.createElement('style');
68
+ const rules = mod.default.cssRules;
69
+ let cssText = '';
70
+ for (let i = 0; i < rules.length; i++) {
71
+ cssText += rules[i].cssText;
72
+ }
73
+ style.textContent = cssText;
74
+ document.head.appendChild(style);
75
+ }, () => { });
11
76
  }
12
- }
13
- if (!cssText) return;
14
- if (shadowRoot) {
15
- const sheet = new CSSStyleSheet();
16
- sheet.replaceSync(cssText);
17
- shadowRoot.adoptedStyleSheets = [
18
- ...shadowRoot.adoptedStyleSheets,
19
- sheet
20
- ];
21
- } else {
22
- const style = document.createElement("style");
23
- style.textContent = cssText;
24
- document.head.appendChild(style);
25
- }
26
77
  }
27
- export {
28
- injectModuleStyle
29
- };
@@ -1,10 +1,7 @@
1
- const ATTR_KIND_ATTRIBUTE = 0;
2
- const ATTR_KIND_COMPLEX = 1;
3
- const ATTR_KIND_BOOLEAN = 2;
4
- const ATTR_KIND_TEMPLATE = 3;
5
- export {
6
- ATTR_KIND_ATTRIBUTE,
7
- ATTR_KIND_BOOLEAN,
8
- ATTR_KIND_COMPLEX,
9
- ATTR_KIND_TEMPLATE
10
- };
1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT license.
3
+ /** Attribute binding kind constants (matches compiled metadata). */
4
+ export const ATTR_KIND_ATTRIBUTE = 0;
5
+ export const ATTR_KIND_COMPLEX = 1;
6
+ export const ATTR_KIND_BOOLEAN = 2;
7
+ export const ATTR_KIND_TEMPLATE = 3;
package/dist/element.d.ts CHANGED
@@ -67,7 +67,14 @@ export declare class WebUIElement extends HTMLElement {
67
67
  * Starts searching from `after` (exclusive) if provided, or from firstChild.
68
68
  */
69
69
  private $findMarker;
70
- /** Find existing SSR text node by mapping template text-node ordinal. */
70
+ /**
71
+ * Find existing SSR text node by mapping template text-node ordinal.
72
+ *
73
+ * Similar to `$resolveSSR`, the SSR DOM may contain extra text nodes
74
+ * inside structural blocks (`<if>`/`<for>`) that are not in the
75
+ * compiled template. We skip `<!--wc-->...<!--/wc-->` and
76
+ * `<!--wr-->...<!--/wr-->` ranges to keep text ordinals aligned.
77
+ */
71
78
  private $findSSRText;
72
79
  /** Extract root tag name from block metadata. */
73
80
  private $rootTag;