jops-core 1.0.15 → 1.1.1

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.
Files changed (30) hide show
  1. package/README.md +356 -29
  2. package/bin/init.js +138 -20
  3. package/lib/jops-core.d.ts +251 -15
  4. package/lib/jops-core.min.js +1 -1
  5. package/lib/nextjs/JopsApp.d.ts +35 -0
  6. package/lib/nextjs/JopsApp.js +50 -0
  7. package/package.json +33 -5
  8. package/templates/nextjs-jops-spa/README.md +34 -0
  9. package/templates/nextjs-jops-spa/data/quotes.json +62 -0
  10. package/templates/nextjs-jops-spa/gitignore +21 -0
  11. package/templates/nextjs-jops-spa/next.config.ts +15 -0
  12. package/templates/nextjs-jops-spa/package.json +25 -0
  13. package/templates/nextjs-jops-spa/public/jops-spa/App.js +10 -0
  14. package/templates/nextjs-jops-spa/public/jops-spa/Login.js +26 -0
  15. package/templates/nextjs-jops-spa/public/jops-spa/Quotes.js +102 -0
  16. package/templates/nextjs-jops-spa/public/jops-spa/api.js +15 -0
  17. package/templates/nextjs-jops-spa/public/jops-spa/badge.js +8 -0
  18. package/templates/nextjs-jops-spa/public/jops-spa/format.js +5 -0
  19. package/templates/nextjs-jops-spa/public/jops-spa/layout/login.html +34 -0
  20. package/templates/nextjs-jops-spa/public/jops-spa/layout/main.html +17 -0
  21. package/templates/nextjs-jops-spa/public/jops-spa/layout/quotes.html +50 -0
  22. package/templates/nextjs-jops-spa/public/jops-spa/layout/quotes_row.html +8 -0
  23. package/templates/nextjs-jops-spa/public/jops-spa/skeleton.js +7 -0
  24. package/templates/nextjs-jops-spa/src/app/about/page.tsx +59 -0
  25. package/templates/nextjs-jops-spa/src/app/api/quotes/route.ts +11 -0
  26. package/templates/nextjs-jops-spa/src/app/globals.css +6 -0
  27. package/templates/nextjs-jops-spa/src/app/jops-spa/page.tsx +11 -0
  28. package/templates/nextjs-jops-spa/src/app/layout.tsx +27 -0
  29. package/templates/nextjs-jops-spa/src/app/page.tsx +5 -0
  30. package/templates/nextjs-jops-spa/tsconfig.json +21 -0
package/bin/init.js CHANGED
@@ -3,14 +3,15 @@ import fs from "fs";
3
3
  import https from "https";
4
4
  import path from "path";
5
5
  import readline from "readline";
6
+ import { spawnSync } from "child_process";
6
7
  import { fileURLToPath } from "url";
7
8
 
8
9
  const [,, command, ...flags] = process.argv;
9
10
 
10
11
  if (command !== "init" && command !== "build" && command !== "update") {
11
12
  console.log("Usage:");
12
- console.log(" npx jops-core init — vendor library and configure index.html");
13
- console.log(" npx jops-core update — copy the latest jops-core.min.js to lib/");
13
+ console.log(" npx jops-core init — vendor library and configure index.html, or create a template project");
14
+ console.log(" npx jops-core update — copy the latest jops-core.min.js to lib/ (public/lib/ in a Next.js project)");
14
15
  console.log(" npx jops-core build — generate production-ready dist/ folder");
15
16
  console.log("");
16
17
  console.log("Options (init, update):");
@@ -143,6 +144,133 @@ function copyDir(src, dest) {
143
144
  }
144
145
  }
145
146
 
