jops-core 1.1.2 → 1.1.4
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 +22 -4
- package/lib/jops-core.d.ts +47 -2
- package/lib/jops-core.min.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -615,7 +615,7 @@ export default class Card extends View {
|
|
|
615
615
|
- `jops`, `type="jops"`, `src`, `layout`, `path` and `jops-event-bind` belong to JOPS and are not props. `id`, `class` and `on*` attributes are props only: they are not copied to the view's element, and an `onclick` prop is not bound.
|
|
616
616
|
- The view's constructor needs no parameters for this: props reach declared views whatever their constructor looks like.
|
|
617
617
|
- A view created in code can get props through the constructor: `new View(layout, { title: "Hi" })`, or `super(undefined, props)` in a subclass.
|
|
618
|
-
- Props come from tags
|
|
618
|
+
- Props come from the tags of declared views (`<X jops …>`) in any layout JOPS renders: the root layout, the Router's route declarations, a route's own layout, the layouts of declared views and of views added with `addSubView()` / `insertSubView()`.
|
|
619
619
|
|
|
620
620
|
#### Layouts with several top-level elements
|
|
621
621
|
|
|
@@ -626,7 +626,7 @@ A layout may have one or more top-level elements:
|
|
|
626
626
|
<table><tbody id="rows"></tbody></table>
|
|
627
627
|
```
|
|
628
628
|
|
|
629
|
-
`this.domNode` is the first one and `this.domNodes` holds all of them. `get()`, `find(id)`, `bindEvents()`, `show()`, `hide()`, `addSubView()`, `insertSubView()` and `destroy()` act on all of them, in order. Top-level text is ignored. The Router's `slide` and `fade` animations need single-root views: a stacked view with several top-level elements switches instantly.
|
|
629
|
+
`this.domNode` is the first one and `this.domNodes` holds all of them. `get()`, `find(id)`, `bindEvents()`, `show()`, `hide()`, `addSubView()`, `insertSubView()` and `destroy()` act on all of them, in order. Top-level text is ignored. The Router's `slide` and `fade` animations need single-root views: a stacked view with several top-level elements switches instantly, except in the Router's screens mode, which animates them too.
|
|
630
630
|
|
|
631
631
|
#### Declarative Event Binding
|
|
632
632
|
|
|
@@ -814,7 +814,7 @@ Hash-based SPA router. Singleton. Declared in the layout as `<Router jops animat
|
|
|
814
814
|
|
|
815
815
|
- The first route declared in `<Router>` is the default: it is shown on startup when the URL has no known route hash, and whenever the hash becomes empty (`href="#"`, a link to the page itself).
|
|
816
816
|
- Each route is resumed only when it is shown; at startup only the selected route gets `onResume()`. Every route gets `onLayout()` at startup.
|
|
817
|
-
-
|
|
817
|
+
- A route's own layout may declare views (`<X jops …>`), like any other layout; they are rendered with the route.
|
|
818
818
|
|
|
819
819
|
#### Animation
|
|
820
820
|
|
|
@@ -826,7 +826,25 @@ Set via the `animation` attribute on the `<Router>` element:
|
|
|
826
826
|
| `"slide"` | Forward: incoming slides in from right. Back: outgoing slides out to right |
|
|
827
827
|
| `"fade"` | Crossfade between outgoing and incoming |
|
|
828
828
|
|
|
829
|
-
Animation only occurs on `navigate()` (forward) and `back()` when the nav stack is non-empty. Flat tab switching is always instant, and so is a transition involving a view whose layout has several top-level elements.
|
|
829
|
+
Animation only occurs on `navigate()` (forward) and `back()` when the nav stack is non-empty. Flat tab switching is always instant, and so is a transition involving a view whose layout has several top-level elements (except in screens mode, below).
|
|
830
|
+
|
|
831
|
+
#### Screens mode
|
|
832
|
+
|
|
833
|
+
Add the `screens` attribute to give the Router a container of its own, with every route and stacked view as a full-size screen in it, as in a native app:
|
|
834
|
+
|
|
835
|
+
```html
|
|
836
|
+
<Router jops screens animation="slide" class="content">
|
|
837
|
+
<Home jops path="/home" src="/src/Home.js"></Home>
|
|
838
|
+
<Files jops path="/files" src="/src/Files.js"></Files>
|
|
839
|
+
</Router>
|
|
840
|
+
```
|
|
841
|
+
|
|
842
|
+
- The `<Router>` tag is replaced by `<div class="jops-router">` (plus the tag's own `id` and `class`), styled `position: relative; overflow: hidden`. Its routes are placed inside it, not in the tag's parent element.
|
|
843
|
+
- **Give the container its size in your CSS**, e.g. fill the rest of a flex column: `.jops-router { flex: 1; min-height: 0; }`. Without a height it has none, and nothing is visible.
|
|
844
|
+
- Each route and each view opened with `navigate()` or `replace()` is wrapped in a `<div class="jops-screen">` that fills the container (`position: absolute; inset: 0`), **scrolls on its own** (`overflow: auto`) and has an opaque background. Set the background with the `--jops-screen-bg` CSS variable (default `Canvas`, the browser's page colour in light and dark mode): `.jops-router { --jops-screen-bg: var(--bg); }`.
|
|
845
|
+
- A new view **covers** the current one, which stays alive underneath. During the transition both are visible; afterwards the covered screen is `visibility: hidden` and `inert` (not painted, not focusable), so it keeps its scroll position and is shown again unchanged on Back. Inactive flat routes are hidden the same way.
|
|
846
|
+
- `slide` and `fade` move only the upper screen (the new one forward, the leaving one on Back), and also work for views with several top-level elements.
|
|
847
|
+
- Without `screens` (the default), routes are placed in the parent element and scroll with the page, as before.
|
|
830
848
|
|
|
831
849
|
#### Layout Declaration
|
|
832
850
|
|
package/lib/jops-core.d.ts
CHANGED
|
@@ -133,7 +133,7 @@ export declare class View {
|
|
|
133
133
|
subViews: any[];
|
|
134
134
|
layout: any;
|
|
135
135
|
style: string;
|
|
136
|
-
domNodes: any[];
|
|
136
|
+
domNodes: any[] | HTMLElement[];
|
|
137
137
|
domNode: any;
|
|
138
138
|
_destroyed: boolean;
|
|
139
139
|
_borrowedDomNode: boolean;
|
|
@@ -205,7 +205,8 @@ export declare class View {
|
|
|
205
205
|
inflate(): void;
|
|
206
206
|
/**
|
|
207
207
|
* Appends `view` into the child element with `id="parentNode"` and runs its
|
|
208
|
-
* full lifecycle (`onLayout` → `onResume` → `bindEvents`).
|
|
208
|
+
* full lifecycle (`onLayout` → `onResume` → `bindEvents`). Renders the
|
|
209
|
+
* declared views (`jops` tags) in the view's layout before its `onLayout()`.
|
|
209
210
|
* A removed (destroyed) view is final: create a new instance to show it again.
|
|
210
211
|
* @param {string} parentNode - The `id` of the container element inside `this.domNode`.
|
|
211
212
|
* @param {View} view - The subview to mount.
|
|
@@ -285,6 +286,8 @@ export declare class View {
|
|
|
285
286
|
removeSubViews(condition?: (view: View) => boolean): void;
|
|
286
287
|
/**
|
|
287
288
|
* Inserts `view` at `index` within `this.domNode` and runs its full lifecycle.
|
|
289
|
+
* Renders the declared views (`jops` tags) in the view's layout before its
|
|
290
|
+
* `onLayout()`.
|
|
288
291
|
* A removed (destroyed) view is final: create a new instance to show it again.
|
|
289
292
|
* @param {View} view - The subview to insert.
|
|
290
293
|
* @param {number} index - Zero-based insertion position.
|
|
@@ -434,6 +437,18 @@ export declare class Store {
|
|
|
434
437
|
* - **Flat** (`href="#path"`) — reuses pre-existing view instances (tab navigation).
|
|
435
438
|
* - **Stack** (`navigate()`) — creates a new instance and pushes onto a nav stack (Android `startActivity`).
|
|
436
439
|
*
|
|
440
|
+
* **Screens mode** (`<Router jops screens>`): the Router renders into its own
|
|
441
|
+
* container, `<div class="jops-router">` (with the tag's `id` and `class`),
|
|
442
|
+
* styled `position: relative; overflow: hidden`; the app's CSS gives it its
|
|
443
|
+
* size (e.g. `flex: 1; min-height: 0` in a flex column). Each route and each
|
|
444
|
+
* stacked view is wrapped in a `<div class="jops-screen">` that fills the
|
|
445
|
+
* container (`position: absolute; inset: 0`), scrolls on its own and has an
|
|
446
|
+
* opaque background (`--jops-screen-bg`, default `Canvas`). A new view covers
|
|
447
|
+
* the current one, which stays alive underneath (`visibility: hidden` and
|
|
448
|
+
* `inert` once the transition has ended, so it keeps its scroll position).
|
|
449
|
+
* Views with several root elements animate too. Without `screens`, routes
|
|
450
|
+
* are placed in the Router tag's parent element (the default).
|
|
451
|
+
*
|
|
437
452
|
* @example
|
|
438
453
|
* // In a layout file:
|
|
439
454
|
* layout`
|
|
@@ -448,6 +463,8 @@ export declare class Router extends View {
|
|
|
448
463
|
rootNode: any;
|
|
449
464
|
routerDomNode: any;
|
|
450
465
|
animation: any;
|
|
466
|
+
/** Screens mode (`<Router jops screens>`), see the class description. @type {boolean} */
|
|
467
|
+
screens: boolean;
|
|
451
468
|
routes: {};
|
|
452
469
|
_navStack: any[];
|
|
453
470
|
_baseView: any;
|
|
@@ -462,6 +479,25 @@ export declare class Router extends View {
|
|
|
462
479
|
*/
|
|
463
480
|
static singleton(): Router;
|
|
464
481
|
constructor(routerDomNode: any);
|
|
482
|
+
/**
|
|
483
|
+
* Shows or hides a route or stacked view: its screen in screens mode,
|
|
484
|
+
* otherwise its own root elements.
|
|
485
|
+
* @param {View} view
|
|
486
|
+
* @param {boolean} visible
|
|
487
|
+
*/
|
|
488
|
+
_setVisible(view: View, visible: boolean): void;
|
|
489
|
+
/**
|
|
490
|
+
* Adds a view created by navigate() or replace() to the page: in screens
|
|
491
|
+
* mode in a new screen at the top of the container (hidden until it is
|
|
492
|
+
* shown), otherwise next to the routes.
|
|
493
|
+
* @param {View} view
|
|
494
|
+
*/
|
|
495
|
+
_place(view: View): void;
|
|
496
|
+
/**
|
|
497
|
+
* Destroys a stacked or replace() view, and its screen in screens mode.
|
|
498
|
+
* @param {View} view
|
|
499
|
+
*/
|
|
500
|
+
_destroyView(view: View): void;
|
|
465
501
|
onLayout(): Promise<void>;
|
|
466
502
|
/**
|
|
467
503
|
* Routes only get their events bound while the Router renders; `onResume()`
|
|
@@ -514,6 +550,15 @@ export declare class Router extends View {
|
|
|
514
550
|
*/
|
|
515
551
|
_onHostNavigation(): void;
|
|
516
552
|
_animateTransition(outgoing: any, incoming: any, direction: any): Promise<void>;
|
|
553
|
+
/**
|
|
554
|
+
* Screens mode: the upper screen (the new one forward, the leaving one on
|
|
555
|
+
* Back) slides or fades over the lower one, which stays visible during the
|
|
556
|
+
* transition; afterwards only the front screen is visible.
|
|
557
|
+
* @param {View} outgoing
|
|
558
|
+
* @param {View} incoming
|
|
559
|
+
* @param {boolean} isForward
|
|
560
|
+
*/
|
|
561
|
+
_screenTransition(outgoing: View, incoming: View, isForward: boolean): Promise<void>;
|
|
517
562
|
_slideTransition(outgoing: any, incoming: any, direction: any): Promise<void>;
|
|
518
563
|
_fadeTransition(outgoing: any, incoming: any): Promise<void>;
|
|
519
564
|
_switchTo(path: any): void;
|
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,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};
|
|
1
|
+
var B=Object.defineProperty;var h=(o,t)=>B(o,"name",{value:t,configurable:!0});var U=new URL(import.meta.url).origin!==location.origin,T=new Map;function N(o){if(!o)return o;if(U){if(o.startsWith("/"))return"."+o}else if(o.startsWith("./"))return o.slice(1);return o}h(N,"normalizePath");var p=null,V=!1,w=0,L=new Set,S=null,M=new Set(["jops","src","layout","path","jops-event-bind"]);function O(o){let t={};for(let{name:e,value:s}of o.attributes)M.has(e)||e==="type"&&s==="jops"||(t[e.replace(/-([a-z])/g,(i,r)=>r.toUpperCase())]=s);return t}h(O,"tagProps");var A=new Set,C=!1,R=!1,x=!1,J={},W={};function I(){if(!x){x=!0;for(let o of["pushState","replaceState"]){let t=history[o],e=h(function(...s){let i=t.apply(this,s);return R||queueMicrotask(()=>y.singleton()?._onHostNavigation()),i},"wrapper");J[o]=t,W[o]=e,history[o]=e}}}h(I,"installHostHistoryHook");function D(){if(!x)return;let o=!0;for(let t of["pushState","replaceState"])history[t]===W[t]?history[t]=J[t]:o=!1;o&&(x=!1)}h(D,"uninstallHostHistoryHook");async function q({app:o,root:t}){if(!(t instanceof HTMLElement))throw new TypeError("JOPS mount(): root must be an HTMLElement");p=t,I();let e=w,s=await import(new URL(N(o),document.baseURI).href);return e!==w?null:new s.default}h(q,"mount");function P(){w+=1,y.singleton()?.destroy(),L.forEach(o=>o.destroy()),L.clear(),p&&(p.innerHTML=""),p=null,j.singleton().clear(),k.clear(),D()}h(P,"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 g=class{static{h(this,"RawHTML")}constructor(t){this.html=t}},X=h(o=>new g(o),"raw");function z(o,...t){return o.reduce((e,s,i)=>{if(i>=t.length)return e+s;let r=t[i];return r==null?e+s:r instanceof g?e+s+r.html:e+s+String(r).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")},"")}h(z,"layout");var v=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}},E=class o{static{h(this,"View")}#t=!1;#i=!1;#s=null;#o=!1;#e=null;#n;constructor(t,e){this._generation=w;let s=S&&S.ClassDef===new.target?S.props:null;this.#n=Object.freeze({...e??s??{}}),this.loadLayout(t),this.subViews=[]}get props(){return this.#n}async loadLayout(t){if(this.#e)return this.#e;if(!(!t||this.#o))return this.#e=this.#a(t).finally(()=>{this.#o=this.layout!=null,this.#e=null}),this.#e}async#a(t){if(t.endsWith(".js"))try{let e=await import(new URL(N(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(N(t),document.baseURI).href;if(T.has(e))this.layout=T.get(e);else{let s=await fetch(e);if(!s.ok)throw new Error(`HTTP ${s.status}`);let i=await s.text();T.set(e,i),this.layout=i}}catch(e){console.error("JOPS: failed to load layout:",t,e)}else this.layout=t}setStyle(t){if(t){if(this.style=t,V){C||(C=!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(A.has(t))return;A.add(t);let e=document.createElement("style");e.setAttribute("data-jops",""),e.textContent=t,document.head.appendChild(e)}}}get(t){return new v(this._roots(),t)}_roots(){return this.domNodes??(this.domNode?[this.domNode]:[])}_setHidden(t){for(let e of this._roots())e.hidden=t,e.style.display=t?"none":""}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 i=s.value.trim();typeof this[i]=="function"&&(e[s.name]=r=>this[i](r))}}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(i=>i!==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.render(),!e._destroyed&&(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=>v.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,k.unsubscribeAll(this),this.subViews.forEach(e=>e.destroy?.()),this.subViews=[],this.#s?.forEach(e=>e?.destroy?.()),this._borrowedDomNode||this.domNode&&(this.domNode===p||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 i of t._roots())this.domNode.insertBefore(i,s);this.subViews.splice(e,0,t),await t.render(),!t._destroyed&&(await t?.onLayout?.(),await t?.onResume?.(),t?.bindEvents?.())}async _startSubView(t){await t?.onResume?.(),t?.bindEvents?.()}async render(){if(!this.layout||this.#i||this._generation!==w)return;let t=!this.domNode;if(t&&V&&!p)return;this.#i=!0;let e=this.domNode??p??document.body;t&&(e.innerHTML=this.layout,L.add(this),this.domNode=e,this._borrowedDomNode=!0);let s=h(()=>this._generation!==w,"ended"),i="[jops], [type='jops']",r=!(this instanceof y),a=e.closest("router"),n=this._roots(),l=n.flatMap(d=>[...r&&d.matches(i)?[d]:[],...d.querySelectorAll(i)]).filter(d=>d.tagName==="ROUTER"||d.closest("router")===a).filter(d=>{let u=d.parentElement?.closest(i);return!u||!r&&u===e?!0:!n.some(c=>c.contains(u))});if(l.length>0&&await this.#c(l,e,s),!(s()||this._destroyed)&&t){if(await this.onLayout?.(),s()||(await this.onResume?.(),s()))return;this.bindEvents()}}async#c(t,e,s){let i=h(()=>s()||this._destroyed,"stopped"),r=new Array(t.length),a=this.#s=[];if(await Promise.all(t.map(async(n,l)=>{if(n.tagName==="ROUTER"){let f=new y(n);if(a.push(f),await f.render(),i())return f.destroy();let m=[...n.children];await f.onLayout(),this.#r(n,f.screens?[f.domNode]:m);return}let d=n.getAttribute("layout"),u=n.getAttribute("src");if(!u&&!d)return;let c;if(u){let f=await import(new URL(N(u),document.baseURI).href);if(i())return;let m=f.default;S={ClassDef:m,props:O(n)};try{c=new m}finally{S=null}}else c=new o(void 0,O(n));if(r[l]=c,a.push(c),await c.loadLayout(d),i())return c.destroy();let b=document.createElement("template");b.innerHTML=c.layout??"";let _=b.content.cloneNode(!0);if(n.children.length>0){let f=_.firstElementChild??_;Array.from(n.children).forEach(m=>f.appendChild(m))}if(c.domNodes=[..._.children],c.routePath=n.getAttribute("path"),n.replaceWith(_),c.domNode=c.domNodes[0],await c.render(),i()||(this.#r(n,c._roots()),await c?.onLayout?.(),i()))return c.destroy();await this._startSubView(c)})),this.#s=null,i()){r.forEach(n=>n?.destroy());return}this.subViews.push(...r.filter(Boolean))}#r(t,e){let s=this._roots(),i=s.indexOf(t);i<0||(s.splice(i,1,...e),this.domNodes=s,this.domNode=s[0],this.domNode||console.warn("JOPS: a view's layout has no elements left after rendering its declared views"))}},k=class o{static{h(this,"EventBus")}static#t={};static subscribe(...t){let[e,s,i]=typeof t[0]=="string"?[null,...t]:t;(o.#t[s]??=[]).push({callback:i,owner:e})}static send(t,e){let s=o.#t[t];if(s){o.#t[t]=s.filter(i=>!i.owner?._destroyed);for(let i of[...o.#t[t]])i.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={}}},j=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("."),i=s.pop(),r=s.reduce((a,n)=>(a[n]||(a[n]={}),a[n]),this._state);r[i]=e}clear(){this._state={}}},y=class o extends E{static{h(this,"Router")}static#t=null;static#i=!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",this.screens=t.hasAttribute("screens")}#s=new WeakMap;#o(t){let e=document.createElement("div");e.className="jops-screen",e.style.cssText="position:absolute;inset:0;overflow:auto;background:var(--jops-screen-bg, Canvas)";let s=t._roots();return s[0]?.parentNode&&s[0].before(e),e.append(...s),this.#s.set(t,e),this.#e(e,!1),e}#e(t,e){t.style.visibility=e?"":"hidden",t.inert=!e}_setVisible(t,e){if(!t)return;let s=this.#s.get(t);s?this.#e(s,e):t._setHidden(!e)}_place(t){this.screens?this.domNode.append(this.#o(t)):this.domNode.append(...t._roots())}_destroyView(t){t&&(t.destroy?.(),this.#s.get(t)?.remove(),this.#s.delete(t))}async onLayout(){if(this.screens){let t=document.createElement("div");t.className=["jops-router",this.routerDomNode.getAttribute("class")].filter(Boolean).join(" ");let e=this.routerDomNode.getAttribute("id");e&&(t.id=e),t.style.cssText="position:relative;overflow:hidden";for(let s of this.subViews)s.routePath&&this.#o(s);t.append(...this.routerDomNode.childNodes),this.routerDomNode.replaceWith(t),this.domNode=t}else{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!==w)return;this.routes={};for(let r of this.subViews)if(r.routePath){let a=r.routePath.slice(1).split("?")[0];this.routes[a]=r}this._navStack=[],this._baseView=null;for(let r of Object.values(this.routes))this._setVisible(r,!1);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],i=s&&this.routes[s]?e:t;this._idx=history.state?.jopsIdx??0,this._history("replaceState",{...history.state,jopsIdx:this._idx},"#"+i),this._historyListener=()=>this._onHistoryChange(),window.addEventListener("popstate",this._historyListener),window.addEventListener("hashchange",this._historyListener),this._switchTo(i)}_onHistoryChange(){if(this._destroyed)return;if(location.pathname!==this._pathname){V&&P();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(()=>this._destroyView(e));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=>this._destroyView(t)),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=>this._destroyView(t)),this._navStack=[],this._baseView=null,this._currentView=null,this.routes={},o.#t===this&&(o.#t=null),super.destroy())}_history(t,e,s){R=!0;try{history[t](e,"",s)}finally{R=!1}}_onHostNavigation(){if(this._destroyed||!this.routes)return;if(location.pathname!==this._pathname){P();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 i=s==="forward";if(this.screens){await this._screenTransition(t,e,i),i||e?.onResume?.();return}let r=t._roots().length>1||e._roots().length>1;r&&this.animation!=="none"&&!o.#i&&(o.#i=!0,console.info("JOPS: Router animations need single-root views; views with several root elements switch without animation.")),this.animation==="slide"&&!r?await this._slideTransition(t,e,s):this.animation==="fade"&&!r?await this._fadeTransition(t,e):(t._setHidden(!0),e._setHidden(!1),this._currentView=e),i||e?.onResume?.()}async _screenTransition(t,e,s){let i=this.#s.get(t),r=this.#s.get(e),a=s?r:i;i&&this.#e(i,!0),r&&this.#e(r,!0);let n=null;if(this.animation==="slide"?n=[{transform:"translateX(100%)"},{transform:"translateX(0)"}]:this.animation==="fade"&&(n=[{opacity:0},{opacity:1}]),n&&a){s||n.reverse();let l=a.animate(n,{duration:this.animation==="fade"?450:300,easing:"ease",fill:"forwards"});await l.finished,l.cancel()}i&&this.#e(i,!1),this._currentView=e}async _slideTransition(t,e,s){let i=this.domNode,r=s==="forward",a=i.getBoundingClientRect(),n=t.domNode.getBoundingClientRect(),l=n.left-a.left,d=n.top-a.top,u=n.width,c=i.offsetWidth-l,b=`position:absolute;left:${l}px;top:${d}px;width:${u}px`;i.style.cssText=`position:relative;overflow:hidden;height:${i.offsetHeight}px`,t.domNode.style.cssText=`${b};z-index:${r?1:2}`,t.domNode.hidden=!1,e.domNode.style.cssText=`${b};z-index:${r?2:1}`,e.domNode.hidden=!1;let _=r?e.domNode:t.domNode,f=r?`${c}px`:"0px",m=r?"0px":`${c}px`,H=_.animate([{transform:`translateX(${f})`},{transform:`translateX(${m})`}],{duration:300,easing:"ease",fill:"forwards"});await H.finished,H.cancel(),t.domNode.style.cssText="",e.domNode.style.cssText="",t._setHidden(!0),i.style.cssText="",this._currentView=e}async _fadeTransition(t,e){let s=this.domNode,i=s.getBoundingClientRect(),r=t.domNode.getBoundingClientRect(),a=r.left-i.left,n=r.top-i.top,l=r.width,d=`position:absolute;left:${a}px;top:${n}px;width:${l}px`;s.style.cssText=`position:relative;overflow:hidden;height:${s.offsetHeight}px`,t.domNode.style.cssText=d,t.domNode.hidden=!1,e.domNode.style.cssText=d,e.domNode.hidden=!1;let u=t.domNode.animate([{opacity:1},{opacity:0}],{duration:450,easing:"ease",fill:"forwards"}),c=e.domNode.animate([{opacity:0},{opacity:1}],{duration:450,easing:"ease",fill:"forwards"});await Promise.all([u.finished,c.finished]),u.cancel(),c.cancel(),t.domNode.style.cssText="",e.domNode.style.cssText="",t._setHidden(!0),s.style.cssText="",this._currentView=e}_switchTo(t){let e=t.split("?")[0],s=this.routes[e];s&&this._pauseCurrent(s);let i=Object.values(this.routes);s&&this.screens&&!i.includes(this._currentView)&&this._setVisible(this._currentView,!1);for(let[r,a]of Object.entries(this.routes)){let n=r===e;this._setVisible(a,n),n&&(this._currentView=a,a?.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,i]=t.replace(/^\//,"").split("?"),r=s,a=e??this.routes[r]?.constructor;if(!a){console.error(`JOPS Router: no class found for path "${t}"`);return}this._navStack.length===0&&(this._baseView=this._currentView);let n=new a;await n.loadLayout(),n.inflate(),this._place(n),this._history("pushState",{jopsNav:!0,jopsIdx:++this._idx},"#"+r+(i?"?"+i:"")),this._pauseCurrent(n),await n.onLayout?.(),await n.onResume?.(),n.bindEvents?.(),this._navStack.push(n),this._animateTransition(this._currentView,n,"forward")}async replace(t,e){this._closeStack();let[s,i]=t.replace(/^\//,"").split("?"),r=s,a="#"+r+(i?"?"+i:"");if(this.routes[r])this._history("replaceState",{...history.state,jopsIdx:this._idx},a),this._switchTo(r);else if(e){let n=new e;await n.loadLayout(),n.inflate(),this._place(n),this._pauseCurrent(n),await n.onLayout?.(),await n.onResume?.(),n.bindEvents?.(),this._setVisible(this._currentView,!1),this.screens&&this._setVisible(n,!0),this._currentView=n,this._history("replaceState",{...history.state,jopsIdx:this._idx},a)}else console.error(`JOPS Router: no instance or class found for path "${t}"`)}},$=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"}),i=URL.createObjectURL(s);this.#t=new Worker(i),URL.revokeObjectURL(i),this.#t.onmessage=r=>this.onMessage(r),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){V=!0,window.JOPS={mount:q,unmount:P},window.dispatchEvent(new Event("jops:ready"));return}let t=document.querySelector("script[data-app]"),e=t?.dataset.app??"./src/App.js";t?.dataset.root&&(p=document.getElementById(t.dataset.root),p||console.warn(`JOPS: data-root element #${t.dataset.root} not found, rendering into <body>`)),import(new URL(N(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{k as EventBus,y as Router,j as Store,$ as Thread,E as View,z as layout,q as mount,X as raw,P as unmount};
|