Home Reference Source

props/Array.js


import PropEvents from './classes/PropEvents.js'
// import props from './index.js'
import { CHANGE } from './events/events.js'
import OBJECT from './Object.js'
import schemaBuilder from './classes/schema.js'
const makeIterator = (array) => {
  let nextIndex = 0
  return {
    next: function () {
      return nextIndex < array.length
        ? {
            value: array[nextIndex++],
            done: false
          }
        : {
            done: true
          }
    }
  }
}
/**
 * The Observable ARRAY Class
 */
class ARRAY extends PropEvents {
  /**
 * @param {Schema} sourceSchema - The Schema to be used as the shape of the items in the Array
 * @param {Object} options
 * @param {Object[]} options.values - The Array of initial values that will be consumed as Observables according to the Schema
 * @param {Object} options.parent - The Parent Object to which this Array is a member of
 * @param {String} options.name -The Name/key in the Parent Object to which this Array is assigned
 */
  constructor (sourceSchema = null, { values, parent, name } = {}) {
    super(parent)
    this._values = []
    this._schema = null
    if (sourceSchema) {
      const [[schema], schemaValues] = schemaBuilder(!(sourceSchema instanceof Array) ? [sourceSchema] : sourceSchema)
      // console.log({ schema })
      this._schema = schema
      // append schemaValues
      this._assign(values || schemaValues)
    }
    if (parent && name) {
      parent[name] = this
      this._name = name
    }
    // this.buildFromSchema(schema)
    // this.assign(values || [])

    if (parent) {
      Object.defineProperty(parent, name, {
        get: () => {
          return this
        },
        set: (v) => {
          this._assign(v)
        }
      })
    }
    Object.defineProperty(this, 'length', {
      get: () => {
        return this.getLength()
      },
      set: (v) => {}
    })
  }

  [Symbol.iterator] () { return makeIterator(this._values) }

  _itemProp () {
    if (this._schema.isObservable) {
      return this._schema
    } else if (this._schema instanceof Array) {
      return ARRAY
    } else {
      return OBJECT
    }
  }

  _assign (values) {
    const Prop = this._schema ? this._itemProp() : null
    if (!(values instanceof Array)) values = [values]
    // console.log({ Prop, values })
    this._values = values.map(val => {
      if (Prop) {
        if (this._schema.isObservable) {
          return new Prop(val, { parent: this })
        } else if (Prop === ARRAY) {
          return new ARRAY(this._schema[0], { parent: this, values: val })
        } else if (Prop === OBJECT) {
          return new OBJECT(this._schema, { parent: this, values: val })
        }
      }
      return null
    }).filter(v => v)
    // console.log(this._values)
  }

  _newItem (props) {
    if (this._schema) {
      const Prop = this._itemProp()
      if (this._schema.isObservable) {
        return new Prop(props, { parent: this })
      } else if (Prop === ARRAY) {
        return new ARRAY(this._schema[0], { parent: this, values: props })
      } else if (Prop === OBJECT) {
        return new OBJECT(this._schema, { parent: this, values: props })
      }
    }
  }

  _changed () {
    this._event[CHANGE]()
  }

  /**
   * Pushes an item onto the Array
 * @param {Object} props - The Object of values that obeys the item Schema
 * @param {Boolean} [preventDefault=false] - If true then the Change event is not fired
 */
  push (props, preventDefault = false) {
    this._values.push(this._newItem(props))
    if (!preventDefault) this._changed()
  }

  /**
   * Removes the first item of the Array and returns that item
 * @param {Boolean} [preventDefault=false] - If true then the Change event is not fired
 * @returns {Object} item - The Observalbe Object of the item removed from the array
 */
  shift (preventDefault = false) {
    const r = this._values.shift()
    if (!preventDefault) this._changed()
    return r
  }

  /**
   * Removes the last item of the Array and returns that item
 * @param {Boolean} [preventDefault=false] - If true then the Change event is not fired
 * @returns {Object} item - The Observalbe Object of the item removed from the array
 */
  pop (preventDefault = false) {
    const r = this._values.pop()
    if (!preventDefault) this._changed()
    return r
  }

