jops-core 1.1.1 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -7
- package/bin/init.js +156 -9
- package/lib/jops-core.d.ts +15 -0
- package/lib/jops-core.min.js +1 -1
- package/package.json +4 -3
- package/templates/wordpress/jops/jops.php +122 -0
- package/templates/wordpress/jops/readme.txt +41 -0
package/README.md
CHANGED
|
@@ -23,6 +23,8 @@ No Webpack. No Babel. No compiler. Just `<script type="module">` and a browser.
|
|
|
23
23
|
|
|
24
24
|
**Next.js teams:** a JOPS SPA can also run inside a Next.js page, next to React, with Next.js as its backend. See [Next.js Integration](#nextjs-integration).
|
|
25
25
|
|
|
26
|
+
**WordPress sites:** a JOPS SPA can run inside a WordPress page (Elementor or the block editor) through the JOPS plugin and its `[jops]` shortcode. See [WordPress Integration](#wordpress-integration).
|
|
27
|
+
|
|
26
28
|
---
|
|
27
29
|
|
|
28
30
|
## JOPS Demos
|
|
@@ -54,7 +56,7 @@ The import map lets your app modules use `import { View } from "jops-core"`. The
|
|
|
54
56
|
|
|
55
57
|
`App.js` is your entry point — a file you provide that instantiates your root `View` and mounts it to the page. Point `data-app` to wherever you place it.
|
|
56
58
|
|
|
57
|
-
Replace `@latest` with a specific version (e.g. `@1.1.
|
|
59
|
+
Replace `@latest` with a specific version (e.g. `@1.1.2`) to pin. No build step, no npm, no bundler.
|
|
58
60
|
|
|
59
61
|
By default the app renders into `<body>`. To render it into an element instead (keeping the rest of the page), add `data-root` with the element's id:
|
|
60
62
|
|
|
@@ -397,11 +399,95 @@ The result is a standard Next.js app: `npm run build` produces `.next/`, and it
|
|
|
397
399
|
|
|
398
400
|
---
|
|
399
401
|
|
|
402
|
+
## WordPress Integration
|
|
403
|
+
|
|
404
|
+
A JOPS SPA can run on a WordPress site, either inside a page next to the theme and the page's other content (built with Elementor or the block editor), or as a separate app in its own folder. WordPress stays the CMS and the backend; the JOPS app's files are served as static files.
|
|
405
|
+
|
|
406
|
+
| | Embedded in a page | Independent SPA |
|
|
407
|
+
| --- | --- | --- |
|
|
408
|
+
| What it is | The app renders inside a WordPress page, below the theme's header and next to the page's content | A complete JOPS app with its own `index.html`, opened at `https://<site>/<folder>/` |
|
|
409
|
+
| How | The **JOPS plugin** and the `[jops]` shortcode, or an Elementor HTML widget | Upload the app folder; no plugin |
|
|
410
|
+
| Shares the theme, menu and page | Yes | No (separate document) |
|
|
411
|
+
|
|
412
|
+
### Install the JOPS plugin
|
|
413
|
+
|
|
414
|
+
The plugin registers the `[jops]` shortcode and includes the jops-core library, so the page needs nothing else. Pick the way that matches your site:
|
|
415
|
+
|
|
416
|
+
| Your site | Install |
|
|
417
|
+
| --- | --- |
|
|
418
|
+
| **No npm** (hosting with wp-admin only, XAMPP, MAMP, LocalWP, …) | Download [`jops-wordpress.zip`](https://github.com/uidb-dev/jops-core/releases/latest/download/jops-wordpress.zip), then wp-admin → **Plugins → Add New Plugin → Upload Plugin** → choose the zip → **Install Now** → **Activate** |
|
|
419
|
+
| **npm project run by [`wp-env`](https://www.npmjs.com/package/@wordpress/env)** (a folder with `.wp-env.json`) | `npx jops-core init --wp` in that folder: it writes the plugin to `plugins/jops/` and adds `"./plugins/jops"` to `.wp-env.json`. `npx wp-env start` then installs and activates it |
|
|
420
|
+
| **npm project inside a WordPress folder** (a theme, a plugin, a Bedrock project, …) | `npx jops-core init --wp`: it searches this folder and the folders above it for WordPress (`wp-load.php` + `wp-content/`, or Bedrock's `web/wp/` + `web/app/`) and writes the plugin to its plugins folder. Use `--wp-root <path>` to point to WordPress yourself. Then activate the plugin in wp-admin (or `wp plugin activate jops`) |
|
|
421
|
+
|
|
422
|
+
The three ways install the same plugin into `wp-content/plugins/jops/`.
|
|
423
|
+
|
|
424
|
+
- **Updating:** without npm, upload the newer zip the same way and choose **Replace current with uploaded**. With npm, run `npm i jops-core@latest` and then `npx jops-core update`, which refreshes the plugin and its library.
|
|
425
|
+
- **Keep `package.json` out of the WordPress root:** a `node_modules/` folder there is served publicly. `init --wp` warns when it finds one.
|
|
426
|
+
|
|
427
|
+
### Put the app in the WordPress root
|
|
428
|
+
|
|
429
|
+
1. **Place the app folder in the WordPress root** (the folder with `wp-config.php`), e.g. `<root>/jops-spa/`. How it gets there is up to you: upload it, copy it with FTP or SSH, deploy it with your pipeline, or map it with a `"mappings"` entry in `.wp-env.json`. A real folder is served as static files: WordPress only handles URLs that are not existing files or folders.
|
|
430
|
+
2. **Use root-based paths everywhere** JOPS loads something: `mount()`/`app`, `src=`, `layout=`, `loadLayout()`, stylesheets and images, e.g. `/jops-spa/src/App.js` and `loadLayout("/jops-spa/src/layout/main.html")`. If WordPress runs in a subfolder (`example.com/blog/`), include it: `/blog/jops-spa/...`. ES module `import` statements inside your `.js` files resolve relative to the file as usual.
|
|
431
|
+
3. **Pick a folder name that is not a page slug** and doesn't start with `wp-`: the folder always wins over a WordPress page with the same address.
|
|
432
|
+
|
|
433
|
+
To start without npm, copy the [hello-world template](https://github.com/uidb-dev/jops-core/tree/main/templates/hello-world) (`src/` and `assets/`) into `<root>/jops-spa/` and change its paths to root-based ones: `/src/...` and `./src/...` become `/jops-spa/src/...`, and `/assets/...` becomes `/jops-spa/assets/...`.
|
|
434
|
+
|
|
435
|
+
### Add the app to a page
|
|
436
|
+
|
|
437
|
+
Edit the page with Elementor, add a **Shortcode** widget (General → Shortcode) and enter:
|
|
438
|
+
|
|
439
|
+
```text
|
|
440
|
+
[jops app="/jops-spa/src/App.js" id="jops-spa-root" class="jops-spa"]
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
In the block editor, use the **Shortcode** block; any editor that runs shortcodes works the same way.
|
|
444
|
+
|
|
445
|
+
| Attribute | Description |
|
|
446
|
+
| --- | --- |
|
|
447
|
+
| `app` | **Required.** Root-based path of the app's entry module (its default export is created) |
|
|
448
|
+
| `id` | `id` of the app's root `<div>` (default `jops-root`) |
|
|
449
|
+
| `class` | `class` of the root `<div>`; use it to scope the app's CSS |
|
|
450
|
+
| `lib` | URL of `jops-core.min.js` (default: the plugin's copy). Use one URL per page, or the browser loads two jops-core instances |
|
|
451
|
+
|
|
452
|
+
- **The app runs on the published page.** In the Elementor editor the widget shows a placeholder ("JOPS app … runs on the published page") and loads no scripts.
|
|
453
|
+
- **One JOPS app per page.** A second `[jops]` on the same page logs a warning and is not started.
|
|
454
|
+
- The shortcode outputs the import map, the library in hosted mode, the root `<div>` and a small script that calls `mount()` once the library has loaded (see [Embedding JOPS in Other Pages](#embedding-jops-in-other-pages-hosted-mode)).
|
|
455
|
+
|
|
456
|
+
**Without the plugin**, an Elementor **HTML** widget can hold the same code by hand. Vendor the library in the app folder (`/jops-spa/lib/jops-core.min.js`, copied from `node_modules/jops-core/lib/` or the [release files](https://github.com/uidb-dev/jops-core/tree/main/lib)):
|
|
457
|
+
|
|
458
|
+
```html
|
|
459
|
+
<script type="importmap">
|
|
460
|
+
{ "imports": { "jops-core": "/jops-spa/lib/jops-core.min.js" } }
|
|
461
|
+
</script>
|
|
462
|
+
<script type="module" src="/jops-spa/lib/jops-core.min.js" data-host></script>
|
|
463
|
+
|
|
464
|
+
<div id="jops-spa-root" class="jops-spa" style="position: relative"></div>
|
|
465
|
+
<script type="module">
|
|
466
|
+
const start = () => window.JOPS.mount({ app: "/jops-spa/src/App.js", root: document.getElementById("jops-spa-root") });
|
|
467
|
+
window.JOPS ? start() : window.addEventListener("jops:ready", start, { once: true });
|
|
468
|
+
</script>
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
Don't combine the shortcode and a hand-written import map on the same page.
|
|
472
|
+
|
|
473
|
+
### Conventions
|
|
474
|
+
|
|
475
|
+
- **CSS:** `setStyle()` has no effect on an embedded app (the page owns the CSS). Link the app's stylesheet from its layout (`<link rel="stylesheet" href="/jops-spa/src/css/styles.css">`) or add it to the theme. It applies to the whole page, so scope every rule under the root class (`.jops-spa .card { … }`) and avoid global rules (`body`, `*`, `:root`, bare tag selectors).
|
|
476
|
+
- **Routing:** WordPress owns the path (`/my-page/`), JOPS owns the hash (`/my-page/#details?id=7`). Leaving the page is a full page load, which ends the app.
|
|
477
|
+
- **Backend:** call the WordPress REST API with `fetch("/wp-json/…")`. Requests are same-origin, so the login cookie is sent; requests that need the logged-in user (and every write) also need a REST nonce (`X-WP-Nonce` header, created with `wp_create_nonce("wp_rest")`), which your own theme or plugin code must provide to the app.
|
|
478
|
+
- **Caching:** the app's files are static, and caching plugins, CDNs and browsers may keep serving old copies after an update. Use versioned paths or short cache headers for the app folder, and hard-reload while developing.
|
|
479
|
+
|
|
480
|
+
### Independent SPA
|
|
481
|
+
|
|
482
|
+
A complete JOPS app with its own `index.html` (as created by `npx jops-core init`) also runs from a folder in the WordPress root: upload it as `<root>/jops-spa/` and open `https://<site>/jops-spa/`. WordPress is not involved. With the library from the CDN, the app's `./` paths work as they are; with a vendored library, use root-based paths (`/jops-spa/src/App.js`, `/jops-spa/lib/jops-core.min.js` in the import map and the script tag).
|
|
483
|
+
|
|
484
|
+
---
|
|
485
|
+
|
|
400
486
|
## Embedding JOPS in Other Pages (Hosted Mode)
|
|
401
487
|
|
|
402
|
-
A JOPS app can run inside any page that is not a React component: a plain HTML page, or a page rendered by another server framework (PHP, ASP.NET, Rails, Django, …). This is the mechanism `JopsApp` uses in Next.js; **inside a Next.js page, use `JopsApp`** instead (see [Next.js Integration](#nextjs-integration)).
|
|
488
|
+
A JOPS app can run inside any page that is not a React component: a plain HTML page, or a page rendered by another server framework (PHP, ASP.NET, Rails, Django, …). This is the mechanism `JopsApp` uses in Next.js and the JOPS plugin uses in WordPress; **inside a Next.js page, use `JopsApp`** instead (see [Next.js Integration](#nextjs-integration)), and on a WordPress site the `[jops]` shortcode (see [WordPress Integration](#wordpress-integration)).
|
|
403
489
|
|
|
404
|
-
Load the library with `data-host` and no `data-app`: nothing starts automatically, and `window.JOPS` exposes `mount()` and `unmount()` (a `jops:ready` event fires
|
|
490
|
+
Load the library with `data-host` and no `data-app`: nothing starts automatically, and `window.JOPS` exposes `mount()` and `unmount()` (a `jops:ready` event fires once jops-core has fully loaded, so a listener may call `mount()` directly). The import map and the script `src` must be the same URL, so the page has a single jops-core instance.
|
|
405
491
|
|
|
406
492
|
```html
|
|
407
493
|
<script type="importmap">
|
|
@@ -493,6 +579,7 @@ Optional hooks, called by the Router:
|
|
|
493
579
|
| `addSubView(containerId, view)` | Appends `view` into the child element with `id="containerId"`, runs its full lifecycle |
|
|
494
580
|
| `insertSubView(view, index)` | Inserts `view` at position `index` within `this.domNode`, runs its full lifecycle |
|
|
495
581
|
| `removeSubView(view)` | Removes `view` from DOM and `this.subViews`, calls `view.destroy()` |
|
|
582
|
+
| `removeSubViews(condition?)` | Removes every subview with `removeSubView()`, or only those matching `condition` (the others keep their order). Direct subviews only; not available on a `Router` (logs a warning) |
|
|
496
583
|
| `show()` | Shows the view again (removes the inline `display` override) |
|
|
497
584
|
| `hide()` | Hides the view (`display: none`) |
|
|
498
585
|
| `destroy()` | Recursively destroys subtree — calls `destroy()` on all subviews, removes the view's elements from the DOM, removes its owned `EventBus` subscriptions, nulls references |
|
|
@@ -580,7 +667,7 @@ Returned by `view.get(id)` (and, as an array, by `view.find(id)`). Wraps a singl
|
|
|
580
667
|
|
|
581
668
|
`set()`, `attr()` and `value()` never throw. When no element has the id, they do nothing (`value()` returns `""`) and log a warning with the id — usually a typo. On a destroyed view (for example after an async request finished when the user had already left), they do nothing silently. Code that uses `element` or `this.domNode` directly has no such guard.
|
|
582
669
|
|
|
583
|
-
> `set()` replaces plain markup. Don't use it to clear a container that holds sub-views: call `removeSubView()` for each of them first, otherwise they stay in the parent's `subViews`.
|
|
670
|
+
> `set()` replaces plain markup. Don't use it to clear a container that holds sub-views: call `removeSubViews()` (or `removeSubView()` for each of them) first, otherwise they stay in the parent's `subViews`.
|
|
584
671
|
|
|
585
672
|
```js
|
|
586
673
|
this.get("username").set("Alice");
|
|
@@ -830,7 +917,7 @@ Prefer the owned form inside views. A global subscription made by a view keeps t
|
|
|
830
917
|
|
|
831
918
|
### `mount()` / `unmount()` — Functions
|
|
832
919
|
|
|
833
|
-
Start and end an app in a host page (hosted mode). Used by `JopsApp` in Next.js; see [Embedding JOPS in Other Pages](#embedding-jops-in-other-pages-hosted-mode). The page must load jops-core with `data-host` (otherwise it starts `data-app` / `./src/App.js` by itself); `window.JOPS.mount` / `window.JOPS.unmount` are the same functions.
|
|
920
|
+
Start and end an app in a host page (hosted mode). Used by `JopsApp` in Next.js and by the JOPS WordPress plugin; see [Embedding JOPS in Other Pages](#embedding-jops-in-other-pages-hosted-mode). The page must load jops-core with `data-host` (otherwise it starts `data-app` / `./src/App.js` by itself); `window.JOPS.mount` / `window.JOPS.unmount` are the same functions.
|
|
834
921
|
|
|
835
922
|
```js
|
|
836
923
|
import { mount, unmount } from "jops-core";
|
|
@@ -882,10 +969,11 @@ t.terminate();
|
|
|
882
969
|
| Command | Description |
|
|
883
970
|
| ---------------------- | -------------------------------------------------------------------------- |
|
|
884
971
|
| `npx jops-core init` | Vendor library, configure `index.html`, inject scripts into `package.json`. Detects Cordova and Capacitor projects (see [Hybrid Mobile Apps](#hybrid-mobile-apps-cordova-ionic--capacitor)). Template 3 creates a Next.js project instead (see [Next.js Integration](#nextjs-integration)) |
|
|
885
|
-
| `npx jops-core
|
|
972
|
+
| `npx jops-core init --wp` | Install the JOPS WordPress plugin: into `plugins/jops/` of a `wp-env` project, otherwise into the plugins folder of the WordPress installation found in this folder or above it (see [WordPress Integration](#wordpress-integration)) |
|
|
973
|
+
| `npx jops-core update` | Copy the latest `jops-core.min.js` from `node_modules` to `lib/` (`public/lib/` in a Next.js project); also refreshes an installed JOPS WordPress plugin |
|
|
886
974
|
| `npx jops-core build` | Generate production-ready `dist/` folder (skipped in Cordova/Capacitor projects) |
|
|
887
975
|
|
|
888
|
-
`init` and `update` accept `--cordova`, `--capacitor` or `--web` to override the detected project type.
|
|
976
|
+
`init` and `update` accept `--cordova`, `--capacitor` or `--web` to override the detected project type, and `--wp-root <path>` to point to a WordPress folder instead of searching for it (implies `--wp`).
|
|
889
977
|
|
|
890
978
|
After each command the CLI checks npm for a newer jops-core and prints a notice if one is available (at most 1.5 s, silent when offline). `build` also warns if `lib/jops-core.min.js` doesn't match the installed version. Turn the check off with `--no-update-check` or `JOPS_NO_UPDATE_CHECK=1`; it is skipped automatically when `CI` is set.
|
|
891
979
|
|
package/bin/init.js
CHANGED
|
@@ -11,11 +11,16 @@ const [,, command, ...flags] = process.argv;
|
|
|
11
11
|
if (command !== "init" && command !== "build" && command !== "update") {
|
|
12
12
|
console.log("Usage:");
|
|
13
13
|
console.log(" npx jops-core init — vendor library and configure index.html, or create a template project");
|
|
14
|
-
console.log(" npx jops-core
|
|
14
|
+
console.log(" npx jops-core init --wp — install the JOPS WordPress plugin: into plugins/jops/ of a wp-env project");
|
|
15
|
+
console.log(" (folder with .wp-env.json), otherwise into the plugins folder of the");
|
|
16
|
+
console.log(" WordPress installation found in this folder or above it");
|
|
17
|
+
console.log(" npx jops-core update — copy the latest jops-core.min.js to lib/ (public/lib/ in a Next.js project);");
|
|
18
|
+
console.log(" also refreshes an installed JOPS WordPress plugin (found as for init --wp)");
|
|
15
19
|
console.log(" npx jops-core build — generate production-ready dist/ folder");
|
|
16
20
|
console.log("");
|
|
17
21
|
console.log("Options (init, update):");
|
|
18
22
|
console.log(" --cordova | --capacitor | --web override the detected project type");
|
|
23
|
+
console.log(" --wp-root <path> WordPress folder to use instead of searching (implies --wp)");
|
|
19
24
|
console.log("Options (all commands):");
|
|
20
25
|
console.log(" --no-update-check skip the check for a newer jops-core version");
|
|
21
26
|
process.exit(0);
|
|
@@ -149,6 +154,73 @@ function isNextProject(dir) {
|
|
|
149
154
|
return ["next.config.ts", "next.config.js", "next.config.mjs"].some((f) => fs.existsSync(path.join(dir, f)));
|
|
150
155
|
}
|
|
151
156
|
|
|
157
|
+
// ─── WordPress ───────────────────────────────────────────────────────────────
|
|
158
|
+
// wp-env project (.wp-env.json here): the plugin is written to
|
|
159
|
+
// <project>/plugins/jops/ and listed in .wp-env.json, which mounts it into
|
|
160
|
+
// WordPress. Any other project: the plugin is written to the plugins folder of
|
|
161
|
+
// the WordPress installation found in this folder or above it (or --wp-root).
|
|
162
|
+
|
|
163
|
+
const wpRootIndex = flags.findIndex((f) => f === "--wp-root" || f.startsWith("--wp-root="));
|
|
164
|
+
const wpRootFlag = wpRootIndex < 0 ? null
|
|
165
|
+
: flags[wpRootIndex].startsWith("--wp-root=") ? flags[wpRootIndex].slice("--wp-root=".length)
|
|
166
|
+
: flags[wpRootIndex + 1];
|
|
167
|
+
if (wpRootIndex >= 0 && (!wpRootFlag || wpRootFlag.startsWith("--"))) {
|
|
168
|
+
console.error("[jops-core] --wp-root needs a path, e.g. --wp-root ../wordpress");
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
const wpMode = flags.includes("--wp") || wpRootIndex >= 0;
|
|
172
|
+
const wpEnvConfig = path.join(process.cwd(), ".wp-env.json");
|
|
173
|
+
const wpPluginDir = path.join(process.cwd(), "plugins", "jops");
|
|
174
|
+
const isWpEnv = !wpRootFlag && fs.existsSync(wpEnvConfig);
|
|
175
|
+
const relCwd = (p) => path.relative(process.cwd(), p) || ".";
|
|
176
|
+
|
|
177
|
+
// A WordPress installation at dir: { root, plugins, layout }, otherwise null.
|
|
178
|
+
// root is the folder served at the site root (where the JOPS app folder goes).
|
|
179
|
+
// wp-config.php is not checked: WordPress allows it one level above the root.
|
|
180
|
+
function wpInstallAt(dir) {
|
|
181
|
+
const has = (...p) => fs.existsSync(path.join(dir, ...p));
|
|
182
|
+
if (has("wp-load.php") && has("wp-content")) {
|
|
183
|
+
return { root: dir, plugins: path.join(dir, "wp-content", "plugins"), layout: "standard" };
|
|
184
|
+
}
|
|
185
|
+
if (has("web", "wp", "wp-load.php") && has("web", "app")) {
|
|
186
|
+
return { root: path.join(dir, "web"), plugins: path.join(dir, "web", "app", "plugins"), layout: "Bedrock" };
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// --wp-root, or the nearest installation in this folder or above it.
|
|
192
|
+
function findWordPress() {
|
|
193
|
+
if (wpRootFlag) return wpInstallAt(path.resolve(wpRootFlag));
|
|
194
|
+
for (let dir = process.cwd(); ; dir = path.dirname(dir)) {
|
|
195
|
+
const found = wpInstallAt(dir);
|
|
196
|
+
if (found || dir === path.dirname(dir)) return found;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Copies the plugin template (version filled in) and the library into dest,
|
|
201
|
+
// replacing earlier copies.
|
|
202
|
+
function installWpPlugin(dest) {
|
|
203
|
+
const src = path.join(__dirname, "../templates/wordpress/jops");
|
|
204
|
+
fs.mkdirSync(path.join(dest, "lib"), { recursive: true });
|
|
205
|
+
for (const name of fs.readdirSync(src)) {
|
|
206
|
+
const text = fs.readFileSync(path.join(src, name), "utf8");
|
|
207
|
+
fs.writeFileSync(path.join(dest, name), text.replaceAll("{{VERSION}}", pkg.version), "utf8");
|
|
208
|
+
}
|
|
209
|
+
fs.copyFileSync(path.join(__dirname, "../lib/jops-core.min.js"), path.join(dest, "lib", "jops-core.min.js"));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Adds "./plugins/jops" to the "plugins" list of .wp-env.json, unless the
|
|
213
|
+
// folder is already listed. Returns true when the file was changed.
|
|
214
|
+
function addWpEnvPlugin(configPath, pluginDir) {
|
|
215
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
216
|
+
const base = path.dirname(configPath);
|
|
217
|
+
const plugins = config.plugins ?? [];
|
|
218
|
+
if (plugins.some((p) => typeof p === "string" && path.resolve(base, p) === pluginDir)) return false;
|
|
219
|
+
config.plugins = [...plugins, "./" + path.relative(base, pluginDir).split(path.sep).join("/")];
|
|
220
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
|
|
152
224
|
// Copies a template project into dest (README.md included, unlike copyDir()).
|
|
153
225
|
// package.json is merged separately; existing files are never overwritten.
|
|
154
226
|
// npm drops .gitignore files from published packages, so the template stores
|
|
@@ -225,7 +297,65 @@ async function askTemplate() {
|
|
|
225
297
|
return answer.trim();
|
|
226
298
|
}
|
|
227
299
|
|
|
228
|
-
const choice = command === "init" ? await askTemplate() : null;
|
|
300
|
+
const choice = command === "init" && !wpMode ? await askTemplate() : null;
|
|
301
|
+
|
|
302
|
+
// ─── init --wp: JOPS WordPress plugin (wp-env project) ───────────────────────
|
|
303
|
+
|
|
304
|
+
if (command === "init" && wpMode && isWpEnv) {
|
|
305
|
+
if (fs.existsSync(wpPluginDir)) {
|
|
306
|
+
console.error(`[jops-core] ${path.relative(process.cwd(), wpPluginDir)}/ already exists. Nothing was changed.`);
|
|
307
|
+
console.error(" Run `npx jops-core update` to refresh the plugin and the library.");
|
|
308
|
+
process.exitCode = 1;
|
|
309
|
+
} else {
|
|
310
|
+
installWpPlugin(wpPluginDir);
|
|
311
|
+
console.log(`[jops-core] Created the JOPS WordPress plugin v${pkg.version} → plugins/jops/ (jops.php, readme.txt, lib/jops-core.min.js)`);
|
|
312
|
+
if (addWpEnvPlugin(wpEnvConfig, wpPluginDir)) {
|
|
313
|
+
console.log('[jops-core] Added "./plugins/jops" to "plugins" in .wp-env.json');
|
|
314
|
+
} else {
|
|
315
|
+
console.log("[jops-core] .wp-env.json already lists plugins/jops");
|
|
316
|
+
}
|
|
317
|
+
console.log("");
|
|
318
|
+
console.log("[jops-core] Next steps:");
|
|
319
|
+
console.log(" 1. npx wp-env start (applies .wp-env.json and activates the plugin; the database is kept)");
|
|
320
|
+
console.log(" 2. Put your JOPS app folder in the WordPress root (e.g. a \"mappings\" entry in .wp-env.json)");
|
|
321
|
+
console.log(' 3. Add a Shortcode widget to the page: [jops app="/jops-spa/src/App.js" id="jops-spa-root"]');
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ─── init --wp: JOPS WordPress plugin (WordPress installation) ───────────────
|
|
326
|
+
|
|
327
|
+
if (command === "init" && wpMode && !isWpEnv) {
|
|
328
|
+
const wp = findWordPress();
|
|
329
|
+
const pluginDir = wp && path.join(wp.plugins, "jops");
|
|
330
|
+
if (!wp) {
|
|
331
|
+
console.error(wpRootFlag
|
|
332
|
+
? `[jops-core] No WordPress installation at ${wpRootFlag} (looked for wp-load.php + wp-content/, or Bedrock's web/wp/ + web/app/).`
|
|
333
|
+
: "[jops-core] No WordPress installation found in this folder or above it (looked for wp-load.php + wp-content/,");
|
|
334
|
+
if (!wpRootFlag) console.error(" or Bedrock's web/wp/ + web/app/), and no .wp-env.json here.");
|
|
335
|
+
console.error(" Point to it with --wp-root <path>, or install the JOPS plugin zip in wp-admin.");
|
|
336
|
+
console.error(" A custom content folder (WP_CONTENT_DIR) other than Bedrock's is not supported.");
|
|
337
|
+
process.exitCode = 1;
|
|
338
|
+
} else if (fs.existsSync(pluginDir)) {
|
|
339
|
+
console.log(`[jops-core] WordPress (${wp.layout}) found at ${relCwd(wp.root)}`);
|
|
340
|
+
console.error(`[jops-core] ${relCwd(pluginDir)}/ already exists. Nothing was changed.`);
|
|
341
|
+
console.error(" Run `npx jops-core update` to refresh the plugin and the library.");
|
|
342
|
+
process.exitCode = 1;
|
|
343
|
+
} else {
|
|
344
|
+
console.log(`[jops-core] WordPress (${wp.layout}) found at ${relCwd(wp.root)} → plugins: ${relCwd(wp.plugins)}/`);
|
|
345
|
+
installWpPlugin(pluginDir);
|
|
346
|
+
console.log(`[jops-core] Created the JOPS WordPress plugin v${pkg.version} → ${relCwd(pluginDir)}/ (jops.php, readme.txt, lib/jops-core.min.js)`);
|
|
347
|
+
if (fs.existsSync(path.join(wp.root, "package.json"))) {
|
|
348
|
+
console.warn("[jops-core] Warning: package.json is in the WordPress root, so its node_modules/ is served publicly.");
|
|
349
|
+
console.warn(" Keep npm projects in a theme or plugin folder, or block node_modules/ on the web server.");
|
|
350
|
+
}
|
|
351
|
+
console.log("");
|
|
352
|
+
console.log("[jops-core] Next steps:");
|
|
353
|
+
console.log(" 1. Activate the plugin: wp-admin > Plugins > JOPS > Activate (or: wp plugin activate jops)");
|
|
354
|
+
console.log(` 2. Put your JOPS app folder in ${relCwd(wp.root)}/ (the WordPress root), e.g. ${relCwd(path.join(wp.root, "jops-spa"))}/`);
|
|
355
|
+
console.log(' 3. Add a Shortcode widget to the page: [jops app="/jops-spa/src/App.js" id="jops-spa-root"]');
|
|
356
|
+
console.log(" 4. Deploy the plugin folder like any other plugin");
|
|
357
|
+
}
|
|
358
|
+
}
|
|
229
359
|
|
|
230
360
|
// ─── init: Next.js JOPS Embedded SPA (template 3) ────────────────────────────
|
|
231
361
|
|
|
@@ -370,7 +500,7 @@ function schemeWarnings(xml) {
|
|
|
370
500
|
|
|
371
501
|
// ─── init ────────────────────────────────────────────────────────────────────
|
|
372
502
|
|
|
373
|
-
if (command === "init" && choice !== "3") {
|
|
503
|
+
if (command === "init" && !wpMode && choice !== "3") {
|
|
374
504
|
fs.mkdirSync(webRoot, { recursive: true });
|
|
375
505
|
if (isHybrid) {
|
|
376
506
|
console.log(`[jops-core] ${forced ? "Using" : "Detected"} ${type} project → web folder: ${rel(webRoot)}/`);
|
|
@@ -572,6 +702,22 @@ ${cordovaBody}${MODULE_SCRIPT}
|
|
|
572
702
|
|
|
573
703
|
// ─── update ──────────────────────────────────────────────────────────────────
|
|
574
704
|
|
|
705
|
+
// An installed JOPS WordPress plugin (found as for init --wp): refresh the
|
|
706
|
+
// plugin and its library. A project's own lib/ is refreshed as well below.
|
|
707
|
+
let wpUpdated = false;
|
|
708
|
+
if (command === "update") {
|
|
709
|
+
const wp = isWpEnv ? null : findWordPress();
|
|
710
|
+
const pluginDir = isWpEnv ? wpPluginDir : wp && path.join(wp.plugins, "jops");
|
|
711
|
+
if (pluginDir && fs.existsSync(path.join(pluginDir, "jops.php"))) {
|
|
712
|
+
installWpPlugin(pluginDir);
|
|
713
|
+
console.log(`[jops-core] Updated the JOPS WordPress plugin in ${relCwd(pluginDir)}/ to v${pkg.version}`);
|
|
714
|
+
wpUpdated = true;
|
|
715
|
+
} else if (wpRootFlag) {
|
|
716
|
+
console.error(`[jops-core] No JOPS plugin found at ${wpRootFlag}. Run 'npx jops-core init --wp' first.`);
|
|
717
|
+
process.exit(1);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
575
721
|
if (command === "update") {
|
|
576
722
|
// A Next.js project serves the library from public/lib/.
|
|
577
723
|
const libRoot = type === "web" && isNextProject(projectRoot) ? path.join(projectRoot, "public") : webRoot;
|
|
@@ -580,13 +726,14 @@ if (command === "update") {
|
|
|
580
726
|
const vendorSrc = path.join(__dirname, "../lib/jops-core.min.js");
|
|
581
727
|
|
|
582
728
|
if (!fs.existsSync(libDir)) {
|
|
583
|
-
|
|
584
|
-
|
|
729
|
+
if (!wpUpdated) {
|
|
730
|
+
console.error(`[jops-core] ${rel(libDir)}/ folder not found. Run 'npx jops-core init' first.`);
|
|
731
|
+
process.exit(1);
|
|
732
|
+
}
|
|
733
|
+
} else {
|
|
734
|
+
fs.copyFileSync(vendorSrc, vendorDest);
|
|
735
|
+
console.log(`[jops-core] Updated ${rel(vendorDest)} to v${pkg.version}`);
|
|
585
736
|
}
|
|
586
|
-
|
|
587
|
-
fs.copyFileSync(vendorSrc, vendorDest);
|
|
588
|
-
|
|
589
|
-
console.log(`[jops-core] Updated ${rel(vendorDest)} to v${pkg.version}`);
|
|
590
737
|
}
|
|
591
738
|
|
|
592
739
|
// ─── build ───────────────────────────────────────────────────────────────────
|
package/lib/jops-core.d.ts
CHANGED
|
@@ -274,6 +274,15 @@ export declare class View {
|
|
|
274
274
|
* @param {View} view - The subview to remove.
|
|
275
275
|
*/
|
|
276
276
|
removeSubView(view: View): void;
|
|
277
|
+
/**
|
|
278
|
+
* Removes subviews and destroys them, each through `removeSubView()`.
|
|
279
|
+
* With a condition, removes only the subviews it matches (the others keep
|
|
280
|
+
* their place and order).
|
|
281
|
+
* @param {(view: View) => boolean} [condition] - Which subviews to remove; all when omitted.
|
|
282
|
+
* @example
|
|
283
|
+
* this.removeSubViews((v) => v instanceof QuoteRow);
|
|
284
|
+
*/
|
|
285
|
+
removeSubViews(condition?: (view: View) => boolean): void;
|
|
277
286
|
/**
|
|
278
287
|
* Inserts `view` at `index` within `this.domNode` and runs its full lifecycle.
|
|
279
288
|
* A removed (destroyed) view is final: create a new instance to show it again.
|
|
@@ -486,6 +495,12 @@ export declare class Router extends View {
|
|
|
486
495
|
* keeping the existing history state, and shows that route.
|
|
487
496
|
*/
|
|
488
497
|
_switchToDefault(): void;
|
|
498
|
+
/**
|
|
499
|
+
* Not supported: a Router's subviews are its routes, and removing them
|
|
500
|
+
* would leave its route map pointing at destroyed views.
|
|
501
|
+
* @param {(view: View) => boolean} [condition]
|
|
502
|
+
*/
|
|
503
|
+
removeSubViews(condition?: (view: View) => boolean): void;
|
|
489
504
|
/**
|
|
490
505
|
* Removes the history listeners, destroys the routes and stacked views and
|
|
491
506
|
* clears the singleton. Called by `unmount()`; safe to call more than once.
|
package/lib/jops-core.min.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var W=Object.defineProperty;var h=(o,t)=>W(o,"name",{value:t,configurable:!0});var B=new URL(import.meta.url).origin!==location.origin,k=new Map;function b(o){if(!o)return o;if(B){if(o.startsWith("/"))return"."+o}else if(o.startsWith("./"))return o.slice(1);return o}h(b,"normalizePath");var u=null,S=!1,m=0,v=new Set,_=null,I=new Set(["jops","src","layout","path","jops-event-bind"]);function H(o){let t={};for(let{name:e,value:s}of o.attributes)I.has(e)||e==="type"&&s==="jops"||(t[e.replace(/-([a-z])/g,(n,i)=>i.toUpperCase())]=s);return t}h(H,"tagProps");var j=new Set,O=!1;(()=>{if(console.log("JOPS initialized"),Array.from(document.querySelectorAll("script[src]")).find(s=>s.src===import.meta.url)?.dataset.host!==void 0){S=!0,window.JOPS={mount:D,unmount:V},window.dispatchEvent(new Event("jops:ready"));return}let t=document.querySelector("script[data-app]"),e=t?.dataset.app??"./src/App.js";t?.dataset.root&&(u=document.getElementById(t.dataset.root),u||console.warn(`JOPS: data-root element #${t.dataset.root} not found, rendering into <body>`)),import(new URL(b(e),document.baseURI).href).then(s=>new s.default).catch(s=>console.error("JOPS: App.js not found or failed to load",s)),t?.dataset.nativeBack!=="off"&&K()})();var T=!1,N=!1,C={},$={};function M(){if(!N){N=!0;for(let o of["pushState","replaceState"]){let t=history[o],e=h(function(...s){let n=t.apply(this,s);return T||queueMicrotask(()=>y.singleton()?._onHostNavigation()),n},"wrapper");C[o]=t,$[o]=e,history[o]=e}}}h(M,"installHostHistoryHook");function q(){if(!N)return;let o=!0;for(let t of["pushState","replaceState"])history[t]===$[t]?history[t]=C[t]:o=!1;o&&(N=!1)}h(q,"uninstallHostHistoryHook");async function D({app:o,root:t}){if(!(t instanceof HTMLElement))throw new TypeError("JOPS mount(): root must be an HTMLElement");u=t,M();let e=m,s=await import(new URL(b(o),document.baseURI).href);return e!==m?null:new s.default}h(D,"mount");function V(){m+=1,y.singleton()?.destroy(),v.forEach(o=>o.destroy()),v.clear(),u&&(u.innerHTML=""),u=null,P.singleton().clear(),L.clear(),q()}h(V,"unmount");function K(){document.addEventListener("deviceready",()=>{let o=window.Capacitor;o?.isNativePlatform?.()&&o.getPlatform()==="android"&&!o.isPluginAvailable("App")&&console.warn("JOPS: install @capacitor/app so the Android back button navigates instead of closing the app.");let t=0;document.addEventListener("backbutton",()=>{let e=Date.now();if(e-t<300)return;t=e;let s=y.singleton();s?.canGoBack()?s.back():!s&&window.navigation?.canGoBack?history.back():navigator.app.exitApp()})},{once:!0})}h(K,"installNativeBackButton");var x=class{static{h(this,"RawHTML")}constructor(t){this.html=t}},z=h(o=>new x(o),"raw");function G(o,...t){return o.reduce((e,s,n)=>{if(n>=t.length)return e+s;let i=t[n];return i==null?e+s:i instanceof x?e+s+i.html:e+s+String(i).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")},"")}h(G,"layout");var g=class o{static{h(this,"Element")}constructor(t,e){this.id=e,this.inert=t.length===0,this.element=null;for(let s of t)if(this.element=s.id===e?s:s.querySelector(`[id="${CSS.escape(e)}"]`),this.element)break}static from(t,e){let s=new o([],e);return s.inert=!1,s.element=t,s}set(t){return this.element?(this.element.innerHTML=t,this):this.#t("set")}attr(t,e){return this.element?(e==null?this.element.removeAttribute(t):this.element.setAttribute(t,String(e)),this):this.#t("attr")}value(){return this.element?this.element.innerHTML:(this.#t("value"),"")}#t(t){return this.inert||console.warn(`JOPS: ${t}() on #${this.id}: no such element in the view`),this}},R=class o{static{h(this,"View")}#t=!1;#e=!1;#o=!1;#s=null;#i;constructor(t,e){this._generation=m;let s=_&&_.ClassDef===new.target?_.props:null;this.#i=Object.freeze({...e??s??{}}),this.loadLayout(t),this.subViews=[]}get props(){return this.#i}async loadLayout(t){if(this.#s)return this.#s;if(!(!t||this.#o))return this.#s=this.#n(t).finally(()=>{this.#o=this.layout!=null,this.#s=null}),this.#s}async#n(t){if(t.endsWith(".js"))try{let e=await import(new URL(b(t),document.baseURI).href);this.layout=e.default}catch(e){console.error("JOPS: failed to load layout:",t,e)}else if(t.endsWith(".html")||t.endsWith(".htm"))try{let e=new URL(b(t),document.baseURI).href;if(k.has(e))this.layout=k.get(e);else{let s=await fetch(e);if(!s.ok)throw new Error(`HTTP ${s.status}`);let n=await s.text();k.set(e,n),this.layout=n}}catch(e){console.error("JOPS: failed to load layout:",t,e)}else this.layout=t}setStyle(t){if(t){if(this.style=t,S){O||(O=!0,console.info("JOPS: setStyle() is ignored in hosted mode; the host provides the CSS."));return}if(t.endsWith(".css")){if(document.querySelector(`link[href="${CSS.escape(t)}"]`))return;let e=document.createElement("link");e.rel="stylesheet",e.href=t,document.head.appendChild(e)}else{if(j.has(t))return;j.add(t);let e=document.createElement("style");e.setAttribute("data-jops",""),e.textContent=t,document.head.appendChild(e)}}}get(t){return new g(this._roots(),t)}_roots(){return this.domNodes??(this.domNode?[this.domNode]:[])}_setHidden(t){for(let e of this._roots())e.hidden=t}bindEvents(){let t=this._roots().flatMap(e=>[...e.hasAttribute("jops-event-bind")||e.getAttribute("type")==="jops-event-bind"?[e]:[],...e.querySelectorAll("[jops-event-bind], [type='jops-event-bind']")]);for(let e of t)for(let s of e.attributes){if(!s.name.startsWith("on"))continue;let n=s.value.trim();typeof this[n]=="function"&&(e[s.name]=i=>this[n](i))}}inflate(){if(!this.layout||this.#t)return;this.#t=!0;let t=document.createElement("template");t.innerHTML=this.layout;let e=t.content.cloneNode(!0);this.domNodes=[...e.children],this.domNode=this.domNodes[0]}async addSubView(t,e){if(!this.domNode)return;if(e._destroyed){console.warn("JOPS: addSubView(): this view was destroyed; create a new instance");return}this.subViews.push(e),e.domNode||(await e.loadLayout(),e.inflate());let s=this.domNode?.querySelector(`#${t}`);if(!e.domNode||!s){this.subViews=this.subViews.filter(n=>n!==e),this.domNode&&console.warn(e.domNode?`JOPS: addSubView(): no element #${t} in the view`:"JOPS: addSubView(): the view has no layout");return}s.append(...e._roots()),await e?.onLayout?.(),await e?.onResume?.(),e?.bindEvents?.()}hide(){for(let t of this._roots())t.style.display="none"}show(){for(let t of this._roots())t.style.display=""}find(t){if(typeof t=="string")return this._roots().flatMap(s=>[...s.id===t?[s]:[],...s.querySelectorAll(`[id="${CSS.escape(t)}"]`)]).map(s=>g.from(s,t));let e=[];for(let s of this.subViews)t(s)&&e.push(s),e.push(...s.find?.(t)??[]);return e}destroy(){this._destroyed=!0,L.unsubscribeAll(this),this.subViews.forEach(e=>e.destroy?.()),this.subViews=[],this._borrowedDomNode||this.domNode&&(this.domNode===u||this.domNode===document.body)?this.domNode.innerHTML="":this._roots().forEach(e=>e.remove()),this.domNode=null,this.domNodes=null,this.layout=null}removeSubView(t){t.destroy?.(),this.subViews=this.subViews.filter(e=>e!==t)}async insertSubView(t,e){if(!this.domNode)return;if(t._destroyed){console.warn("JOPS: insertSubView(): this view was destroyed; create a new instance");return}if(t.domNode||(await t.loadLayout(),t.inflate()),!this.domNode)return;if(!t.domNode){console.warn("JOPS: insertSubView(): the view has no layout");return}let s=this.domNode.children[e]??null;for(let n of t._roots())this.domNode.insertBefore(n,s);this.subViews.splice(e,0,t),await t?.onLayout?.(),await t?.onResume?.(),t?.bindEvents?.()}async _startSubView(t){await t?.onResume?.(),t?.bindEvents?.()}async render(){if(!this.layout||this.#e||this._generation!==m)return;let t=!this.domNode;if(t&&S&&!u)return;this.#e=!0;let e=this.domNode??u??document.body;t&&(e.innerHTML=this.layout,v.add(this),this.domNode=e,this._borrowedDomNode=!0);let s=h(()=>this._generation!==m,"ended"),n=Array.from(e.querySelectorAll("[jops], [type='jops']")).filter(i=>i.tagName==="ROUTER"||!i.closest("router")||i.closest("router")===e);if(n.length>0&&await this.#r(n,e,s),!s()&&t){if(await this.onLayout?.(),s()||(await this.onResume?.(),s()))return;this.bindEvents()}}async#r(t,e,s){let n=new Array(t.length);await Promise.all(t.map(async(i,a)=>{if(i.tagName==="ROUTER"){let l=new y(i);if(await l.render(),s())return;await l.onLayout();return}let r=i.getAttribute("layout"),f=i.getAttribute("src");if(!f&&!r)return;let c;if(f){let l=await import(new URL(b(f),document.baseURI).href);if(s())return;let w=l.default;_={ClassDef:w,props:H(i)};try{c=new w}finally{_=null}}else c=new o(void 0,H(i));if(n[a]=c,await c.loadLayout(r),s())return;let p=document.createElement("template");p.innerHTML=c.layout??"";let d=p.content.cloneNode(!0);if(i.children.length>0){let l=d.firstElementChild??d;Array.from(i.children).forEach(w=>l.appendChild(w))}c.domNodes=[...d.children],c.routePath=i.getAttribute("path"),i.replaceWith(d),c.domNode=c.domNodes[0],await c.render(),!s()&&(await c?.onLayout?.(),!s()&&await this._startSubView(c))})),!s()&&this.subViews.push(...n.filter(Boolean))}},L=class o{static{h(this,"EventBus")}static#t={};static subscribe(...t){let[e,s,n]=typeof t[0]=="string"?[null,...t]:t;(o.#t[s]??=[]).push({callback:n,owner:e})}static send(t,e){let s=o.#t[t];if(s){o.#t[t]=s.filter(n=>!n.owner?._destroyed);for(let n of[...o.#t[t]])n.callback(e)}}static unsubscribe(t,e){o.#t[t]&&(o.#t[t]=o.#t[t].filter(s=>s.callback!==e))}static unsubscribeAll(t){for(let e of Object.keys(o.#t))o.#t[e]=o.#t[e].filter(s=>s.owner!==t)}static clear(){o.#t={}}},P=class o{static{h(this,"Store")}static#t=null;static singleton(){return o.#t||(o.#t=new o),o.#t}constructor(){this._state={}}get(t){return t.split(".").reduce((e,s)=>e?.[s],this._state)}set(t,e){let s=t.split("."),n=s.pop(),i=s.reduce((a,r)=>(a[r]||(a[r]={}),a[r]),this._state);i[n]=e}clear(){this._state={}}},y=class o extends R{static{h(this,"Router")}static#t=null;static#e=!1;static singleton(){return o.#t}constructor(t){super(),o.#t=this,this.rootNode=t.parentNode,this.routerDomNode=t,this.domNode=t,this.layout=t.innerHTML,this.animation=t.getAttribute("animation")??"none"}async onLayout(){let t=document.createDocumentFragment();for(;this.routerDomNode.firstChild;)t.appendChild(this.routerDomNode.firstChild);this.routerDomNode.replaceWith(t),this.domNode=this.rootNode,this.initRouter()}async _startSubView(t){t?.bindEvents?.()}initRouter(){if(this._generation!==m)return;this.routes={};for(let i of this.subViews)if(i.routePath){let a=i.routePath.slice(1).split("?")[0];this.routes[a]=i}this._navStack=[],this._baseView=null;for(let i of Object.values(this.routes))i._setHidden(!0);let t=Object.keys(this.routes)[0];this._defaultPath=t,this._pathname=location.pathname;let e=window.location.hash.slice(1),s=e.split("?")[0],n=s&&this.routes[s]?e:t;this._idx=history.state?.jopsIdx??0,this._history("replaceState",{...history.state,jopsIdx:this._idx},"#"+n),this._historyListener=()=>this._onHistoryChange(),window.addEventListener("popstate",this._historyListener),window.addEventListener("hashchange",this._historyListener),this._switchTo(n)}_onHistoryChange(){if(this._destroyed)return;if(location.pathname!==this._pathname){S&&V();return}let t=history.state?.jopsIdx;if(t===this._idx)return;if(t===void 0){this._idx+=1,this._history("replaceState",{...history.state,jopsIdx:this._idx},location.href),this._onForward();return}let e=t<this._idx;this._idx=t,e?this._onBack():this._onForward()}_onBack(){if(this._navStack.length>0){let e=this._navStack.pop(),s=this._navStack.at(-1)??this._baseView;e?.onBackPressed?.(),this._animateTransition(e,s,"back").then(()=>e.destroy());return}let t=window.location.hash.slice(1).split("?")[0];if(!this.routes[t]){history.back();return}this._currentView?.onBackPressed?.(),this._switchToHash()}_onForward(){this._closeStack(),this._switchToHash()}_closeStack(){this._navStack.length!==0&&(this._navStack.includes(this._currentView)&&(this._currentView=null),this._navStack.forEach(t=>t.destroy?.()),this._navStack=[],this._baseView=null)}_pauseCurrent(t){this._currentView&&this._currentView!==t&&this._currentView.onPause?.()}_switchToHash(){let t=window.location.hash.slice(1),e=t.split("?")[0];e?this.routes[e]&&this._switchTo(t):this._switchToDefault()}_switchToDefault(){this._history("replaceState",{...history.state,jopsIdx:this._idx},"#"+this._defaultPath),this._switchTo(this._defaultPath)}destroy(){this._destroyed||(this._destroyed=!0,this._historyListener&&(window.removeEventListener("popstate",this._historyListener),window.removeEventListener("hashchange",this._historyListener),this._historyListener=null),this._navStack?.forEach(t=>t.destroy?.()),this._navStack=[],this._baseView=null,this._currentView=null,this.routes={},o.#t===this&&(o.#t=null),super.destroy())}_history(t,e,s){T=!0;try{history[t](e,"",s)}finally{T=!1}}_onHostNavigation(){if(this._destroyed||!this.routes)return;if(location.pathname!==this._pathname){V();return}let t=location.hash.slice(1).split("?")[0],e=history.state?.jopsIdx===void 0;t?e&&this.routes[t]&&(this._idx+=1,this._history("replaceState",{...history.state,jopsIdx:this._idx},location.href),this._onForward()):(e&&(this._idx+=1),this._closeStack(),this._switchToDefault())}async _animateTransition(t,e,s){let n=s==="forward",i=t._roots().length>1||e._roots().length>1;i&&this.animation!=="none"&&!o.#e&&(o.#e=!0,console.info("JOPS: Router animations need single-root views; views with several root elements switch without animation.")),this.animation==="slide"&&!i?await this._slideTransition(t,e,s):this.animation==="fade"&&!i?await this._fadeTransition(t,e):(t._setHidden(!0),e._setHidden(!1),this._currentView=e),n||e?.onResume?.()}async _slideTransition(t,e,s){let n=this.domNode,i=s==="forward",a=n.getBoundingClientRect(),r=t.domNode.getBoundingClientRect(),f=r.left-a.left,c=r.top-a.top,p=r.width,d=n.offsetWidth-f,l=`position:absolute;left:${f}px;top:${c}px;width:${p}px`;n.style.cssText=`position:relative;overflow:hidden;height:${n.offsetHeight}px`,t.domNode.style.cssText=`${l};z-index:${i?1:2}`,t.domNode.hidden=!1,e.domNode.style.cssText=`${l};z-index:${i?2:1}`,e.domNode.hidden=!1;let w=i?e.domNode:t.domNode,J=i?`${d}px`:"0px",U=i?"0px":`${d}px`,E=w.animate([{transform:`translateX(${J})`},{transform:`translateX(${U})`}],{duration:300,easing:"ease",fill:"forwards"});await E.finished,E.cancel(),t.domNode.hidden=!0,t.domNode.style.cssText="",e.domNode.style.cssText="",n.style.cssText="",this._currentView=e}async _fadeTransition(t,e){let s=this.domNode,n=s.getBoundingClientRect(),i=t.domNode.getBoundingClientRect(),a=i.left-n.left,r=i.top-n.top,f=i.width,c=`position:absolute;left:${a}px;top:${r}px;width:${f}px`;s.style.cssText=`position:relative;overflow:hidden;height:${s.offsetHeight}px`,t.domNode.style.cssText=c,t.domNode.hidden=!1,e.domNode.style.cssText=c,e.domNode.hidden=!1;let p=t.domNode.animate([{opacity:1},{opacity:0}],{duration:450,easing:"ease",fill:"forwards"}),d=e.domNode.animate([{opacity:0},{opacity:1}],{duration:450,easing:"ease",fill:"forwards"});await Promise.all([p.finished,d.finished]),p.cancel(),d.cancel(),t.domNode.hidden=!0,t.domNode.style.cssText="",e.domNode.style.cssText="",s.style.cssText="",this._currentView=e}_switchTo(t){let e=t.split("?")[0],s=this.routes[e];s&&this._pauseCurrent(s);for(let[n,i]of Object.entries(this.routes)){let a=n===e;i._setHidden(!a),a&&(this._currentView=i,i?.onResume?.())}}back(){history.back()}canGoBack(){return this._idx>0}getParams(){let t=window.location.hash.split("?")[1];return t?Object.fromEntries(new URLSearchParams(t)):{}}async navigate(t,e){let[s,n]=t.replace(/^\//,"").split("?"),i=s,a=e??this.routes[i]?.constructor;if(!a){console.error(`JOPS Router: no class found for path "${t}"`);return}this._navStack.length===0&&(this._baseView=this._currentView);let r=new a;await r.loadLayout(),r.inflate(),this.domNode.append(...r._roots()),this._history("pushState",{jopsNav:!0,jopsIdx:++this._idx},"#"+i+(n?"?"+n:"")),this._pauseCurrent(r),await r.onLayout?.(),await r.onResume?.(),r.bindEvents?.(),this._navStack.push(r),this._animateTransition(this._currentView,r,"forward")}async replace(t,e){this._closeStack();let[s,n]=t.replace(/^\//,"").split("?"),i=s,a="#"+i+(n?"?"+n:"");if(this.routes[i])this._history("replaceState",{...history.state,jopsIdx:this._idx},a),this._switchTo(i);else if(e){let r=new e;await r.loadLayout(),r.inflate(),this.domNode.append(...r._roots()),this._pauseCurrent(r),await r.onLayout?.(),await r.onResume?.(),r.bindEvents?.(),this._currentView&&this._currentView._setHidden(!0),this._currentView=r,this._history("replaceState",{...history.state,jopsIdx:this._idx},a)}else console.error(`JOPS Router: no instance or class found for path "${t}"`)}},A=class{static{h(this,"Thread")}#t=null;run(t){}onMessage(t){}start(t){if(typeof Worker>"u"){console.warn("JOPS Thread: Web Workers are not supported in this environment.");return}let e=this.run.toString();!e.startsWith("function")&&!e.startsWith("(")&&!e.startsWith("async")&&(e="function "+e);let s=new Blob([`const run = ${e};`,"self.onmessage = (e) => { run(e.data); };"],{type:"application/javascript"}),n=URL.createObjectURL(s);this.#t=new Worker(n),URL.revokeObjectURL(n),this.#t.onmessage=i=>this.onMessage(i),this.#t.postMessage(t)}terminate(){if(!this.#t){console.warn("JOPS Thread: no active worker to terminate.");return}this.#t.terminate(),this.#t=null}};export{L as EventBus,y as Router,P as Store,A as Thread,R as View,G as layout,D as mount,z as raw,V as unmount};
|
|
1
|
+
var W=Object.defineProperty;var h=(o,t)=>W(o,"name",{value:t,configurable:!0});var B=new URL(import.meta.url).origin!==location.origin,L=new Map;function b(o){if(!o)return o;if(B){if(o.startsWith("/"))return"."+o}else if(o.startsWith("./"))return o.slice(1);return o}h(b,"normalizePath");var u=null,S=!1,m=0,k=new Set,_=null,I=new Set(["jops","src","layout","path","jops-event-bind"]);function H(o){let t={};for(let{name:e,value:s}of o.attributes)I.has(e)||e==="type"&&s==="jops"||(t[e.replace(/-([a-z])/g,(n,i)=>i.toUpperCase())]=s);return t}h(H,"tagProps");var j=new Set,O=!1,V=!1,N=!1,C={},$={};function M(){if(!N){N=!0;for(let o of["pushState","replaceState"]){let t=history[o],e=h(function(...s){let n=t.apply(this,s);return V||queueMicrotask(()=>y.singleton()?._onHostNavigation()),n},"wrapper");C[o]=t,$[o]=e,history[o]=e}}}h(M,"installHostHistoryHook");function q(){if(!N)return;let o=!0;for(let t of["pushState","replaceState"])history[t]===$[t]?history[t]=C[t]:o=!1;o&&(N=!1)}h(q,"uninstallHostHistoryHook");async function D({app:o,root:t}){if(!(t instanceof HTMLElement))throw new TypeError("JOPS mount(): root must be an HTMLElement");u=t,M();let e=m,s=await import(new URL(b(o),document.baseURI).href);return e!==m?null:new s.default}h(D,"mount");function T(){m+=1,y.singleton()?.destroy(),k.forEach(o=>o.destroy()),k.clear(),u&&(u.innerHTML=""),u=null,P.singleton().clear(),v.clear(),q()}h(T,"unmount");function K(){document.addEventListener("deviceready",()=>{let o=window.Capacitor;o?.isNativePlatform?.()&&o.getPlatform()==="android"&&!o.isPluginAvailable("App")&&console.warn("JOPS: install @capacitor/app so the Android back button navigates instead of closing the app.");let t=0;document.addEventListener("backbutton",()=>{let e=Date.now();if(e-t<300)return;t=e;let s=y.singleton();s?.canGoBack()?s.back():!s&&window.navigation?.canGoBack?history.back():navigator.app.exitApp()})},{once:!0})}h(K,"installNativeBackButton");var x=class{static{h(this,"RawHTML")}constructor(t){this.html=t}},z=h(o=>new x(o),"raw");function G(o,...t){return o.reduce((e,s,n)=>{if(n>=t.length)return e+s;let i=t[n];return i==null?e+s:i instanceof x?e+s+i.html:e+s+String(i).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")},"")}h(G,"layout");var g=class o{static{h(this,"Element")}constructor(t,e){this.id=e,this.inert=t.length===0,this.element=null;for(let s of t)if(this.element=s.id===e?s:s.querySelector(`[id="${CSS.escape(e)}"]`),this.element)break}static from(t,e){let s=new o([],e);return s.inert=!1,s.element=t,s}set(t){return this.element?(this.element.innerHTML=t,this):this.#t("set")}attr(t,e){return this.element?(e==null?this.element.removeAttribute(t):this.element.setAttribute(t,String(e)),this):this.#t("attr")}value(){return this.element?this.element.innerHTML:(this.#t("value"),"")}#t(t){return this.inert||console.warn(`JOPS: ${t}() on #${this.id}: no such element in the view`),this}},R=class o{static{h(this,"View")}#t=!1;#e=!1;#o=!1;#s=null;#i;constructor(t,e){this._generation=m;let s=_&&_.ClassDef===new.target?_.props:null;this.#i=Object.freeze({...e??s??{}}),this.loadLayout(t),this.subViews=[]}get props(){return this.#i}async loadLayout(t){if(this.#s)return this.#s;if(!(!t||this.#o))return this.#s=this.#n(t).finally(()=>{this.#o=this.layout!=null,this.#s=null}),this.#s}async#n(t){if(t.endsWith(".js"))try{let e=await import(new URL(b(t),document.baseURI).href);this.layout=e.default}catch(e){console.error("JOPS: failed to load layout:",t,e)}else if(t.endsWith(".html")||t.endsWith(".htm"))try{let e=new URL(b(t),document.baseURI).href;if(L.has(e))this.layout=L.get(e);else{let s=await fetch(e);if(!s.ok)throw new Error(`HTTP ${s.status}`);let n=await s.text();L.set(e,n),this.layout=n}}catch(e){console.error("JOPS: failed to load layout:",t,e)}else this.layout=t}setStyle(t){if(t){if(this.style=t,S){O||(O=!0,console.info("JOPS: setStyle() is ignored in hosted mode; the host provides the CSS."));return}if(t.endsWith(".css")){if(document.querySelector(`link[href="${CSS.escape(t)}"]`))return;let e=document.createElement("link");e.rel="stylesheet",e.href=t,document.head.appendChild(e)}else{if(j.has(t))return;j.add(t);let e=document.createElement("style");e.setAttribute("data-jops",""),e.textContent=t,document.head.appendChild(e)}}}get(t){return new g(this._roots(),t)}_roots(){return this.domNodes??(this.domNode?[this.domNode]:[])}_setHidden(t){for(let e of this._roots())e.hidden=t}bindEvents(){let t=this._roots().flatMap(e=>[...e.hasAttribute("jops-event-bind")||e.getAttribute("type")==="jops-event-bind"?[e]:[],...e.querySelectorAll("[jops-event-bind], [type='jops-event-bind']")]);for(let e of t)for(let s of e.attributes){if(!s.name.startsWith("on"))continue;let n=s.value.trim();typeof this[n]=="function"&&(e[s.name]=i=>this[n](i))}}inflate(){if(!this.layout||this.#t)return;this.#t=!0;let t=document.createElement("template");t.innerHTML=this.layout;let e=t.content.cloneNode(!0);this.domNodes=[...e.children],this.domNode=this.domNodes[0]}async addSubView(t,e){if(!this.domNode)return;if(e._destroyed){console.warn("JOPS: addSubView(): this view was destroyed; create a new instance");return}this.subViews.push(e),e.domNode||(await e.loadLayout(),e.inflate());let s=this.domNode?.querySelector(`#${t}`);if(!e.domNode||!s){this.subViews=this.subViews.filter(n=>n!==e),this.domNode&&console.warn(e.domNode?`JOPS: addSubView(): no element #${t} in the view`:"JOPS: addSubView(): the view has no layout");return}s.append(...e._roots()),await e?.onLayout?.(),await e?.onResume?.(),e?.bindEvents?.()}hide(){for(let t of this._roots())t.style.display="none"}show(){for(let t of this._roots())t.style.display=""}find(t){if(typeof t=="string")return this._roots().flatMap(s=>[...s.id===t?[s]:[],...s.querySelectorAll(`[id="${CSS.escape(t)}"]`)]).map(s=>g.from(s,t));let e=[];for(let s of this.subViews)t(s)&&e.push(s),e.push(...s.find?.(t)??[]);return e}destroy(){this._destroyed=!0,v.unsubscribeAll(this),this.subViews.forEach(e=>e.destroy?.()),this.subViews=[],this._borrowedDomNode||this.domNode&&(this.domNode===u||this.domNode===document.body)?this.domNode.innerHTML="":this._roots().forEach(e=>e.remove()),this.domNode=null,this.domNodes=null,this.layout=null}removeSubView(t){t.destroy?.(),this.subViews=this.subViews.filter(e=>e!==t)}removeSubViews(t){for(let e of[...this.subViews])(!t||t(e))&&this.removeSubView(e)}async insertSubView(t,e){if(!this.domNode)return;if(t._destroyed){console.warn("JOPS: insertSubView(): this view was destroyed; create a new instance");return}if(t.domNode||(await t.loadLayout(),t.inflate()),!this.domNode)return;if(!t.domNode){console.warn("JOPS: insertSubView(): the view has no layout");return}let s=this.domNode.children[e]??null;for(let n of t._roots())this.domNode.insertBefore(n,s);this.subViews.splice(e,0,t),await t?.onLayout?.(),await t?.onResume?.(),t?.bindEvents?.()}async _startSubView(t){await t?.onResume?.(),t?.bindEvents?.()}async render(){if(!this.layout||this.#e||this._generation!==m)return;let t=!this.domNode;if(t&&S&&!u)return;this.#e=!0;let e=this.domNode??u??document.body;t&&(e.innerHTML=this.layout,k.add(this),this.domNode=e,this._borrowedDomNode=!0);let s=h(()=>this._generation!==m,"ended"),n=Array.from(e.querySelectorAll("[jops], [type='jops']")).filter(i=>i.tagName==="ROUTER"||!i.closest("router")||i.closest("router")===e);if(n.length>0&&await this.#r(n,e,s),!s()&&t){if(await this.onLayout?.(),s()||(await this.onResume?.(),s()))return;this.bindEvents()}}async#r(t,e,s){let n=new Array(t.length);await Promise.all(t.map(async(i,a)=>{if(i.tagName==="ROUTER"){let l=new y(i);if(await l.render(),s())return;await l.onLayout();return}let r=i.getAttribute("layout"),f=i.getAttribute("src");if(!f&&!r)return;let c;if(f){let l=await import(new URL(b(f),document.baseURI).href);if(s())return;let w=l.default;_={ClassDef:w,props:H(i)};try{c=new w}finally{_=null}}else c=new o(void 0,H(i));if(n[a]=c,await c.loadLayout(r),s())return;let p=document.createElement("template");p.innerHTML=c.layout??"";let d=p.content.cloneNode(!0);if(i.children.length>0){let l=d.firstElementChild??d;Array.from(i.children).forEach(w=>l.appendChild(w))}c.domNodes=[...d.children],c.routePath=i.getAttribute("path"),i.replaceWith(d),c.domNode=c.domNodes[0],await c.render(),!s()&&(await c?.onLayout?.(),!s()&&await this._startSubView(c))})),!s()&&this.subViews.push(...n.filter(Boolean))}},v=class o{static{h(this,"EventBus")}static#t={};static subscribe(...t){let[e,s,n]=typeof t[0]=="string"?[null,...t]:t;(o.#t[s]??=[]).push({callback:n,owner:e})}static send(t,e){let s=o.#t[t];if(s){o.#t[t]=s.filter(n=>!n.owner?._destroyed);for(let n of[...o.#t[t]])n.callback(e)}}static unsubscribe(t,e){o.#t[t]&&(o.#t[t]=o.#t[t].filter(s=>s.callback!==e))}static unsubscribeAll(t){for(let e of Object.keys(o.#t))o.#t[e]=o.#t[e].filter(s=>s.owner!==t)}static clear(){o.#t={}}},P=class o{static{h(this,"Store")}static#t=null;static singleton(){return o.#t||(o.#t=new o),o.#t}constructor(){this._state={}}get(t){return t.split(".").reduce((e,s)=>e?.[s],this._state)}set(t,e){let s=t.split("."),n=s.pop(),i=s.reduce((a,r)=>(a[r]||(a[r]={}),a[r]),this._state);i[n]=e}clear(){this._state={}}},y=class o extends R{static{h(this,"Router")}static#t=null;static#e=!1;static singleton(){return o.#t}constructor(t){super(),o.#t=this,this.rootNode=t.parentNode,this.routerDomNode=t,this.domNode=t,this.layout=t.innerHTML,this.animation=t.getAttribute("animation")??"none"}async onLayout(){let t=document.createDocumentFragment();for(;this.routerDomNode.firstChild;)t.appendChild(this.routerDomNode.firstChild);this.routerDomNode.replaceWith(t),this.domNode=this.rootNode,this.initRouter()}async _startSubView(t){t?.bindEvents?.()}initRouter(){if(this._generation!==m)return;this.routes={};for(let i of this.subViews)if(i.routePath){let a=i.routePath.slice(1).split("?")[0];this.routes[a]=i}this._navStack=[],this._baseView=null;for(let i of Object.values(this.routes))i._setHidden(!0);let t=Object.keys(this.routes)[0];this._defaultPath=t,this._pathname=location.pathname;let e=window.location.hash.slice(1),s=e.split("?")[0],n=s&&this.routes[s]?e:t;this._idx=history.state?.jopsIdx??0,this._history("replaceState",{...history.state,jopsIdx:this._idx},"#"+n),this._historyListener=()=>this._onHistoryChange(),window.addEventListener("popstate",this._historyListener),window.addEventListener("hashchange",this._historyListener),this._switchTo(n)}_onHistoryChange(){if(this._destroyed)return;if(location.pathname!==this._pathname){S&&T();return}let t=history.state?.jopsIdx;if(t===this._idx)return;if(t===void 0){this._idx+=1,this._history("replaceState",{...history.state,jopsIdx:this._idx},location.href),this._onForward();return}let e=t<this._idx;this._idx=t,e?this._onBack():this._onForward()}_onBack(){if(this._navStack.length>0){let e=this._navStack.pop(),s=this._navStack.at(-1)??this._baseView;e?.onBackPressed?.(),this._animateTransition(e,s,"back").then(()=>e.destroy());return}let t=window.location.hash.slice(1).split("?")[0];if(!this.routes[t]){history.back();return}this._currentView?.onBackPressed?.(),this._switchToHash()}_onForward(){this._closeStack(),this._switchToHash()}_closeStack(){this._navStack.length!==0&&(this._navStack.includes(this._currentView)&&(this._currentView=null),this._navStack.forEach(t=>t.destroy?.()),this._navStack=[],this._baseView=null)}_pauseCurrent(t){this._currentView&&this._currentView!==t&&this._currentView.onPause?.()}_switchToHash(){let t=window.location.hash.slice(1),e=t.split("?")[0];e?this.routes[e]&&this._switchTo(t):this._switchToDefault()}_switchToDefault(){this._history("replaceState",{...history.state,jopsIdx:this._idx},"#"+this._defaultPath),this._switchTo(this._defaultPath)}removeSubViews(t){console.warn("JOPS: removeSubViews() is not supported on a Router")}destroy(){this._destroyed||(this._destroyed=!0,this._historyListener&&(window.removeEventListener("popstate",this._historyListener),window.removeEventListener("hashchange",this._historyListener),this._historyListener=null),this._navStack?.forEach(t=>t.destroy?.()),this._navStack=[],this._baseView=null,this._currentView=null,this.routes={},o.#t===this&&(o.#t=null),super.destroy())}_history(t,e,s){V=!0;try{history[t](e,"",s)}finally{V=!1}}_onHostNavigation(){if(this._destroyed||!this.routes)return;if(location.pathname!==this._pathname){T();return}let t=location.hash.slice(1).split("?")[0],e=history.state?.jopsIdx===void 0;t?e&&this.routes[t]&&(this._idx+=1,this._history("replaceState",{...history.state,jopsIdx:this._idx},location.href),this._onForward()):(e&&(this._idx+=1),this._closeStack(),this._switchToDefault())}async _animateTransition(t,e,s){let n=s==="forward",i=t._roots().length>1||e._roots().length>1;i&&this.animation!=="none"&&!o.#e&&(o.#e=!0,console.info("JOPS: Router animations need single-root views; views with several root elements switch without animation.")),this.animation==="slide"&&!i?await this._slideTransition(t,e,s):this.animation==="fade"&&!i?await this._fadeTransition(t,e):(t._setHidden(!0),e._setHidden(!1),this._currentView=e),n||e?.onResume?.()}async _slideTransition(t,e,s){let n=this.domNode,i=s==="forward",a=n.getBoundingClientRect(),r=t.domNode.getBoundingClientRect(),f=r.left-a.left,c=r.top-a.top,p=r.width,d=n.offsetWidth-f,l=`position:absolute;left:${f}px;top:${c}px;width:${p}px`;n.style.cssText=`position:relative;overflow:hidden;height:${n.offsetHeight}px`,t.domNode.style.cssText=`${l};z-index:${i?1:2}`,t.domNode.hidden=!1,e.domNode.style.cssText=`${l};z-index:${i?2:1}`,e.domNode.hidden=!1;let w=i?e.domNode:t.domNode,J=i?`${d}px`:"0px",U=i?"0px":`${d}px`,E=w.animate([{transform:`translateX(${J})`},{transform:`translateX(${U})`}],{duration:300,easing:"ease",fill:"forwards"});await E.finished,E.cancel(),t.domNode.hidden=!0,t.domNode.style.cssText="",e.domNode.style.cssText="",n.style.cssText="",this._currentView=e}async _fadeTransition(t,e){let s=this.domNode,n=s.getBoundingClientRect(),i=t.domNode.getBoundingClientRect(),a=i.left-n.left,r=i.top-n.top,f=i.width,c=`position:absolute;left:${a}px;top:${r}px;width:${f}px`;s.style.cssText=`position:relative;overflow:hidden;height:${s.offsetHeight}px`,t.domNode.style.cssText=c,t.domNode.hidden=!1,e.domNode.style.cssText=c,e.domNode.hidden=!1;let p=t.domNode.animate([{opacity:1},{opacity:0}],{duration:450,easing:"ease",fill:"forwards"}),d=e.domNode.animate([{opacity:0},{opacity:1}],{duration:450,easing:"ease",fill:"forwards"});await Promise.all([p.finished,d.finished]),p.cancel(),d.cancel(),t.domNode.hidden=!0,t.domNode.style.cssText="",e.domNode.style.cssText="",s.style.cssText="",this._currentView=e}_switchTo(t){let e=t.split("?")[0],s=this.routes[e];s&&this._pauseCurrent(s);for(let[n,i]of Object.entries(this.routes)){let a=n===e;i._setHidden(!a),a&&(this._currentView=i,i?.onResume?.())}}back(){history.back()}canGoBack(){return this._idx>0}getParams(){let t=window.location.hash.split("?")[1];return t?Object.fromEntries(new URLSearchParams(t)):{}}async navigate(t,e){let[s,n]=t.replace(/^\//,"").split("?"),i=s,a=e??this.routes[i]?.constructor;if(!a){console.error(`JOPS Router: no class found for path "${t}"`);return}this._navStack.length===0&&(this._baseView=this._currentView);let r=new a;await r.loadLayout(),r.inflate(),this.domNode.append(...r._roots()),this._history("pushState",{jopsNav:!0,jopsIdx:++this._idx},"#"+i+(n?"?"+n:"")),this._pauseCurrent(r),await r.onLayout?.(),await r.onResume?.(),r.bindEvents?.(),this._navStack.push(r),this._animateTransition(this._currentView,r,"forward")}async replace(t,e){this._closeStack();let[s,n]=t.replace(/^\//,"").split("?"),i=s,a="#"+i+(n?"?"+n:"");if(this.routes[i])this._history("replaceState",{...history.state,jopsIdx:this._idx},a),this._switchTo(i);else if(e){let r=new e;await r.loadLayout(),r.inflate(),this.domNode.append(...r._roots()),this._pauseCurrent(r),await r.onLayout?.(),await r.onResume?.(),r.bindEvents?.(),this._currentView&&this._currentView._setHidden(!0),this._currentView=r,this._history("replaceState",{...history.state,jopsIdx:this._idx},a)}else console.error(`JOPS Router: no instance or class found for path "${t}"`)}},A=class{static{h(this,"Thread")}#t=null;run(t){}onMessage(t){}start(t){if(typeof Worker>"u"){console.warn("JOPS Thread: Web Workers are not supported in this environment.");return}let e=this.run.toString();!e.startsWith("function")&&!e.startsWith("(")&&!e.startsWith("async")&&(e="function "+e);let s=new Blob([`const run = ${e};`,"self.onmessage = (e) => { run(e.data); };"],{type:"application/javascript"}),n=URL.createObjectURL(s);this.#t=new Worker(n),URL.revokeObjectURL(n),this.#t.onmessage=i=>this.onMessage(i),this.#t.postMessage(t)}terminate(){if(!this.#t){console.warn("JOPS Thread: no active worker to terminate.");return}this.#t.terminate(),this.#t=null}};(()=>{if(console.log("JOPS initialized"),Array.from(document.querySelectorAll("script[src]")).find(s=>s.src===import.meta.url)?.dataset.host!==void 0){S=!0,window.JOPS={mount:D,unmount:T},window.dispatchEvent(new Event("jops:ready"));return}let t=document.querySelector("script[data-app]"),e=t?.dataset.app??"./src/App.js";t?.dataset.root&&(u=document.getElementById(t.dataset.root),u||console.warn(`JOPS: data-root element #${t.dataset.root} not found, rendering into <body>`)),import(new URL(b(e),document.baseURI).href).then(s=>new s.default).catch(s=>console.error("JOPS: App.js not found or failed to load",s)),t?.dataset.nativeBack!=="off"&&K()})();export{v as EventBus,y as Router,P as Store,A as Thread,R as View,G as layout,D as mount,z as raw,T as unmount};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jops-core",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "A no-bundler, OOP-first SPA framework with Android-style view composition",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
],
|
|
28
28
|
"scripts": {
|
|
29
29
|
"build:nextjs": "esbuild jops-nextjs/JopsApp.tsx --format=esm --jsx=automatic --target=es2022 --outdir=lib/nextjs && tsc -p tsconfig.nextjs.json",
|
|
30
|
-
"build": "esbuild src/jops-core.js --bundle --format=esm --minify --keep-names --outfile=lib/jops-core.min.js && tsc -p tsconfig.types.json && npm run build:nextjs && npm pack"
|
|
30
|
+
"build": "esbuild src/jops-core.js --bundle --format=esm --minify --keep-names --outfile=lib/jops-core.min.js && tsc -p tsconfig.types.json && npm run build:nextjs && node scripts/build-wp-plugin.mjs && npm pack"
|
|
31
31
|
},
|
|
32
32
|
"homepage": "https://cdn.ui-db.com",
|
|
33
33
|
"repository": {
|
|
@@ -67,7 +67,8 @@
|
|
|
67
67
|
"nextjs",
|
|
68
68
|
"next-js",
|
|
69
69
|
"next.js",
|
|
70
|
-
"ssr"
|
|
70
|
+
"ssr",
|
|
71
|
+
"wordpress"
|
|
71
72
|
],
|
|
72
73
|
"author": "arik@ui-db.com",
|
|
73
74
|
"license": "MIT",
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
/**
|
|
3
|
+
* Plugin Name: JOPS
|
|
4
|
+
* Plugin URI: https://github.com/uidb-dev/jops-core
|
|
5
|
+
* Description: Embeds a JOPS single-page app in a page with the [jops] shortcode, e.g. in Elementor's Shortcode widget: [jops app="/jops-spa/src/App.js" id="jops-spa-root"]
|
|
6
|
+
* Version: {{VERSION}}
|
|
7
|
+
* Requires at least: 6.5
|
|
8
|
+
* Requires PHP: 7.4
|
|
9
|
+
* Author: UIDB
|
|
10
|
+
* Author URI: https://cdn.ui-db.com
|
|
11
|
+
* License: MIT
|
|
12
|
+
* License URI: https://opensource.org/licenses/MIT
|
|
13
|
+
* Text Domain: jops
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
if ( ! defined( 'ABSPATH' ) ) {
|
|
17
|
+
exit;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* [jops app="/jops-spa/src/App.js" id="jops-spa-root" class="" lib=""]
|
|
22
|
+
*
|
|
23
|
+
* Outputs the same block as the Elementor HTML-widget snippet: the "jops-core"
|
|
24
|
+
* import map, the library (hosted mode, data-host), the app's root <div> and an
|
|
25
|
+
* inline module that mounts the app. Module scripts run in document order after
|
|
26
|
+
* the page is parsed, so the library has fully loaded when the mount script
|
|
27
|
+
* runs. The mount script still waits for import(lib), which returns the module
|
|
28
|
+
* the library tag loads once it has fully run (or fails if it cannot load), so
|
|
29
|
+
* it also works when something inserts the block with JavaScript (Elementor
|
|
30
|
+
* popups, AJAX content) or replays its scripts later (script-delay plugins). One app per page: a second [jops] logs a warning and is not
|
|
31
|
+
* started. In the Elementor editor the shortcode outputs only a placeholder:
|
|
32
|
+
* the app runs on the published page.
|
|
33
|
+
*
|
|
34
|
+
* - app: root-based path of the app's entry module (required).
|
|
35
|
+
* - id: id of the root <div> (default "jops-root").
|
|
36
|
+
* - class: class of the root <div>, e.g. to scope the app's CSS.
|
|
37
|
+
* - lib: URL of jops-core.min.js (default: the copy in this plugin's lib/).
|
|
38
|
+
* Must be the same URL on the whole page, or the browser loads two
|
|
39
|
+
* jops-core instances.
|
|
40
|
+
*
|
|
41
|
+
* @param array|string $atts Shortcode attributes.
|
|
42
|
+
* @return string
|
|
43
|
+
*/
|
|
44
|
+
function jops_shortcode( $atts ) {
|
|
45
|
+
$atts = shortcode_atts(
|
|
46
|
+
array(
|
|
47
|
+
'app' => '',
|
|
48
|
+
'id' => 'jops-root',
|
|
49
|
+
'class' => '',
|
|
50
|
+
'lib' => '',
|
|
51
|
+
),
|
|
52
|
+
$atts,
|
|
53
|
+
'jops'
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
$app = trim( $atts['app'] );
|
|
57
|
+
if ( '' === $app || '/' !== $app[0] ) {
|
|
58
|
+
return "\n<!-- JOPS: the app attribute must be a root-based path, e.g. [jops app=\"/jops-spa/src/App.js\"] -->\n";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
$lib = '' !== trim( $atts['lib'] ) ? esc_url_raw( trim( $atts['lib'] ) ) : plugins_url( 'lib/jops-core.min.js', __FILE__ );
|
|
62
|
+
$config = array(
|
|
63
|
+
'app' => $app,
|
|
64
|
+
'root' => $atts['id'],
|
|
65
|
+
'lib' => $lib,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
if ( jops_in_elementor_editor() ) {
|
|
69
|
+
return sprintf(
|
|
70
|
+
'<div style="padding: 1em; border: 1px dashed currentColor; opacity: 0.7">JOPS app %s (runs on the published page)</div>',
|
|
71
|
+
esc_html( $app )
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
$json = JSON_HEX_TAG | JSON_HEX_AMP | JSON_UNESCAPED_SLASHES;
|
|
76
|
+
$map = wp_json_encode( array( 'imports' => array( 'jops-core' => $lib ) ), $json );
|
|
77
|
+
$div = sprintf(
|
|
78
|
+
'<div id="%s"%s data-jops-root style="position: relative"></div>',
|
|
79
|
+
esc_attr( $atts['id'] ),
|
|
80
|
+
'' !== $atts['class'] ? ' class="' . esc_attr( $atts['class'] ) . '"' : ''
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
return '
|
|
84
|
+
<script type="importmap">' . $map . '</script>
|
|
85
|
+
<script type="module" src="' . esc_url( $lib ) . '" data-host="wordpress"></script>
|
|
86
|
+
' . $div . '
|
|
87
|
+
<script type="module">
|
|
88
|
+
const c = ' . wp_json_encode( $config, $json ) . ';
|
|
89
|
+
const root = document.getElementById(c.root);
|
|
90
|
+
const fail = () => console.error("JOPS: could not load " + c.lib + "; #" + c.root + " was not started.");
|
|
91
|
+
const start = () => {
|
|
92
|
+
if (!window.JOPS) return fail();
|
|
93
|
+
if (document.querySelector("[data-jops-mounted]")) {
|
|
94
|
+
console.warn("JOPS: one app per page; #" + c.root + " was not started.");
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
root.setAttribute("data-jops-mounted", "");
|
|
98
|
+
window.JOPS.mount({ app: c.app, root });
|
|
99
|
+
};
|
|
100
|
+
// Only wait for the library once its tag is in the page: import() alone would
|
|
101
|
+
// load jops-core without data-host (standalone mode).
|
|
102
|
+
if (document.querySelector("script[data-host]")) import(c.lib).then(start, fail);
|
|
103
|
+
else fail();
|
|
104
|
+
</script>
|
|
105
|
+
';
|
|
106
|
+
}
|
|
107
|
+
add_shortcode( 'jops', 'jops_shortcode' );
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Whether the shortcode is rendered by the Elementor editor: a widget rendered
|
|
111
|
+
* while editing, or the editor's preview iframe.
|
|
112
|
+
*
|
|
113
|
+
* @return bool
|
|
114
|
+
*/
|
|
115
|
+
function jops_in_elementor_editor() {
|
|
116
|
+
if ( ! did_action( 'elementor/loaded' ) || ! class_exists( '\\Elementor\\Plugin' ) ) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
$elementor = \Elementor\Plugin::$instance;
|
|
120
|
+
return ( isset( $elementor->editor ) && $elementor->editor->is_edit_mode() )
|
|
121
|
+
|| ( isset( $elementor->preview ) && $elementor->preview->is_preview_mode() );
|
|
122
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
=== JOPS ===
|
|
2
|
+
Contributors: uidb
|
|
3
|
+
Tags: spa, javascript, elementor, shortcode, embed
|
|
4
|
+
Requires at least: 6.5
|
|
5
|
+
Requires PHP: 7.4
|
|
6
|
+
Stable tag: {{VERSION}}
|
|
7
|
+
License: MIT
|
|
8
|
+
License URI: https://opensource.org/licenses/MIT
|
|
9
|
+
|
|
10
|
+
Embeds a JOPS single-page app in a WordPress page with the [jops] shortcode.
|
|
11
|
+
|
|
12
|
+
== Description ==
|
|
13
|
+
|
|
14
|
+
JOPS (jops-core) is a no-bundler, class-based SPA framework. This plugin renders a JOPS app inside a WordPress page, next to the theme and the page's other content. It includes the jops-core library (lib/jops-core.min.js), so the page needs nothing else.
|
|
15
|
+
|
|
16
|
+
== Installation ==
|
|
17
|
+
|
|
18
|
+
Without npm: download jops-wordpress.zip (https://github.com/uidb-dev/jops-core/releases/latest/download/jops-wordpress.zip), then Plugins > Add New Plugin > Upload Plugin > choose the zip > Install Now. To update, upload the newer zip the same way and choose "Replace current with uploaded".
|
|
19
|
+
With npm: npx jops-core init --wp (installs the plugin into the WordPress project).
|
|
20
|
+
|
|
21
|
+
1. Activate the plugin (Plugins > Installed Plugins > JOPS > Activate).
|
|
22
|
+
2. Put your JOPS app folder in the WordPress root directory (next to wp-config.php), e.g. <root>/jops-spa/.
|
|
23
|
+
3. Use root-based paths everywhere in the app (/jops-spa/src/..., layouts, images, stylesheets). If WordPress runs in a subfolder (example.com/blog/), include it: /blog/jops-spa/...
|
|
24
|
+
4. Edit the page with Elementor, add a Shortcode widget (General > Shortcode) and enter:
|
|
25
|
+
|
|
26
|
+
[jops app="/jops-spa/src/App.js" id="jops-spa-root"]
|
|
27
|
+
|
|
28
|
+
Any editor that runs shortcodes works the same way (block editor: Shortcode block).
|
|
29
|
+
|
|
30
|
+
== Shortcode attributes ==
|
|
31
|
+
|
|
32
|
+
* app (required): root-based path of the app's entry module (its default export is the App class).
|
|
33
|
+
* id: id of the app's root element (default: jops-root).
|
|
34
|
+
* class: class of the app's root element; use it to scope the app's CSS.
|
|
35
|
+
* lib: URL of jops-core.min.js (default: this plugin's copy). Use one URL per page.
|
|
36
|
+
|
|
37
|
+
== Notes ==
|
|
38
|
+
|
|
39
|
+
* One JOPS app per page.
|
|
40
|
+
* Do not combine the shortcode with a hand-written JOPS import map on the same page: the browser would load two copies of jops-core.
|
|
41
|
+
* The app's CSS applies to the whole page: scope it under the root element's class.
|