aframe
Version:
A web framework for building virtual reality experiences.
270 lines (235 loc) • 7.92 kB
JavaScript
/* global CustomEvent */
var registerElement = require('./a-register-element').registerElement;
var isNode = require('./a-register-element').isNode;
var utils = require('../utils/');
var warn = utils.debug('core:a-node:warn');
var error = utils.debug('core:a-node:error');
/**
* Base class for A-Frame that manages loading of objects.
*
* Nodes can be modified using mixins.
* Nodes emit a `loaded` event when they and their children have initialized.
*/
module.exports = registerElement('a-node', {
prototype: Object.create(window.HTMLElement.prototype, {
createdCallback: {
value: function () {
this.computedMixinStr = '';
this.hasLoaded = false;
this.isNode = true;
this.mixinEls = [];
},
writable: window.debug
},
attachedCallback: {
value: function () {
var mixins;
this.sceneEl = this.closestScene();
if (!this.sceneEl) {
warn('You are attempting to attach <' + this.tagName + '> outside of an A-Frame ' +
'scene. Append this element to `<a-scene>` instead.');
}
this.hasLoaded = false;
this.emit('nodeready', undefined, false);
if (!this.isMixin) {
mixins = this.getAttribute('mixin');
if (mixins) { this.updateMixins(mixins); }
}
},
writable: window.debug
},
/**
* Handle mixin.
*/
attributeChangedCallback: {
value: function (attr, oldVal, newVal) {
// Ignore if `<a-node>` code is just updating computed mixin in the DOM.
if (newVal === this.computedMixinStr) { return; }
if (attr === 'mixin' && !this.isMixin) {
this.updateMixins(newVal, oldVal);
}
}
},
/**
* Returns the first scene by traversing up the tree starting from and
* including receiver element.
*/
closestScene: {
value: function closest () {
var element = this;
while (element) {
if (element.isScene) { break; }
element = element.parentElement;
}
return element;
}
},
/**
* Returns first element matching a selector by traversing up the tree starting
* from and including receiver element.
*
* @param {string} selector - Selector of element to find.
*/
closest: {
value: function closest (selector) {
var matches = this.matches || this.mozMatchesSelector ||
this.msMatchesSelector || this.oMatchesSelector || this.webkitMatchesSelector;
var element = this;
while (element) {
if (matches.call(element, selector)) { break; }
element = element.parentElement;
}
return element;
}
},
detachedCallback: {
value: function () {
this.hasLoaded = false;
}
},
/**
* Wait for children to load, if any.
* Then emit `loaded` event and set `hasLoaded`.
*/
load: {
value: function (cb, childFilter) {
var children;
var childrenLoaded;
var self = this;
if (this.hasLoaded) { return; }
// Default to waiting for all nodes.
childFilter = childFilter || isNode;
// Wait for children to load (if any), then load.
children = this.getChildren();
childrenLoaded = children.filter(childFilter).map(function (child) {
return new Promise(function waitForLoaded (resolve) {
if (child.hasLoaded) { return resolve(); }
child.addEventListener('loaded', resolve);
});
});
Promise.all(childrenLoaded).then(function emitLoaded () {
self.hasLoaded = true;
if (cb) { cb(); }
self.emit('loaded', undefined, false);
}).catch(function (err) {
error('Failure loading node: ', err);
});
},
writable: true
},
getChildren: {
value: function () {
return Array.prototype.slice.call(this.children, 0);
}
},
/**
* Unregister old mixins and listeners.
* Register new mixins and listeners.
* Registering means to update `this.mixinEls` with listeners.
*/
updateMixins: {
value: (function () {
var newMixinIdArray = [];
var oldMixinIdArray = [];
var mixinIds = {};
return function (newMixins, oldMixins) {
var i;
var newMixinIds;
var oldMixinIds;
newMixinIdArray.length = 0;
oldMixinIdArray.length = 0;
newMixinIds = newMixins ? utils.split(newMixins.trim(), /\s+/) : newMixinIdArray;
oldMixinIds = oldMixins ? utils.split(oldMixins.trim(), /\s+/) : oldMixinIdArray;
mixinIds.newMixinIds = newMixinIds;
mixinIds.oldMixinIds = oldMixinIds;
// Unregister old mixins.
for (i = 0; i < oldMixinIds.length; i++) {
if (newMixinIds.indexOf(oldMixinIds[i]) === -1) {
this.unregisterMixin(oldMixinIds[i]);
}
}
// Register new mixins.
this.computedMixinStr = '';
this.mixinEls.length = 0;
for (i = 0; i < newMixinIds.length; i++) {
this.registerMixin(document.getElementById(newMixinIds[i]));
}
// Update DOM. Keep track of `computedMixinStr` to not recurse back here after
// update.
if (this.computedMixinStr) {
this.computedMixinStr = this.computedMixinStr.trim();
window.HTMLElement.prototype.setAttribute.call(this, 'mixin',
this.computedMixinStr);
}
return mixinIds;
};
})()
},
/**
* From mixin ID, add mixin element to `mixinEls`.
*
* @param {Element} mixinEl
*/
registerMixin: {
value: function (mixinEl) {
var compositedMixinIds;
var i;
var mixin;
if (!mixinEl) { return; }
// Register composited mixins (if mixin has mixins).
mixin = mixinEl.getAttribute('mixin');
if (mixin) {
compositedMixinIds = utils.split(mixin.trim(), /\s+/);
for (i = 0; i < compositedMixinIds.length; i++) {
this.registerMixin(document.getElementById(compositedMixinIds[i]));
}
}
// Register mixin.
this.computedMixinStr = this.computedMixinStr + ' ' + mixinEl.id;
this.mixinEls.push(mixinEl);
}
},
setAttribute: {
value: function (attr, newValue) {
if (attr === 'mixin') { this.updateMixins(newValue); }
window.HTMLElement.prototype.setAttribute.call(this, attr, newValue);
}
},
unregisterMixin: {
value: function (mixinId) {
var i;
var mixinEls = this.mixinEls;
var mixinEl;
for (i = 0; i < mixinEls.length; ++i) {
mixinEl = mixinEls[i];
if (mixinId === mixinEl.id) {
mixinEls.splice(i, 1);
break;
}
}
}
},
/**
* Emit a DOM event.
*
* @param {string} name - Name of event.
* @param {object} [detail={}] - Custom data to pass as `detail` to the event.
* @param {boolean} [bubbles=true] - Whether the event should bubble.
* @param {object} [extraData] - Extra data to pass to the event, if any.
*/
emit: {
value: (function () {
var data = {};
return function (name, detail, bubbles, extraData) {
if (bubbles === undefined) { bubbles = true; }
data.bubbles = !!bubbles;
data.detail = detail;
// If extra data is present, we need to create a new object.
if (extraData) { data = utils.extend({}, extraData, data); }
this.dispatchEvent(new CustomEvent(name, data));
};
})(),
writable: window.debug
}
})
});