jops-core 1.1.3 → 1.1.5

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,26 @@ 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
+ - **Styling screens:** `.jops-screen` and `.jops-router` are hooks for your own CSS, e.g. padding inside every screen: `.jops-screen { padding: 0 24px 32px; }`. The properties the Router sets inline (`position`, `inset`, `overflow` and `background` on screens; `position` and `overflow` on the container) take precedence over your stylesheet; use `--jops-screen-bg` for the background.
846
+ - 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.
847
+ - `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.
848
+ - Without `screens` (the default), routes are placed in the parent element and scroll with the page, as before.
830
849
 
831
850
  #### Layout Declaration
832
851
 
@@ -437,6 +437,18 @@ export declare class Store {
437
437
  * - **Flat** (`href="#path"`) — reuses pre-existing view instances (tab navigation).
438
438
  * - **Stack** (`navigate()`) — creates a new instance and pushes onto a nav stack (Android `startActivity`).
439
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
+ *
440
452
  * @example
441
453
  * // In a layout file:
442
454
  * layout`
@@ -451,6 +463,8 @@ export declare class Router extends View {
451
463
  rootNode: any;
452
464
  routerDomNode: any;
453
465
  animation: any;
466
+ /** Screens mode (`<Router jops screens>`), see the class description. @type {boolean} */
467
+ screens: boolean;
454
468
  routes: {};
455
469
  _navStack: any[];
456
470
  _baseView: any;
@@ -465,6 +479,25 @@ export declare class Router extends View {
465
479
  */
466
480
  static singleton(): Router;
467
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;
468
501
  onLayout(): Promise<void>;
469
502
  /**
470
503
  * Routes only get their events bound while the Router renders; `onResume()`
@@ -517,6 +550,15 @@ export declare class Router extends View {
517
550
  */
518
551
  _onHostNavigation(): void;
519
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>;
520
562
  _slideTransition(outgoing: any, incoming: any, direction: any): Promise<void>;
521
563
  _fadeTransition(outgoing: any, incoming: any): Promise<void>;
522
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,k=new Map;function N(o){if(!o)return o;if(B){if(o.startsWith("/"))return"."+o}else if(o.startsWith("./"))return o.slice(1);return o}h(N,"normalizePath");var p=null,x=!1,w=0,T=new Set,S=null,I=new Set(["jops","src","layout","path","jops-event-bind"]);function O(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,(i,n)=>n.toUpperCase())]=s);return t}h(O,"tagProps");var A=new Set,C=!1,R=!1,g=!1,J={},U={};function M(){if(!g){g=!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,U[o]=e,history[o]=e}}}h(M,"installHostHistoryHook");function q(){if(!g)return;let o=!0;for(let t of["pushState","replaceState"])history[t]===U[t]?history[t]=J[t]:o=!1;o&&(g=!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");p=t,M();let e=w,s=await import(new URL(N(o),document.baseURI).href);return e!==w?null:new s.default}h(D,"mount");function P(){w+=1,y.singleton()?.destroy(),T.forEach(o=>o.destroy()),T.clear(),p&&(p.innerHTML=""),p=null,H.singleton().clear(),V.clear(),q()}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 v=class{static{h(this,"RawHTML")}constructor(t){this.html=t}},z=h(o=>new v(o),"raw");function G(o,...t){return o.reduce((e,s,i)=>{if(i>=t.length)return e+s;let n=t[i];return n==null?e+s:n instanceof v?e+s+n.html:e+s+String(n).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")},"")}h(G,"layout");var L=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;#e=!1;#o=null;#i=!1;#s=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.#s)return this.#s;if(!(!t||this.#i))return this.#s=this.#a(t).finally(()=>{this.#i=this.layout!=null,this.#s=null}),this.#s}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(k.has(e))this.layout=k.get(e);else{let s=await fetch(e);if(!s.ok)throw new Error(`HTTP ${s.status}`);let i=await s.text();k.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,x){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 L(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]=n=>this[i](n))}}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=>L.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.#o?.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.#e||this._generation!==w)return;let t=!this.domNode;if(t&&x&&!p)return;this.#e=!0;let e=this.domNode??p??document.body;t&&(e.innerHTML=this.layout,T.add(this),this.domNode=e,this._borrowedDomNode=!0);let s=h(()=>this._generation!==w,"ended"),i="[jops], [type='jops']",n=!(this instanceof y),c=e.closest("router"),r=this._roots(),f=r.flatMap(d=>[...n&&d.matches(i)?[d]:[],...d.querySelectorAll(i)]).filter(d=>d.tagName==="ROUTER"||d.closest("router")===c).filter(d=>{let l=d.parentElement?.closest(i);return!l||!n&&l===e?!0:!r.some(a=>a.contains(l))});if(f.length>0&&await this.#c(f,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"),n=new Array(t.length),c=this.#o=[];if(await Promise.all(t.map(async(r,f)=>{if(r.tagName==="ROUTER"){let u=new y(r);if(c.push(u),await u.render(),i())return u.destroy();let m=[...r.children];await u.onLayout(),this.#r(r,m);return}let d=r.getAttribute("layout"),l=r.getAttribute("src");if(!l&&!d)return;let a;if(l){let u=await import(new URL(N(l),document.baseURI).href);if(i())return;let m=u.default;S={ClassDef:m,props:O(r)};try{a=new m}finally{S=null}}else a=new o(void 0,O(r));if(n[f]=a,c.push(a),await a.loadLayout(d),i())return a.destroy();let b=document.createElement("template");b.innerHTML=a.layout??"";let _=b.content.cloneNode(!0);if(r.children.length>0){let u=_.firstElementChild??_;Array.from(r.children).forEach(m=>u.appendChild(m))}if(a.domNodes=[..._.children],a.routePath=r.getAttribute("path"),r.replaceWith(_),a.domNode=a.domNodes[0],await a.render(),i()||(this.#r(r,a._roots()),await a?.onLayout?.(),i()))return a.destroy();await this._startSubView(a)})),this.#o=null,i()){n.forEach(r=>r?.destroy());return}this.subViews.push(...n.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"))}},V=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={}}},H=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(),n=s.reduce((c,r)=>(c[r]||(c[r]={}),c[r]),this._state);n[i]=e}clear(){this._state={}}},y=class o extends E{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!==w)return;this.routes={};for(let n of this.subViews)if(n.routePath){let c=n.routePath.slice(1).split("?")[0];this.routes[c]=n}this._navStack=[],this._baseView=null;for(let n of Object.values(this.routes))n._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],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){x&&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(()=>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){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",n=t._roots().length>1||e._roots().length>1;n&&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"&&!n?await this._slideTransition(t,e,s):this.animation==="fade"&&!n?await this._fadeTransition(t,e):(t._setHidden(!0),e._setHidden(!1),this._currentView=e),i||e?.onResume?.()}async _slideTransition(t,e,s){let i=this.domNode,n=s==="forward",c=i.getBoundingClientRect(),r=t.domNode.getBoundingClientRect(),f=r.left-c.left,d=r.top-c.top,l=r.width,a=i.offsetWidth-f,b=`position:absolute;left:${f}px;top:${d}px;width:${l}px`;i.style.cssText=`position:relative;overflow:hidden;height:${i.offsetHeight}px`,t.domNode.style.cssText=`${b};z-index:${n?1:2}`,t.domNode.hidden=!1,e.domNode.style.cssText=`${b};z-index:${n?2:1}`,e.domNode.hidden=!1;let _=n?e.domNode:t.domNode,u=n?`${a}px`:"0px",m=n?"0px":`${a}px`,j=_.animate([{transform:`translateX(${u})`},{transform:`translateX(${m})`}],{duration:300,easing:"ease",fill:"forwards"});await j.finished,j.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(),n=t.domNode.getBoundingClientRect(),c=n.left-i.left,r=n.top-i.top,f=n.width,d=`position:absolute;left:${c}px;top:${r}px;width:${f}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 l=t.domNode.animate([{opacity:1},{opacity:0}],{duration:450,easing:"ease",fill:"forwards"}),a=e.domNode.animate([{opacity:0},{opacity:1}],{duration:450,easing:"ease",fill:"forwards"});await Promise.all([l.finished,a.finished]),l.cancel(),a.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);for(let[i,n]of Object.entries(this.routes)){let c=i===e;n._setHidden(!c),c&&(this._currentView=n,n?.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("?"),n=s,c=e??this.routes[n]?.constructor;if(!c){console.error(`JOPS Router: no class found for path "${t}"`);return}this._navStack.length===0&&(this._baseView=this._currentView);let r=new c;await r.loadLayout(),r.inflate(),this.domNode.append(...r._roots()),this._history("pushState",{jopsNav:!0,jopsIdx:++this._idx},"#"+n+(i?"?"+i:"")),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,i]=t.replace(/^\//,"").split("?"),n=s,c="#"+n+(i?"?"+i:"");if(this.routes[n])this._history("replaceState",{...history.state,jopsIdx:this._idx},c),this._switchTo(n);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},c)}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=n=>this.onMessage(n),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){x=!0,window.JOPS={mount:D,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{V as EventBus,y as Router,H as Store,$ as Thread,E as View,G as layout,D as mount,z as raw,P 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.3",
3
+ "version": "1.1.5",
4
4
  "description": "A no-bundler, OOP-first SPA framework with Android-style view composition",
5
5
  "type": "module",
6
6
  "exports": {