147
+ // A Next.js project: the library is served from public/lib/.
148
+ function isNextProject(dir) {
149
+ return ["next.config.ts", "next.config.js", "next.config.mjs"].some((f) => fs.existsSync(path.join(dir, f)));
150
+ }
151
+
152
+ // Copies a template project into dest (README.md included, unlike copyDir()).
153
+ // package.json is merged separately; existing files are never overwritten.
154
+ // npm drops .gitignore files from published packages, so the template stores
155
+ // it as "gitignore".
156
+ function copyTemplateProject(src, dest, root = dest) {
157
+ const SKIP = new Set(["node_modules", ".next", ".DS_Store", "package.json"]);
158
+ fs.mkdirSync(dest, { recursive: true });
159
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
160
+ if (dest === root && SKIP.has(entry.name)) continue;
161
+ if (entry.name === ".DS_Store") continue;
162
+ const srcPath = path.join(src, entry.name);
163
+ const destPath = path.join(dest, entry.name === "gitignore" ? ".gitignore" : entry.name);
164
+ if (entry.isDirectory()) copyTemplateProject(srcPath, destPath, root);
165
+ else if (fs.existsSync(destPath)) console.log(`[jops-core] Kept the existing ${path.relative(root, destPath)}`);
166
+ else fs.copyFileSync(srcPath, destPath);
167
+ }
168
+ }
169
+
170
+ // Creates the template's package.json, or merges its dependencies and scripts
171
+ // into an existing one: existing entries (e.g. an installed jops-core) are kept.
172
+ function mergeTemplatePackageJson(templatePath, targetPath) {
173
+ const template = JSON.parse(fs.readFileSync(templatePath, "utf8"));
174
+ if (!fs.existsSync(targetPath)) {
175
+ fs.writeFileSync(targetPath, JSON.stringify(template, null, 2) + "\n", "utf8");
176
+ console.log("[jops-core] Created package.json");
177
+ return;
178
+ }
179
+ const pkg = JSON.parse(fs.readFileSync(targetPath, "utf8"));
180
+ pkg.private ??= true;
181
+ // `npm init -y` writes "type": "commonjs", which makes Next.js (Turbopack)
182
+ // reject the project's ES module sources; Next.js projects have no "type".
183
+ if (pkg.type === "commonjs") {
184
+ delete pkg.type;
185
+ console.log('[jops-core] Removed "type": "commonjs" from package.json (Next.js uses ES modules)');
186
+ }
187
+ for (const section of ["dependencies", "devDependencies"]) {
188
+ for (const [name, spec] of Object.entries(template[section] ?? {})) {
189
+ const existing = pkg.dependencies?.[name] ?? pkg.devDependencies?.[name];
190
+ if (existing) {
191
+ if (name === "jops-core") console.log(`[jops-core] Kept the installed jops-core (${existing})`);
192
+ continue;
193
+ }
194
+ pkg[section] = { ...pkg[section], [name]: spec };
195
+ }
196
+ }
197
+ for (const [name, command] of Object.entries(template.scripts ?? {})) {
198
+ const existing = pkg.scripts?.[name];
199
+ if (existing && existing !== command) {
200
+ console.log(`[jops-core] Kept the existing "${name}" script (${existing}); the template uses "${command}"`);
201
+ continue;
202
+ }
203
+ pkg.scripts = { ...pkg.scripts, [name]: command };
204
+ }
205
+ fs.writeFileSync(targetPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
206
+ console.log("[jops-core] Added the Next.js dependencies and scripts to package.json");
207
+ }
208
+
209
+ // ─── template selection ──────────────────────────────────────────────────────
210
+ // Asked before init writes anything, because the Next.js template creates its
211
+ // own project folder instead of configuring the current one.
212
+
213
+ async function askTemplate() {
214
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
215
+ const answer = await new Promise((resolve) => rl.question(
216
+ "\nCreate JOPS project from existing templates:\n" +
217
+ " 1. Empty JOPS project (basic skeleton for JOPS project architecture)\n" +
218
+ " 2. Hello World JOPS project (an example project utilizing all features)\n" +
219
+ " 3. Next.js JOPS Embedded SPA (a Next.js project hosting a JOPS SPA)\n" +
220
+ " 4. None (I'll create my own architecture)\n" +
221
+ "Enter choice (1-4): ",
222
+ resolve,
223
+ ));
224
+ rl.close();
225
+ return answer.trim();
226
+ }
227
+
228
+ const choice = command === "init" ? await askTemplate() : null;
229
+
230
+ // ─── init: Next.js JOPS Embedded SPA (template 3) ────────────────────────────
231
+
232
+ if (command === "init" && choice === "3") {
233
+ const cwd = process.cwd();
234
+ const templateDir = path.join(__dirname, "../templates/nextjs-jops-spa");
235
+ // Paths that would clash with an existing project: nothing is written then.
236
+ const conflicts = [
237
+ "next.config.ts", "next.config.js", "next.config.mjs",
238
+ "tsconfig.json", "src/app", "public/jops-spa", "data/quotes.json",
239
+ ].filter((p) => fs.existsSync(path.join(cwd, p)));
240
+
241
+ if (isHybrid) {
242
+ console.error(`[jops-core] This is a ${type} project; create the Next.js project in another folder.`);
243
+ process.exitCode = 1;
244
+ } else if (conflicts.length) {
245
+ console.error(`[jops-core] This folder already has ${conflicts.join(", ")}. Nothing was changed.`);
246
+ console.error(" Run init in an empty folder (or a fresh npm project) to create the Next.js project.");
247
+ process.exitCode = 1;
248
+ } else {
249
+ copyTemplateProject(templateDir, cwd);
250
+ mergeTemplatePackageJson(path.join(templateDir, "package.json"), path.join(cwd, "package.json"));
251
+ console.log("[jops-core] Created the Next.js JOPS Embedded SPA project in this folder.");
252
+
253
+ const libDir = path.join(cwd, "public", "lib");
254
+ fs.mkdirSync(libDir, { recursive: true });
255
+ fs.copyFileSync(path.join(__dirname, "../lib/jops-core.min.js"), path.join(libDir, "jops-core.min.js"));
256
+ console.log("[jops-core] Vendored jops-core.min.js → public/lib/jops-core.min.js");
257
+
258
+ console.log("[jops-core] Installing dependencies (npm install)…");
259
+ const install = spawnSync("npm", ["install"], {
260
+ cwd,
261
+ stdio: "inherit",
262
+ shell: process.platform === "win32",
263
+ });
264
+ if (install.status !== 0) {
265
+ console.warn("[jops-core] npm install did not complete. Run it yourself: npm install");
266
+ }
267
+ console.log("");
268
+ console.log("[jops-core] Next steps:");
269
+ console.log(" npm run dev");
270
+ console.log(" open http://localhost:3000/about and http://localhost:3000/jops-spa");
271
+ }
272
+ }
273
+
146
274
  // ─── index.html helpers (hybrid projects) ────────────────────────────────────
147
275
 
148
276
  // Same policy as Cordova's default, plus 'unsafe-inline' for the inline import map
@@ -242,7 +370,7 @@ function schemeWarnings(xml) {
242
370
 
243
371
  // ─── init ────────────────────────────────────────────────────────────────────
244
372
 
245
- if (command === "init") {
373
+ if (command === "init" && choice !== "3") {
246
374
  fs.mkdirSync(webRoot, { recursive: true });
247
375
  if (isHybrid) {
248
376
  console.log(`[jops-core] ${forced ? "Using" : "Detected"} ${type} project → web folder: ${rel(webRoot)}/`);
@@ -405,22 +533,10 @@ ${cordovaBody}${MODULE_SCRIPT}
405
533
  if (changed) fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
406
534
  }
407
535
 
408
- // Template selection
409
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
410
- const ask = (q) => new Promise(resolve => rl.question(q, resolve));
411
-
412
- const choice = await ask(
413
- "\nCreate JOPS project from existing templates:\n" +
414
- " 1. Empty JOPS project (basic skeleton for JOPS project architecture)\n" +
415
- " 2. Hello World JOPS project (an example project utilizing all features)\n" +
416
- " 3. None (I'll create my own architecture)\n" +
417
- "Enter choice (1-3): "
418
- );
419
- rl.close();
420
-
536
+ // Template choice: asked at the start (see askTemplate()).
421
537
  // Cordova's sample page (css/index.css, js/index.js, #deviceready) conflicts with a JOPS
422
538
  // template: back it up and start from the JOPS Cordova index.html instead.
423
- if (type === "cordova" && ["1", "2"].includes(choice.trim()) &&
539
+ if (type === "cordova" && ["1", "2"].includes(choice) &&
424
540
  fs.readFileSync(indexPath, "utf8").includes('id="deviceready"')) {
425
541
  let backup = path.join(webRoot, "index.cordova.html");
426
542
  for (let n = 1; fs.existsSync(backup); n++) backup = path.join(webRoot, `index.cordova-${n}.html`);
@@ -430,7 +546,7 @@ ${cordovaBody}${MODULE_SCRIPT}
430
546
  }
431
547
 
432
548
  const templateSrc = path.join(__dirname, "../templates");
433
- if (choice.trim() === "1") {
549
+ if (choice === "1") {
434
550
  copyDir(path.join(templateSrc, "empty"), webRoot);
435
551
  // Add data-app to the module script in index.html
436
552
  let html = fs.readFileSync(indexPath, "utf8");
@@ -440,7 +556,7 @@ ${cordovaBody}${MODULE_SCRIPT}
440
556
  );
441
557
  fs.writeFileSync(indexPath, html, "utf8");
442
558
  console.log("[jops-core] Created empty JOPS project skeleton.");
443
- } else if (choice.trim() === "2") {
559
+ } else if (choice === "2") {
444
560
  copyDir(path.join(templateSrc, "hello-world"), webRoot);
445
561
  let html = fs.readFileSync(indexPath, "utf8");
446
562
  html = html.replace(
@@ -457,7 +573,9 @@ ${cordovaBody}${MODULE_SCRIPT}
457
573
  // ─── update ──────────────────────────────────────────────────────────────────
458
574
 
459
575
  if (command === "update") {
460
- const libDir = path.join(webRoot, "lib");
576
+ // A Next.js project serves the library from public/lib/.
577
+ const libRoot = type === "web" && isNextProject(projectRoot) ? path.join(projectRoot, "public") : webRoot;
578
+ const libDir = path.join(libRoot, "lib");
461
579
  const vendorDest = path.join(libDir, "jops-core.min.js");
462
580
  const vendorSrc = path.join(__dirname, "../lib/jops-core.min.js");
463
581
 
@@ -1,3 +1,32 @@
1
+ /**
2
+ * Starts a JOPS app inside a given element instead of `<body>`. Used by hosts
3
+ * that control when and where the app runs, such as `<JopsApp>` in Next.js
4
+ * (jops-core loaded with `data-host`, which exposes `window.JOPS.mount`).
5
+ * @param {{ app: string, root: HTMLElement }} options - `app`: URL of the app's
6
+ * entry module (default export is the App class); `root`: element the app's
7
+ * root view renders into.
8
+ * @returns {Promise<object>} The App instance.
9
+ * @example
10
+ * await mount({ app: "/quotes/App.js", root: document.getElementById("quotes") });
11
+ */
12
+ export declare function mount({ app, root }: {
13
+ app: string;
14
+ root: HTMLElement;
15
+ }): Promise<object>;
16
+ /**
17
+ * Ends the hosted app completely, like closing a standalone app: destroys the
18
+ * Router (listeners, singleton, routes, stacked views) and every view, empties
19
+ * the host root, clears the Store and the EventBus, and restores the host's
20
+ * history methods. The current URL and history entry are left alone: they
21
+ * belong to the host page. Safe to call more than once.
22
+ *
23
+ * Called by the Router itself when history leaves the app's page (Back past
24
+ * the entry the app was launched from, or a host link to another page), and
25
+ * by hosts such as `<JopsApp>` when they remove the app.
26
+ * @example
27
+ * window.JOPS.unmount();
28
+ */
29
+ export declare function unmount(): void;
1
30
  declare class RawHTML {
2
31
  html: any;
3
32
  constructor(html: any);
@@ -26,13 +55,43 @@ export declare function layout(strings: TemplateStringsArray, ...values: any[]):
26
55
  * Wraps a single child element for reading and writing.
27
56
  */
28
57
  declare class Element {
29
- element: any;
30
- constructor(domNode: any, id: any);
58
+ #private;
59
+ id: string;
60
+ inert: boolean;
61
+ element: globalThis.Element;
31
62
  /**
32
- * Sets the `innerHTML` of the element.
33
- * @param {string} value - Plain text or HTML string.
63
+ * @param {HTMLElement[]} roots - The view's root elements, searched in order.
64
+ * @param {string} id - The id to look up; the first root that has it wins.
34
65
  */
35
- set(value: string): void;
66
+ constructor(roots: HTMLElement[], id: string);
67
+ /**
68
+ * Wraps an element that was already found (used by `View.find(id)`).
69
+ * @param {HTMLElement} element
70
+ * @param {string} id
71
+ * @returns {Element}
72
+ */
73
+ static from(element: HTMLElement, id: string): Element;
74
+ /**
75
+ * Sets the `innerHTML` of the element. The value is not escaped: pass values
76
+ * from users or the network through the `layout` tag.
77
+ * @param {string} html - Plain text or HTML string.
78
+ * @returns {Element} This wrapper, for chaining.
79
+ * @example
80
+ * this.get("title").set(layout`${quote.number}`);
81
+ */
82
+ set(html: string): Element;
83
+ /**
84
+ * Sets an attribute, or removes it when `value` is `null` or `undefined`.
85
+ * An empty string sets a boolean attribute (`disabled`, `hidden`, `required`).
86
+ * @param {string} name - Attribute name, e.g. "href".
87
+ * @param {string|number|boolean|null} [value] - Attribute value.
88
+ * @returns {Element} This wrapper, for chaining.
89
+ * @example
90
+ * this.get("quote_id").set(layout`${q.number}`).attr("href", `#quote?id=${q.id}`);
91
+ * this.get("save").attr("disabled", ""); // set
92
+ * this.get("save").attr("disabled", null); // remove
93
+ */
94
+ attr(name: string, value?: string | number | boolean | null): Element;
36
95
  /**
37
96
  * Returns the current `innerHTML` of the element.
38
97
  * @returns {string}
@@ -46,9 +105,16 @@ declare class Element {
46
105
  * 1. {@link View#loadLayout} — load or assign `this.layout`
47
106
  * 2. {@link View#inflate} — parse `this.layout` into `this.domNode`
48
107
  * 3. {@link View#onLayout} — DOM is live; add subviews, read initial data
49
- * 4. {@link View#onResume} — view becomes visible; called again on every return navigation
108
+ * 4. {@link View#onResume} — view becomes visible; called again on every return navigation,
109
+ * and when the same route is shown again with new parameters
50
110
  * 5. {@link View#bindEvents} — wire `on*` attributes to class methods
51
111
  *
112
+ * Optional hooks, also called by the framework:
113
+ * - `onPause()` — a different view comes to the front and this one stays alive
114
+ * (hidden route, or covered by a stacked view). Not called when the view is destroyed
115
+ * (Back from a stacked view, a stack closed by a flat link, unmount()).
116
+ * - `onBackPressed()` — Back navigation from this view (called before onPause() for routes).
117
+ *
52
118
  * @example
53
119
  * import { View, layout } from "jops-core";
54
120
  *
@@ -63,17 +129,35 @@ declare class Element {
63
129
  */
64
130
  export declare class View {
65
131
  #private;
132
+ _generation: number;
66
133
  subViews: any[];
67
134
  layout: any;
68
135
  style: string;
136
+ domNodes: any[];
69
137
  domNode: any;
138
+ _destroyed: boolean;
139
+ _borrowedDomNode: boolean;
70
140
  /**
71
141
  * @param {string} [layout] - Optional layout path or inline HTML string.
142
+ * @param {Record<string, string>} [props] - Input properties. A view declared
143
+ * in a layout (`<Card jops src="…" title="Hi">`) gets them from the tag's
144
+ * attributes automatically; a view created in code may pass them here.
72
145
  */
73
- constructor(layout?: string);
146
+ constructor(layout?: string, props?: Record<string, string>);
147
+ /**
148
+ * The view's input properties (read-only): the attributes of its declared
149
+ * jops tag (except `jops`, `type="jops"`, `src`, `layout`, `path`,
150
+ * `jops-event-bind`), names in camelCase, values as strings; or the props
151
+ * passed to the constructor. `{}` when there are none.
152
+ * @returns {Readonly<Record<string, string>>}
153
+ */
154
+ get props(): Readonly<Record<string, string>>;
74
155
  /**
75
156
  * Override to load a layout file or set `this.layout` inline.
76
157
  * Called automatically by the framework before `inflate()`.
158
+ * Loads once per view: overlapping calls (with or without a path) wait for
159
+ * the load in progress, and once a layout is set later calls return at once.
160
+ * A failed load leaves the view unloaded, so it can be retried.
77
161
  * @param {string} [layout] - Path to a `.js` layout module, a `.html` file, or an inline HTML string.
78
162
  * `.js` — dynamically imported as an ES module; must export a `layout\`...\`` string as default.
79
163
  * `.html` — fetched and cached for the page lifetime; plain HTML fragment, no import/export needed.
@@ -83,6 +167,7 @@ export declare class View {
83
167
  loadLayout(layout?: string): Promise<void>;
84
168
  /**
85
169
  * Injects a `.css` file link or an inline `<style>` tag into `<head>`, deduped.
170
+ * No effect in hosted mode (e.g. Next.js through `<JopsApp>`): the host owns the CSS.
86
171
  * @param {string} style - Path to a `.css` file, or a raw CSS string.
87
172
  */
88
173
  setStyle(style: string): void;
@@ -94,6 +179,19 @@ export declare class View {
94
179
  * this.get("username").set("Alice");
95
180
  */
96
181
  get(id: string): Element;
182
+ /**
183
+ * The view's root elements: every top-level element of its layout (a layout
184
+ * may have more than one), or the borrowed container of a root view.
185
+ * @returns {HTMLElement[]}
186
+ * @protected
187
+ */
188
+ protected _roots(): HTMLElement[];
189
+ /**
190
+ * Hides or shows every root element (used by the Router).
191
+ * @param {boolean} hidden
192
+ * @protected
193
+ */
194
+ protected _setHidden(hidden: boolean): void;
97
195
  /**
98
196
  * Wires `on*` HTML attributes on `[jops-event-bind]` elements to
99
197
  * same-named methods on this view instance.
@@ -108,6 +206,7 @@ export declare class View {
108
206
  /**
109
207
  * Appends `view` into the child element with `id="parentNode"` and runs its
110
208
  * full lifecycle (`onLayout` → `onResume` → `bindEvents`).
209
+ * A removed (destroyed) view is final: create a new instance to show it again.
111
210
  * @param {string} parentNode - The `id` of the container element inside `this.domNode`.
112
211
  * @param {View} view - The subview to mount.
113
212
  * @returns {Promise<void>}
@@ -121,14 +220,50 @@ export declare class View {
121
220
  * Removes the inline `display` override, restoring `this.domNode` visibility.
122
221
  */
123
222
  show(): void;
223
+ /**
224
+ * Every element with the given id across all root elements of the layout
225
+ * (and the DOM of its sub-views), in page order.
226
+ * @overload
227
+ * @param {string} id - Element id.
228
+ * @returns {Element[]}
229
+ * @example
230
+ * this.find("quote_id").forEach((el) => el.attr("tabindex", 0));
231
+ */
124
232
  /**
125
233
  * Depth-first search across the full subview subtree.
126
- * @param {function(View): boolean} condition - Predicate to match against each view.
234
+ * @overload
235
+ * @param {(view: View) => boolean} condition - Predicate to match against each view.
127
236
  * @returns {View[]} Flat array of all matching views.
128
237
  * @example
129
238
  * const forms = root.find(v => v instanceof FormView);
130
239
  */
131
- find(condition: Function): View[];
240
+ /**
241
+ * @param {string | ((view: View) => boolean)} arg
242
+ * @returns {Element[] | View[]}
243
+ */
244
+ find(id: string): Element[];
245
+ /**
246
+ * Every element with the given id across all root elements of the layout
247
+ * (and the DOM of its sub-views), in page order.
248
+ * @overload
249
+ * @param {string} id - Element id.
250
+ * @returns {Element[]}
251
+ * @example
252
+ * this.find("quote_id").forEach((el) => el.attr("tabindex", 0));
253
+ */
254
+ /**
255
+ * Depth-first search across the full subview subtree.
256
+ * @overload
257
+ * @param {(view: View) => boolean} condition - Predicate to match against each view.
258
+ * @returns {View[]} Flat array of all matching views.
259
+ * @example
260
+ * const forms = root.find(v => v instanceof FormView);
261
+ */
262
+ /**
263
+ * @param {string | ((view: View) => boolean)} arg
264
+ * @returns {Element[] | View[]}
265
+ */
266
+ find(condition: (view: View) => boolean): View[];
132
267
  /**
133
268
  * Recursively destroys the subview subtree: calls `destroy()` on all
134
269
  * subviews, removes `this.domNode` from the DOM, and nulls references.
@@ -141,11 +276,21 @@ export declare class View {
141
276
  removeSubView(view: View): void;
142
277
  /**
143
278
  * Inserts `view` at `index` within `this.domNode` and runs its full lifecycle.
279
+ * A removed (destroyed) view is final: create a new instance to show it again.
144
280
  * @param {View} view - The subview to insert.
145
281
  * @param {number} index - Zero-based insertion position.
146
282
  * @returns {Promise<void>}
147
283
  */
148
284
  insertSubView(view: View, index: number): Promise<void>;
285
+ /**
286
+ * Final lifecycle steps for a sub-view declared in this view's layout,
287
+ * called by `render()` after its `onLayout()`. Router overrides it so a
288
+ * route is resumed only when it is shown.
289
+ * @param {View} subView
290
+ * @returns {Promise<void>}
291
+ * @protected
292
+ */
293
+ protected _startSubView(subView: View): Promise<void>;
149
294
  /**
150
295
  * Mounts the view to the DOM, resolves all nested `[jops]` view
151
296
  * elements and `<Router>` declarations, and runs each one's full lifecycle.
@@ -159,30 +304,83 @@ export declare class View {
159
304
  * Static pub/sub event bus for cross-view communication.
160
305
  * All methods are static — no instance needed.
161
306
  *
307
+ * A subscription with an owner (a view) is removed automatically when the
308
+ * owner is destroyed; a global subscription (no owner) stays until it is
309
+ * unsubscribed (the developer's responsibility).
310
+ *
162
311
  * @example
163
- * EventBus.subscribe("user:login", data => console.log(data.name));
312
+ * EventBus.subscribe(this, "quotes:changed", (q) => this.refresh(q)); // owned
313
+ * EventBus.subscribe("user:login", onLogin); // global
164
314
  * EventBus.send("user:login", { name: "Alice" });
165
315
  */
166
316
  export declare class EventBus {
167
317
  #private;
168
318
  /**
169
- * Registers a listener for the named event.
319
+ * Registers a global listener; unsubscribe it with `unsubscribe()`.
320
+ * @overload
321
+ * @param {string} event - Event name.
322
+ * @param {(data: any) => void} callback - Handler called with the event payload.
323
+ * @returns {void}
324
+ */
325
+ /**
326
+ * Registers a listener owned by `owner` (usually a view): it is removed
327
+ * automatically when the owner is destroyed.
328
+ * @overload
329
+ * @param {object} owner - The object owning the subscription, e.g. `this`.
330
+ * @param {string} event - Event name.
331
+ * @param {(data: any) => void} callback - Handler called with the event payload.
332
+ * @returns {void}
333
+ */
334
+ /**
335
+ * @param {...*} args - `(event, callback)` or `(owner, event, callback)`.
336
+ * @returns {void}
337
+ */
338
+ static subscribe(event: string, callback: (data: any) => void): void;
339
+ /**
340
+ * Registers a global listener; unsubscribe it with `unsubscribe()`.
341
+ * @overload
342
+ * @param {string} event - Event name.
343
+ * @param {(data: any) => void} callback - Handler called with the event payload.
344
+ * @returns {void}
345
+ */
346
+ /**
347
+ * Registers a listener owned by `owner` (usually a view): it is removed
348
+ * automatically when the owner is destroyed.
349
+ * @overload
350
+ * @param {object} owner - The object owning the subscription, e.g. `this`.
170
351
  * @param {string} event - Event name.
171
- * @param {function(*): void} callback - Handler called with the event payload.
352
+ * @param {(data: any) => void} callback - Handler called with the event payload.
353
+ * @returns {void}
172
354
  */
173
- static subscribe(event: string, callback: Function): void;
174
355
  /**
175
- * Fires the named event to all subscribers.
356
+ * @param {...*} args - `(event, callback)` or `(owner, event, callback)`.
357
+ * @returns {void}
358
+ */
359
+ static subscribe(owner: object, event: string, callback: (data: any) => void): void;
360
+ /**
361
+ * Fires the named event to all subscribers. Subscriptions of destroyed
362
+ * owners are skipped and removed (a safety net: `destroy()` normally
363
+ * removes them first).
176
364
  * @param {string} event - Event name.
177
365
  * @param {*} [data] - Optional payload passed to every subscriber.
178
366
  */
179
367
  static send(event: string, data?: any): void;
180
368
  /**
181
- * Removes a specific listener by reference.
369
+ * Removes a specific listener by reference (global or owned).
182
370
  * @param {string} event - Event name.
183
371
  * @param {function(*): void} callback - The exact function reference passed to `subscribe`.
184
372
  */
185
373
  static unsubscribe(event: string, callback: Function): void;
374
+ /**
375
+ * Removes every subscription owned by `owner`, across all events (called by
376
+ * `View.destroy()`). Global subscriptions are not touched.
377
+ * @param {object} owner - The owner passed to `subscribe(owner, event, callback)`.
378
+ */
379
+ static unsubscribeAll(owner: object): void;
380
+ /**
381
+ * Removes every listener of every event (called by `unmount()`).
382
+ */
383
+ static clear(): void;
186
384
  }
187
385
  /**
188
386
  * App-wide runtime state container. Singleton. No reactivity.
@@ -244,7 +442,10 @@ export declare class Router extends View {
244
442
  routes: {};
245
443
  _navStack: any[];
246
444
  _baseView: any;
445
+ _defaultPath: string;
446
+ _pathname: string;
247
447
  _idx: any;
448
+ _historyListener: () => void;
248
449
  _currentView: any;
249
450
  /**
250
451
  * Returns the single Router instance after it has been rendered.
@@ -253,6 +454,15 @@ export declare class Router extends View {
253
454
  static singleton(): Router;
254
455
  constructor(routerDomNode: any);
255
456
  onLayout(): Promise<void>;
457
+ /**
458
+ * Routes only get their events bound while the Router renders; `onResume()`
459
+ * runs when a route is shown (`_switchTo()` and the transitions), so hidden
460
+ * routes are not resumed at startup.
461
+ * @param {View} subView
462
+ * @returns {Promise<void>}
463
+ * @protected
464
+ */
465
+ protected _startSubView(subView: View): Promise<void>;
256
466
  initRouter(): void;
257
467
  /**
258
468
  * Single entry point for popstate and hashchange. Decides the direction
@@ -261,7 +471,33 @@ export declare class Router extends View {
261
471
  _onHistoryChange(): void;
262
472
  _onBack(): void;
263
473
  _onForward(): void;
474
+ /** Destroys every stacked navigate() view (switching to a flat route). */
475
+ _closeStack(): void;
476
+ /**
477
+ * onPause() on the view leaving the front: a different view is coming to
478
+ * the front and the current one stays alive (hidden or covered). Not used
479
+ * when a view is destroyed (stack pop on Back, _closeStack(), unmount()).
480
+ * @param {View} incoming
481
+ */
482
+ _pauseCurrent(incoming: View): void;
264
483
  _switchToHash(): void;
484
+ /**
485
+ * Replaces the current entry's empty hash with the default (first) route,
486
+ * keeping the existing history state, and shows that route.
487
+ */
488
+ _switchToDefault(): void;
489
+ /**
490
+ * Removes the history listeners, destroys the routes and stacked views and
491
+ * clears the singleton. Called by `unmount()`; safe to call more than once.
492
+ */
493
+ destroy(): void;
494
+ /** pushState/replaceState marked as JOPS's own (see installHostHistoryHook). */
495
+ _history(method: any, state: any, url: any): void;
496
+ /**
497
+ * Hosted mode: called after the host changed the URL with pushState or
498
+ * replaceState (no browser event fires for those).
499
+ */
500
+ _onHostNavigation(): void;
265
501
  _animateTransition(outgoing: any, incoming: any, direction: any): Promise<void>;
266
502
  _slideTransition(outgoing: any, incoming: any, direction: any): Promise<void>;
267
503
  _fadeTransition(outgoing: any, incoming: any): Promise<void>;