jops-core 1.0.10 → 1.0.12
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 +34 -1
- package/lib/jops-core.min.js +1 -1
- package/package.json +1 -1
- package/templates/empty/src/App.js +1 -1
- package/templates/empty/src/layout/mainlayout.html +1 -0
- package/templates/hello-world/src/App.js +4 -11
- package/templates/hello-world/src/DataSection.js +1 -1
- package/templates/hello-world/src/Features.js +0 -9
- package/templates/hello-world/src/Form.js +1 -1
- package/templates/hello-world/src/layout/layout_data.html +19 -0
- package/templates/hello-world/src/layout/layout_footer.html +8 -0
- package/templates/hello-world/src/layout/layout_form.html +72 -0
- package/templates/hello-world/src/layout/layout_header.html +13 -0
- package/templates/hello-world/src/layout/layout_hero.html +8 -0
- package/templates/hello-world/src/layout/mainlayout.html +21 -0
- package/templates/empty/src/layout/mainlayout.js +0 -2
- package/templates/hello-world/src/Contact.js +0 -20
- package/templates/hello-world/src/layout/layout_data.js +0 -25
- package/templates/hello-world/src/layout/layout_footer.js +0 -8
- package/templates/hello-world/src/layout/layout_form.js +0 -44
- package/templates/hello-world/src/layout/layout_header.js +0 -17
- package/templates/hello-world/src/layout/layout_hero.js +0 -9
- package/templates/hello-world/src/layout/mainlayout.js +0 -39
package/README.md
CHANGED
|
@@ -133,7 +133,7 @@ export default class MyView extends View { ... }
|
|
|
133
133
|
|
|
134
134
|
| Method | When called |
|
|
135
135
|
| ------------------------- | ------------------------------------------------------------------------------ |
|
|
136
|
-
| `async loadLayout(path?)` | Override to load a layout `.js` module
|
|
136
|
+
| `async loadLayout(path?)` | Override to load a layout `.js` module or `.html` file |
|
|
137
137
|
| `inflate()` | Converts `this.layout` string into `this.domNode` |
|
|
138
138
|
| `async onLayout()` | DOM is live — add subviews, read initial data |
|
|
139
139
|
| `async onResume()` | View becomes visible — called on first display and on every return navigation |
|
|
@@ -197,6 +197,39 @@ const html = layout`<p>${raw("<b>trusted</b>")}</p>`; // raw HTML
|
|
|
197
197
|
|
|
198
198
|
---
|
|
199
199
|
|
|
200
|
+
### HTML file layouts
|
|
201
|
+
|
|
202
|
+
In addition to `.js` layout modules, `loadLayout()` accepts a path to a plain `.html` file. The framework fetches the file, caches it for the lifetime of the page, and assigns its contents as the view's layout — no import, no export, no tagged template required.
|
|
203
|
+
|
|
204
|
+
**Via `layout` attribute (declarative):**
|
|
205
|
+
```html
|
|
206
|
+
<View jops layout="/src/layout/mainlayout.html"></View>
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**Via `loadLayout()` (programmatic):**
|
|
210
|
+
```js
|
|
211
|
+
await root.loadLayout("/src/layout/mainlayout.html");
|
|
212
|
+
root.render();
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
`mainlayout.html` is a plain HTML fragment:
|
|
216
|
+
|
|
217
|
+
```html
|
|
218
|
+
<div class="jops-root">
|
|
219
|
+
<View jops layout="/src/layout/layout_header.html"></View>
|
|
220
|
+
<main>
|
|
221
|
+
<Router jops animation="slide">
|
|
222
|
+
<View path="/home" jops layout="/src/layout/layout_home.html"></View>
|
|
223
|
+
<MyView path="/detail" jops src="/src/MyView.js"></MyView>
|
|
224
|
+
</Router>
|
|
225
|
+
</main>
|
|
226
|
+
</div>
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
> **Note:** `.html` layouts are fetched at runtime and cached in memory — identical dedup behaviour to the browser's module cache for `.js` layouts. Each unique path is fetched once per page load.
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
200
233
|
### `raw(html)` — Function
|
|
201
234
|
|
|
202
235
|
Wraps a trusted HTML string to bypass escaping inside a `layout` template.
|
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 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};
|
|
1
|
+
var k=Object.defineProperty;var c=(i,t)=>k(i,"name",{value:t,configurable:!0});var _=new URL(import.meta.url).origin!==location.origin,p=new Map;function f(i){if(!i)return i;if(_){if(i.startsWith("/"))return"."+i}else if(i.startsWith("./"))return i.slice(1);return i}c(f,"normalizePath");(()=>{console.log("JOPS initialized");let i=document.querySelector("script[data-app]")?.dataset.app??"./src/App.js";import(new URL(f(i),document.baseURI).href).then(t=>new t.default).catch(t=>console.error("JOPS: App.js not found or failed to load",t))})();var m=class{static{c(this,"RawHTML")}constructor(t){this.html=t}},E=c(i=>new m(i),"raw");function v(i,...t){return i.reduce((e,s,n)=>{if(n>=t.length)return e+s;let o=t[n];return o==null?e+s:o instanceof m?e+s+o.html:e+s+String(o).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")},"")}c(v,"layout");var w=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}},y=class i{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(f(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(f(t),document.baseURI).href;if(p.has(e))this.layout=p.get(e);else{let s=await fetch(e);if(!s.ok)throw new Error(`HTTP ${s.status}`);let n=await s.text();p.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,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 w(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 l=new b(s);await l.render(),await l.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(f(o),document.baseURI).href)).default;r=new h}else r=new i;this.subViews.push(r),await r.loadLayout(n);let a=document.createElement("template");a.innerHTML=r.layout??"";let d=a.content.cloneNode(!0);if(s.children.length>0){let l=d.firstElementChild??d;Array.from(s.children).forEach(h=>l.appendChild(h))}let u=d.firstElementChild;r.routePath=s.getAttribute("path"),s.replaceWith(d),r.domNode=u,await r.render(),await r?.onLayout?.(),await r?.onResume?.(),r?.bindEvents?.()}))}},R=class i{static{c(this,"EventBus")}static#t={};static subscribe(t,e){i.#t[t]||(i.#t[t]=[]),i.#t[t].push(e)}static send(t,e){i.#t[t]?.forEach(s=>s(e))}static unsubscribe(t,e){i.#t[t]&&(i.#t[t]=i.#t[t].filter(s=>s!==e))}},S=class i{static{c(this,"Store")}static#t=null;static singleton(){return i.#t||(i.#t=new i),i.#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,a)=>(r[a]||(r[a]={}),r[a]),this._state);o[n]=e}clear(){this._state={}}},b=class i extends y{static{c(this,"Router")}static#t=null;static singleton(){return i.#t}constructor(t){super(),i.#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 o of this.subViews)if(o.routePath){let r=o.routePath.slice(1).split("?")[0];this.routes[r]=o}this._navStack=[],this._baseView=null;for(let o of Object.values(this.routes))o.domNode.hidden=!0;let t=Object.keys(this.routes)[0],e=window.location.hash.slice(1),s=e.split("?")[0],n=s&&this.routes[s]?e:t;history.replaceState(null,"","#"+n),window.addEventListener("hashchange",()=>{let o=window.location.hash.slice(1),r=o.split("?")[0];if(!(!r||!this.routes[r])){if(this._pendingBack){this._pendingBack=!1;return}this._switchTo(o)}}),window.addEventListener("popstate",()=>{if(this._navStack.length>0){let a=this._navStack.pop(),d=this._navStack.at(-1)??this._baseView;a?.onBackPressed?.(),this._animateTransition(a,d,"back").then(()=>a.destroy());return}this._currentView?.onBackPressed?.();let o=window.location.hash.slice(1),r=o.split("?")[0];r&&this.routes[r]&&(this._pendingBack=!0,this._switchTo(o))}),this._switchTo(n)}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(),a=t.domNode.getBoundingClientRect(),d=a.left-r.left,u=a.top-r.top,l=a.width,h=n.offsetWidth-d,N=`position:absolute;left:${d}px;top:${u}px;width:${l}px`;n.style.cssText=`position:relative;overflow:hidden;height:${n.offsetHeight}px`,t.domNode.style.cssText=`${N};z-index:${o?1:2}`,t.domNode.hidden=!1,e.domNode.style.cssText=`${N};z-index:${o?2:1}`,e.domNode.hidden=!1;let L=o?e.domNode:t.domNode,g=o?`${h}px`:"0px",V=o?"0px":`${h}px`,T=L.animate([{transform:`translateX(${g})`},{transform:`translateX(${V})`}],{duration:300,easing:"ease",fill:"forwards"});await T.finished,T.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,a=o.top-n.top,d=o.width,u=`position:absolute;left:${r}px;top:${a}px;width:${d}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 l=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([l.finished,h.finished]),l.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 a=new r;await a.loadLayout(),a.inflate(),this.domNode.appendChild(a.domNode),history.pushState({jopsNav:!0},"","#"+o+(n?"?"+n:"")),await a.onLayout?.(),await a.onResume?.(),a.bindEvents?.(),this._navStack.push(a),this._animateTransition(this._currentView,a,"forward")}async replace(t,e){this._navStack.forEach(a=>a.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 a=new e;await a.loadLayout(),a.inflate(),this.domNode.appendChild(a.domNode),await a.onLayout?.(),await a.onResume?.(),a.bindEvents?.(),this._currentView&&(this._currentView.domNode.hidden=!0),this._currentView=a,history.replaceState(null,"",r)}else console.error(`JOPS Router: no instance or class found for path "${t}"`)}},x=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{R as EventBus,b as Router,S as Store,x as Thread,y as View,v as layout,E as raw};
|
package/package.json
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<div>I'm your main layout!</div>
|
|
@@ -1,21 +1,14 @@
|
|
|
1
1
|
import { View } from "jops-core";
|
|
2
|
-
import mainLayout from "./layout/mainlayout.js";
|
|
3
2
|
|
|
4
3
|
export default class App {
|
|
5
4
|
constructor() {
|
|
6
5
|
console.log("JOPS App mounted");
|
|
7
6
|
|
|
8
7
|
const mainContainer = new View();
|
|
9
|
-
mainContainer.setStyle("
|
|
8
|
+
mainContainer.setStyle("/src/css/styles.css");
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
mainContainer.render();
|
|
14
|
-
})();*/
|
|
15
|
-
|
|
16
|
-
// "/src/layout/mainlayout.js"
|
|
17
|
-
mainContainer.loadLayout(mainLayout).then(() => {
|
|
18
|
-
mainContainer.render();
|
|
19
|
-
});
|
|
10
|
+
mainContainer
|
|
11
|
+
.loadLayout("/src/layout/mainlayout.html")
|
|
12
|
+
.then(() => mainContainer.render());
|
|
20
13
|
}
|
|
21
14
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { View, layout } from "jops-core";
|
|
2
|
-
// import Card from "./Card.js";
|
|
3
2
|
|
|
4
3
|
export default class Features extends View {
|
|
5
4
|
constructor() {
|
|
@@ -14,10 +13,6 @@ export default class Features extends View {
|
|
|
14
13
|
this.inflate();
|
|
15
14
|
}
|
|
16
15
|
|
|
17
|
-
//async loadLayout() {
|
|
18
|
-
// await super.loadLayout("/src/layout/layout_features.js");
|
|
19
|
-
//}
|
|
20
|
-
|
|
21
16
|
async onLayout() {
|
|
22
17
|
for (const item of data) {
|
|
23
18
|
await this.addSubView("grid", new Card(item.title, item.content));
|
|
@@ -39,10 +34,6 @@ class Card extends View {
|
|
|
39
34
|
this.inflate();
|
|
40
35
|
}
|
|
41
36
|
|
|
42
|
-
// async loadLayout() {
|
|
43
|
-
// await super.loadLayout("/src/layout/card.js");
|
|
44
|
-
// }
|
|
45
|
-
|
|
46
37
|
onLayout() {
|
|
47
38
|
this.get("title").set(this.title);
|
|
48
39
|
this.get("content").set(this.content);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<section id="data" class="data-section">
|
|
2
|
+
<h2 class="section__title">Data Table</h2>
|
|
3
|
+
<div class="table-wrapper">
|
|
4
|
+
<table class="table">
|
|
5
|
+
<thead>
|
|
6
|
+
<tr>
|
|
7
|
+
<th>ID</th>
|
|
8
|
+
<th>Name</th>
|
|
9
|
+
<th>Role</th>
|
|
10
|
+
<th>Status</th>
|
|
11
|
+
</tr>
|
|
12
|
+
</thead>
|
|
13
|
+
<tbody id="tableBody"></tbody>
|
|
14
|
+
</table>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<h2 class="section__title section__title--mt">Item List</h2>
|
|
18
|
+
<ul id="list" class="list"></ul>
|
|
19
|
+
</section>
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<section id="form" class="form-section">
|
|
2
|
+
<h2 class="section__title">Sample Form</h2>
|
|
3
|
+
<form class="form" jops-event-bind onsubmit="onSubmit">
|
|
4
|
+
<div class="form__group">
|
|
5
|
+
<label class="form__label" for="fullname">Full Name</label>
|
|
6
|
+
<input
|
|
7
|
+
class="form__input"
|
|
8
|
+
type="text"
|
|
9
|
+
id="fullname"
|
|
10
|
+
name="fullname"
|
|
11
|
+
placeholder="Enter your full name"
|
|
12
|
+
/>
|
|
13
|
+
</div>
|
|
14
|
+
<div class="form__group">
|
|
15
|
+
<label class="form__label" for="email">Email</label>
|
|
16
|
+
<input
|
|
17
|
+
class="form__input"
|
|
18
|
+
type="email"
|
|
19
|
+
id="email"
|
|
20
|
+
name="email"
|
|
21
|
+
placeholder="Enter your email"
|
|
22
|
+
/>
|
|
23
|
+
</div>
|
|
24
|
+
<div class="form__group">
|
|
25
|
+
<label class="form__label" for="role">Role</label>
|
|
26
|
+
<select class="form__select" id="role" name="role">
|
|
27
|
+
<option value="">Select a role</option>
|
|
28
|
+
<option value="frontend">Frontend Developer</option>
|
|
29
|
+
<option value="backend">Backend Developer</option>
|
|
30
|
+
<option value="fullstack">Full Stack Developer</option>
|
|
31
|
+
<option value="mobile">Mobile Developer</option>
|
|
32
|
+
</select>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="form__group">
|
|
35
|
+
<label class="form__label">Experience</label>
|
|
36
|
+
<div class="form__radio-group">
|
|
37
|
+
<label class="form__radio-label"
|
|
38
|
+
><input type="radio" name="experience" value="junior" /> Junior</label
|
|
39
|
+
>
|
|
40
|
+
<label class="form__radio-label"
|
|
41
|
+
><input type="radio" name="experience" value="mid" /> Mid</label
|
|
42
|
+
>
|
|
43
|
+
<label class="form__radio-label"
|
|
44
|
+
><input type="radio" name="experience" value="senior" /> Senior</label
|
|
45
|
+
>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
<div class="form__group">
|
|
49
|
+
<label class="form__label" for="message">Message</label>
|
|
50
|
+
<textarea
|
|
51
|
+
class="form__textarea"
|
|
52
|
+
id="message"
|
|
53
|
+
name="message"
|
|
54
|
+
rows="4"
|
|
55
|
+
placeholder="Write your message here..."
|
|
56
|
+
></textarea>
|
|
57
|
+
</div>
|
|
58
|
+
<div class="form__group form__group--inline">
|
|
59
|
+
<input
|
|
60
|
+
class="form__checkbox"
|
|
61
|
+
type="checkbox"
|
|
62
|
+
id="agree"
|
|
63
|
+
name="agree"
|
|
64
|
+
value="yes"
|
|
65
|
+
/>
|
|
66
|
+
<label class="form__label form__label--inline" for="agree"
|
|
67
|
+
>I agree to the terms and conditions</label
|
|
68
|
+
>
|
|
69
|
+
</div>
|
|
70
|
+
<button class="btn btn--primary" type="submit">Submit</button>
|
|
71
|
+
</form>
|
|
72
|
+
</section>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<header class="header">
|
|
2
|
+
<div class="header__brand">
|
|
3
|
+
<span class="header__logo">JOPS</span>
|
|
4
|
+
<span class="header__tagline">Hello World</span>
|
|
5
|
+
</div>
|
|
6
|
+
<nav class="header__nav">
|
|
7
|
+
<a class="header__nav-link" href="#home">Home</a>
|
|
8
|
+
<a class="header__nav-link" href="#features">Features</a>
|
|
9
|
+
<a class="header__nav-link" href="#form">Form</a>
|
|
10
|
+
<a class="header__nav-link" href="#data">Data</a>
|
|
11
|
+
<a class="header__nav-link" href="#media">Media</a>
|
|
12
|
+
</nav>
|
|
13
|
+
</header>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<section id="home" class="hero">
|
|
2
|
+
<h1 class="hero__title">Welcome to JOPS</h1>
|
|
3
|
+
<p class="hero__subtitle">
|
|
4
|
+
A no-bundler, vanilla-ES6-class SPA framework built for developers who think
|
|
5
|
+
in OOP.
|
|
6
|
+
</p>
|
|
7
|
+
<button class="btn btn--primary">Get Started</button>
|
|
8
|
+
</section>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<link rel="stylesheet" href="./src/css/styles.css" />
|
|
2
|
+
|
|
3
|
+
<div class="jops-root">
|
|
4
|
+
<View jops layout="/src/layout/layout_header.html"></View>
|
|
5
|
+
|
|
6
|
+
<main class="main">
|
|
7
|
+
<Router jops animation="slide">
|
|
8
|
+
<View path="/home" jops layout="/src/layout/layout_hero.html"></View>
|
|
9
|
+
|
|
10
|
+
<Features path="/features" jops src="/src/Features.js"></Features>
|
|
11
|
+
|
|
12
|
+
<form path="/form" jops src="/src/Form.js"></form>
|
|
13
|
+
|
|
14
|
+
<DataSection path="/data" jops src="/src/DataSection.js"> </DataSection>
|
|
15
|
+
|
|
16
|
+
<Media path="/media" jops src="./src/Media.js"> </Media>
|
|
17
|
+
</Router>
|
|
18
|
+
</main>
|
|
19
|
+
|
|
20
|
+
<View jops layout="/src/layout/layout_footer.html"></View>
|
|
21
|
+
</div>
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { View } from "jops-core";
|
|
2
|
-
|
|
3
|
-
export default class Contact extends View {
|
|
4
|
-
constructor() {
|
|
5
|
-
super();
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
async loadLayout() {
|
|
9
|
-
await super.loadLayout("/src/layout/layout_contact.js");
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
onSubmit(event) {
|
|
13
|
-
event.preventDefault();
|
|
14
|
-
const data = Object.fromEntries(new FormData(event.target));
|
|
15
|
-
const message = Object.entries(data)
|
|
16
|
-
.map(([key, value]) => `${key}: ${value}`)
|
|
17
|
-
.join("\n");
|
|
18
|
-
alert(`Message Sent:\n\n${message}`);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { layout } from "jops-core";
|
|
2
|
-
|
|
3
|
-
export default layout`
|
|
4
|
-
<section id="data" class="data-section">
|
|
5
|
-
<h2 class="section__title">Data Table</h2>
|
|
6
|
-
<div class="table-wrapper">
|
|
7
|
-
<table class="table">
|
|
8
|
-
<thead>
|
|
9
|
-
<tr>
|
|
10
|
-
<th>ID</th>
|
|
11
|
-
<th>Name</th>
|
|
12
|
-
<th>Role</th>
|
|
13
|
-
<th>Status</th>
|
|
14
|
-
</tr>
|
|
15
|
-
</thead>
|
|
16
|
-
<tbody id="tableBody">
|
|
17
|
-
</tbody>
|
|
18
|
-
</table>
|
|
19
|
-
</div>
|
|
20
|
-
|
|
21
|
-
<h2 class="section__title section__title--mt">Item List</h2>
|
|
22
|
-
<ul id="list" class="list">
|
|
23
|
-
</ul>
|
|
24
|
-
</section>
|
|
25
|
-
`;
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { layout } from "jops-core";
|
|
2
|
-
|
|
3
|
-
export default layout`
|
|
4
|
-
<footer id="contact" class="footer">
|
|
5
|
-
<p class="footer__text">JOPS Framework — Built with vanilla ES6. No bundler required.</p>
|
|
6
|
-
<p class="footer__text footer__text--small">© 2026 JOPS. All rights reserved.</p>
|
|
7
|
-
</footer>
|
|
8
|
-
`;
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { layout } from "jops-core";
|
|
2
|
-
|
|
3
|
-
export default layout`
|
|
4
|
-
<section id="form" class="form-section">
|
|
5
|
-
<h2 class="section__title">Sample Form</h2>
|
|
6
|
-
<form class="form" jops-event-bind onsubmit="onSubmit">
|
|
7
|
-
<div class="form__group">
|
|
8
|
-
<label class="form__label" for="fullname">Full Name</label>
|
|
9
|
-
<input class="form__input" type="text" id="fullname" name="fullname" placeholder="Enter your full name">
|
|
10
|
-
</div>
|
|
11
|
-
<div class="form__group">
|
|
12
|
-
<label class="form__label" for="email">Email</label>
|
|
13
|
-
<input class="form__input" type="email" id="email" name="email" placeholder="Enter your email">
|
|
14
|
-
</div>
|
|
15
|
-
<div class="form__group">
|
|
16
|
-
<label class="form__label" for="role">Role</label>
|
|
17
|
-
<select class="form__select" id="role" name="role">
|
|
18
|
-
<option value="">Select a role</option>
|
|
19
|
-
<option value="frontend">Frontend Developer</option>
|
|
20
|
-
<option value="backend">Backend Developer</option>
|
|
21
|
-
<option value="fullstack">Full Stack Developer</option>
|
|
22
|
-
<option value="mobile">Mobile Developer</option>
|
|
23
|
-
</select>
|
|
24
|
-
</div>
|
|
25
|
-
<div class="form__group">
|
|
26
|
-
<label class="form__label">Experience</label>
|
|
27
|
-
<div class="form__radio-group">
|
|
28
|
-
<label class="form__radio-label"><input type="radio" name="experience" value="junior"> Junior</label>
|
|
29
|
-
<label class="form__radio-label"><input type="radio" name="experience" value="mid"> Mid</label>
|
|
30
|
-
<label class="form__radio-label"><input type="radio" name="experience" value="senior"> Senior</label>
|
|
31
|
-
</div>
|
|
32
|
-
</div>
|
|
33
|
-
<div class="form__group">
|
|
34
|
-
<label class="form__label" for="message">Message</label>
|
|
35
|
-
<textarea class="form__textarea" id="message" name="message" rows="4" placeholder="Write your message here..."></textarea>
|
|
36
|
-
</div>
|
|
37
|
-
<div class="form__group form__group--inline">
|
|
38
|
-
<input class="form__checkbox" type="checkbox" id="agree" name="agree" value="yes">
|
|
39
|
-
<label class="form__label form__label--inline" for="agree">I agree to the terms and conditions</label>
|
|
40
|
-
</div>
|
|
41
|
-
<button class="btn btn--primary" type="submit">Submit</button>
|
|
42
|
-
</form>
|
|
43
|
-
</section>
|
|
44
|
-
`;
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { layout } from "jops-core";
|
|
2
|
-
|
|
3
|
-
export default layout`
|
|
4
|
-
<header class="header">
|
|
5
|
-
<div class="header__brand">
|
|
6
|
-
<span class="header__logo">JOPS</span>
|
|
7
|
-
<span class="header__tagline">Hello World</span>
|
|
8
|
-
</div>
|
|
9
|
-
<nav class="header__nav">
|
|
10
|
-
<a class="header__nav-link" href="#home">Home</a>
|
|
11
|
-
<a class="header__nav-link" href="#features">Features</a>
|
|
12
|
-
<a class="header__nav-link" href="#form">Form</a>
|
|
13
|
-
<a class="header__nav-link" href="#data">Data</a>
|
|
14
|
-
<a class="header__nav-link" href="#media">Media</a>
|
|
15
|
-
</nav>
|
|
16
|
-
</header>
|
|
17
|
-
`;
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { layout } from "jops-core";
|
|
2
|
-
|
|
3
|
-
export default layout`
|
|
4
|
-
<section id="home" class="hero">
|
|
5
|
-
<h1 class="hero__title">Welcome to JOPS</h1>
|
|
6
|
-
<p class="hero__subtitle">A no-bundler, vanilla-ES6-class SPA framework built for developers who think in OOP.</p>
|
|
7
|
-
<button class="btn btn--primary">Get Started</button>
|
|
8
|
-
</section>
|
|
9
|
-
`;
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { layout } from "jops-core";
|
|
2
|
-
|
|
3
|
-
export default layout`
|
|
4
|
-
<link rel="stylesheet" href="./src/css/styles.css">
|
|
5
|
-
|
|
6
|
-
<div class="jops-root">
|
|
7
|
-
|
|
8
|
-
<View jops layout="/src/layout/layout_header.js"></View>
|
|
9
|
-
|
|
10
|
-
<main class="main">
|
|
11
|
-
|
|
12
|
-
<Router jops animation="slide">
|
|
13
|
-
|
|
14
|
-
<View path="/home" jops layout="/src/layout/layout_hero.js"></View>
|
|
15
|
-
|
|
16
|
-
<Features path="/features" jops
|
|
17
|
-
src="/src/Features.js">
|
|
18
|
-
</Features>
|
|
19
|
-
|
|
20
|
-
<Form path="/form" jops
|
|
21
|
-
src="/src/Form.js">
|
|
22
|
-
</Form>
|
|
23
|
-
|
|
24
|
-
<DataSection path="/data" jops
|
|
25
|
-
src="/src/DataSection.js">
|
|
26
|
-
</DataSection>
|
|
27
|
-
|
|
28
|
-
<Media path="/media" jops
|
|
29
|
-
src="/src/Media.js">
|
|
30
|
-
</Media>
|
|
31
|
-
|
|
32
|
-
</Router>
|
|
33
|
-
|
|
34
|
-
</main>
|
|
35
|
-
|
|
36
|
-
<View jops layout="/src/layout/layout_footer.js"></View>
|
|
37
|
-
|
|
38
|
-
</div>
|
|
39
|
-
`;
|