  /**
   * Removes a range of items from the Array and returns them
 * @param {Integer} idx - The index of the first item to be removed
 * @param {Integer} toremove - The number of items to be removed
 * @param {Object[]} [props = []] - The array of objects (obeying the Schema) to inserted in the resulting gap
 * @param {Boolean} [preventDefault=false] - If true then the Change event is not fired
 * @returns {Object[]} items - The Observalbe Objects of the items removed from the array
 */
  splice (idx, toremove, props = [], preventDefault = false) {
    if (!(props instanceof Array)) props = [props]
    const r = this._values.splice(idx, toremove, props.map(prop => this._newItem(prop)))
    if (!preventDefault) this._changed()
    return r
  }

  /**
   * Returns a range of items from the Array
 * @param {Integer} idxFrom - The index of the first item to be selected
 * @param {Integer} idxTo - The index of the last item to be selected
 * @returns {Object[]} items - The Observalbe Objects of the items selected from the array
 */
  slice (idxFrom, idxTo) {
    const r = this._values.slice(idxFrom, idxTo)
    return r
  }

  /**
   * Removes any matching Items from the Array
 * @param {Object[]} items - An Array of Observable Objects/Items to be removed
 * @param {Boolean} [preventDefault=false] - If true then the Change event is not fired
 * @returns {Boolean} success - True if any of the Items were found and removed
 */
  remove (items, preventDefault = false) {
    if (!(typeof items instanceof Array)) items = [items]
    let success = false
    const vals = this._values
    for (let v = vals.length - 1; v >= 0; v--) {
      if (items.includes(vals[v])) {
        this._values.splice(v, 1)
        vals.splice(v, 1, true)
        success = true
      }
    }
    if (success && !preventDefault) this._changed()
    return success
  }

  /**
   * Returns an Array of the matching items found in the Array according to the provided method
 * @param {function(item: Object): Boolean} method - The predictate method for filtering the items
 * @returns {Boolean} success - True if any of the Items were found and removed
 */
  filter (method) {
    if (!(method instanceof Function)) return this._values
    return this._values.filter(method)
  }

  /**
   * Finds a matching item in the Array according to the provided method
 * @param {function(item: Object): Boolean} method - The predictate method for matching the item
 * @returns {Object} item - The found Observable Item
 */
  find (method) {
    if (!(method instanceof Function)) return this._values
    return this._values.find(method)
  }

  /**
   * Moves an item from one index to another
 * @param {Object} indexes
 * @param {Integer} indexes.from - The index of the item to move
 * @param {Integer} indexes.to - The index in the Array to move the item to
 * @param {Boolean} [preventDefault=false] - If true then the Change event is not fired
 */
  moveItem ({ from, to }, preventDefault = false) {
    if (from < to) {
      to--
    }
    this._values.splice(to, 0, this._values.splice(from, 1)[0])
    if (!preventDefault) this._changed()
  }

  /**
   * Returns the index of the provided Observable Item
 * @param {Object} item - The Observable Item to return the index of
 * @returns {Integer} index - The index of the found Observable Item
 */
  indexOf (item) {
    return this._values.indexOf(item)
  }

  /**
   * Returns a transposed Array
 * @param {function(item: Object): Object} method - The transposing method applied to each item in the Array
 * @returns {Object[]} items - The Array of transposed Items
 */
  map (method) {
    return this._values.map(method)
  }

  /**
   * Loop through the Array
 * @param {function(item: Object)} method - The method to be run on each Item
 */
  forEach (method) {
    return this._values.forEach(method)
  }

  /**
   * Reverses the Array order
 * @param {Boolean} [preventDefault=false] - If true then the Change event is not fired
 * @returns {Object[]} items - The new reversed Array of Items
 */
  reverse (preventDefault = false) {
    this._values = this._values.reverse()
    if (!preventDefault) this._changed()
    return this._values
  }

  /**
   * Determines if the provided item is in the Array
 * @param {Object|String|Number|Boolean} value - The value of an item (according to the Schema) to check for
 * @returns {Boolean} found - True if the item/value has been found
 */
  includes (value) {
    const toCheck = (value && value.get ? value.get() : (value.valueOf ? value.valueOf() : value))
    return !!this._values.find(val => {
      return (val && val.get ? val.get() : (val && val.valueOf ? val.valueOf() : val)) === toCheck
    })
  }

  /**
   * Returns the Observable Item by the Index
 * @param {Integer} [idx=null] - The Index of the item to return (if null, all items are returned)
 * @returns {Object} item - The item found at the requested index
 */
  get (idx = null) {
    return idx === null
      ? this._values
      : (
          idx >= this._values.length ? null : this._values[idx]
        )
  }

