Newer
Older
reroad-test / 2020-ryusei / aframe-master / src / core / a-node.js
@ryusei ryusei on 22 Oct 2020 7 KB パノラマ表示
/* 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
    }
  })
});