Home Reference Source

src/model/item.js

'use strict';

import { eachEntry } from '../core/util.js'
import Core from '../core/core.js'

/**
 * Item.
 * this can be an element of List.
 * 
 * @memberOf flatout
 */
export class Item extends Core {

  /**
   * Constructor.
   * 
   * @param {object} [defaultData] - default data
   */
  constructor(defaultData = {}) {
    super();
    Object.assign(this, defaultData);
  }

  /**
   * Add value to field-value.
   * 
   * @param {string} field - adding field name
   * @param {Any} value - adding value
   */
  add(field, value) {
    const cur = this[field];
    const isArr = cur !== undefined && cur instanceof Array;
    let newVal;
    if (isArr) {
      cur.push(value);
      newVal = cur;
    } else {
      newVal = cur + value;
    }
    this._updateField(field, newVal);
  }

  /**
   * Toggle boolean field-value
   * 
   * @param {string} field - toggling field name
   */
  toggle(field) {
    const cur = Boolean(this[field]);
    this._updateField(field, !cur);
  }

  /**
   * Update the pairs of field-value.
   * 
   * @param {Object} pairs - updating target pairs
   */
  update(pairs) {
    eachEntry(pairs, ([key, val]) => this._updateField(key, val))
  }

  /**
   * Destroy me.
   */
  destroy() {
    this.say('destroy', {});
  }

  /**
   * Update field with value.
   * 
   * @param {string} field - updating target
   * @param {Any} value    - new value
   */
  _updateField(field, value) {
    const cur = this[field];

    if (cur instanceof Item) {
      if (cur instanceof List) {
        cur.reset(value)
      } else {
        cur.update(value)
      }
    } else {
      this[field] = value;
      this.say('update', { field, newValue: value, oldValue: cur });
    }
  }
}

/**
 * List for plain object or Item.
 *
 * @memberOf flatout
 */
export class List extends Item {

  /**
   * Constructor.
   * 
   * @param {Array} [defaultData] - default data array
   * @param {object} [opts] - options
   * @param {boolean|Class<Item>} [opts.wrapItem] - Whether wrapping Item or not, or the sub class of Item.
   */
  constructor(defaultData = [], opts = {}) {
    super();

    if (opts.wrapItem) {
      if (opts.wrapItem === true) {
        this._F_itemClass = Item;
      } else {
        this._F_itemClass = opts.wrapItem;
      }
    } else {
      this._F_itemClass = null;
    }

    this._data = defaultData.map(item => this._wrapItem(item));
  }

  /**
   * If you dynamically change wrapping item class according to the item, override this method.
   *
   * @param {object} item - an item
   * @return {Class<View>}
   */
  itemClass(item) {
    return this._F_itemClass
  }

  /**
   * Return an item at position.
   * 
   * @param  {number} index - item position.
   * @return {*} item specified by index.
   */
  get(index) {
    return this._data[index];
  }

  /**
   * Add an item.
   * 
   * @param {Any} item - item
   * @param {number} [insertIndex] - optional insert position, add last if not defined
   */
  add(item, insertIndex) {
    item = this._wrapItem(item);
    if (insertIndex === undefined) {
      this._data.push(item);
    } else {
      this._data.splice(insertIndex, 0, item);
    }
    this.say('add', { item, index: insertIndex });
  }

  /**
   * Update an item at index.
   * 
   * @param {Any} item - item
   * @param {number} index - target index
   */
  update(item, index) {
    if (index === undefined) {
      // for when item updated
      index = this._data.indexOf(item);
    }
    const cur = this._data[index];
    if (cur !== item) {
      item = this._wrapItem(item);
      this._data[index] = item;
    }
    this.say('update', { item, index });
  }

  /**
   * Remove an item (specified by index).
   * 
   * @param {object|number} itemOrIndex - target item or the position.
   */
  remove(itemOrIndex) {
    let index;
    if (typeof itemOrIndex !== 'number') {
      index = this._data.indexOf(itemOrIndex) // find item
    } else {
      index = itemOrIndex;
    }
    const item = this._data.splice(index, 1)[0];
    this.say('remove', { item, index });
  }

  /**
   * Add items.
   * 
   * @param {Array} items - adding items
   * @param {number} [insertIndex] - insert position
   */
  addAll(items, insertIndex) {
    items.forEach(item => this.add(item, insertIndex))
  }

  /**
   * Remove all items.
   * 
   * @param {Object} opts - if opts.reverse is true, removing from last to first.
   */
  removeAll(opts = {}) {
    const { reverse = false } = opts;
    if (reverse) {
      for (let idx = this.length - 1; idx >= 0; idx--) {
        this.remove(idx);
      }
    } else {
      while(this._data.length) {
        this.remove(0);
      }
    }
  }

  /**
   * Remove last item.
   */
  removeLast() {
    this.remove(this.length - 1);
  }

  /**
   * replace all items
   *
   * @param {Array} [newValues] - new values or default empty array
   */
  reset(newValues = []) {
    this.removeAll();
    this.addAll(newValues);
  }

  /**
   * Iterates each item of self, return an index of the first item predicate returns true.
   * 
   * @param  {string|Function} predictOrField - predicate function or target field
   * @param  {*} [value] - finding value for target field
   * @return {object} matched first item or undefined
   */
  find(predictOrField, value) {
    const i = this.indexOf(predictOrField, value);
    return i >= 0 ? this._data[i] : undefined;
  }

  /**
   * Iterates each item of self, return the first item predicate returns true.
   * 
   * @param  {string|Function|Object} predictOrField - predicate function or target field, target object
   * @param  {*} [value] - finding value for target field
   * @return {number} matched first item index or -1 if not found
   */
  indexOf(predictOrField, value) {
    let judge;
    if (value !== undefined) {
      judge = it => it[predictOrField] === value
    } else if (this._isFn(predictOrField)) {
      judge = predictOrField;
    } else {
      return this._data.indexOf(predictOrField)
    }

    for (let i = 0, len = this._data.length; i < len; i++) {
      if (judge(this._data[i])) return i;
    }
    return -1;
  }

  /*
   * forEach method for data
   * @param {Function} cb - callback.
   */
  forEach(cb) {
    this._data.forEach(cb)
  }

  /*
   * some method for data
   * @param {Function} cb - callback returns true or false.
   */
  some(cb) {
    return this._data.some(cb)
  }

  /*
   * Data size
   * @type {number}
   */
  get length() {
    return this._data.length
  }

  /*
   * @return {Iterator} data iterator
   */
  [Symbol.iterator]() {
    return this._data[Symbol.iterator]()
  }

  /**
   * Wrap an item by Item class
   */
  _wrapItem(item) {
    if (!(item instanceof Item)) {
      const cls = this.itemClass(item);
      if (cls) {
        return new cls(item);
      }
    }
    return item;
  }
}