  /**
   * Sets the internal array of items
 * @param {Object[]} [props=[]] - The array of objects to construct the Observable Items (according to the Schema)
 * @param {Boolean} [preventDefault=false] - If true then the Change event is not fired
 * @returns {Object[]} items - The new array of Observable Items
 */
  set (props = [], preventDefault = false) {
    // console.log(props, this.getKey())
    if (!(props instanceof Array)) {
      // console.log({props})
      throw new Error('Expected Array in props')
    }
    // const Schema = this._schema
    // console.log({props})
    this._values = props.map(p => {
      // if (typeof Schema === 'function' && p instanceof Schema ) return p
      return this._newItem(p)
    })
    // if (!this._schema) console.log(this._values)
    // console.log(this)
    if (!preventDefault) this._changed()
    return this._values
  }

  _arrayState () {
    return JSON.stringify(this._values.sort((a, b) => {
      if (a._id) {
        return a._id - b._id
      } else {
        return a - b
      }
    }))
  }

  /**
   * Update the items within the Array. If an item has the same prop *_id*, then the item reference is preserved and the props are updated
 * @param {Object[]} [props=[]] - The array of objects to construct the Observable Items (according to the Schema)
 * @param {Boolean} [preventDefault=false] - If true then the Change event is not fired
 * @returns {Object[]} items - The new array of Observable Items
 */
  update (props, preventDefault = false) {
    if (!(props instanceof Array)) throw new Error('Expected Array in props')
    const currentLength = this._values.length
    // const currentState = this.arrayState()
    const foundItems = []
    let idx = 0
    // const list = []
    let isReordered = false

    while (props.length > 0) {
      const data = props.shift()
      let foundItem = this._values.find(v => {
        return v.equals(data)
      })
      if (data._id && !foundItem) {
        foundItem = this._values.find(v => {
          // console.log(v._id.valueOf())
          return (!v._id || !v._id.valueOf()) && (!foundItems.includes(v))
        })
      }
      if (foundItem) {
        foundItems.push(foundItem)
        foundItem.update(data, preventDefault)
        if (this._values.indexOf(foundItem) !== idx) isReordered = true
      } else {
        const _newItem = this._newItem(data)
        foundItems.push(_newItem)
        // this._values.push(_newItem)
      }
      // } else {

      // }
      idx++
    }
    // console.log(this.getKey())
    // console.log(JSON.parse(JSON.stringify(foundItems)))
    this._values = foundItems
    // for (let l = this._values.length - 1; l >= 0; l--) {
    //   if (!foundItems.includes(this._values[l])) this._values.splice(l, 1)
    // }

    // const newState = this.arrayState()
    // if (this.getKey() === 'pending_invite') {
    // console.log(props)
    // }
    const newLength = this._values.length
    // console.log(currentLength , newLength)
    if (!preventDefault && (isReordered || currentLength !== newLength)) this._changed(true)
    // if (!preventDefault && currentState !== newState) this._changed(true)
    return this._values
  }

  /**
   * Returns the length of the Array. Used by the virtual property *length*
 * @returns {Integer} length - The number of items in the Array
 */
  getLength () {
    return this._values.length
  }

  /**
   * Returns an Array of the parsed values of each item
 * @returns {Object[]} values - Array of primitive item values
 */
  valueOf () {
    return this._values.map(r => r.valueOf ? r.valueOf() : r)
  }

  /**
   * Returns the Array of Observable Items
 * @returns {Object[]} items - Array of Observable Items
 */
  toJSON () {
    return this._values
  }

  /**
   * Returns the Array of Objects
 * @returns {Object[]} objects - Array of Objects
 */
  toObject () {
    return this._values.map(r => r.toObject ? r.toObject() : r)
  }

  /**
   * Reorders the Array
 * @param {function(a: Object, b: Object): Integer} sortMethod - The sort method that determines whether items a and b swap places
 * @param {Boolean} [preventDefault=false] - If true then the Change event is not fired
 * @returns {Object[]} items - The new array of Observable Items
 */
  sort (sortMethod = (a, b) => {
    return b - a
  }, preventDefault = false) {
    const was = this._arrayState()
    this._values = this._values.sort(sortMethod)
    const isNow = this._arrayState()
    if (was !== isNow && !preventDefault) this._changed()
    return this._values
  }
}

export default ARRAY