diff --git a/app/helloworld.js b/app/helloworld.js
index 0db7cb5..d9fed91 100644
--- a/app/helloworld.js
+++ b/app/helloworld.js
@@ -3,13 +3,16 @@ AFRAME.registerComponent('helloworld', {
foo: { type:"string"}
},
- dependencies:{
+ requires:{
"html": "https://unpkg.com/aframe-htmlmesh@2.1.0/build/aframe-html.js", // html to AFRAME
"stylis": "https://unpkg.com/stylis@4.3.1/dist/umd/stylis.js" // modern CSS (https://stylis.js.org)
},
+ dependencies: ['windowmanager'],
+
dom: {
scale: 3.5,
+ modal: true,
events: ['click','input'],
html: (me) => `
@@ -41,7 +44,10 @@ AFRAME.registerComponent('helloworld', {
},
events:{
- html: function( ){ console.log("htmlmesh component mounted!") }, // html-component was added to this AFRAME entity
+ html: function( ){
+ console.log("html-mesh mounted")
+ this.el.setAttribute("html",`html:#${this.el.uid}; cursor:#cursor`)
+ }, // html-component was added to this AFRAME entity
click: function(e){ // a click was detected on this.el.dom or AFRAME entity
let el = e.detail.target || e.detail.srcElement
if( !el ) return
@@ -53,13 +59,12 @@ AFRAME.registerComponent('helloworld', {
if( e.detail.target.id == 'myRange' ) this.data.value = e.detail.target.value // reactive demonstration
},
value: function(e){ this.el.dom.querySelector("#value").innerHTML = e.detail.v }, // this.data.title was changed
+ ready: function(){
+ }
},
init: function () {
- this.require( this.dependencies )
- .then( () => {
- this.el.setAttribute("html",`html:#${this.el.uid}; cursor:#cursor`)
- })
+ this.require( this.requires )
},
manifest: { // HTML5 manifest to identify app to xrsh
diff --git a/app/windowmanager.js b/app/windowmanager.js
index ee615a5..606db5d 100644
--- a/app/windowmanager.js
+++ b/app/windowmanager.js
@@ -1,5 +1,5 @@
//
-// this is just an AFRAME wrapper for https://golden-layout.com
+// this is just an AFRAME wrapper for golden-layout v2 (docs: https://golden-layout.github.io/golden-layout/)
//
//
@@ -7,61 +7,77 @@ AFRAME.registerComponent('windowmanager', {
schema: {
},
- dependencies:{
- "cash": "https://cdn.jsdelivr.net/npm/cash-dom/dist/cash.min.js", // tiny jquery replacement
- "goldenlayout": "https://golden-layout.com/files/latest/js/goldenlayout.min.js",
- "goldenlayout_css1": "https://golden-layout.com/files/latest/css/goldenlayout-base.css",
- "goldenlayout_css2": "https://golden-layout.com/files/latest/css/goldenlayout-dark-theme.css"
+ requires:{
+ "goldenlayout_css1": "https://unpkg.com/golden-layout@2.6.0/dist/css/goldenlayout-base.css",
+ "goldenlayout_css2": "https://unpkg.com/golden-layout@2.6.0/dist/css/themes/goldenlayout-dark-theme.css"
+ },
+
+ dom: {
+ events: [],
+
+ // for stylesheet see bottom of file
+ html: (me) => `
+
`,
+
},
events:{
ready: function(){
- //this.initLayout()
+ this.initLayout(this)
}
},
init: function () {
- this.require( this.dependencies )
+ this.require( this.requires )
},
initLayout: async function(){
- // const goldenlayout_module = "https://cdn.skypack.dev/pin/golden-layout@v2.5.0-dAz3xMzxIRpbnbfEAik0/mode=imports/optimized/golden-layout.js";
- // const { ComponentContainer, ComponentItemConfig, GoldenLayout, ItemType } = await import(goldenlayout_module)
+ if( this.goldenLayout !== undefined || !this.el.dom.querySelector(".modals")) return console.warn("TODO: fix duplicate ready-events")
- this.el.layout = document.createElement('div')
- document.querySelector("#overlay").appendChild(this.el.layout)
+ let { GoldenLayout } = await import("https://cdn.skypack.dev/pin/golden-layout@v2.5.0-dAz3xMzxIRpbnbfEAik0/mode=imports/optimized/golden-layout.js");
- class MyComponent {
+ class Modal {
constructor(container) {
this.container = container;
this.rootElement = container.element;
- this.rootElement.innerHTML = '
' + 'Component Type: MyComponent' + '
';
+ this.rootElement.innerHTML = ''
this.resizeWithContainerAutomatically = true;
}
}
-
const myLayout = {
- content: [
- {
- title: 'Terminal 1',
- type: 'component',
- componentType: 'MyComponent',
- width: 50,
- },
- {
- title: 'My Component 2',
- type: 'component',
- componentType: 'MyComponent',
- // componentState: { text: 'Component 2' }
- }
- ]
+ root: {
+ type: 'row',
+ content: [
+ {
+ title: 'Terminal',
+ type: 'component',
+ componentType: 'Modal',
+ width: 50,
+ },
+ {
+ title: 'Welcome to XR shell',
+ type: 'component',
+ componentType: 'Modal',
+ // componentState: { text: 'Component 2' }
+ }
+ ]
+ }
};
+ this.goldenLayout = new GoldenLayout( this.el.dom.querySelector('.modals'));
+ this.goldenLayout.registerComponent('Modal', Modal);
+ this.goldenLayout.loadLayout(myLayout);
- const goldenLayout = new GoldenLayout(this.el.layout);
- goldenLayout.registerComponent( 'MyComponent', MyComponent);
- goldenLayout.loadLayout(myLayout);
- goldenLayout.addComponent('MyComponent', undefined, 'Added Component');
+ },
+
+ add: function(title,el){
+ setTimeout( () => {
+ let item = this.goldenLayout.addComponent('Modal', undefined, title )
+ console.dir(item)
+ item.parentItem.contentItems[ item.parentItem.contentItems.length-1 ].element.querySelector('.lm_content').appendChild(el)
+ },1000)
},
manifest: { // HTML5 manifest to identify app to xrsh
@@ -100,3 +116,301 @@ This is a core XRSH system application
});
+// monkeypatching updateProperties will detect a component config like:
+// dom: {
+// html: `
Welcome to XR shell
`,
+// css: `#hello {color:red}`,
+// events: ['click']
+// }
+// and use it to create a reactive DOM-component (using native javascript Proxy)
+// which delegates all related DOM-events AND data-changes back to the AFRAME component
+AFRAME.AComponent.prototype.updateProperties = function(updateProperties){
+ return function(){
+ updateProperties.apply(this,arguments)
+ if( this.dom && this.data && this.data.uri ){
+
+ tasks = {
+
+ generateUniqueId: () => {
+ this.el.uid = String(Math.random()).substr(2)
+ return tasks
+ },
+
+ ensureOverlay: () => {
+ let overlay = document.querySelector('#overlay')
+ if( !overlay ){
+ overlay = document.createElement('div')
+ overlay.id = "overlay"
+ document.body.appendChild(overlay)
+ document.querySelector("a-scene").setAttribute("webxr","overlayElement:#overlay")
+ }
+ tasks.overlay = overlay
+ return tasks
+ },
+
+ createReactiveDOMElement: () => {
+ const reactify = (el,aframe) => new Proxy(this.data,{
+ get(me,k,v) { return me[k] },
+ set(me,k,v){
+ me[k] = v
+ aframe.emit(k,{el,k,v})
+ }
+ })
+ this.el.dom = document.createElement('div')
+ this.el.dom.className = this.parseAppURI(this.data.uri).component
+ this.el.dom.innerHTML = this.dom.html(this)
+ this.data = reactify( this.dom.el, this.el )
+ this.dom.events.map( (e) => this.el.dom.addEventListener(e, (ev) => this.el.emit(e,ev) ) )
+ return tasks
+ },
+
+ addCSS: () => {
+ if( this.dom.css && !document.head.querySelector(`style#${this.attrName}`) ){
+ document.head.innerHTML += ``
+ }
+ return tasks
+ },
+
+ scaleDOMvsXR: () => {
+ if( this.dom.scale ) this.el.setAttribute('scale',`${this.dom.scale} ${this.dom.scale} ${this.dom.scale}`)
+ return tasks
+ },
+
+ addModalFunctions: () => {
+ this.el.close = () => {
+ this.el.dom.remove()
+ this.el.removeAttribute("html")
+ }
+ this.el.toggleFold = () => {
+ this.el.dom.querySelector(".modal").classList.toggle('fold')
+ this.el.dom.querySelector('.top .fold').innerText = this.el.dom.querySelector('.modal').className.match(/fold/) ? '▢' : '_'
+ }
+ return tasks
+ },
+ }
+
+ tasks
+ .generateUniqueId()
+ .ensureOverlay()
+ .addCSS()
+ .createReactiveDOMElement()
+ .scaleDOMvsXR()
+ .addModalFunctions()
+ // finally lets add the bad boy to the DOM
+ if( this.dom && this.dom.modal ){
+ document.querySelector('[windowmanager]').components['windowmanager'].add( this.parseAppURI(this.data.uri).component, this.el.dom )
+ }else tasks.overlay.appendChild(this.el.dom)
+ }
+ }
+}( AFRAME.AComponent.prototype.updateProperties)
+
+
+//
+// base CSS for XRSH apps
+//
+// limitations / some guidelines for html-mesh compatibility:
+// * no icon libraries (favicon e.g.)
+// * 'border-radius: 2px 3px 4px 5px' (applies 2px to all corners)
+//
+
+document.head.innerHTML += `
+
+`
diff --git a/com/app.js b/com/app.js
index 569d24c..43a04b0 100644
--- a/com/app.js
+++ b/com/app.js
@@ -4,38 +4,60 @@ AFRAME.registerComponent('app', {
schema:{
"uri":{ type:"string"}
},
- init: function() {
- let id = this.data.uri.split("/").pop() // 'a/b/c/mycom.js' => 'mycom.js'
- let component = id.split(".js").shift() // 'mycom.js' => 'mycom'
- let mount = () => {
+
+ events:{
+ "app:ready": function(){
+ let {id,component,type} = this.parseAppURI(this.data.uri)
let entity = document.createElement("a-entity")
this.el.setAttribute(component,this.data)
}
- if( AFRAME.components[component] || document.head.querySelector(`script#${id}`) ) mount()
- else this.require([ this.data.uri ]).then( mount ).catch(console.error)
},
+ init: function() {
+ let {id,component,type} = this.parseAppURI(this.data.uri)
+ if( AFRAME.components[component] || document.head.querySelector(`script#${id}`) ) this.el.emit('app:ready',{})
+ else this.require([ this.data.uri ], 'app:ready')
+ },
+
+ parseAppURI: AFRAME.AComponent.prototype.parseAppURI = function(uri){
+ return {
+ id: String(uri).split("/").pop(), // 'a/b/c/mycom.js' => 'mycom.js'
+ component: String(uri).split("/").pop().split(".js").shift(), // 'mycom.js' => 'mycom'
+ type: String(uri).split(".").pop() // 'mycom.js' => 'js'
+ }
+ },
// usage: require(["./app/foo.js"])
// require({foo: "https://foo.com/foo.js"})
- require: AFRAME.AComponent.prototype.require = function(packages){
+ require: AFRAME.AComponent.prototype.require = function(packages,readyEvent){
let deps = []
if( !packages.map ) packages = Object.values(packages)
packages.map( (package) => {
let id = package.split("/").pop()
if( !document.head.querySelector(`script#${id}`) ){
+ let {id,component,type} = this.parseAppURI(package)
let p = new Promise( (resolve,reject) => {
- let script = document.createElement("script")
- script.id = id
- script.src = package
- script.onload = () => resolve()
- script.onerror = (e) => reject(e)
- document.head.appendChild(script)
+ switch(type){
+ case "js": let script = document.createElement("script")
+ script.id = id
+ script.src = package
+ script.onload = () => resolve()
+ script.onerror = (e) => reject(e)
+ document.head.appendChild(script)
+ break;
+ case "css": let link = document.createElement("link")
+ link.id = id
+ link.href = package
+ link.rel = 'stylesheet'
+ document.head.appendChild(link)
+ resolve()
+ break;
+ }
})
deps.push(p)
}
})
- return Promise.all(deps)
+ Promise.all(deps).then( () => this.el.emit( readyEvent||'ready', packages) )
}
})
@@ -56,264 +78,3 @@ AFRAME.AComponent.prototype.initComponent = function(initComponent){
}( AFRAME.AComponent.prototype.initComponent)
-// monkeypatching updateProperties will detect a component config like:
-// dom: {
-// html: `
hello
`,
-// css: `#hello {color:red}`,
-// events: ['click']
-// }
-// and use it to create a reactive DOM-component (using native javascript Proxy)
-// which delegates all related DOM-events AND data-changes back to the AFRAME component
-
-AFRAME.AComponent.prototype.updateProperties = function(updateProperties){
- return function(){
- updateProperties.apply(this,arguments)
- if( this.dom && this.data){
-
- tasks = {
-
- generateUniqueId: () => {
- this.el.uid = String(Math.random()).substr(2)
- return tasks
- },
-
- ensureOverlay: () => {
- let overlay = document.querySelector('#overlay')
- if( !overlay ){
- overlay = document.createElement('div')
- overlay.id = "#overlay"
- document.body.appendChild(overlay)
- document.querySelector("a-scene").setAttribute("webxr","overlayElement:#overlay")
- }
- tasks.overlay = overlay
- return tasks
- },
-
- createReactiveDOMElement: () => {
- const reactify = (el,aframe) => new Proxy(this.data,{
- get(me,k,v) { return me[k] },
- set(me,k,v){
- me[k] = v
- aframe.emit(k,{el,k,v})
- }
- })
- this.el.dom = document.createElement('div')
- this.el.dom.innerHTML = this.dom.html(this)
- this.data = reactify( this.dom.el, this.el )
- this.dom.events.map( (e) => this.el.dom.addEventListener(e, (ev) => this.el.emit(e,ev) ) )
- return tasks
- },
-
- addCSS: () => {
- if( this.dom.css && !document.head.querySelector(`style#${this.attrName}`) ){
- document.head.innerHTML += ``
- }
- return tasks
- },
-
- scaleDOMvsXR: () => {
- if( this.dom.scale ) this.el.setAttribute('scale',`${this.dom.scale} ${this.dom.scale} ${this.dom.scale}`)
- return tasks
- },
-
- addModalFunctions: () => {
- this.el.close = () => {
- this.el.dom.remove()
- this.el.removeAttribute("html")
- }
- this.el.toggleFold = () => {
- this.el.dom.querySelector(".modal").classList.toggle('fold')
- this.el.dom.querySelector('.top .fold').innerText = this.el.dom.querySelector('.modal').className.match(/fold/) ? '▢' : '_'
- }
- return tasks
- },
- }
-
- tasks
- .generateUniqueId()
- .ensureOverlay()
- .addCSS()
- .createReactiveDOMElement()
- .scaleDOMvsXR()
- .addModalFunctions()
- // finally lets add the bad boy to the DOM
- tasks.overlay.appendChild(this.el.dom)
- }
- }
-}( AFRAME.AComponent.prototype.updateProperties)
-
-//
-// base CSS for XRSH apps
-//
-// limitations / some guidelines for html-mesh compatibility:
-// * no icon libraries (favicon e.g.)
-// * 'border-radius: 2px 3px 4px 5px' (applies 2px to all corners)
-//
-document.head.innerHTML += `
-
-`