Source: View.js

View.js

import Parser from './Parser';
import {elements} from './main';
import {isSubsetArray} from './utils';

/**
 * The root element of the entire tree of objects. Responsible for loading the tree and manipulating the elements.
 * @example
 * const mark = new markus.View('view.mark', () => {
 *   mark.get('resources').load(() => {
 *     mark.get('app').start();
 *   });
 * }, [Player, Bottle, EnemyController]);
 * @class
 * @memberof markus
 * @param [filepath] {string} File path to main markfile
 * @param [onReady] {function} Called when the markfile is loaded and the rendering
 */
export default class View {
  constructor(filepath, onReady) {
    /**
     * Root child list which contains all the root elements of the mark file
     * @member {Element[]}
     */
    this.childList = [];

    /**
     * Marklang parser
     * @member {markus.Parser}
     */
    this.parser = new Parser('ajax');
    if(filepath) {
      this.parser.parseMarkfile(filepath).then(tree => {
        this.add(tree);
        onReady && onReady(this);
      });
    }

    /**
     * Main ticker for update all elements. Initially active.
     * @member {PIXI.ticker.Ticker}
     */
    this.ticker = new PIXI.ticker.Ticker();
    this.ticker.add(dt => this.updateElements(dt));
    this.ticker.start();
  }

  /**
   * Update all elements in root childList
   * @private
   * @param dt {number} Delta time
   * @param [elms=view.childList] {Array} Array of element
   */
  updateElements(dt, elms=this.childList) {
    for(let i = 0; i < elms.length; i++) {
      elms[i].tick && elms[i].tick(dt);
      this.updateElements(dt, elms[i].childList);
    }
  }


  /**
   * Add elements to parent node
   * @param value {(string|string[]|Preset|Preset[])} Elements to be added can be either a string or an array of marklang markup strings, or a Preset or an array of Presets.
   * @param [parent=view] {Element} Parent element
   * @returns {(Element|Element[])} Returns added items
   *
   * @example
   * mark.add('enemy.zombie(level=23)', enemyController);
   * mark.add(['sprite.tag(prop=1)', 'text | SOME TEXT']);
   * mark.add([presetEnemy, presetEnemy], enemyController);
   * mark.add(presetEnemy, enemyController);
   */
  add(value, parent=this) {
    if(!Array.isArray(value)) {
      return this.addPreset(value, parent);
    }

    let res = [];
    for(let i = 0; i < value.length; i++) {
      let preset = value[i];
      if(typeof preset === 'string') {
        preset = this.parser.parsePreset(preset);
      }
      res.push(this.addPreset(preset, parent));
    }
    return res;
  }


  /**
   * Add Preset to parent node
   * @param preset {Preset} Preset object
   * @param [parent=view] {Element} Parent element
   * @returns {Element} Returns added element
   */
  addPreset(preset={}, parent=this) {
    if(preset.type !== 'elementNode') {
      throw Error('Preset cannot be activate. His type is not elementNode');
    }

    let elementName = preset.element.slice(0, 1).toUpperCase() + preset.element.slice(1);
    let elmCtor = elements[elementName] || elements[preset.element];

    if(!elmCtor) {
      throw Error('Element "' + preset.element + '" is not defined');
    }

    // merge props with mixins
    let props = preset.props;
    if(this.get('mixins')) {
      props = this.get('mixins').merge(preset, preset.props);
    }

    let elm = new elmCtor(Object.assign(preset, {parent: parent, view: this, props: props}));
    elm.setProps(elm.props);
    return elm;
  }


  /**
   * Remove elements from parent node
   * @param value {(string|string[]|Element|Element[])} Elements to be removed can be either a string or an array of marklang markup selectors strings, or a Element or an array of Elements.
   * @param [parent=view] {Element} Parent element
   * @returns {(Element|Element[])} Returns removed items
   *
   * @example
   * mark.remove('enemy.zombie(level=23)', enemyController);
   * mark.remove([Enemy, Enemy], enemyController);
   * mark.remove(Enemy, enemyController);
   */
  remove(value, parent=this) {
    if(!Array.isArray(value)) {
      return this.removeElement(value, value.parentElement);
    }

    let res = [];
    for(let i = 0; i < value.length; i++) {
      let preset = value[i];
      if(typeof preset === 'string') {
        preset = this.get(preset, parent);
      }
      res.push(this.removeElement(preset, preset.parentElement));
    }
    return res;
  }

  /**
   * Remove Element from parent node
   * @param el {ELement} Element class
   * @param [parent=view] {Element} Parent element
   * @returns {Element} Returns removed element
   */
  removeElement(el, parent=this) {
    let index = parent.childList.indexOf(el);
    if(index !== -1) {
      el.onRemove && el.onRemove();
      parent.childList.splice(index, 1);
      if(el instanceof PIXI.DisplayObject) {
        if(parent instanceof PIXI.DisplayObject) {
          parent.removeChild(el);
        }
        else {
          parent.stage.removeChild(el);
        }
      }
    }
    return el;
  }

  /**
   * Get element by selector
   * @param selector {ELement} Element selector
   * @param [parent=view] {Element} Parent element for search
   * @returns {Element}
   *
   * @example
   * mark.get('sprite.cat');
   * > Sprite
   *
   * mark.get('sprite.cat.black');
   * > null
   */
  get(selector, parent=this) {
    let q = this.parser.parseQuery(selector);
    return this.find(q, parent.childList);
  }

  /**
   * Get elements by selector
   * @param selector {ELement} Elements selector
   * @param [parent=view] {Element} Parent element for search
   * @returns {Element[]}
   *
   * @example
   * mark.getAll('sprite.cat');
   * > [Sprite, Sptite, Sprite]
   *
   * mark.getAll('#cat');
   * > null
   */
  getAll(selector, parent=this) {
    let q = this.parser.parseQuery(selector);
    return this.find(q, parent.childList, true);
  }

  /**
   * General method for searching items on request
   * @param q {Object} Query object
   * @param q.id {string} Query id
   * @param q.element {string} Query element name
   * @param q.tags {string[]} Query element tags
   * @param elms {Element[]} List Elements
   * @param [isAll=false] {booleon} Do I need to search for all elements by q
   * @returns {(Element[]|Element)}
   *
   * @example
   * mark.find({element: 'sprite', tags: ['cat'], mark.root, true});
   * > [Sprite, Sptite, Sprite]
   */
  find(q, elms, isAll=false) {
    let res = [];
    for(let i = 0; i < elms.length; i++) {
      if(this.isSelectorOfElement(q, elms[i])) {
        if(isAll) {
          res.push(elms[i]);
        }
        else {
          return elms[i];
        }
      }

      // If childList has children and the search is performed globally
      if(elms[i].childList.length) {
        let find = this.find(q, elms[i].childList, isAll);
        if(!isAll && find) {
          return find;
        }
        else {
          res = res.concat(find);
        }
      }
    }
    return isAll ? res : null;
  }

  /**
   * Checks if the request is suitable for the item
   * @param q {Object} Query object
   * @param q.id {string} Query id
   * @param q.element {string} Query element name
   * @param q.tags {string[]} Query element tags
   * @param elm {Element} Checked Element
   * @returns {booleon}
   */
  isSelectorOfElement(q, elm) {
    let isEl = q.element ? q.element === elm.element : true;
    let isId = q.id ? q.id === elm.id : true;
    let isTags = isSubsetArray(elm.tags, q.tags);

    return isEl && isId && isTags;
  }
}