const negate = require('lodash/negate'); const Element = require('./Element'); const ArraySlice = require('../ArraySlice'); /** * @class * * @param {Element[]} content * @param meta * @param attributes */ class ArrayElement extends Element { constructor(content, meta, attributes) { super(content || [], meta, attributes); this.element = 'array'; } primitive() { return 'array'; } /** * @returns {Element} */ get(index) { return this.content[index]; } /** * Helper for returning the value of an item * This works for both ArrayElement and ObjectElement instances */ getValue(indexOrKey) { const item = this.get(indexOrKey); if (item) { return item.toValue(); } return undefined; } /** * @returns {Element} */ getIndex(index) { return this.content[index]; } set(index, value) { this.content[index] = this.refract(value); return this; } remove(index) { const removed = this.content.splice(index, 1); if (removed.length) { return removed[0]; } return null; } /** * @param callback - Function to execute for each element * @param thisArg - Value to use as this (i.e the reference Object) when executing callback */ map(callback, thisArg) { return this.content.map(callback, thisArg); } /** * Maps and then flattens the results. * @param callback - Function to execute for each element. * @param thisArg - Value to use as this (i.e the reference Object) when executing callback * @returns {array} */ flatMap(callback, thisArg) { return this .map(callback, thisArg) .reduce((a, b) => a.concat(b), []); } /** * Returns an array containing the truthy results of calling the given transformation with each element of this sequence * @param transform - A closure that accepts an element of this array as its argument and returns an optional value. * @param thisArg - Value to use as this (i.e the reference Object) when executing callback * @memberof ArrayElement.prototype * @returns An array of the non-undefined results of calling transform with each element of the array */ compactMap(transform, thisArg) { const results = []; this.forEach((element) => { const result = transform.bind(thisArg)(element); if (result) { results.push(result); } }); return results; } /** * @param callback - Function to execute for each element * @param thisArg - Value to use as this (i.e the reference Object) when executing callback * @returns {ArraySlice} */ filter(callback, thisArg) { return new ArraySlice(this.content.filter(callback, thisArg)); } /** * @param callback - Function to execute for each element * @param thisArg - Value to use as this (i.e the reference Object) when executing callback * @returns {ArraySlice} */ reject(callback, thisArg) { return this.filter(negate(callback), thisArg); } /** * This is a reduce function specifically for Minim arrays and objects. It * allows for returning normal values or Minim instances, so it converts any * primitives on each step. */ reduce(callback, initialValue) { let startIndex; let memo; // Allows for defining a starting value of the reduce if (initialValue !== undefined) { startIndex = 0; memo = this.refract(initialValue); } else { startIndex = 1; // Object Element content items are member elements. Because of this, // the memo should start out as the member value rather than the // actual member itself. memo = this.primitive() === 'object' ? this.first.value : this.first; } // Sending each function call to the registry allows for passing Minim // instances through the function return. This means you can return // primitive values or return Minim instances and reduce will still work. for (let i = startIndex; i < this.length; i += 1) { const item = this.content[i]; if (this.primitive() === 'object') { memo = this.refract(callback(memo, item.value, item.key, item, this)); } else { memo = this.refract(callback(memo, item, i, this)); } } return memo; } /** * @callback forEachCallback * @param {Element} currentValue * @param {NumberElement} index */ /** * @param {forEachCallback} callback - Function to execute for each element * @param thisArg - Value to use as this (i.e the reference Object) when executing callback * @memberof ArrayElement.prototype */ forEach(callback, thisArg) { this.content.forEach((item, index) => { callback.bind(thisArg)(item, this.refract(index)); }); } /** * @returns {Element} */ shift() { return this.content.shift(); } /** * @param value */ unshift(value) { this.content.unshift(this.refract(value)); } /** * @param value */ push(value) { this.content.push(this.refract(value)); return this; } /** * @param value */ add(value) { this.push(value); } /** * Recusively search all descendents using a condition function. * @returns {Element[]} */ findElements(condition, givenOptions) { const options = givenOptions || {}; const recursive = !!options.recursive; const results = options.results === undefined ? [] : options.results; // The forEach method for Object Elements returns value, key, and member. // This passes those along to the condition function below. this.forEach((item, keyOrIndex, member) => { // We use duck-typing here to support any registered class that // may contain other elements. if (recursive && (item.findElements !== undefined)) { item.findElements(condition, { results, recursive, }); } if (condition(item, keyOrIndex, member)) { results.push(item); } }); return results; } /** * Recusively search all descendents using a condition function. * @param condition * @returns {ArraySlice} */ find(condition) { return new ArraySlice(this.findElements(condition, { recursive: true })); } /** * @param {string} element * @returns {ArraySlice} */ findByElement(element) { return this.find(item => item.element === element); } /** * @param {string} className * @returns {ArraySlice} * @memberof ArrayElement.prototype */ findByClass(className) { return this.find(item => item.classes.includes(className)); } /** * Search the tree recursively and find the element with the matching ID * @param {string} id * @returns {Element} * @memberof ArrayElement.prototype */ getById(id) { return this.find(item => item.id.toValue() === id).first; } /** * Looks for matching children using deep equality * @param value * @returns {boolean} */ includes(value) { return this.content.some(element => element.equals(value)); } /** * Looks for matching children using deep equality * @param value * @returns {boolean} * @see includes * @deprecated method was replaced by includes */ contains(value) { return this.includes(value); } // Fantasy Land /** * @returns {ArrayElement} An empty array element */ empty() { return new this.constructor([]); } ['fantasy-land/empty']() { return this.empty(); } /** * @param {ArrayElement} other * @returns {ArrayElement} */ concat(other) { return new this.constructor(this.content.concat(other.content)); } ['fantasy-land/concat'](other) { return this.concat(other); } ['fantasy-land/map'](transform) { return new this.constructor(this.map(transform)); } ['fantasy-land/chain'](transform) { return this .map(element => transform(element), this) .reduce((a, b) => a.concat(b), this.empty()); } ['fantasy-land/filter'](callback) { return new this.constructor(this.content.filter(callback)); } ['fantasy-land/reduce'](transform, initialValue) { return this.content.reduce(transform, initialValue); } /** * Returns the length of the collection * @type number */ get length() { return this.content.length; } /** * Returns whether the collection is empty * @type boolean */ get isEmpty() { return this.content.length === 0; } /** * Return the first item in the collection * @type Element */ get first() { return this.getIndex(0); } /** * Return the second item in the collection * @type Element */ get second() { return this.getIndex(1); } /** * Return the last item in the collection * @type Element */ get last() { return this.getIndex(this.length - 1); } } /** * @returns {ArrayElement} An empty array element */ ArrayElement.empty = function empty() { return new this(); }; ArrayElement['fantasy-land/empty'] = ArrayElement.empty; if (typeof Symbol !== 'undefined') { ArrayElement.prototype[Symbol.iterator] = function symbol() { return this.content[Symbol.iterator](); }; } module.exports = ArrayElement;