kopular 0.21.3 → 0.22.0
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/LLM.md +42 -12
- package/README.md +63 -17
- package/package.json +3 -2
- package/src/component.js +1 -1
- package/src/dom.js.map +1 -1
- package/src/dom.ks +5 -0
- package/src/testing.js +8 -0
- package/src/vdom.js +13 -0
- package/src/vdom.js.map +1 -1
- package/src/vdom.ks +26 -0
package/LLM.md
CHANGED
|
@@ -300,12 +300,12 @@ w.Bump(); // re-render + diff + patch
|
|
|
300
300
|
now returns (see `kopscript`'s own `LLM.md`) for a subscription made in the constructor.
|
|
301
301
|
A component built once at app startup and never removed (every `Router` page, most real
|
|
302
302
|
apps' top-level structure) never needs this at all.
|
|
303
|
-
- `Router`'s own outlet
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
-
|
|
308
|
-
|
|
303
|
+
- `Router`'s own outlet uses this mechanism too — `Render()` embeds the matched page via
|
|
304
|
+
`VElement.Mount`, not the older `AfterRender` pattern above (that migration landed once
|
|
305
|
+
this existed; a single, always-present slot never needed the reordering/removal part,
|
|
306
|
+
but there was no reason not to use the same real mechanism everywhere).
|
|
307
|
+
- Templates get the same capability via `*mount="expr"` — see `kopscript`'s own `LLM.md`
|
|
308
|
+
for the syntax; it desugars to exactly this `VElement.Mount` call.
|
|
309
309
|
- **Content projection (React's `children`/Angular's `<ng-content>`) needs no separate
|
|
310
310
|
mechanism** — pass a `() => VElement` into a child's constructor and call it from inside
|
|
311
311
|
its own `Render()`; it's invoked fresh every render and closes over the parent's `this`,
|
|
@@ -386,6 +386,34 @@ class Counter : Component {
|
|
|
386
386
|
unaware templates exist; a template-generated `Render()` is indistinguishable from a
|
|
387
387
|
hand-written one to every other part of the framework.
|
|
388
388
|
|
|
389
|
+
## Scoped styles — `styles from` (see KopScript's own `LLM.md` for the CSS-parsing reference)
|
|
390
|
+
|
|
391
|
+
```ks
|
|
392
|
+
class Widget : Component {
|
|
393
|
+
constructor() : base() { }
|
|
394
|
+
template from "./widget.html";
|
|
395
|
+
styles from "./widget.css";
|
|
396
|
+
}
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
- Independent of `template from` — works with a hand-written `Render()` too. The compiler
|
|
400
|
+
rewrites the referenced `.css` so every selector requires a per-class
|
|
401
|
+
`data-kop-scope="<id>"` attribute, then splices one `ScopedStyles.Inject(id, css);` call
|
|
402
|
+
into the constructor. **Unlike `template from`, this needed real framework code**:
|
|
403
|
+
`ScopedStyles` (`vdom.ks`, new) — idempotent, static-array-registry dedup shape same as
|
|
404
|
+
`Batching`, injects one real `<style>` per component *type* (not per instance) into
|
|
405
|
+
`document.head` (`dom.ks`'s `head { get; }`, new) the first time any instance is
|
|
406
|
+
constructed, never removed afterward.
|
|
407
|
+
- `template from` elements get `data-kop-scope` automatically on every element
|
|
408
|
+
`buildElement` produces. A hand-written `Render()` gets a `protected string ScopeId;`
|
|
409
|
+
field instead — apply it manually: `el.SetAttr("data-kop-scope", this.ScopeId);`.
|
|
410
|
+
- `styles from` on a class with no constructor is a compile error (`KS3012` — nowhere to
|
|
411
|
+
splice `Inject`), same shape as auto-subscribe's `KS3005` for a template referencing
|
|
412
|
+
`state<T>` with no constructor.
|
|
413
|
+
- A `*mount`ed (or otherwise nested) child never inherits a parent's scope — each class's
|
|
414
|
+
`styles from` gets its own independently-computed `id`, and a mounted child's elements
|
|
415
|
+
come entirely from its own `Render()`/template, never the parent's.
|
|
416
|
+
|
|
389
417
|
## `Router` (`router.ks`)
|
|
390
418
|
|
|
391
419
|
Real URLs via the History API (`pushState`/`popstate`), not hash routing.
|
|
@@ -766,12 +794,14 @@ class CounterService {
|
|
|
766
794
|
of the tree's own data, not wired against a live DOM node until `Materialize`/`Patch`
|
|
767
795
|
runs) — an event Kopular doesn't have a named field for isn't reachable from `Render()`
|
|
768
796
|
at all yet.
|
|
769
|
-
- **A `Component` embedded via `AfterRender`/`Mount()` (imperative
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
797
|
+
- **A `Component` embedded via `AfterRender`/`Mount()` (imperative) is NOT the same as one
|
|
798
|
+
embedded via `VElement.Mount()` (declarative, part of the tree)** — only the latter gets
|
|
799
|
+
real reconciliation (patch in place, reorder, tear down); this includes `Router`'s own
|
|
800
|
+
outlet, which uses `VElement.Mount` internally, not the older `AfterRender` pattern. If
|
|
801
|
+
you find yourself calling `.Mount()`/`.Update()` on a child by hand from `AfterRender`
|
|
802
|
+
for anything that needs to be added, removed, or reordered, use
|
|
803
|
+
`VElement.Mount(component)` in `Render()` (or `*mount="expr"` in a template) instead —
|
|
804
|
+
see "Component" above.
|
|
775
805
|
|
|
776
806
|
## Does not exist
|
|
777
807
|
|
package/README.md
CHANGED
|
@@ -145,6 +145,48 @@ about `Component`, `Update()`, or any other Kopular API differs between them. Th
|
|
|
145
145
|
still has one manual `Subscribe`. `*if`/`*for` in a template are covered under
|
|
146
146
|
"Structural directives" below, alongside their hand-written-`Render()` equivalents.
|
|
147
147
|
|
|
148
|
+
## Scoped component styles
|
|
149
|
+
|
|
150
|
+
A class-body `styles from "./x.css";` — also a KopScript language feature (see
|
|
151
|
+
[KopScript](https://dev.azure.com/koppinator/Koppindependence/_git/KopScript)'s own
|
|
152
|
+
README/LLM.md for the full syntax/CSS-parsing reference) — scopes a real stylesheet so it
|
|
153
|
+
only matches elements *that class itself* renders, never a sibling's or a child's:
|
|
154
|
+
|
|
155
|
+
```ks
|
|
156
|
+
class Widget : Component {
|
|
157
|
+
constructor() : base() { }
|
|
158
|
+
template from "./widget.html";
|
|
159
|
+
styles from "./widget.css";
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
```css
|
|
164
|
+
/* widget.css */
|
|
165
|
+
.title { color: royalblue; }
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Unlike `template from`, this **did** need real framework code — `ScopedStyles.Inject`
|
|
169
|
+
(`vdom.ks`) is the runtime half: idempotent, injects one real `<style>` per component
|
|
170
|
+
*type* into `document.head` the first time any instance of that type is constructed
|
|
171
|
+
(dedup is per-type, not per-instance — every instance's constructor calls `Inject` with
|
|
172
|
+
the same compile-time `id`/rewritten-`css`, so only the first actually creates a tag).
|
|
173
|
+
Never removed once injected — a scoped stylesheet is global infrastructure for as long as
|
|
174
|
+
the page lives, not per-instance content `Teardown()` would ever clean up. `document.head`
|
|
175
|
+
(`dom.ks`) is new too, added specifically for this.
|
|
176
|
+
|
|
177
|
+
Independent of `template from` — works with a hand-written `Render()` as well, which gets
|
|
178
|
+
a `protected string ScopeId;` field to apply manually (`template from` elements get their
|
|
179
|
+
`data-kop-scope` attribute automatically, so this is only needed by hand-written code):
|
|
180
|
+
|
|
181
|
+
```ks
|
|
182
|
+
public override VElement Render() {
|
|
183
|
+
VElement el = VElement.Create("div");
|
|
184
|
+
el.ClassName = "title";
|
|
185
|
+
el.SetAttr("data-kop-scope", this.ScopeId);
|
|
186
|
+
return el;
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
148
190
|
## Dependency injection: the composition root pattern
|
|
149
191
|
|
|
150
192
|
Kopular has no injector because KopScript has nothing for one to hook into — no
|
|
@@ -188,11 +230,12 @@ class RoutedApp : Component {
|
|
|
188
230
|
return VElement.Create("div");
|
|
189
231
|
}
|
|
190
232
|
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
//
|
|
233
|
+
// VElement.Mount(this.Nav) in Render() (see "Nested component composition"
|
|
234
|
+
// below) would work here too, but a single, always-present root slot has
|
|
235
|
+
// no reordering or removal to get right, so the older, simpler
|
|
236
|
+
// AfterRender is just as correct — it's called with the real DOM node
|
|
237
|
+
// Render()'s tree just became, once Mount()/Update() has actually
|
|
238
|
+
// materialized/patched it, here that's the container itself.
|
|
196
239
|
protected override void AfterRender(Element root) {
|
|
197
240
|
this.Nav.Mount(root);
|
|
198
241
|
}
|
|
@@ -245,10 +288,10 @@ nav.AddRoute("/dogs/:id", new DogPage(nav));
|
|
|
245
288
|
el.TextContent = "Dog #" + this.Nav.Param;
|
|
246
289
|
```
|
|
247
290
|
|
|
248
|
-
`Param` is a plain `string`, deliberately not `state<T>` — Router's own `
|
|
249
|
-
|
|
250
|
-
which re-runs that page's `Render()` (reading the fresh `Param`)
|
|
251
|
-
`Subscribe()` needed on it.
|
|
291
|
+
`Param` is a plain `string`, deliberately not `state<T>` — `Router`'s own `Render()` already
|
|
292
|
+
re-embeds the matched page into its outlet (via `VElement.Mount`) on every
|
|
293
|
+
`Navigate()`/`popstate`, which re-runs that page's `Render()` (reading the fresh `Param`)
|
|
294
|
+
with no extra step. No `Subscribe()` needed on it.
|
|
252
295
|
|
|
253
296
|
**More than one dynamic segment**, and a trailing **wildcard** segment, both work too —
|
|
254
297
|
read each by name via `Router.Params(name)` instead (`Param` above still holds the
|
|
@@ -493,11 +536,14 @@ must stop, not just outlive an app that was going to unload anyway. Removing a c
|
|
|
493
536
|
that itself mounted further children tears the *whole* subtree down, however many levels
|
|
494
537
|
deep — a middle component overriding its own `OnUnmount()` doesn't skip its children's.
|
|
495
538
|
|
|
496
|
-
`Router`'s own outlet
|
|
497
|
-
|
|
498
|
-
|
|
539
|
+
`Router`'s own outlet uses this mechanism too — its `Render()` embeds the matched page via
|
|
540
|
+
`VElement.Mount`, not the older `AfterRender`-based pattern still shown in "Router, and
|
|
541
|
+
deploying it" below for mounting the `Router` itself into an app's root container (a
|
|
542
|
+
single, always-present slot, where there's nothing this mechanism would add). Reach for
|
|
499
543
|
`VElement.Mount` for anything with real add/remove/reorder: a list of components, a modal
|
|
500
|
-
that comes and goes, anything genuinely dynamic.
|
|
544
|
+
that comes and goes, anything genuinely dynamic. A template gets the same capability via
|
|
545
|
+
`*mount="expr"` — see "Templates" in `kopscript`'s own README/LLM.md for the syntax; it
|
|
546
|
+
desugars to exactly this call.
|
|
501
547
|
|
|
502
548
|
## Content projection — a parent giving a child what to render
|
|
503
549
|
|
|
@@ -863,10 +909,10 @@ v1 / hobby-project scope, same as KopScript itself. `Update()` diffs and patches
|
|
|
863
909
|
live child Component can be embedded directly in a parent's own tree via
|
|
864
910
|
`VElement.Mount(child)` — see "Nested component composition" above — so a parent's
|
|
865
911
|
re-render really can create/patch/reorder/tear down nested children declaratively now,
|
|
866
|
-
closing what used to be documented here as the framework's biggest reconciliation gap.
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
912
|
+
closing what used to be documented here as the framework's biggest reconciliation gap. A
|
|
913
|
+
template (`.html`) embeds one too, via `*mount="expr"` (`kopscript@0.25.0`+), the same way
|
|
914
|
+
`*if`/`*for` embed structural logic — see "Templates" in `kopscript`'s own docs. A list
|
|
915
|
+
child (`Mounted` or plain) without a stable
|
|
870
916
|
`VElement.Id` still renders correctly across a reorder, but isn't guaranteed to keep the
|
|
871
917
|
same real DOM node (see "Structural directives" above). `Update()` called before `Mount()`
|
|
872
918
|
is a safe no-op — see `LLM.md`'s `Component` section for exactly when that happens
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kopular",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "Kopular: a small component framework for KopScript — components, reactive state, constructor-injected services, routing, real compiled templates, and HTTP, with no DI container",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"./forms": "./src/forms.js",
|
|
36
36
|
"./computed": "./src/computed.js",
|
|
37
37
|
"./resource": "./src/resource.js",
|
|
38
|
+
"./vdom": "./src/vdom.js",
|
|
38
39
|
"./testing": "./src/testing.js"
|
|
39
40
|
},
|
|
40
41
|
"files": [
|
|
@@ -62,7 +63,7 @@
|
|
|
62
63
|
"@types/jsdom": "^30.0.0",
|
|
63
64
|
"@types/node": "^20.14.0",
|
|
64
65
|
"jsdom": "^25.0.1",
|
|
65
|
-
"kopscript": "^0.
|
|
66
|
+
"kopscript": "^0.26.0",
|
|
66
67
|
"typescript": "^5.5.0",
|
|
67
68
|
"vitest": "^4.1.11"
|
|
68
69
|
},
|
package/src/component.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Event, Element, Document, Location, History, Window, document, location, history, window } from "./dom.js";
|
|
2
2
|
import { VElement, NoOpEventHandler } from "./velement.js";
|
|
3
|
-
import { Batching, Materialize, Patch, UnmountPrevious, PatchChildren, UnmountTree } from "./vdom.js";
|
|
3
|
+
import { Batching, ScopedStyles, Materialize, Patch, UnmountPrevious, PatchChildren, UnmountTree } from "./vdom.js";
|
|
4
4
|
|
|
5
5
|
export class Component {
|
|
6
6
|
constructor() {
|
package/src/dom.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dom.js","sources":["dom.ks"],"sourcesContent":["// Minimal browser DOM bindings. All ambient (no `from` clause) — document,\n// Element, and Event genuinely exist as globals in a browser, no import\n// needed. Member names use the real JS casing exactly (camelCase), since\n// extern declarations describe an existing external contract rather than\n// idiomatic KopScript code — there's no per-member rename mechanism. `extern\n// class` declarations end in `;`, like the other two extern forms.\n\nextern class Event {\n Element target { get; }\n void preventDefault();\n};\n\nextern class Element {\n string textContent { get; set; }\n string id { get; set; }\n string className { get; set; }\n // The one property the vdom patch engine (src/vdom.ks) always sets via\n // direct property assignment, never setAttribute — setAttribute(\"value\",\n // x) sets the DEFAULT value attribute, not the current live one, a real\n // DOM footgun (and the exact property behind the original typing bug\n // this whole diffing effort traces back to).\n string value { get; set; }\n // Only ever assigned by the vdom patch engine for a VElement.RawHtml leaf\n // — never diffed into, an opaque blob the same way `raw string` is.\n string innerHTML { get; set; }\n void appendChild(Element child);\n void replaceChild(Element newChild, Element oldChild);\n // `referenceChild` is nullable — real DOM insertBefore(node, null) means\n // \"append at the end,\" used by the patch engine's child-reordering step.\n void insertBefore(Element newChild, Element? referenceChild);\n void removeChild(Element child);\n // The generic escape hatch VElement.SetAttr's ExtraNames/ExtraValues\n // patch through — real HTML attributes only (href, src, alt,\n // placeholder, ...), never `value` (see above).\n void setAttribute(string name, string value);\n void addEventListener(string eventType, (Event) => void handler);\n void removeEventListener(string eventType, (Event) => void handler);\n};\n\nextern class Document {\n Element createElement(string tagName);\n Element getElementById(string id);\n Element body { get; }\n};\n\nextern class Location {\n string pathname { get; }\n // The real query string including its leading \"?\" (e.g. \"?sort=name\"),\n // or \"\" if the current URL has none — see Router.ParseQuery/Query.\n string search { get; }\n};\n\nextern class History {\n // Param named `historyState`, not `state` — `state` is a KopScript\n // keyword (state<T>), not a valid parameter name.\n void pushState(string historyState, string title, string url);\n};\n\nextern class Window {\n void addEventListener(string eventType, (Event) => void handler);\n};\n\nextern Document document;\nextern Location location;\nextern History history;\nextern Window window;\n"],"names":[],"mappings":"AAOA;AAKA;AA2BA;
|
|
1
|
+
{"version":3,"file":"dom.js","sources":["dom.ks"],"sourcesContent":["// Minimal browser DOM bindings. All ambient (no `from` clause) — document,\n// Element, and Event genuinely exist as globals in a browser, no import\n// needed. Member names use the real JS casing exactly (camelCase), since\n// extern declarations describe an existing external contract rather than\n// idiomatic KopScript code — there's no per-member rename mechanism. `extern\n// class` declarations end in `;`, like the other two extern forms.\n\nextern class Event {\n Element target { get; }\n void preventDefault();\n};\n\nextern class Element {\n string textContent { get; set; }\n string id { get; set; }\n string className { get; set; }\n // The one property the vdom patch engine (src/vdom.ks) always sets via\n // direct property assignment, never setAttribute — setAttribute(\"value\",\n // x) sets the DEFAULT value attribute, not the current live one, a real\n // DOM footgun (and the exact property behind the original typing bug\n // this whole diffing effort traces back to).\n string value { get; set; }\n // Only ever assigned by the vdom patch engine for a VElement.RawHtml leaf\n // — never diffed into, an opaque blob the same way `raw string` is.\n string innerHTML { get; set; }\n void appendChild(Element child);\n void replaceChild(Element newChild, Element oldChild);\n // `referenceChild` is nullable — real DOM insertBefore(node, null) means\n // \"append at the end,\" used by the patch engine's child-reordering step.\n void insertBefore(Element newChild, Element? referenceChild);\n void removeChild(Element child);\n // The generic escape hatch VElement.SetAttr's ExtraNames/ExtraValues\n // patch through — real HTML attributes only (href, src, alt,\n // placeholder, ...), never `value` (see above).\n void setAttribute(string name, string value);\n void addEventListener(string eventType, (Event) => void handler);\n void removeEventListener(string eventType, (Event) => void handler);\n};\n\nextern class Document {\n Element createElement(string tagName);\n Element getElementById(string id);\n Element body { get; }\n // Where ScopedStyles.Inject (vdom.ks) puts a component type's own\n // rewritten <style> — a scoped stylesheet is global infrastructure\n // (injected once per type, never removed), not page content, so it\n // belongs in <head>, not wherever a given instance happens to mount.\n Element head { get; }\n};\n\nextern class Location {\n string pathname { get; }\n // The real query string including its leading \"?\" (e.g. \"?sort=name\"),\n // or \"\" if the current URL has none — see Router.ParseQuery/Query.\n string search { get; }\n};\n\nextern class History {\n // Param named `historyState`, not `state` — `state` is a KopScript\n // keyword (state<T>), not a valid parameter name.\n void pushState(string historyState, string title, string url);\n};\n\nextern class Window {\n void addEventListener(string eventType, (Event) => void handler);\n};\n\nextern Document document;\nextern Location location;\nextern History history;\nextern Window window;\n"],"names":[],"mappings":"AAOA;AAKA;AA2BA;AAWA;AAOA;AAMA;AAIA;AACA;AACA;AACA"}
|
package/src/dom.ks
CHANGED
|
@@ -41,6 +41,11 @@ extern class Document {
|
|
|
41
41
|
Element createElement(string tagName);
|
|
42
42
|
Element getElementById(string id);
|
|
43
43
|
Element body { get; }
|
|
44
|
+
// Where ScopedStyles.Inject (vdom.ks) puts a component type's own
|
|
45
|
+
// rewritten <style> — a scoped stylesheet is global infrastructure
|
|
46
|
+
// (injected once per type, never removed), not page content, so it
|
|
47
|
+
// belongs in <head>, not wherever a given instance happens to mount.
|
|
48
|
+
Element head { get; }
|
|
44
49
|
};
|
|
45
50
|
|
|
46
51
|
extern class Location {
|
package/src/testing.js
CHANGED
|
@@ -200,11 +200,19 @@ export async function runKopularApp(srcDir, entryFileName, options = {}) {
|
|
|
200
200
|
* @param {string} source - the entry file's full KopScript source.
|
|
201
201
|
* @param {object} [options] - same as `runKopularApp`, plus:
|
|
202
202
|
* @param {string} [options.entryFileName] - default "main.ks".
|
|
203
|
+
* @param {Record<string, string>} [options.extraSource] - inline content
|
|
204
|
+
* for auxiliary fixture files (filename -> content), written alongside
|
|
205
|
+
* the entry file — for a fixture whose `template from`/`styles from`
|
|
206
|
+
* references its own `.html`/`.css` file, which (unlike Kopular's own
|
|
207
|
+
* `.ks` sources) doesn't exist anywhere `copySources` would find it.
|
|
203
208
|
*/
|
|
204
209
|
export async function runKopularFixture(source, options = {}) {
|
|
205
210
|
const dir = mkTempDir();
|
|
206
211
|
copySources(OWN_SRC_DIR, dir, options.extraFiles ?? ["http_runtime.js"]);
|
|
207
212
|
const entryFileName = options.entryFileName ?? "main.ks";
|
|
208
213
|
writeFileSync(join(dir, entryFileName), source, "utf-8");
|
|
214
|
+
for (const [name, content] of Object.entries(options.extraSource ?? {})) {
|
|
215
|
+
writeFileSync(join(dir, name), content, "utf-8");
|
|
216
|
+
}
|
|
209
217
|
return compileAndRun(dir, entryFileName, options);
|
|
210
218
|
}
|
package/src/vdom.js
CHANGED
|
@@ -32,6 +32,19 @@ export class Batching {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
+
export class ScopedStyles {
|
|
36
|
+
static Injected = [];
|
|
37
|
+
|
|
38
|
+
static Inject(scopeId, css) {
|
|
39
|
+
if (!ScopedStyles.Injected.includes(scopeId)) {
|
|
40
|
+
ScopedStyles.Injected = [...ScopedStyles.Injected, scopeId];
|
|
41
|
+
let style = document.createElement("style");
|
|
42
|
+
style.setAttribute("data-kop-scope-sheet", scopeId);
|
|
43
|
+
style.textContent = css;
|
|
44
|
+
document.head.appendChild(style);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
35
48
|
export function Materialize(tree, parent) {
|
|
36
49
|
let maybeMounted = tree.Mounted;
|
|
37
50
|
if ((maybeMounted !== null)) {
|
package/src/vdom.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vdom.js","sources":["vdom.ks"],"sourcesContent":["using \"./dom\";\nusing \"./velement\";\n\n// The diff/patch engine behind real vdom diffing: Component.Update() (see\n// component.ks) calls Patch() with the PREVIOUS render's VElement tree\n// (which carries each node's real, live DOM counterpart via its own\n// RealNode field) and the NEW tree Render() just produced, and gets back\n// real DOM mutated/reused in place wherever possible instead of a full\n// subtree rebuild.\n//\n// Deliberately never reads the live DOM back to rediscover structure (no\n// \"get current children\"/\"get current tag\" API exists, or is needed) —\n// the retained *previous* VElement tree already records everything Patch()\n// needs to know about what's currently there. This is what makes the whole\n// approach work without a generic children/attributes read-back API that\n// KopScript's narrow, curated DOM binding doesn't have.\n\n// A component-like thing Batching (below) can flush once every DOM-event-\n// triggered Update() call inside one batch has registered itself. Declared\n// HERE, not in component.ks, specifically so THIS file's own Materialize/\n// Patch — which need to start/stop a batch around every real event\n// dispatch — never have to `using \"./component\"`: component.ks already\n// `using`s this file for Materialize/Patch themselves, and KopScript\n// rejects a circular `using` outright. Component (component.ks) is the\n// one real implementer, via `class Component : Flushable`.\ninterface Flushable {\n void FlushUpdate();\n}\n\n// Coalesces every Update() call made while a real DOM event handler\n// Kopular itself attached (see Materialize/Patch below, both of which wrap\n// each listener in Batching.Run before attaching it) into one flush per\n// affected component, applied once that handler returns — not one flush\n// per state<T> write inside it. See Component.Update()'s own comment\n// (component.ks) for the full rationale; this is just the mechanism.\n//\n// Deliberately class-level, shared across every batch/component rather\n// than per-instance — a single click can trigger Update() on more than one\n// component (e.g. a shared service's state<T> notifying two sibling\n// pages), and all of them need to flush together, once, when that one\n// handler finishes.\nclass Batching {\n private static number Depth = 0;\n private static Flushable[] Pending = [];\n\n public static bool IsActive() {\n return Batching.Depth > 0;\n }\n\n public static void Defer(Flushable f) {\n if (!Batching.Pending.Includes(f)) {\n Batching.Pending = Batching.Pending.Push(f);\n }\n }\n\n // `try`/`finally`, not a bare sequence: a handler that throws must still\n // decrement Depth and flush whatever already-triggered renders are\n // pending, or one uncaught exception would wedge every future click/\n // input on the page into \"always batching, never rendering.\" The\n // exception itself still propagates unchanged — this never catches it,\n // only guarantees the cleanup runs.\n public static void Run(() => void action) {\n Batching.Depth = Batching.Depth + 1;\n try {\n action();\n } finally {\n Batching.Depth = Batching.Depth - 1;\n if (Batching.Depth == 0) {\n Flushable[] toFlush = Batching.Pending;\n Batching.Pending = [];\n foreach (Flushable f in toFlush) {\n f.FlushUpdate();\n }\n }\n }\n }\n}\n\n// Builds a brand-new, fully real DOM subtree from a VElement tree with no\n// diffing at all — first mount, or whenever Patch() decides a subtree must\n// be replaced outright (no previous node to reuse, or the tag changed).\n// Mutates `tree.RealNode` (and recursively every descendant's) as a side\n// effect, so the tree this was called on becomes the new \"previous tree\"\n// the next Patch() call diffs against. Takes `parent` — every call site\n// already knows it (either Patch's own `parent` parameter, or the real\n// element a recursive child call is about to be appendChild'd into) —\n// solely to hand to a Mounted slot's own MountAsChild, which needs to\n// remember its real parent for THAT component's own future self-triggered\n// re-renders; an ordinary (non-Mounted) VElement never uses it.\nElement Materialize(VElement tree, Element parent) {\n Mountable? maybeMounted = tree.Mounted;\n if (maybeMounted != null) {\n Mountable m = maybeMounted;\n Element mountedRoot = m.MountAsChild(parent);\n tree.RealNode = mountedRoot;\n return mountedRoot;\n }\n\n Element el = document.createElement(tree.Tag);\n\n if (tree.RawHtml.Length > 0) {\n el.innerHTML = tree.RawHtml;\n } else if (tree.Children.Length > 0) {\n foreach (VElement child in tree.Children) {\n el.appendChild(Materialize(child, el));\n }\n } else {\n el.textContent = tree.TextContent;\n }\n\n el.className = tree.ClassName;\n el.id = tree.Id;\n el.value = tree.Value;\n\n for (number i = 0; i < tree.ExtraNames.Length; i = i + 1) {\n el.setAttribute(tree.ExtraNames[i], tree.ExtraValues[i]);\n }\n\n // Skip attaching VElement's own shared no-op (see velement.ks) — it does\n // nothing when invoked, so registering it costs real work (a listener\n // list entry, held onto for nothing) for zero benefit. A real handler\n // (never equal to the shared no-op) always gets attached as before —\n // wrapped in Batching.Run so every Update() it triggers coalesces with\n // any others from the same dispatch (see Component.Update()'s own\n // comment). The wrapper itself, not tree.OnClick, is what actually gets\n // registered — recorded on tree.AttachedOnClick (velement.ks) so a later\n // Patch() can remove this exact reference, not the handler value itself.\n if (tree.OnClick != NoOpEventHandler) {\n (Event) => void wrapped = (Event e) => Batching.Run(() => tree.OnClick(e));\n tree.AttachedOnClick = wrapped;\n el.addEventListener(\"click\", wrapped);\n }\n if (tree.OnInput != NoOpEventHandler) {\n (Event) => void wrapped = (Event e) => Batching.Run(() => tree.OnInput(e));\n tree.AttachedOnInput = wrapped;\n el.addEventListener(\"input\", wrapped);\n }\n if (tree.OnBlur != NoOpEventHandler) {\n (Event) => void wrapped = (Event e) => Batching.Run(() => tree.OnBlur(e));\n tree.AttachedOnBlur = wrapped;\n el.addEventListener(\"blur\", wrapped);\n }\n if (tree.OnChange != NoOpEventHandler) {\n (Event) => void wrapped = (Event e) => Batching.Run(() => tree.OnChange(e));\n tree.AttachedOnChange = wrapped;\n el.addEventListener(\"change\", wrapped);\n }\n\n tree.RealNode = el;\n return el;\n}\n\n// Diffs `updated` against `old` (the previous render's tree for this exact\n// position, or null if there is none — first mount) and returns the real\n// DOM node now representing `updated`, reusing `old`'s real node in place\n// whenever the tag matches. `parent` is only used to attach/replace at the\n// top of whatever subtree Patch() is called on — child-level attach/replace\n// happens inside PatchChildren.\n//\n// Deliberately all positive-branch `if (x != null) { ... } else { ... }`,\n// never an early-return guard clause — KopScript's nullable narrowing is\n// scope-based, not reachability-based, so `if (x == null) { return; }\n// use(x);` would NOT narrow `x` afterward (see KopScript's own LLM.md \"Common\n// mistakes\"). Every nullable member-access path (`oldTree.RealNode`,\n// never narrows directly either) is read into a local first for the same\n// reason.\nElement Patch(Element parent, VElement? old, VElement updated) {\n Mountable? newMounted = updated.Mounted;\n Mountable? oldMounted = null;\n if (old != null) {\n VElement oldTreeForMount = old;\n oldMounted = oldTreeForMount.Mounted;\n }\n\n // A live child slot (VElement.Mounted — see velement.ks) is handled\n // entirely separately from the ordinary Tag-based logic below: it's not\n // Kopular's own DOM element to create/reuse at all, and its \"same node\n // or replace\" decision is instance identity (== on the Mountable itself,\n // real reference equality — interfaces erase to the underlying object),\n // not Tag equality.\n if (newMounted != null) {\n Mountable nm = newMounted;\n if (oldMounted != null) {\n Mountable om = oldMounted;\n if (om == nm) {\n // Same instance still in this slot: patch it in place, don't touch\n // the DOM position at all (its own real node, reused or not, is\n // already exactly where it needs to be).\n Element reused = nm.PatchAsChild();\n updated.RealNode = reused;\n return reused;\n }\n }\n // First time this slot has anything mounted, or a DIFFERENT instance\n // took over the slot: release whatever was here, then mount fresh.\n UnmountPrevious(parent, old, oldMounted);\n Element created = nm.MountAsChild(parent);\n parent.appendChild(created);\n updated.RealNode = created;\n return created;\n }\n\n if (oldMounted != null) {\n // Slot reverted from a live component back to plain content: release\n // the old one, then fall through to an ordinary fresh materialize.\n UnmountPrevious(parent, old, oldMounted);\n Element created = Materialize(updated, parent);\n parent.appendChild(created);\n return created;\n }\n\n if (old != null) {\n VElement oldTree = old;\n Element? maybeOldNode = oldTree.RealNode;\n if (maybeOldNode != null) {\n Element realNode = maybeOldNode;\n if (oldTree.Tag != updated.Tag) {\n Element created = Materialize(updated, parent);\n parent.replaceChild(created, realNode);\n return created;\n } else {\n updated.RealNode = realNode;\n\n if (updated.RawHtml.Length > 0 || oldTree.RawHtml.Length > 0) {\n if (updated.RawHtml != oldTree.RawHtml) {\n realNode.innerHTML = updated.RawHtml;\n }\n } else {\n if (updated.TextContent != oldTree.TextContent) {\n realNode.textContent = updated.TextContent;\n }\n PatchChildren(realNode, oldTree.Children, updated.Children);\n }\n\n if (updated.ClassName != oldTree.ClassName) {\n realNode.className = updated.ClassName;\n }\n if (updated.Id != oldTree.Id) {\n realNode.id = updated.Id;\n }\n // Always assigned, never conditionally on updated.Value != oldTree.Value\n // — unlike TextContent/ClassName/Id, an <input>/<select>'s live value\n // can diverge from the last-recorded VElement.Value purely through\n // user interaction (typing, picking an option) with no Update() ever\n // running in between (a real, deliberate pattern — see KopularDemo's\n // dogs_page.ks, which never Update()s on input/change). The recorded\n // oldTree.Value only reflects the tree as of the last actual render,\n // so comparing against it can't tell \"genuinely unchanged\" apart from\n // \"changed live in the DOM since then, framework never told\" — the\n // same reason a real \"controlled input\" (React's own term for this)\n // always writes value on every render rather than diffing it.\n realNode.value = updated.Value;\n\n // Same-length is the overwhelmingly common case (the same Render()\n // code path calls SetAttr the same number of times, in the same\n // order, on every call) — compare aligned by index and only touch\n // the real DOM for an entry that actually changed, rather than\n // reapplying every extra attribute on every patch regardless. A\n // length mismatch (the rarer case: a SetAttr call was added,\n // removed, or made conditional between renders) falls back to\n // reapplying everything, since index-aligned comparison isn't\n // meaningful once the two arrays don't correspond entry-for-entry.\n if (updated.ExtraNames.Length == oldTree.ExtraNames.Length) {\n for (number i = 0; i < updated.ExtraNames.Length; i = i + 1) {\n if (updated.ExtraNames[i] != oldTree.ExtraNames[i] || updated.ExtraValues[i] != oldTree.ExtraValues[i]) {\n realNode.setAttribute(updated.ExtraNames[i], updated.ExtraValues[i]);\n }\n }\n } else {\n for (number i = 0; i < updated.ExtraNames.Length; i = i + 1) {\n realNode.setAttribute(updated.ExtraNames[i], updated.ExtraValues[i]);\n }\n }\n\n // Swap a listener only when the handler reference actually changed.\n // A node that never sets a real OnClick/OnInput/OnBlur/OnChange\n // keeps VElement's own shared NoOpEventHandler reference on both\n // sides (see velement.ks) — comparing by `!=` costs nothing and\n // skips two real DOM API calls per event per node for the (common)\n // case of \"this node has no handler of this kind either time,\"\n // which matters a lot on a list with hundreds/thousands of rows\n // most of which set at most one or two of the four. A node WITH a\n // real handler still gets a fresh closure every render (it\n // captures per-render values, like a loop's own item), so it still\n // swaps every time — correctly, since the old closure really is\n // stale.\n //\n // The actually-attached listener is always a Batching.Run wrapper\n // (see Materialize above), never `oldTree.OnClick`/`updated.OnClick`\n // themselves — removeEventListener only works when passed the exact\n // reference addEventListener received, so removal always goes\n // through `oldTree.AttachedOnClick` (the wrapper Materialize/a\n // previous Patch actually registered), and a real swap builds a\n // fresh wrapper the same way Materialize does. When nothing swaps,\n // `updated.AttachedOnClick` still needs to carry `oldTree`'s\n // forward — `updated` is a brand-new VElement whose own\n // AttachedOnClick starts back at the shared no-op (velement.ks's\n // constructor), and it becomes the retained \"old tree\" the very\n // next Patch() call diffs against.\n if (updated.OnClick != oldTree.OnClick) {\n realNode.removeEventListener(\"click\", oldTree.AttachedOnClick);\n (Event) => void wrappedClick = (Event e) => Batching.Run(() => updated.OnClick(e));\n updated.AttachedOnClick = wrappedClick;\n realNode.addEventListener(\"click\", wrappedClick);\n } else {\n updated.AttachedOnClick = oldTree.AttachedOnClick;\n }\n if (updated.OnInput != oldTree.OnInput) {\n realNode.removeEventListener(\"input\", oldTree.AttachedOnInput);\n (Event) => void wrappedInput = (Event e) => Batching.Run(() => updated.OnInput(e));\n updated.AttachedOnInput = wrappedInput;\n realNode.addEventListener(\"input\", wrappedInput);\n } else {\n updated.AttachedOnInput = oldTree.AttachedOnInput;\n }\n if (updated.OnBlur != oldTree.OnBlur) {\n realNode.removeEventListener(\"blur\", oldTree.AttachedOnBlur);\n (Event) => void wrappedBlur = (Event e) => Batching.Run(() => updated.OnBlur(e));\n updated.AttachedOnBlur = wrappedBlur;\n realNode.addEventListener(\"blur\", wrappedBlur);\n } else {\n updated.AttachedOnBlur = oldTree.AttachedOnBlur;\n }\n if (updated.OnChange != oldTree.OnChange) {\n realNode.removeEventListener(\"change\", oldTree.AttachedOnChange);\n (Event) => void wrappedChange = (Event e) => Batching.Run(() => updated.OnChange(e));\n updated.AttachedOnChange = wrappedChange;\n realNode.addEventListener(\"change\", wrappedChange);\n } else {\n updated.AttachedOnChange = oldTree.AttachedOnChange;\n }\n\n return realNode;\n }\n } else {\n // Shouldn't happen in practice (every previously-rendered tree has a\n // real node by the time a second render diffs against it) — treated\n // as \"nothing to reuse\" rather than a crash, same defensive spirit\n // as Component's own IsMounted guard.\n Element created = Materialize(updated, parent);\n parent.appendChild(created);\n return created;\n }\n } else {\n Element created = Materialize(updated, parent);\n parent.appendChild(created);\n return created;\n }\n}\n\n// Releases whatever was previously mounted in a slot (calling its\n// Teardown, which — for a Component — cascades into anything IT mounted\n// too, however many levels deep, before its own app-facing OnUnmount runs)\n// and removes its real node from the DOM, before a different Mountable (or\n// plain content) takes over that slot. A no-op when there was nothing\n// mounted here before — the common \"first render of this slot\" case.\nvoid UnmountPrevious(Element parent, VElement? old, Mountable? oldMounted) {\n // Teardown() only applies when there really was a Mounted instance here\n // before (nothing to tear down for plain content) — but the real node\n // itself needs removing whenever `old` had one, Mounted or not: a plain\n // VElement's slot turning into a Mounted one is exactly as much a\n // wholesale replacement as the reverse direction (handled by falling\n // through to Materialize below in Patch), and both need the OLD node\n // gone before the NEW one is appended, not left behind as a stray\n // sibling.\n if (oldMounted != null) {\n Mountable m = oldMounted;\n m.Teardown();\n }\n if (old != null) {\n VElement oldTree = old;\n Element? maybeOldNode = oldTree.RealNode;\n if (maybeOldNode != null) {\n Element oldNode = maybeOldNode;\n parent.removeChild(oldNode);\n }\n }\n}\n\n// Keyed reconciliation: each VElement's own Id is its key when non-empty —\n// a real, existing DOM convention, needing no new API or syntax. A new\n// child whose Id matches an old child's Id is patched against that old\n// child (reusing its real node) regardless of position; a new child with\n// no Id, or an Id not present among the old children, falls back to\n// pairing positionally against whatever old children are still unconsumed,\n// in order. DOCUMENTED, REAL LIMITATION: without stable Ids, a reordered\n// list still produces the correct final output, but a given item's real\n// DOM node (and anything stateful attached to it, like focus) isn't\n// guaranteed to follow its data across the reorder — give list items a\n// stable Id for that guarantee.\nvoid PatchChildren(Element parent, VElement[] oldChildren, VElement[] newChildren) {\n // Map, not a Push loop — Push is deliberately non-mutating (a real\n // spread-copy every call, see KopScript's own README), so building an\n // n-length array by Push-ing once per element in a loop is an\n // accidental O(n^2) on every single PatchChildren call, however small\n // the actual diff. Map is a real, single O(n) pass straight to\n // Array.prototype.map.\n bool[] oldConsumed = oldChildren.Map((VElement c) => false);\n number[] matchedOldIndex = newChildren.Map((VElement c) => -1);\n\n // Pass 1: keyed matches, by Id.\n for (number i = 0; i < newChildren.Length; i = i + 1) {\n if (newChildren[i].Id.Length == 0) { continue; }\n for (number j = 0; j < oldChildren.Length; j = j + 1) {\n if (!oldConsumed[j] && oldChildren[j].Id == newChildren[i].Id) {\n matchedOldIndex[i] = j;\n oldConsumed[j] = true;\n break;\n }\n }\n }\n\n // Pass 2: positional fallback for everything Pass 1 didn't match —\n // pair each remaining new child against the next still-unconsumed old\n // child, in order.\n number nextOld = 0;\n for (number i = 0; i < newChildren.Length; i = i + 1) {\n if (matchedOldIndex[i] >= 0) { continue; }\n while (nextOld < oldChildren.Length && oldConsumed[nextOld]) {\n nextOld = nextOld + 1;\n }\n if (nextOld < oldChildren.Length) {\n matchedOldIndex[i] = nextOld;\n oldConsumed[nextOld] = true;\n nextOld = nextOld + 1;\n }\n }\n\n // Whether anything is actually moving at all — same length, and every\n // new position matched the *same* old position. The extremely common\n // case for a list that's only had some of its rows' own content change\n // (e.g. \"update every 10th row\"), where reconciliation still has real\n // work to do (see Pass 1/2 above and Patch() itself) but nothing needs\n // to physically move in the DOM at all.\n bool needsReorder = oldChildren.Length != newChildren.Length;\n for (number i = 0; i < newChildren.Length; i = i + 1) {\n if (matchedOldIndex[i] != i) {\n needsReorder = true;\n break;\n }\n }\n\n // Pass 3: patch/create each new child in order, then — only if the list\n // actually needs reordering — move it into its correct final position.\n // appendChild on a node already attached elsewhere in the DOM MOVES it\n // (real DOM semantics), so processing new children in their final\n // desired order and always appending naturally builds up the correct\n // sequence, no separate insertBefore/reference-node bookkeeping needed.\n // Safe because VElement.Children is always the COMPLETE list of a\n // node's children — nothing else ever shares `parent`. Skipping the\n // move entirely when `needsReorder` is false avoids a real DOM API call\n // per child for the common no-reorder case — Patch() itself already\n // updates or replaces a reused/changed node exactly in place either way.\n for (number i = 0; i < newChildren.Length; i = i + 1) {\n VElement? matchedOld = null;\n if (matchedOldIndex[i] >= 0) {\n matchedOld = oldChildren[matchedOldIndex[i]];\n }\n Element childNode = Patch(parent, matchedOld, newChildren[i]);\n if (needsReorder) {\n parent.appendChild(childNode);\n }\n }\n\n // Pass 4: remove whatever old children never got reused — releasing\n // anything Mounted first (see velement.ks) so a child genuinely dropped\n // from a list (not replaced in the same slot by a different one, which\n // Patch() itself already handles via UnmountPrevious — removed outright)\n // still gets torn down. This is the one place THAT case is reachable\n // from, since Patch() only ever sees one slot at a time, never a slot\n // disappearing entirely.\n for (number j = 0; j < oldChildren.Length; j = j + 1) {\n if (oldConsumed[j]) { continue; }\n Mountable? maybeOldMounted = oldChildren[j].Mounted;\n if (maybeOldMounted != null) {\n Mountable m = maybeOldMounted;\n m.Teardown();\n }\n Element? maybeOldNode = oldChildren[j].RealNode;\n if (maybeOldNode != null) {\n Element oldNode = maybeOldNode;\n parent.removeChild(oldNode);\n }\n }\n}\n\n// Recursively finds and tears down every Mounted node within `tree` (tree\n// itself included) — used by Component's own Teardown (see component.ks)\n// so removing ONE component transitively releases everything IT rendered,\n// however many Mounted levels deep, not only the outermost one. A pure\n// data walk over the retained VElement tree; never touches the real DOM\n// itself (the caller already owns removing whatever real node is actually\n// attached).\nvoid UnmountTree(VElement tree) {\n Mountable? maybeMounted = tree.Mounted;\n if (maybeMounted != null) {\n Mountable m = maybeMounted;\n m.Teardown();\n }\n foreach (VElement child in tree.Children) {\n UnmountTree(child);\n }\n}\n"],"names":[],"mappings":";;;AAyCA;EACiB;;EACA;;EAED;IACZ;;;EAGY;IACZ;MACmB;;;;EAUP;IACG;IACf;MACQ;;MAES;MACf;QACE;QACiB;QACjB;UACe;;;;;;AAkBvB;EACE;EACA;IACE;IACA;IACc;IACd;;EAGF;EAEA;IACe;;IAEb;MACgB;;;IAGD;;EAGJ;EACP;EACG;EAET;IACiB;;EAYjB;IACE;IACqB;IACF;;EAErB;IACE;IACqB;IACF;;EAErB;IACE;IACoB;IACD;;EAErB;IACE;IACsB;IACH;;EAGP;EACd;;AAiBF;EACE;EACA;EACA;IACE;IACW;;EASb;IACE;IACA;MACE;MACA;QAIE;QACiB;QACjB;;;IAKW;IACf;IACkB;IACD;IACjB;;EAGF;IAGiB;IACf;IACkB;IAClB;;EAGF;IACE;IACA;IACA;MACE;MACA;QACE;QACmB;QACnB;;QAEiB;QAEjB;UACE;YACqB;;;UAGrB;YACuB;;UAEV;;QAGf;UACqB;;QAErB;UACc;;QAaC;QAWf;UACE;YACE;cACuB;;;;UAIzB;YACuB;;;QA6BzB;UAC8B;UAC5B;UACwB;UACC;;UAED;;QAE1B;UAC8B;UAC5B;UACwB;UACC;;UAED;;QAE1B;UAC8B;UAC5B;UACuB;UACE;;UAEF;;QAEzB;UAC8B;UAC5B;UACyB;UACA;;UAEA;;QAG3B;;;MAOF;MACkB;MAClB;;;IAGF;IACkB;IAClB;;;AAUJ;EASE;IACE;IACU;;EAEZ;IACE;IACA;IACA;MACE;MACkB;;;;AAgBxB;EAOE;EACA;EAGA;IACE;MAAqC;;IACrC;MACE;QACqB;QACJ;QACf;;;;EAQN;EACA;IACE;MAA+B;;IAC/B;MACU;;IAEV;MACqB;MACE;MACb;;;EAUZ;EACA;IACE;MACe;MACb;;;EAeJ;IACE;IACA;MACa;;IAEb;IACA;MACoB;;;EAWtB;IACE;MAAsB;;IACtB;IACA;MACE;MACU;;IAEZ;IACA;MACE;MACkB;;;;AAYxB;EACE;EACA;IACE;IACU;;EAEZ;IACa"}
|
|
1
|
+
{"version":3,"file":"vdom.js","sources":["vdom.ks"],"sourcesContent":["using \"./dom\";\nusing \"./velement\";\n\n// The diff/patch engine behind real vdom diffing: Component.Update() (see\n// component.ks) calls Patch() with the PREVIOUS render's VElement tree\n// (which carries each node's real, live DOM counterpart via its own\n// RealNode field) and the NEW tree Render() just produced, and gets back\n// real DOM mutated/reused in place wherever possible instead of a full\n// subtree rebuild.\n//\n// Deliberately never reads the live DOM back to rediscover structure (no\n// \"get current children\"/\"get current tag\" API exists, or is needed) —\n// the retained *previous* VElement tree already records everything Patch()\n// needs to know about what's currently there. This is what makes the whole\n// approach work without a generic children/attributes read-back API that\n// KopScript's narrow, curated DOM binding doesn't have.\n\n// A component-like thing Batching (below) can flush once every DOM-event-\n// triggered Update() call inside one batch has registered itself. Declared\n// HERE, not in component.ks, specifically so THIS file's own Materialize/\n// Patch — which need to start/stop a batch around every real event\n// dispatch — never have to `using \"./component\"`: component.ks already\n// `using`s this file for Materialize/Patch themselves, and KopScript\n// rejects a circular `using` outright. Component (component.ks) is the\n// one real implementer, via `class Component : Flushable`.\ninterface Flushable {\n void FlushUpdate();\n}\n\n// Coalesces every Update() call made while a real DOM event handler\n// Kopular itself attached (see Materialize/Patch below, both of which wrap\n// each listener in Batching.Run before attaching it) into one flush per\n// affected component, applied once that handler returns — not one flush\n// per state<T> write inside it. See Component.Update()'s own comment\n// (component.ks) for the full rationale; this is just the mechanism.\n//\n// Deliberately class-level, shared across every batch/component rather\n// than per-instance — a single click can trigger Update() on more than one\n// component (e.g. a shared service's state<T> notifying two sibling\n// pages), and all of them need to flush together, once, when that one\n// handler finishes.\nclass Batching {\n private static number Depth = 0;\n private static Flushable[] Pending = [];\n\n public static bool IsActive() {\n return Batching.Depth > 0;\n }\n\n public static void Defer(Flushable f) {\n if (!Batching.Pending.Includes(f)) {\n Batching.Pending = Batching.Pending.Push(f);\n }\n }\n\n // `try`/`finally`, not a bare sequence: a handler that throws must still\n // decrement Depth and flush whatever already-triggered renders are\n // pending, or one uncaught exception would wedge every future click/\n // input on the page into \"always batching, never rendering.\" The\n // exception itself still propagates unchanged — this never catches it,\n // only guarantees the cleanup runs.\n public static void Run(() => void action) {\n Batching.Depth = Batching.Depth + 1;\n try {\n action();\n } finally {\n Batching.Depth = Batching.Depth - 1;\n if (Batching.Depth == 0) {\n Flushable[] toFlush = Batching.Pending;\n Batching.Pending = [];\n foreach (Flushable f in toFlush) {\n f.FlushUpdate();\n }\n }\n }\n }\n}\n\n// The runtime half of kopscript's `styles from \"<path>.css\";` (see its own\n// README/LLM.md \"Templates\" section) — the compiler rewrites a class's own\n// stylesheet at compile time (every selector scoped to that class's\n// `data-kop-scope=\"<id>\"` attribute) and splices exactly one\n// `ScopedStyles.Inject(id, css);` call into its constructor. Same\n// static-array-registry-with-Includes-dedup shape as Batching above,\n// deliberately: dedup here is per component TYPE, not per instance — every\n// instance's constructor calls Inject with the same `id`/`css` (both\n// compile-time constants for that class), so only the first one actually\n// creates a <style> tag; the rest are no-ops. Never removed once injected\n// — a scoped stylesheet is global infrastructure for as long as the page\n// lives, not per-instance content Teardown() would ever need to clean up.\nclass ScopedStyles {\n private static string[] Injected = [];\n\n public static void Inject(string scopeId, string css) {\n if (!ScopedStyles.Injected.Includes(scopeId)) {\n ScopedStyles.Injected = ScopedStyles.Injected.Push(scopeId);\n Element style = document.createElement(\"style\");\n style.setAttribute(\"data-kop-scope-sheet\", scopeId);\n style.textContent = css;\n document.head.appendChild(style);\n }\n }\n}\n\n// Builds a brand-new, fully real DOM subtree from a VElement tree with no\n// diffing at all — first mount, or whenever Patch() decides a subtree must\n// be replaced outright (no previous node to reuse, or the tag changed).\n// Mutates `tree.RealNode` (and recursively every descendant's) as a side\n// effect, so the tree this was called on becomes the new \"previous tree\"\n// the next Patch() call diffs against. Takes `parent` — every call site\n// already knows it (either Patch's own `parent` parameter, or the real\n// element a recursive child call is about to be appendChild'd into) —\n// solely to hand to a Mounted slot's own MountAsChild, which needs to\n// remember its real parent for THAT component's own future self-triggered\n// re-renders; an ordinary (non-Mounted) VElement never uses it.\nElement Materialize(VElement tree, Element parent) {\n Mountable? maybeMounted = tree.Mounted;\n if (maybeMounted != null) {\n Mountable m = maybeMounted;\n Element mountedRoot = m.MountAsChild(parent);\n tree.RealNode = mountedRoot;\n return mountedRoot;\n }\n\n Element el = document.createElement(tree.Tag);\n\n if (tree.RawHtml.Length > 0) {\n el.innerHTML = tree.RawHtml;\n } else if (tree.Children.Length > 0) {\n foreach (VElement child in tree.Children) {\n el.appendChild(Materialize(child, el));\n }\n } else {\n el.textContent = tree.TextContent;\n }\n\n el.className = tree.ClassName;\n el.id = tree.Id;\n el.value = tree.Value;\n\n for (number i = 0; i < tree.ExtraNames.Length; i = i + 1) {\n el.setAttribute(tree.ExtraNames[i], tree.ExtraValues[i]);\n }\n\n // Skip attaching VElement's own shared no-op (see velement.ks) — it does\n // nothing when invoked, so registering it costs real work (a listener\n // list entry, held onto for nothing) for zero benefit. A real handler\n // (never equal to the shared no-op) always gets attached as before —\n // wrapped in Batching.Run so every Update() it triggers coalesces with\n // any others from the same dispatch (see Component.Update()'s own\n // comment). The wrapper itself, not tree.OnClick, is what actually gets\n // registered — recorded on tree.AttachedOnClick (velement.ks) so a later\n // Patch() can remove this exact reference, not the handler value itself.\n if (tree.OnClick != NoOpEventHandler) {\n (Event) => void wrapped = (Event e) => Batching.Run(() => tree.OnClick(e));\n tree.AttachedOnClick = wrapped;\n el.addEventListener(\"click\", wrapped);\n }\n if (tree.OnInput != NoOpEventHandler) {\n (Event) => void wrapped = (Event e) => Batching.Run(() => tree.OnInput(e));\n tree.AttachedOnInput = wrapped;\n el.addEventListener(\"input\", wrapped);\n }\n if (tree.OnBlur != NoOpEventHandler) {\n (Event) => void wrapped = (Event e) => Batching.Run(() => tree.OnBlur(e));\n tree.AttachedOnBlur = wrapped;\n el.addEventListener(\"blur\", wrapped);\n }\n if (tree.OnChange != NoOpEventHandler) {\n (Event) => void wrapped = (Event e) => Batching.Run(() => tree.OnChange(e));\n tree.AttachedOnChange = wrapped;\n el.addEventListener(\"change\", wrapped);\n }\n\n tree.RealNode = el;\n return el;\n}\n\n// Diffs `updated` against `old` (the previous render's tree for this exact\n// position, or null if there is none — first mount) and returns the real\n// DOM node now representing `updated`, reusing `old`'s real node in place\n// whenever the tag matches. `parent` is only used to attach/replace at the\n// top of whatever subtree Patch() is called on — child-level attach/replace\n// happens inside PatchChildren.\n//\n// Deliberately all positive-branch `if (x != null) { ... } else { ... }`,\n// never an early-return guard clause — KopScript's nullable narrowing is\n// scope-based, not reachability-based, so `if (x == null) { return; }\n// use(x);` would NOT narrow `x` afterward (see KopScript's own LLM.md \"Common\n// mistakes\"). Every nullable member-access path (`oldTree.RealNode`,\n// never narrows directly either) is read into a local first for the same\n// reason.\nElement Patch(Element parent, VElement? old, VElement updated) {\n Mountable? newMounted = updated.Mounted;\n Mountable? oldMounted = null;\n if (old != null) {\n VElement oldTreeForMount = old;\n oldMounted = oldTreeForMount.Mounted;\n }\n\n // A live child slot (VElement.Mounted — see velement.ks) is handled\n // entirely separately from the ordinary Tag-based logic below: it's not\n // Kopular's own DOM element to create/reuse at all, and its \"same node\n // or replace\" decision is instance identity (== on the Mountable itself,\n // real reference equality — interfaces erase to the underlying object),\n // not Tag equality.\n if (newMounted != null) {\n Mountable nm = newMounted;\n if (oldMounted != null) {\n Mountable om = oldMounted;\n if (om == nm) {\n // Same instance still in this slot: patch it in place, don't touch\n // the DOM position at all (its own real node, reused or not, is\n // already exactly where it needs to be).\n Element reused = nm.PatchAsChild();\n updated.RealNode = reused;\n return reused;\n }\n }\n // First time this slot has anything mounted, or a DIFFERENT instance\n // took over the slot: release whatever was here, then mount fresh.\n UnmountPrevious(parent, old, oldMounted);\n Element created = nm.MountAsChild(parent);\n parent.appendChild(created);\n updated.RealNode = created;\n return created;\n }\n\n if (oldMounted != null) {\n // Slot reverted from a live component back to plain content: release\n // the old one, then fall through to an ordinary fresh materialize.\n UnmountPrevious(parent, old, oldMounted);\n Element created = Materialize(updated, parent);\n parent.appendChild(created);\n return created;\n }\n\n if (old != null) {\n VElement oldTree = old;\n Element? maybeOldNode = oldTree.RealNode;\n if (maybeOldNode != null) {\n Element realNode = maybeOldNode;\n if (oldTree.Tag != updated.Tag) {\n Element created = Materialize(updated, parent);\n parent.replaceChild(created, realNode);\n return created;\n } else {\n updated.RealNode = realNode;\n\n if (updated.RawHtml.Length > 0 || oldTree.RawHtml.Length > 0) {\n if (updated.RawHtml != oldTree.RawHtml) {\n realNode.innerHTML = updated.RawHtml;\n }\n } else {\n if (updated.TextContent != oldTree.TextContent) {\n realNode.textContent = updated.TextContent;\n }\n PatchChildren(realNode, oldTree.Children, updated.Children);\n }\n\n if (updated.ClassName != oldTree.ClassName) {\n realNode.className = updated.ClassName;\n }\n if (updated.Id != oldTree.Id) {\n realNode.id = updated.Id;\n }\n // Always assigned, never conditionally on updated.Value != oldTree.Value\n // — unlike TextContent/ClassName/Id, an <input>/<select>'s live value\n // can diverge from the last-recorded VElement.Value purely through\n // user interaction (typing, picking an option) with no Update() ever\n // running in between (a real, deliberate pattern — see KopularDemo's\n // dogs_page.ks, which never Update()s on input/change). The recorded\n // oldTree.Value only reflects the tree as of the last actual render,\n // so comparing against it can't tell \"genuinely unchanged\" apart from\n // \"changed live in the DOM since then, framework never told\" — the\n // same reason a real \"controlled input\" (React's own term for this)\n // always writes value on every render rather than diffing it.\n realNode.value = updated.Value;\n\n // Same-length is the overwhelmingly common case (the same Render()\n // code path calls SetAttr the same number of times, in the same\n // order, on every call) — compare aligned by index and only touch\n // the real DOM for an entry that actually changed, rather than\n // reapplying every extra attribute on every patch regardless. A\n // length mismatch (the rarer case: a SetAttr call was added,\n // removed, or made conditional between renders) falls back to\n // reapplying everything, since index-aligned comparison isn't\n // meaningful once the two arrays don't correspond entry-for-entry.\n if (updated.ExtraNames.Length == oldTree.ExtraNames.Length) {\n for (number i = 0; i < updated.ExtraNames.Length; i = i + 1) {\n if (updated.ExtraNames[i] != oldTree.ExtraNames[i] || updated.ExtraValues[i] != oldTree.ExtraValues[i]) {\n realNode.setAttribute(updated.ExtraNames[i], updated.ExtraValues[i]);\n }\n }\n } else {\n for (number i = 0; i < updated.ExtraNames.Length; i = i + 1) {\n realNode.setAttribute(updated.ExtraNames[i], updated.ExtraValues[i]);\n }\n }\n\n // Swap a listener only when the handler reference actually changed.\n // A node that never sets a real OnClick/OnInput/OnBlur/OnChange\n // keeps VElement's own shared NoOpEventHandler reference on both\n // sides (see velement.ks) — comparing by `!=` costs nothing and\n // skips two real DOM API calls per event per node for the (common)\n // case of \"this node has no handler of this kind either time,\"\n // which matters a lot on a list with hundreds/thousands of rows\n // most of which set at most one or two of the four. A node WITH a\n // real handler still gets a fresh closure every render (it\n // captures per-render values, like a loop's own item), so it still\n // swaps every time — correctly, since the old closure really is\n // stale.\n //\n // The actually-attached listener is always a Batching.Run wrapper\n // (see Materialize above), never `oldTree.OnClick`/`updated.OnClick`\n // themselves — removeEventListener only works when passed the exact\n // reference addEventListener received, so removal always goes\n // through `oldTree.AttachedOnClick` (the wrapper Materialize/a\n // previous Patch actually registered), and a real swap builds a\n // fresh wrapper the same way Materialize does. When nothing swaps,\n // `updated.AttachedOnClick` still needs to carry `oldTree`'s\n // forward — `updated` is a brand-new VElement whose own\n // AttachedOnClick starts back at the shared no-op (velement.ks's\n // constructor), and it becomes the retained \"old tree\" the very\n // next Patch() call diffs against.\n if (updated.OnClick != oldTree.OnClick) {\n realNode.removeEventListener(\"click\", oldTree.AttachedOnClick);\n (Event) => void wrappedClick = (Event e) => Batching.Run(() => updated.OnClick(e));\n updated.AttachedOnClick = wrappedClick;\n realNode.addEventListener(\"click\", wrappedClick);\n } else {\n updated.AttachedOnClick = oldTree.AttachedOnClick;\n }\n if (updated.OnInput != oldTree.OnInput) {\n realNode.removeEventListener(\"input\", oldTree.AttachedOnInput);\n (Event) => void wrappedInput = (Event e) => Batching.Run(() => updated.OnInput(e));\n updated.AttachedOnInput = wrappedInput;\n realNode.addEventListener(\"input\", wrappedInput);\n } else {\n updated.AttachedOnInput = oldTree.AttachedOnInput;\n }\n if (updated.OnBlur != oldTree.OnBlur) {\n realNode.removeEventListener(\"blur\", oldTree.AttachedOnBlur);\n (Event) => void wrappedBlur = (Event e) => Batching.Run(() => updated.OnBlur(e));\n updated.AttachedOnBlur = wrappedBlur;\n realNode.addEventListener(\"blur\", wrappedBlur);\n } else {\n updated.AttachedOnBlur = oldTree.AttachedOnBlur;\n }\n if (updated.OnChange != oldTree.OnChange) {\n realNode.removeEventListener(\"change\", oldTree.AttachedOnChange);\n (Event) => void wrappedChange = (Event e) => Batching.Run(() => updated.OnChange(e));\n updated.AttachedOnChange = wrappedChange;\n realNode.addEventListener(\"change\", wrappedChange);\n } else {\n updated.AttachedOnChange = oldTree.AttachedOnChange;\n }\n\n return realNode;\n }\n } else {\n // Shouldn't happen in practice (every previously-rendered tree has a\n // real node by the time a second render diffs against it) — treated\n // as \"nothing to reuse\" rather than a crash, same defensive spirit\n // as Component's own IsMounted guard.\n Element created = Materialize(updated, parent);\n parent.appendChild(created);\n return created;\n }\n } else {\n Element created = Materialize(updated, parent);\n parent.appendChild(created);\n return created;\n }\n}\n\n// Releases whatever was previously mounted in a slot (calling its\n// Teardown, which — for a Component — cascades into anything IT mounted\n// too, however many levels deep, before its own app-facing OnUnmount runs)\n// and removes its real node from the DOM, before a different Mountable (or\n// plain content) takes over that slot. A no-op when there was nothing\n// mounted here before — the common \"first render of this slot\" case.\nvoid UnmountPrevious(Element parent, VElement? old, Mountable? oldMounted) {\n // Teardown() only applies when there really was a Mounted instance here\n // before (nothing to tear down for plain content) — but the real node\n // itself needs removing whenever `old` had one, Mounted or not: a plain\n // VElement's slot turning into a Mounted one is exactly as much a\n // wholesale replacement as the reverse direction (handled by falling\n // through to Materialize below in Patch), and both need the OLD node\n // gone before the NEW one is appended, not left behind as a stray\n // sibling.\n if (oldMounted != null) {\n Mountable m = oldMounted;\n m.Teardown();\n }\n if (old != null) {\n VElement oldTree = old;\n Element? maybeOldNode = oldTree.RealNode;\n if (maybeOldNode != null) {\n Element oldNode = maybeOldNode;\n parent.removeChild(oldNode);\n }\n }\n}\n\n// Keyed reconciliation: each VElement's own Id is its key when non-empty —\n// a real, existing DOM convention, needing no new API or syntax. A new\n// child whose Id matches an old child's Id is patched against that old\n// child (reusing its real node) regardless of position; a new child with\n// no Id, or an Id not present among the old children, falls back to\n// pairing positionally against whatever old children are still unconsumed,\n// in order. DOCUMENTED, REAL LIMITATION: without stable Ids, a reordered\n// list still produces the correct final output, but a given item's real\n// DOM node (and anything stateful attached to it, like focus) isn't\n// guaranteed to follow its data across the reorder — give list items a\n// stable Id for that guarantee.\nvoid PatchChildren(Element parent, VElement[] oldChildren, VElement[] newChildren) {\n // Map, not a Push loop — Push is deliberately non-mutating (a real\n // spread-copy every call, see KopScript's own README), so building an\n // n-length array by Push-ing once per element in a loop is an\n // accidental O(n^2) on every single PatchChildren call, however small\n // the actual diff. Map is a real, single O(n) pass straight to\n // Array.prototype.map.\n bool[] oldConsumed = oldChildren.Map((VElement c) => false);\n number[] matchedOldIndex = newChildren.Map((VElement c) => -1);\n\n // Pass 1: keyed matches, by Id.\n for (number i = 0; i < newChildren.Length; i = i + 1) {\n if (newChildren[i].Id.Length == 0) { continue; }\n for (number j = 0; j < oldChildren.Length; j = j + 1) {\n if (!oldConsumed[j] && oldChildren[j].Id == newChildren[i].Id) {\n matchedOldIndex[i] = j;\n oldConsumed[j] = true;\n break;\n }\n }\n }\n\n // Pass 2: positional fallback for everything Pass 1 didn't match —\n // pair each remaining new child against the next still-unconsumed old\n // child, in order.\n number nextOld = 0;\n for (number i = 0; i < newChildren.Length; i = i + 1) {\n if (matchedOldIndex[i] >= 0) { continue; }\n while (nextOld < oldChildren.Length && oldConsumed[nextOld]) {\n nextOld = nextOld + 1;\n }\n if (nextOld < oldChildren.Length) {\n matchedOldIndex[i] = nextOld;\n oldConsumed[nextOld] = true;\n nextOld = nextOld + 1;\n }\n }\n\n // Whether anything is actually moving at all — same length, and every\n // new position matched the *same* old position. The extremely common\n // case for a list that's only had some of its rows' own content change\n // (e.g. \"update every 10th row\"), where reconciliation still has real\n // work to do (see Pass 1/2 above and Patch() itself) but nothing needs\n // to physically move in the DOM at all.\n bool needsReorder = oldChildren.Length != newChildren.Length;\n for (number i = 0; i < newChildren.Length; i = i + 1) {\n if (matchedOldIndex[i] != i) {\n needsReorder = true;\n break;\n }\n }\n\n // Pass 3: patch/create each new child in order, then — only if the list\n // actually needs reordering — move it into its correct final position.\n // appendChild on a node already attached elsewhere in the DOM MOVES it\n // (real DOM semantics), so processing new children in their final\n // desired order and always appending naturally builds up the correct\n // sequence, no separate insertBefore/reference-node bookkeeping needed.\n // Safe because VElement.Children is always the COMPLETE list of a\n // node's children — nothing else ever shares `parent`. Skipping the\n // move entirely when `needsReorder` is false avoids a real DOM API call\n // per child for the common no-reorder case — Patch() itself already\n // updates or replaces a reused/changed node exactly in place either way.\n for (number i = 0; i < newChildren.Length; i = i + 1) {\n VElement? matchedOld = null;\n if (matchedOldIndex[i] >= 0) {\n matchedOld = oldChildren[matchedOldIndex[i]];\n }\n Element childNode = Patch(parent, matchedOld, newChildren[i]);\n if (needsReorder) {\n parent.appendChild(childNode);\n }\n }\n\n // Pass 4: remove whatever old children never got reused — releasing\n // anything Mounted first (see velement.ks) so a child genuinely dropped\n // from a list (not replaced in the same slot by a different one, which\n // Patch() itself already handles via UnmountPrevious — removed outright)\n // still gets torn down. This is the one place THAT case is reachable\n // from, since Patch() only ever sees one slot at a time, never a slot\n // disappearing entirely.\n for (number j = 0; j < oldChildren.Length; j = j + 1) {\n if (oldConsumed[j]) { continue; }\n Mountable? maybeOldMounted = oldChildren[j].Mounted;\n if (maybeOldMounted != null) {\n Mountable m = maybeOldMounted;\n m.Teardown();\n }\n Element? maybeOldNode = oldChildren[j].RealNode;\n if (maybeOldNode != null) {\n Element oldNode = maybeOldNode;\n parent.removeChild(oldNode);\n }\n }\n}\n\n// Recursively finds and tears down every Mounted node within `tree` (tree\n// itself included) — used by Component's own Teardown (see component.ks)\n// so removing ONE component transitively releases everything IT rendered,\n// however many Mounted levels deep, not only the outermost one. A pure\n// data walk over the retained VElement tree; never touches the real DOM\n// itself (the caller already owns removing whatever real node is actually\n// attached).\nvoid UnmountTree(VElement tree) {\n Mountable? maybeMounted = tree.Mounted;\n if (maybeMounted != null) {\n Mountable m = maybeMounted;\n m.Teardown();\n }\n foreach (VElement child in tree.Children) {\n UnmountTree(child);\n }\n}\n"],"names":[],"mappings":";;;AAyCA;EACiB;;EACA;;EAED;IACZ;;;EAGY;IACZ;MACmB;;;;EAUP;IACG;IACf;MACQ;;MAES;MACf;QACE;QACiB;QACjB;UACe;;;;;;AAmBvB;EACiB;;EAED;IACZ;MACwB;MACtB;MACkB;MACA;MACO;;;;AAgB/B;EACE;EACA;IACE;IACA;IACc;IACd;;EAGF;EAEA;IACe;;IAEb;MACgB;;;IAGD;;EAGJ;EACP;EACG;EAET;IACiB;;EAYjB;IACE;IACqB;IACF;;EAErB;IACE;IACqB;IACF;;EAErB;IACE;IACoB;IACD;;EAErB;IACE;IACsB;IACH;;EAGP;EACd;;AAiBF;EACE;EACA;EACA;IACE;IACW;;EASb;IACE;IACA;MACE;MACA;QAIE;QACiB;QACjB;;;IAKW;IACf;IACkB;IACD;IACjB;;EAGF;IAGiB;IACf;IACkB;IAClB;;EAGF;IACE;IACA;IACA;MACE;MACA;QACE;QACmB;QACnB;;QAEiB;QAEjB;UACE;YACqB;;;UAGrB;YACuB;;UAEV;;QAGf;UACqB;;QAErB;UACc;;QAaC;QAWf;UACE;YACE;cACuB;;;;UAIzB;YACuB;;;QA6BzB;UAC8B;UAC5B;UACwB;UACC;;UAED;;QAE1B;UAC8B;UAC5B;UACwB;UACC;;UAED;;QAE1B;UAC8B;UAC5B;UACuB;UACE;;UAEF;;QAEzB;UAC8B;UAC5B;UACyB;UACA;;UAEA;;QAG3B;;;MAOF;MACkB;MAClB;;;IAGF;IACkB;IAClB;;;AAUJ;EASE;IACE;IACU;;EAEZ;IACE;IACA;IACA;MACE;MACkB;;;;AAgBxB;EAOE;EACA;EAGA;IACE;MAAqC;;IACrC;MACE;QACqB;QACJ;QACf;;;;EAQN;EACA;IACE;MAA+B;;IAC/B;MACU;;IAEV;MACqB;MACE;MACb;;;EAUZ;EACA;IACE;MACe;MACb;;;EAeJ;IACE;IACA;MACa;;IAEb;IACA;MACoB;;;EAWtB;IACE;MAAsB;;IACtB;IACA;MACE;MACU;;IAEZ;IACA;MACE;MACkB;;;;AAYxB;EACE;EACA;IACE;IACU;;EAEZ;IACa"}
|
package/src/vdom.ks
CHANGED
|
@@ -76,6 +76,32 @@ class Batching {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
// The runtime half of kopscript's `styles from "<path>.css";` (see its own
|
|
80
|
+
// README/LLM.md "Templates" section) — the compiler rewrites a class's own
|
|
81
|
+
// stylesheet at compile time (every selector scoped to that class's
|
|
82
|
+
// `data-kop-scope="<id>"` attribute) and splices exactly one
|
|
83
|
+
// `ScopedStyles.Inject(id, css);` call into its constructor. Same
|
|
84
|
+
// static-array-registry-with-Includes-dedup shape as Batching above,
|
|
85
|
+
// deliberately: dedup here is per component TYPE, not per instance — every
|
|
86
|
+
// instance's constructor calls Inject with the same `id`/`css` (both
|
|
87
|
+
// compile-time constants for that class), so only the first one actually
|
|
88
|
+
// creates a <style> tag; the rest are no-ops. Never removed once injected
|
|
89
|
+
// — a scoped stylesheet is global infrastructure for as long as the page
|
|
90
|
+
// lives, not per-instance content Teardown() would ever need to clean up.
|
|
91
|
+
class ScopedStyles {
|
|
92
|
+
private static string[] Injected = [];
|
|
93
|
+
|
|
94
|
+
public static void Inject(string scopeId, string css) {
|
|
95
|
+
if (!ScopedStyles.Injected.Includes(scopeId)) {
|
|
96
|
+
ScopedStyles.Injected = ScopedStyles.Injected.Push(scopeId);
|
|
97
|
+
Element style = document.createElement("style");
|
|
98
|
+
style.setAttribute("data-kop-scope-sheet", scopeId);
|
|
99
|
+
style.textContent = css;
|
|
100
|
+
document.head.appendChild(style);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
79
105
|
// Builds a brand-new, fully real DOM subtree from a VElement tree with no
|
|
80
106
|
// diffing at all — first mount, or whenever Patch() decides a subtree must
|
|
81
107
|
// be replaced outright (no previous node to reuse, or the tag changed).
|