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 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 in static layouts processed by `render()`: the root layout, the Router's route declarations, and the layouts of declared views. Tags inside the layout of a view added with `addSubView()`, or inside a route's own layout, are not created as views; add those children in code.
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
- - **Screen rule:** put screen views in `<Router>`. A route's own layout is not scanned for declared views, so a screen adds its child views in code with `addSubView()`.
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
 
@@ -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;
@@ -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,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")},"")}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,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")},"")}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};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jops-core",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "description": "A no-bundler, OOP-first SPA framework with Android-style view composition",
5
5
  "type": "module",
6
6
  "exports": {