jops-core 1.1.0 → 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 +124 -25
- 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,14 @@ 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.
|
|
59
|
+
Replace `@latest` with a specific version (e.g. `@1.1.2`) to pin. No build step, no npm, no bundler.
|
|
60
|
+
|
|
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:
|
|
62
|
+
|
|
63
|
+
```html
|
|
64
|
+
<div id="app"></div>
|
|
65
|
+
<script type="module" src="https://cdn.jsdelivr.net/npm/jops-core@latest/lib/jops-core.min.js" data-app="./src/App.js" data-root="app"></script>
|
|
66
|
+
```
|
|
58
67
|
|
|
59
68
|
> **Note:** ES modules require an HTTP server — open your project with `npx serve` (or any static server) rather than double-clicking `index.html`. Browsers block `import` on `file://` URLs.
|
|
60
69
|
|
|
@@ -378,42 +387,130 @@ The root `<div>` has `position: relative`, which the Router's slide and fade ani
|
|
|
378
387
|
- **Next.js owns the CSS.** `setStyle()` has no effect inside Next.js (a console message says so once). Tailwind CSS v4 also scans the files in `public/`, so Tailwind classes in JOPS layouts and views are generated by the Next.js build. Write class names as complete literal strings (`"bg-sky-50"`, never `` `bg-${color}-50` ``). Put other CSS in `globals.css` or a CSS file imported by the page, scoped under the app's root class (`.my-app table td { … }`), since it applies to the whole site.
|
|
379
388
|
- **Backend calls:** `fetch("/api/…")` to Route Handlers on the same origin; the session cookie is sent automatically. A JOPS app can't call Server Actions.
|
|
380
389
|
- **One JOPS app per page.** `Router`, `Store` and `EventBus` are page-wide singletons.
|
|
390
|
+
- **Always use `JopsApp` inside a Next.js page.** Don't add the jops-core `<script>` or an import map to a page or layout yourself: React doesn't run scripts it renders on soft navigation, and nothing would end the app when the page is left.
|
|
391
|
+
|
|
392
|
+
### Deploying
|
|
393
|
+
|
|
394
|
+
The result is a standard Next.js app: `npm run build` produces `.next/`, and it runs on a Node.js server when it uses server features such as Route Handlers. Deploy it like any Next.js app (see the [Next.js deployment docs](https://nextjs.org/docs/app/getting-started/deploying)). Points specific to JOPS:
|
|
395
|
+
|
|
396
|
+
- **Deploy `public/` with the app**: the whole JOPS app and the library live there. With `output: "standalone"`, copy `public/` (and `.next/static/`) into `.next/standalone/` yourself.
|
|
397
|
+
- **Caching:** files in `public/` are not content-hashed, so a CDN or the browser may keep serving an old `jops-core.min.js` or view after a deploy. Use short cache headers for those paths, or versioned paths.
|
|
398
|
+
- **Library version:** run `npx jops-core update` after updating the package and before `npm run build`.
|
|
399
|
+
|
|
400
|
+
---
|
|
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.
|
|
381
444
|
|
|
382
|
-
|
|
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)).
|
|
383
455
|
|
|
384
|
-
|
|
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)):
|
|
385
457
|
|
|
386
458
|
```html
|
|
387
|
-
<script type="importmap">
|
|
388
|
-
|
|
389
|
-
|
|
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>
|
|
390
465
|
<script type="module">
|
|
391
|
-
const start = () => window.JOPS.mount({ app: "/
|
|
466
|
+
const start = () => window.JOPS.mount({ app: "/jops-spa/src/App.js", root: document.getElementById("jops-spa-root") });
|
|
392
467
|
window.JOPS ? start() : window.addEventListener("jops:ready", start, { once: true });
|
|
393
468
|
</script>
|
|
394
469
|
```
|
|
395
470
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
+
|
|
486
|
+
## Embedding JOPS in Other Pages (Hosted Mode)
|
|
400
487
|
|
|
401
|
-
|
|
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)).
|
|
402
489
|
|
|
403
|
-
|
|
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.
|
|
404
491
|
|
|
405
492
|
```html
|
|
406
|
-
<
|
|
407
|
-
|
|
408
|
-
|
|
493
|
+
<script type="importmap">
|
|
494
|
+
{ "imports": { "jops-core": "https://cdn.jsdelivr.net/npm/jops-core@latest/lib/jops-core.min.js" } }
|
|
495
|
+
</script>
|
|
496
|
+
<script type="module" src="https://cdn.jsdelivr.net/npm/jops-core@latest/lib/jops-core.min.js" data-host></script>
|
|
409
497
|
|
|
410
|
-
|
|
498
|
+
<div id="app" style="position: relative"></div>
|
|
499
|
+
<script type="module">
|
|
500
|
+
const start = () => window.JOPS.mount({ app: "./my-app/App.js", root: document.getElementById("app") });
|
|
501
|
+
window.JOPS ? start() : window.addEventListener("jops:ready", start, { once: true });
|
|
502
|
+
</script>
|
|
503
|
+
```
|
|
411
504
|
|
|
412
|
-
|
|
505
|
+
| Function | Description |
|
|
506
|
+
| --- | --- |
|
|
507
|
+
| `mount({ app, root })` | Imports `app` and creates its default export; the root view renders into `root` (an `HTMLElement`) instead of `<body>`. Returns a promise of the App instance |
|
|
508
|
+
| `unmount()` | Ends the app completely: destroys the Router and every view, empties `root`, clears `Store` and `EventBus`, and restores the history methods JOPS wrapped. Safe to call more than once; `mount()` can start an app again afterwards |
|
|
413
509
|
|
|
414
|
-
- **
|
|
415
|
-
- **
|
|
416
|
-
-
|
|
510
|
+
- **Paths depend on where the library comes from.** With the library from the CDN (as above), JOPS resolves paths relative to the **hosting page**: `./my-app/App.js` on `example.com/admin/users` means `example.com/admin/my-app/App.js`, and root-absolute `/my-app/...` paths are turned page-relative too. With a vendored library on the same origin (`/lib/jops-core.min.js` in both the import map and `src`), write root-absolute paths (`/my-app/App.js`, `src="/my-app/Users.js"`), which work on every page.
|
|
511
|
+
- **Ending the app:** a full page load ends it anyway. A host that removes the app's element without a page load, or navigates with `history.pushState()`, calls `unmount()`; when history moves to another path the Router also ends the app by itself.
|
|
512
|
+
- `mount` and `unmount` are also exported by the module (`import { mount, unmount } from "jops-core"`) for modules running on the same page.
|
|
513
|
+
- In hosted mode `setStyle()` has no effect (the host page provides the CSS) and the Android back button integration is off.
|
|
417
514
|
|
|
418
515
|
---
|
|
419
516
|
|
|
@@ -482,6 +579,7 @@ Optional hooks, called by the Router:
|
|
|
482
579
|
| `addSubView(containerId, view)` | Appends `view` into the child element with `id="containerId"`, runs its full lifecycle |
|
|
483
580
|
| `insertSubView(view, index)` | Inserts `view` at position `index` within `this.domNode`, runs its full lifecycle |
|
|
484
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) |
|
|
485
583
|
| `show()` | Shows the view again (removes the inline `display` override) |
|
|
486
584
|
| `hide()` | Hides the view (`display: none`) |
|
|
487
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 |
|
|
@@ -569,7 +667,7 @@ Returned by `view.get(id)` (and, as an array, by `view.find(id)`). Wraps a singl
|
|
|
569
667
|
|
|
570
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.
|
|
571
669
|
|
|
572
|
-
> `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`.
|
|
573
671
|
|
|
574
672
|
```js
|
|
575
673
|
this.get("username").set("Alice");
|
|
@@ -819,7 +917,7 @@ Prefer the owned form inside views. A global subscription made by a view keeps t
|
|
|
819
917
|
|
|
820
918
|
### `mount()` / `unmount()` — Functions
|
|
821
919
|
|
|
822
|
-
Start and end an app in a host page (hosted mode). Used by `JopsApp` in Next.js; see [
|
|
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.
|
|
823
921
|
|
|
824
922
|
```js
|
|
825
923
|
import { mount, unmount } from "jops-core";
|
|
@@ -871,10 +969,11 @@ t.terminate();
|
|
|
871
969
|
| Command | Description |
|
|
872
970
|
| ---------------------- | -------------------------------------------------------------------------- |
|
|
873
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)) |
|
|
874
|
-
| `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 |
|
|
875
974
|
| `npx jops-core build` | Generate production-ready `dist/` folder (skipped in Cordova/Capacitor projects) |
|
|
876
975
|
|
|
877
|
-
`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`).
|
|
878
977
|
|
|
879
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.
|
|
880
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.
|