// this is a highlevel way of loading buildless 'apps' (a collection of js components) AFRAME.required = {} AFRAME.app = new Proxy({ order:0, components: {}, // component-strings in this array are automatically // added to each app add(component, entity){ // categorize by component to prevent similar apps loading duplicate dependencies simultaniously this[component] = this[component] || [] this[component].push(entity) entity.data.order = entity.data.order || this.count() }, count(){ let n = 0 this.foreach( () => n++ ) return n }, foreach(cb){ const isArray = (e) => e.push let arr = [] for( let i in this ){ if( isArray(this[i]) ) this[i].map( (app) => arr.push(app.el.app) ) } arr.sort( (a,b) => a.data.order > b.data.order ) .map( cb ) } },{ get(me,k) { return me[k] }, set(me,k,v){ me[k] = v } }) /* * This is the abstract 'app' component */ appComponent = { schema:{ "uri":{ type:"string"} }, events:{ "app:ready": function(){ let {id,component,type} = this.parseAppURI(this.data.uri) AFRAME.app[component].map( (app) => { if( !app.el.getAttribute(component) ){ if( AFRAME.components[ component ] ){ app.el.setAttribute(component,app.data) }else console.warn(`${component} was not fully downloaded yet (${app.data.uri})`) } }) }, "requires:ready": function(){ let {id,component,type} = this.parseAppURI(this.data.uri) AFRAME.app[component].map( (app) => { if( app.readyFired ) return setTimeout( () => { if( this.el.dom ) this.el.dom.style.display = '' // finally show dom elements app.el.emit('ready') app.readyFired = true },400) // big js scripts need some parsing time }) }, }, init: function() { let {id,component,type} = this.parseAppURI(this.data.uri) let sel = `script#${component}` if( AFRAME.app[component] || AFRAME.components[component] || document.head.querySelector(sel) ) return AFRAME.app.add(component,this) AFRAME.app.add(component,this) 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,readyEvent){ let deps = [] if( !packages.map ) packages = Object.values(packages) packages.map( (package) => { try{ let id = package.split("/").pop() // prevent duplicate requests if( AFRAME.required[id] ) return AFRAME.required[id] = true if( !document.head.querySelector(`script#${id}`) ){ let {component,type} = this.parseAppURI(package) let p = new Promise( (resolve,reject) => { 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) } }catch(e){ console.error(`package ${package} could not be retrieved..aborting :(`); throw e; } }) Promise.all(deps).then( () => { this.el.emit( readyEvent || 'requireReady', packages) }) } } AFRAME.registerComponent('app', appComponent) AFRAME.registerComponent('com', appComponent) /* * Here are monkeypatched AFRAME component prototype functions * * monkeypatching initComponent will trigger events when components * are initialized (that way apps can react to attached components) * basically, in both situations: * * * * event 'foo' will be triggered as both entities (in)directly require component 'foo' */ AFRAME.AComponent.prototype.initComponent = function(initComponent){ return function(){ this.el.emit( this.attrName, this) this.scene = AFRAME.scenes[0] // mount scene for convenience return initComponent.apply(this,arguments) } }( AFRAME.AComponent.prototype.initComponent) AFRAME.AComponent.prototype.updateProperties = function(updateProperties){ return function(){ updateProperties.apply(this,arguments) if( !this.data || !this.data.uri || this.isApp ) return // only deal with apps (once) // ensure overlay 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") } 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}) } }) // reactify components with dom-definition if( this.data.uri && this.dom && !this.el.dom ){ tasks = { createReactiveDOMElement: () => { 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.el.dom.style.display = 'none' 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 }, setupListeners: () => { this.scene.addEventListener('apps:2D', () => this.el.setAttribute('visible', false) ) this.scene.addEventListener('apps:XR', () => { this.el.setAttribute('visible', true) this.el.setAttribute("html",`html:#${this.el.uid}; cursor:#cursor`) }) return tasks }, initAutoComponents: () => { for ( let i in AFRAME.app.components ) { this.el.setAttribute( i, AFRAME.app.components[i] ) } return tasks }, triggerKeyboardForInputs: () => { // https://developer.oculus.com/documentation/web/webxr-keyboard ; [...this.el.dom.querySelectorAll('[type=text]')].map( (input) => { let triggerKeyboard = function(){ this.focus() console.log("focus") } input.addEventListener('click', triggerKeyboard ) }) return tasks } } tasks .addCSS() .createReactiveDOMElement() .scaleDOMvsXR() .triggerKeyboardForInputs() .setupListeners() .initAutoComponents() document.querySelector('#overlay').appendChild(this.el.dom) this.el.emit('DOMready',{el: this.el.dom}) }else this.data = reactify( this.el, this.el ) // assign unique app id if( !this.el.uid ) this.el.uid = '_'+String(Math.random()).substr(10) // require coms let requires = {} for ( let i in AFRAME.app.components ) { if( !AFRAME.components[i] ) requires[i] = AFRAME.app.components[i] } if( this.requires ) requires = {...requires, ...this.requires } if( Object.values(requires).length ) this.require( requires, 'requires:ready' ) else this.el.emit('requires:ready' ) // mark app as being initialized this.isApp = true this.el.app = this } }( AFRAME.AComponent.prototype.updateProperties) document.head.innerHTML += ` `