jops-core 1.0.8 → 1.0.10
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
|
@@ -23,6 +23,14 @@ No Webpack. No Babel. No compiler. Just `<script type="module">` and a browser.
|
|
|
23
23
|
|
|
24
24
|
---
|
|
25
25
|
|
|
26
|
+
## JOPS Demos
|
|
27
|
+
|
|
28
|
+
| Demo | Description |
|
|
29
|
+
| ---- | ----------- |
|
|
30
|
+
| [JOPS Hello World](https://cdn.ui-db.com/hello-world/index.html) | A minimal starter app showing views, layouts, routing, and event binding |
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
26
34
|
## Installation
|
|
27
35
|
|
|
28
36
|
### CDN
|
|
@@ -129,7 +137,7 @@ export default class MyView extends View { ... }
|
|
|
129
137
|
| `inflate()` | Converts `this.layout` string into `this.domNode` |
|
|
130
138
|
| `async onLayout()` | DOM is live — add subviews, read initial data |
|
|
131
139
|
| `async onResume()` | View becomes visible — called on first display and on every return navigation |
|
|
132
|
-
| `bindEvents()` | Wires `on*` attributes on `[
|
|
140
|
+
| `bindEvents()` | Wires `on*` attributes on `[jops-event-bind]` elements to class methods |
|
|
133
141
|
| `onBackPressed()` | Called by Router when back navigation is detected on this view |
|
|
134
142
|
|
|
135
143
|
#### Instance Methods
|
|
@@ -148,10 +156,10 @@ export default class MyView extends View { ... }
|
|
|
148
156
|
|
|
149
157
|
#### Declarative Event Binding
|
|
150
158
|
|
|
151
|
-
Mark any element with `
|
|
159
|
+
Mark any element with `jops-event-bind` and add `on*` attributes whose values are method names on the View class:
|
|
152
160
|
|
|
153
161
|
```html
|
|
154
|
-
<button
|
|
162
|
+
<button jops-event-bind onclick="onSave">Save</button>
|
|
155
163
|
```
|
|
156
164
|
|
|
157
165
|
```js
|
|
@@ -203,7 +211,7 @@ raw("<b>Hello</b>"); // passed through as-is inside layout``
|
|
|
203
211
|
|
|
204
212
|
### `Router` — extends `View`
|
|
205
213
|
|
|
206
|
-
Hash-based SPA router. Singleton. Declared in the layout as `<Router
|
|
214
|
+
Hash-based SPA router. Singleton. Declared in the layout as `<Router jops animation="slide">`.
|
|
207
215
|
|
|
208
216
|
#### Two Navigation Paradigms
|
|
209
217
|
|
|
@@ -243,12 +251,14 @@ Animation only occurs on `navigate()` (forward) and `back()` when the nav stack
|
|
|
243
251
|
#### Layout Declaration
|
|
244
252
|
|
|
245
253
|
```html
|
|
246
|
-
<Router
|
|
247
|
-
<view
|
|
248
|
-
<view
|
|
254
|
+
<Router jops animation="slide">
|
|
255
|
+
<view jops src="/src/Home.js" path="/home"></view>
|
|
256
|
+
<view jops src="/src/Form.js" path="/form"></view>
|
|
249
257
|
</Router>
|
|
250
258
|
```
|
|
251
259
|
|
|
260
|
+
> **Note:** The legacy `type="jops"` and `type="jops-event-bind"` syntax is still supported for backwards compatibility.
|
|
261
|
+
|
|
252
262
|
#### Query Params
|
|
253
263
|
|
|
254
264
|
```js
|
package/lib/jops-core.d.ts
CHANGED
|
@@ -92,7 +92,7 @@ export declare class View {
|
|
|
92
92
|
*/
|
|
93
93
|
get(id: string): Element;
|
|
94
94
|
/**
|
|
95
|
-
* Wires `on*` HTML attributes on `[
|
|
95
|
+
* Wires `on*` HTML attributes on `[jops-event-bind]` elements to
|
|
96
96
|
* same-named methods on this view instance.
|
|
97
97
|
* Called automatically after `onResume()`. Override to add custom wiring.
|
|
98
98
|
*/
|
|
@@ -144,7 +144,7 @@ export declare class View {
|
|
|
144
144
|
*/
|
|
145
145
|
insertSubView(view: View, index: number): Promise<void>;
|
|
146
146
|
/**
|
|
147
|
-
* Mounts the view to the DOM, resolves all nested `[
|
|
147
|
+
* Mounts the view to the DOM, resolves all nested `[jops]` view
|
|
148
148
|
* elements and `<Router>` declarations, and runs each one's full lifecycle.
|
|
149
149
|
* Call once on the root view; nested views are handled automatically.
|
|
150
150
|
* Idempotent — safe to call multiple times.
|
|
@@ -218,7 +218,7 @@ export declare class Store {
|
|
|
218
218
|
}
|
|
219
219
|
/**
|
|
220
220
|
* Hash-based SPA router. Singleton. Extends {@link View}.
|
|
221
|
-
* Declared in a layout string as `<Router
|
|
221
|
+
* Declared in a layout string as `<Router jops animation="slide">`.
|
|
222
222
|
*
|
|
223
223
|
* Supports two navigation paradigms:
|
|
224
224
|
* - **Flat** (`href="#path"`) — reuses pre-existing view instances (tab navigation).
|
|
@@ -227,9 +227,9 @@ export declare class Store {
|
|
|
227
227
|
* @example
|
|
228
228
|
* // In a layout file:
|
|
229
229
|
* layout`
|
|
230
|
-
* <Router
|
|
231
|
-
* <Home path="/home"
|
|
232
|
-
* <Detail path="/detail"
|
|
230
|
+
* <Router jops animation="slide">
|
|
231
|
+
* <Home path="/home" jops src="/src/Home.js"></Home>
|
|
232
|
+
* <Detail path="/detail" jops src="/src/Detail.js"></Detail>
|
|
233
233
|
* </Router>
|
|
234
234
|
* `
|
|
235
235
|
*/
|
package/lib/jops-core.min.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var V=Object.defineProperty;var c=(a,t)=>V(a,"name",{value:t,configurable:!0});(()=>{console.log("JOPS initialized");let a=document.querySelector("script[data-app]")?.dataset.app??"./src/App.js";import(new URL(a,document.baseURI).href).then(t=>new t.default).catch(t=>console.error("JOPS: App.js not found or failed to load",t))})();var f=class{static{c(this,"RawHTML")}constructor(t){this.html=t}},k=c(a=>new f(a),"raw");function _(a,...t){return a.reduce((e,s,n)=>{if(n>=t.length)return e+s;let o=t[n];return o==null?e+s:o instanceof f?e+s+o.html:e+s+String(o).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")},"")}c(_,"layout");var
|
|
1
|
+
var V=Object.defineProperty;var c=(a,t)=>V(a,"name",{value:t,configurable:!0});(()=>{console.log("JOPS initialized");let a=document.querySelector("script[data-app]")?.dataset.app??"./src/App.js";import(new URL(a,document.baseURI).href).then(t=>new t.default).catch(t=>console.error("JOPS: App.js not found or failed to load",t))})();var f=class{static{c(this,"RawHTML")}constructor(t){this.html=t}},k=c(a=>new f(a),"raw");function _(a,...t){return a.reduce((e,s,n)=>{if(n>=t.length)return e+s;let o=t[n];return o==null?e+s:o instanceof f?e+s+o.html:e+s+String(o).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")},"")}c(_,"layout");var p=class{static{c(this,"Element")}constructor(t,e){this.element=t?.id===e?t:t.querySelector(`#${e}`)}set(t){this.element.innerHTML=t}value(){return this.element.innerHTML}},m=class a{static{c(this,"View")}#t=!1;#e=!1;constructor(t){this.loadLayout(t),this.subViews=[]}async loadLayout(t){if(t)if(t.endsWith(".js"))try{let e=await import(new URL(t,document.baseURI).href);this.layout=e.default}catch(e){console.error("JOPS: failed to load layout:",t,e)}else this.layout=t}setStyle(t){if(t)if(this.style=t,t.endsWith(".css")){if(document.querySelector(`link[href="${t}"]`))return;let e=document.createElement("link");e.rel="stylesheet",e.href=t,document.head.appendChild(e)}else{if(document.querySelector(`style[data-jops="${t}"]`))return;let e=document.createElement("style");e.setAttribute("data-jops",t),e.textContent=t,document.head.appendChild(e)}}get(t){return new p(this.domNode,t)}bindEvents(){if(!this.domNode)return;let t=[...this.domNode.querySelectorAll("[jops-event-bind], [type='jops-event-bind']")];(this.domNode.hasAttribute("jops-event-bind")||this.domNode.getAttribute("type")==="jops-event-bind")&&t.unshift(this.domNode);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]=o=>this[n](o))}}inflate(){if(!this.layout||this.#t)return;this.#t=!0;let t=document.createElement("template");t.innerHTML=this.layout;let s=t.content.cloneNode(!0).firstElementChild;this.domNode=s}async addSubView(t,e){this.subViews.push(e),e.domNode||(await e.loadLayout(),e.inflate()),this.domNode.querySelector(`#${t}`).appendChild(e.domNode),await e?.onLayout?.(),await e?.onResume?.(),e?.bindEvents?.()}hide(){this.domNode.style.display="none"}show(){this.domNode.style.display=""}find(t){let e=[];for(let s of this.subViews)t(s)&&e.push(s),e.push(...s.find?.(t)??[]);return e}destroy(){this.subViews.forEach(t=>t.destroy?.()),this.subViews=[],this.domNode?.remove(),this.domNode=null,this.layout=null}removeSubView(t){t.destroy?.(),this.subViews=this.subViews.filter(e=>e!==t)}async insertSubView(t,e){t.domNode||(await t.loadLayout(),t.inflate());let s=this.domNode.children[e]??null;this.domNode.insertBefore(t.domNode,s),this.subViews.splice(e,0,t),await t?.onLayout?.(),await t?.onResume?.(),t?.bindEvents?.()}async render(){if(!this.layout||this.#e)return;this.#e=!0;let t=this.domNode??document.body;t===document.body&&(t.innerHTML=this.layout);let e=Array.from(t.querySelectorAll("[jops], [type='jops']")).filter(s=>s.tagName==="ROUTER"||!s.closest("router")||s.closest("router")===t);e.length!==0&&await Promise.all(e.map(async s=>{if(s.tagName==="ROUTER"){let d=new y(s);await d.render(),await d.onLayout();return}let n=s.getAttribute("layout"),o=s.getAttribute("src");if(!o&&!n)return;let r;if(o){let h=(await import(new URL(o,document.baseURI).href)).default;r=new h}else r=new a;this.subViews.push(r),await r.loadLayout(n);let i=document.createElement("template");i.innerHTML=r.layout??"";let l=i.content.cloneNode(!0);if(s.children.length>0){let d=l.firstElementChild??l;Array.from(s.children).forEach(h=>d.appendChild(h))}let u=l.firstElementChild;r.routePath=s.getAttribute("path"),s.replaceWith(l),r.domNode=u,await r.render(),await r?.onLayout?.(),await r?.onResume?.(),r?.bindEvents?.()}))}},N=class a{static{c(this,"EventBus")}static#t={};static subscribe(t,e){a.#t[t]||(a.#t[t]=[]),a.#t[t].push(e)}static send(t,e){a.#t[t]?.forEach(s=>s(e))}static unsubscribe(t,e){a.#t[t]&&(a.#t[t]=a.#t[t].filter(s=>s!==e))}},T=class a{static{c(this,"Store")}static#t=null;static singleton(){return a.#t||(a.#t=new a),a.#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(),o=s.reduce((r,i)=>(r[i]||(r[i]={}),r[i]),this._state);o[n]=e}clear(){this._state={}}},y=class a extends m{static{c(this,"Router")}static#t=null;static singleton(){return a.#t}constructor(t){super(),a.#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()}initRouter(){this.routes={};for(let e of this.subViews)if(e.routePath){let s=e.routePath.slice(1).split("?")[0];this.routes[s]=e}this._navStack=[],this._baseView=null;let t=Object.keys(this.routes)[0];history.replaceState(null,"","#"+t),window.addEventListener("hashchange",()=>{let e=window.location.hash.slice(1),s=e.split("?")[0];if(!(!s||!this.routes[s])){if(this._pendingBack){this._pendingBack=!1;return}this._switchTo(e)}}),window.addEventListener("popstate",()=>{if(this._navStack.length>0){let n=this._navStack.pop(),o=this._navStack.at(-1)??this._baseView;n?.onBackPressed?.(),this._animateTransition(n,o,"back").then(()=>n.destroy());return}this._currentView?.onBackPressed?.();let e=window.location.hash.slice(1),s=e.split("?")[0];s&&this.routes[s]&&(this._pendingBack=!0,this._switchTo(e))}),this._switchTo(t)}async _animateTransition(t,e,s){this.animation==="slide"?await this._slideTransition(t,e,s):this.animation==="fade"?await this._fadeTransition(t,e):(t.domNode.hidden=!0,e.domNode.hidden=!1,this._currentView=e,e?.onResume?.())}async _slideTransition(t,e,s){let n=this.domNode,o=s==="forward",r=n.getBoundingClientRect(),i=t.domNode.getBoundingClientRect(),l=i.left-r.left,u=i.top-r.top,d=i.width,h=n.offsetWidth-l,w=`position:absolute;left:${l}px;top:${u}px;width:${d}px`;n.style.cssText=`position:relative;overflow:hidden;height:${n.offsetHeight}px`,t.domNode.style.cssText=`${w};z-index:${o?1:2}`,t.domNode.hidden=!1,e.domNode.style.cssText=`${w};z-index:${o?2:1}`,e.domNode.hidden=!1;let x=o?e.domNode:t.domNode,R=o?`${h}px`:"0px",L=o?"0px":`${h}px`,b=x.animate([{transform:`translateX(${R})`},{transform:`translateX(${L})`}],{duration:300,easing:"ease",fill:"forwards"});await b.finished,b.cancel(),t.domNode.hidden=!0,t.domNode.style.cssText="",e.domNode.style.cssText="",n.style.cssText="",this._currentView=e,e?.onResume?.()}async _fadeTransition(t,e){let s=this.domNode,n=s.getBoundingClientRect(),o=t.domNode.getBoundingClientRect(),r=o.left-n.left,i=o.top-n.top,l=o.width,u=`position:absolute;left:${r}px;top:${i}px;width:${l}px`;s.style.cssText=`position:relative;overflow:hidden;height:${s.offsetHeight}px`,t.domNode.style.cssText=u,t.domNode.hidden=!1,e.domNode.style.cssText=u,e.domNode.hidden=!1;let d=t.domNode.animate([{opacity:1},{opacity:0}],{duration:450,easing:"ease",fill:"forwards"}),h=e.domNode.animate([{opacity:0},{opacity:1}],{duration:450,easing:"ease",fill:"forwards"});await Promise.all([d.finished,h.finished]),d.cancel(),h.cancel(),t.domNode.hidden=!0,t.domNode.style.cssText="",e.domNode.style.cssText="",s.style.cssText="",this._currentView=e,e?.onResume?.()}_switchTo(t){let e=t.split("?")[0];for(let[s,n]of Object.entries(this.routes)){let o=s===e;n.domNode.hidden=!o,o&&(this._currentView=n,n?.onResume?.())}}back(){history.back()}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("?"),o=s,r=e??this.routes[o]?.constructor;if(!r){console.error(`JOPS Router: no class found for path "${t}"`);return}this._navStack.length===0&&(this._baseView=this._currentView);let i=new r;await i.loadLayout(),i.inflate(),this.domNode.appendChild(i.domNode),history.pushState({jopsNav:!0},"","#"+o+(n?"?"+n:"")),await i.onLayout?.(),await i.onResume?.(),i.bindEvents?.(),this._navStack.push(i),this._animateTransition(this._currentView,i,"forward")}async replace(t,e){this._navStack.forEach(i=>i.destroy?.()),this._navStack=[];let[s,n]=t.replace(/^\//,"").split("?"),o=s,r="#"+o+(n?"?"+n:"");if(this.routes[o])history.replaceState(null,"",r),this._switchTo(o);else if(e){let i=new e;await i.loadLayout(),i.inflate(),this.domNode.appendChild(i.domNode),await i.onLayout?.(),await i.onResume?.(),i.bindEvents?.(),this._currentView&&(this._currentView.domNode.hidden=!0),this._currentView=i,history.replaceState(null,"",r)}else console.error(`JOPS Router: no instance or class found for path "${t}"`)}},S=class{static{c(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=o=>this.onMessage(o),this.#t.postMessage(t)}terminate(){if(!this.#t){console.warn("JOPS Thread: no active worker to terminate.");return}this.#t.terminate(),this.#t=null}};export{N as EventBus,y as Router,T as Store,S as Thread,m as View,_ as layout,k as raw};
|
package/package.json
CHANGED
|
@@ -25,7 +25,7 @@ class MediaCard extends View {
|
|
|
25
25
|
this.imgSrc = imgSrc;
|
|
26
26
|
this.caption = caption;
|
|
27
27
|
this.layout = layout`
|
|
28
|
-
<div class="media-card"
|
|
28
|
+
<div class="media-card" jops-event-bind onclick="showMedia">
|
|
29
29
|
<div id="imgSrc" class="media-card__img-placeholder"></div>
|
|
30
30
|
<p id="caption" class="media-card__caption"></p>
|
|
31
31
|
</div>
|
|
@@ -53,7 +53,7 @@ class MediaDialog extends View {
|
|
|
53
53
|
super();
|
|
54
54
|
this.layout = layout`
|
|
55
55
|
<div class="media-dialog">
|
|
56
|
-
<button class="media-dialog__back btn btn--secondary"
|
|
56
|
+
<button class="media-dialog__back btn btn--secondary" jops-event-bind onclick="onBack">← Back</button>
|
|
57
57
|
<div id="dialogImage" class="media-dialog__image"></div>
|
|
58
58
|
<p id="dialogCaption" class="media-dialog__caption"></p>
|
|
59
59
|
</div>
|
|
@@ -3,7 +3,7 @@ import { layout } from "jops-core";
|
|
|
3
3
|
export default layout`
|
|
4
4
|
<section id="form" class="form-section">
|
|
5
5
|
<h2 class="section__title">Sample Form</h2>
|
|
6
|
-
<form class="form"
|
|
6
|
+
<form class="form" jops-event-bind onsubmit="onSubmit">
|
|
7
7
|
<div class="form__group">
|
|
8
8
|
<label class="form__label" for="fullname">Full Name</label>
|
|
9
9
|
<input class="form__input" type="text" id="fullname" name="fullname" placeholder="Enter your full name">
|
|
@@ -5,27 +5,27 @@ export default layout`
|
|
|
5
5
|
|
|
6
6
|
<div class="jops-root">
|
|
7
7
|
|
|
8
|
-
<View
|
|
8
|
+
<View jops layout="/src/layout/layout_header.js"></View>
|
|
9
9
|
|
|
10
10
|
<main class="main">
|
|
11
11
|
|
|
12
|
-
<Router
|
|
12
|
+
<Router jops animation="slide">
|
|
13
13
|
|
|
14
|
-
<View path="/home"
|
|
14
|
+
<View path="/home" jops layout="/src/layout/layout_hero.js"></View>
|
|
15
15
|
|
|
16
|
-
<Features path="/features"
|
|
16
|
+
<Features path="/features" jops
|
|
17
17
|
src="/src/Features.js">
|
|
18
18
|
</Features>
|
|
19
19
|
|
|
20
|
-
<Form path="/form"
|
|
20
|
+
<Form path="/form" jops
|
|
21
21
|
src="/src/Form.js">
|
|
22
22
|
</Form>
|
|
23
23
|
|
|
24
|
-
<DataSection path="/data"
|
|
24
|
+
<DataSection path="/data" jops
|
|
25
25
|
src="/src/DataSection.js">
|
|
26
26
|
</DataSection>
|
|
27
27
|
|
|
28
|
-
<Media path="/media"
|
|
28
|
+
<Media path="/media" jops
|
|
29
29
|
src="/src/Media.js">
|
|
30
30
|
</Media>
|
|
31
31
|
|
|
@@ -33,7 +33,7 @@ export default layout`
|
|
|
33
33
|
|
|
34
34
|
</main>
|
|
35
35
|
|
|
36
|
-
<View
|
|
36
|
+
<View jops layout="/src/layout/layout_footer.js"></View>
|
|
37
37
|
|
|
38
38
|
</div>
|
|
39
39
|
`;
|