UNPKG

vis-data

Version:

Manage unstructured data using DataSet. Add, update, and remove data, and listen for changes in the data.

1,428 lines (1,422 loc) 72.3 kB
/** * vis-data * http://visjs.org/ * * Manage unstructured data using DataSet. Add, update, and remove data, and listen for changes in the data. * * @version 7.0.0 * @date 2020-08-02T17:48:43.502Z * * @copyright (c) 2011-2017 Almende B.V, http://almende.com * @copyright (c) 2017-2019 visjs contributors, https://github.com/visjs * * @license * vis.js is dual licensed under both * * 1. The Apache 2.0 License * http://www.apache.org/licenses/LICENSE-2.0 * * and * * 2. The MIT License * http://opensource.org/licenses/MIT * * vis.js may be distributed under either license. */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('uuid'), require('vis-util/esnext/umd/vis-util.js')) : typeof define === 'function' && define.amd ? define(['exports', 'uuid', 'vis-util/esnext/umd/vis-util.js'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.vis = global.vis || {}, global.uuidv4, global.vis)); }(this, (function (exports, uuid, esnext) { /* eslint @typescript-eslint/member-ordering: ["error", { "classes": ["field", "constructor", "method"] }] */ /** * Create new data pipe. * * @param from - The source data set or data view. * * @remarks * Example usage: * ```typescript * interface AppItem { * whoami: string; * appData: unknown; * visData: VisItem; * } * interface VisItem { * id: number; * label: string; * color: string; * x: number; * y: number; * } * * const ds1 = new DataSet<AppItem, "whoami">([], { fieldId: "whoami" }); * const ds2 = new DataSet<VisItem, "id">(); * * const pipe = createNewDataPipeFrom(ds1) * .filter((item): boolean => item.enabled === true) * .map<VisItem, "id">((item): VisItem => item.visData) * .to(ds2); * * pipe.start(); * ``` * * @returns A factory whose methods can be used to configure the pipe. */ function createNewDataPipeFrom(from) { return new DataPipeUnderConstruction(from); } /** * Internal implementation of the pipe. This should be accessible only through * `createNewDataPipeFrom` from the outside. * * @typeparam SI - Source item type. * @typeparam SP - Source item type's id property name. * @typeparam TI - Target item type. * @typeparam TP - Target item type's id property name. */ class SimpleDataPipe { /** * Create a new data pipe. * * @param _source - The data set or data view that will be observed. * @param _transformers - An array of transforming functions to be used to * filter or transform the items in the pipe. * @param _target - The data set or data view that will receive the items. */ constructor(_source, _transformers, _target) { this._source = _source; this._transformers = _transformers; this._target = _target; /** * Bound listeners for use with `DataInterface['on' | 'off']`. */ this._listeners = { add: this._add.bind(this), remove: this._remove.bind(this), update: this._update.bind(this), }; } /** @inheritdoc */ all() { this._target.update(this._transformItems(this._source.get())); return this; } /** @inheritdoc */ start() { this._source.on("add", this._listeners.add); this._source.on("remove", this._listeners.remove); this._source.on("update", this._listeners.update); return this; } /** @inheritdoc */ stop() { this._source.off("add", this._listeners.add); this._source.off("remove", this._listeners.remove); this._source.off("update", this._listeners.update); return this; } /** * Apply the transformers to the items. * * @param items - The items to be transformed. * * @returns The transformed items. */ _transformItems(items) { return this._transformers.reduce((items, transform) => { return transform(items); }, items); } /** * Handle an add event. * * @param _name - Ignored. * @param payload - The payload containing the ids of the added items. */ _add(_name, payload) { if (payload == null) { return; } this._target.add(this._transformItems(this._source.get(payload.items))); } /** * Handle an update event. * * @param _name - Ignored. * @param payload - The payload containing the ids of the updated items. */ _update(_name, payload) { if (payload == null) { return; } this._target.update(this._transformItems(this._source.get(payload.items))); } /** * Handle a remove event. * * @param _name - Ignored. * @param payload - The payload containing the data of the removed items. */ _remove(_name, payload) { if (payload == null) { return; } this._target.remove(this._transformItems(payload.oldData)); } } /** * Internal implementation of the pipe factory. This should be accessible * only through `createNewDataPipeFrom` from the outside. * * @typeparam TI - Target item type. * @typeparam TP - Target item type's id property name. */ class DataPipeUnderConstruction { /** * Create a new data pipe factory. This is an internal constructor that * should never be called from outside of this file. * * @param _source - The source data set or data view for this pipe. */ constructor(_source) { this._source = _source; /** * Array transformers used to transform items within the pipe. This is typed * as any for the sake of simplicity. */ this._transformers = []; } /** * Filter the items. * * @param callback - A filtering function that returns true if given item * should be piped and false if not. * * @returns This factory for further configuration. */ filter(callback) { this._transformers.push((input) => input.filter(callback)); return this; } /** * Map each source item to a new type. * * @param callback - A mapping function that takes a source item and returns * corresponding mapped item. * * @typeparam TI - Target item type. * @typeparam TP - Target item type's id property name. * * @returns This factory for further configuration. */ map(callback) { this._transformers.push((input) => input.map(callback)); return this; } /** * Map each source item to zero or more items of a new type. * * @param callback - A mapping function that takes a source item and returns * an array of corresponding mapped items. * * @typeparam TI - Target item type. * @typeparam TP - Target item type's id property name. * * @returns This factory for further configuration. */ flatMap(callback) { this._transformers.push((input) => input.flatMap(callback)); return this; } /** * Connect this pipe to given data set. * * @param target - The data set that will receive the items from this pipe. * * @returns The pipe connected between given data sets and performing * configured transformation on the processed items. */ to(target) { return new SimpleDataPipe(this._source, this._transformers, target); } } /** * Determine whether a value can be used as an id. * * @param value - Input value of unknown type. * * @returns True if the value is valid id, false otherwise. */ function isId(value) { return typeof value === "string" || typeof value === "number"; } /* eslint @typescript-eslint/member-ordering: ["error", { "classes": ["field", "constructor", "method"] }] */ /** * A queue. * * @typeParam T - The type of method names to be replaced by queued versions. */ class Queue { /** * Construct a new Queue. * * @param options - Queue configuration. */ constructor(options) { this._queue = []; this._timeout = null; this._extended = null; // options this.delay = null; this.max = Infinity; this.setOptions(options); } /** * Update the configuration of the queue. * * @param options - Queue configuration. */ setOptions(options) { if (options && typeof options.delay !== "undefined") { this.delay = options.delay; } if (options && typeof options.max !== "undefined") { this.max = options.max; } this._flushIfNeeded(); } /** * Extend an object with queuing functionality. * The object will be extended with a function flush, and the methods provided in options.replace will be replaced with queued ones. * * @param object - The object to be extended. * @param options - Additional options. * * @returns The created queue. */ static extend(object, options) { const queue = new Queue(options); if (object.flush !== undefined) { throw new Error("Target object already has a property flush"); } object.flush = () => { queue.flush(); }; const methods = [ { name: "flush", original: undefined, }, ]; if (options && options.replace) { for (let i = 0; i < options.replace.length; i++) { const name = options.replace[i]; methods.push({ name: name, // @TODO: better solution? original: object[name], }); // @TODO: better solution? queue.replace(object, name); } } queue._extended = { object: object, methods: methods, }; return queue; } /** * Destroy the queue. The queue will first flush all queued actions, and in case it has extended an object, will restore the original object. */ destroy() { this.flush(); if (this._extended) { const object = this._extended.object; const methods = this._extended.methods; for (let i = 0; i < methods.length; i++) { const method = methods[i]; if (method.original) { // @TODO: better solution? object[method.name] = method.original; } else { // @TODO: better solution? delete object[method.name]; } } this._extended = null; } } /** * Replace a method on an object with a queued version. * * @param object - Object having the method. * @param method - The method name. */ replace(object, method) { /* eslint-disable-next-line @typescript-eslint/no-this-alias */ const me = this; const original = object[method]; if (!original) { throw new Error("Method " + method + " undefined"); } object[method] = function (...args) { // add this call to the queue me.queue({ args: args, fn: original, context: this, }); }; } /** * Queue a call. * * @param entry - The function or entry to be queued. */ queue(entry) { if (typeof entry === "function") { this._queue.push({ fn: entry }); } else { this._queue.push(entry); } this._flushIfNeeded(); } /** * Check whether the queue needs to be flushed. */ _flushIfNeeded() { // flush when the maximum is exceeded. if (this._queue.length > this.max) { this.flush(); } // flush after a period of inactivity when a delay is configured if (this._timeout != null) { clearTimeout(this._timeout); this._timeout = null; } if (this.queue.length > 0 && typeof this.delay === "number") { this._timeout = setTimeout(() => { this.flush(); }, this.delay); } } /** * Flush all queued calls */ flush() { this._queue.splice(0).forEach((entry) => { entry.fn.apply(entry.context || entry.fn, entry.args || []); }); } } /* eslint-disable @typescript-eslint/member-ordering */ /** * [[DataSet]] code that can be reused in [[DataView]] or other similar implementations of [[DataInterface]]. * * @typeParam Item - Item type that may or may not have an id. * @typeParam IdProp - Name of the property that contains the id. */ class DataSetPart { constructor() { this._subscribers = { "*": [], add: [], remove: [], update: [], }; /** * @deprecated Use on instead (PS: DataView.subscribe === DataView.on). */ this.subscribe = DataSetPart.prototype.on; /** * @deprecated Use off instead (PS: DataView.unsubscribe === DataView.off). */ this.unsubscribe = DataSetPart.prototype.off; } /** * Trigger an event * * @param event - Event name. * @param payload - Event payload. * @param senderId - Id of the sender. */ _trigger(event, payload, senderId) { if (event === "*") { throw new Error("Cannot trigger event *"); } [...this._subscribers[event], ...this._subscribers["*"]].forEach((subscriber) => { subscriber(event, payload, senderId != null ? senderId : null); }); } /** * Subscribe to an event, add an event listener. * * @remarks Non-function callbacks are ignored. * * @param event - Event name. * @param callback - Callback method. */ on(event, callback) { if (typeof callback === "function") { this._subscribers[event].push(callback); } // @TODO: Maybe throw for invalid callbacks? } /** * Unsubscribe from an event, remove an event listener. * * @remarks If the same callback was subscribed more than once **all** occurences will be removed. * * @param event - Event name. * @param callback - Callback method. */ off(event, callback) { this._subscribers[event] = this._subscribers[event].filter((subscriber) => subscriber !== callback); } } /** * Data stream * * @remarks * [[DataStream]] offers an always up to date stream of items from a [[DataSet]] or [[DataView]]. * That means that the stream is evaluated at the time of iteration, conversion to another data type or when [[cache]] is called, not when the [[DataStream]] was created. * Multiple invocations of for example [[toItemArray]] may yield different results (if the data source like for example [[DataSet]] gets modified). * * @typeparam Item - The item type this stream is going to work with. */ class DataStream { /** * Create a new data stream. * * @param _pairs - The id, item pairs. */ constructor(_pairs) { this._pairs = _pairs; } /** * Return an iterable of key, value pairs for every entry in the stream. */ *[Symbol.iterator]() { for (const [id, item] of this._pairs) { yield [id, item]; } } /** * Return an iterable of key, value pairs for every entry in the stream. */ *entries() { for (const [id, item] of this._pairs) { yield [id, item]; } } /** * Return an iterable of keys in the stream. */ *keys() { for (const [id] of this._pairs) { yield id; } } /** * Return an iterable of values in the stream. */ *values() { for (const [, item] of this._pairs) { yield item; } } /** * Return an array containing all the ids in this stream. * * @remarks * The array may contain duplicities. * * @returns The array with all ids from this stream. */ toIdArray() { return [...this._pairs].map((pair) => pair[0]); } /** * Return an array containing all the items in this stream. * * @remarks * The array may contain duplicities. * * @returns The array with all items from this stream. */ toItemArray() { return [...this._pairs].map((pair) => pair[1]); } /** * Return an array containing all the entries in this stream. * * @remarks * The array may contain duplicities. * * @returns The array with all entries from this stream. */ toEntryArray() { return [...this._pairs]; } /** * Return an object map containing all the items in this stream accessible by ids. * * @remarks * In case of duplicate ids (coerced to string so `7 == '7'`) the last encoutered appears in the returned object. * * @returns The object map of all id → item pairs from this stream. */ toObjectMap() { const map = Object.create(null); for (const [id, item] of this._pairs) { map[id] = item; } return map; } /** * Return a map containing all the items in this stream accessible by ids. * * @returns The map of all id → item pairs from this stream. */ toMap() { return new Map(this._pairs); } /** * Return a set containing all the (unique) ids in this stream. * * @returns The set of all ids from this stream. */ toIdSet() { return new Set(this.toIdArray()); } /** * Return a set containing all the (unique) items in this stream. * * @returns The set of all items from this stream. */ toItemSet() { return new Set(this.toItemArray()); } /** * Cache the items from this stream. * * @remarks * This method allows for items to be fetched immediatelly and used (possibly multiple times) later. * It can also be used to optimize performance as [[DataStream]] would otherwise reevaluate everything upon each iteration. * * ## Example * ```javascript * const ds = new DataSet([…]) * * const cachedStream = ds.stream() * .filter(…) * .sort(…) * .map(…) * .cached(…) // Data are fetched, processed and cached here. * * ds.clear() * chachedStream // Still has all the items. * ``` * * @returns A new [[DataStream]] with cached items (detached from the original [[DataSet]]). */ cache() { return new DataStream([...this._pairs]); } /** * Get the distinct values of given property. * * @param callback - The function that picks and possibly converts the property. * * @typeparam T - The type of the distinct value. * * @returns A set of all distinct properties. */ distinct(callback) { const set = new Set(); for (const [id, item] of this._pairs) { set.add(callback(item, id)); } return set; } /** * Filter the items of the stream. * * @param callback - The function that decides whether an item will be included. * * @returns A new data stream with the filtered items. */ filter(callback) { const pairs = this._pairs; return new DataStream({ *[Symbol.iterator]() { for (const [id, item] of pairs) { if (callback(item, id)) { yield [id, item]; } } }, }); } /** * Execute a callback for each item of the stream. * * @param callback - The function that will be invoked for each item. */ forEach(callback) { for (const [id, item] of this._pairs) { callback(item, id); } } /** * Map the items into a different type. * * @param callback - The function that does the conversion. * * @typeparam Mapped - The type of the item after mapping. * * @returns A new data stream with the mapped items. */ map(callback) { const pairs = this._pairs; return new DataStream({ *[Symbol.iterator]() { for (const [id, item] of pairs) { yield [id, callback(item, id)]; } }, }); } /** * Get the item with the maximum value of given property. * * @param callback - The function that picks and possibly converts the property. * * @returns The item with the maximum if found otherwise null. */ max(callback) { const iter = this._pairs[Symbol.iterator](); let curr = iter.next(); if (curr.done) { return null; } let maxItem = curr.value[1]; let maxValue = callback(curr.value[1], curr.value[0]); while (!(curr = iter.next()).done) { const [id, item] = curr.value; const value = callback(item, id); if (value > maxValue) { maxValue = value; maxItem = item; } } return maxItem; } /** * Get the item with the minimum value of given property. * * @param callback - The function that picks and possibly converts the property. * * @returns The item with the minimum if found otherwise null. */ min(callback) { const iter = this._pairs[Symbol.iterator](); let curr = iter.next(); if (curr.done) { return null; } let minItem = curr.value[1]; let minValue = callback(curr.value[1], curr.value[0]); while (!(curr = iter.next()).done) { const [id, item] = curr.value; const value = callback(item, id); if (value < minValue) { minValue = value; minItem = item; } } return minItem; } /** * Reduce the items into a single value. * * @param callback - The function that does the reduction. * @param accumulator - The initial value of the accumulator. * * @typeparam T - The type of the accumulated value. * * @returns The reduced value. */ reduce(callback, accumulator) { for (const [id, item] of this._pairs) { accumulator = callback(accumulator, item, id); } return accumulator; } /** * Sort the items. * * @param callback - Item comparator. * * @returns A new stream with sorted items. */ sort(callback) { return new DataStream({ [Symbol.iterator]: () => [...this._pairs] .sort(([idA, itemA], [idB, itemB]) => callback(itemA, itemB, idA, idB))[Symbol.iterator](), }); } } /* eslint @typescript-eslint/member-ordering: ["error", { "classes": ["field", "constructor", "method"] }] */ /** * Add an id to given item if it doesn't have one already. * * @remarks * The item will be modified. * * @param item - The item that will have an id after a call to this function. * @param idProp - The key of the id property. * * @typeParam Item - Item type that may or may not have an id. * @typeParam IdProp - Name of the property that contains the id. * * @returns true */ function ensureFullItem(item, idProp) { if (item[idProp] == null) { // generate an id item[idProp] = uuid.v4(); } return item; } /** * # DataSet * * Vis.js comes with a flexible DataSet, which can be used to hold and * manipulate unstructured data and listen for changes in the data. The DataSet * is key/value based. Data items can be added, updated and removed from the * DataSet, and one can subscribe to changes in the DataSet. The data in the * DataSet can be filtered and ordered. Data can be normalized when appending it * to the DataSet as well. * * ## Example * * The following example shows how to use a DataSet. * * ```javascript * // create a DataSet * var options = {}; * var data = new vis.DataSet(options); * * // add items * // note that the data items can contain different properties and data formats * data.add([ * {id: 1, text: 'item 1', date: new Date(2013, 6, 20), group: 1, first: true}, * {id: 2, text: 'item 2', date: '2013-06-23', group: 2}, * {id: 3, text: 'item 3', date: '2013-06-25', group: 2}, * {id: 4, text: 'item 4'} * ]); * * // subscribe to any change in the DataSet * data.on('*', function (event, properties, senderId) { * console.log('event', event, properties); * }); * * // update an existing item * data.update({id: 2, group: 1}); * * // remove an item * data.remove(4); * * // get all ids * var ids = data.getIds(); * console.log('ids', ids); * * // get a specific item * var item1 = data.get(1); * console.log('item1', item1); * * // retrieve a filtered subset of the data * var items = data.get({ * filter: function (item) { * return item.group == 1; * } * }); * console.log('filtered items', items); * ``` * * @typeParam Item - Item type that may or may not have an id. * @typeParam IdProp - Name of the property that contains the id. */ class DataSet extends DataSetPart { /** * Construct a new DataSet. * * @param data - Initial data or options. * @param options - Options (type error if data is also options). */ constructor(data, options) { super(); // correctly read optional arguments if (data && !Array.isArray(data)) { options = data; data = []; } this._options = options || {}; this._data = new Map(); // map with data indexed by id this.length = 0; // number of items in the DataSet this._idProp = this._options.fieldId || "id"; // name of the field containing id // add initial data when provided if (data && data.length) { this.add(data); } this.setOptions(options); } /** * Set new options. * * @param options - The new options. */ setOptions(options) { if (options && options.queue !== undefined) { if (options.queue === false) { // delete queue if loaded if (this._queue) { this._queue.destroy(); delete this._queue; } } else { // create queue and update its options if (!this._queue) { this._queue = Queue.extend(this, { replace: ["add", "update", "remove"], }); } if (options.queue && typeof options.queue === "object") { this._queue.setOptions(options.queue); } } } } /** * Add a data item or an array with items. * * After the items are added to the DataSet, the DataSet will trigger an event `add`. When a `senderId` is provided, this id will be passed with the triggered event to all subscribers. * * ## Example * * ```javascript * // create a DataSet * const data = new vis.DataSet() * * // add items * const ids = data.add([ * { id: 1, text: 'item 1' }, * { id: 2, text: 'item 2' }, * { text: 'item without an id' } * ]) * * console.log(ids) // [1, 2, '<UUIDv4>'] * ``` * * @param data - Items to be added (ids will be generated if missing). * @param senderId - Sender id. * * @returns addedIds - Array with the ids (generated if not present) of the added items. * * @throws When an item with the same id as any of the added items already exists. */ add(data, senderId) { const addedIds = []; let id; if (Array.isArray(data)) { // Array const idsToAdd = data.map((d) => d[this._idProp]); if (idsToAdd.some((id) => this._data.has(id))) { throw new Error("A duplicate id was found in the parameter array."); } for (let i = 0, len = data.length; i < len; i++) { id = this._addItem(data[i]); addedIds.push(id); } } else if (data && typeof data === "object") { // Single item id = this._addItem(data); addedIds.push(id); } else { throw new Error("Unknown dataType"); } if (addedIds.length) { this._trigger("add", { items: addedIds }, senderId); } return addedIds; } /** * Update existing items. When an item does not exist, it will be created. * * @remarks * The provided properties will be merged in the existing item. When an item does not exist, it will be created. * * After the items are updated, the DataSet will trigger an event `add` for the added items, and an event `update`. When a `senderId` is provided, this id will be passed with the triggered event to all subscribers. * * ## Example * * ```javascript * // create a DataSet * const data = new vis.DataSet([ * { id: 1, text: 'item 1' }, * { id: 2, text: 'item 2' }, * { id: 3, text: 'item 3' } * ]) * * // update items * const ids = data.update([ * { id: 2, text: 'item 2 (updated)' }, * { id: 4, text: 'item 4 (new)' } * ]) * * console.log(ids) // [2, 4] * ``` * * ## Warning for TypeScript users * This method may introduce partial items into the data set. Use add or updateOnly instead for better type safety. * * @param data - Items to be updated (if the id is already present) or added (if the id is missing). * @param senderId - Sender id. * * @returns updatedIds - The ids of the added (these may be newly generated if there was no id in the item from the data) or updated items. * * @throws When the supplied data is neither an item nor an array of items. */ update(data, senderId) { const addedIds = []; const updatedIds = []; const oldData = []; const updatedData = []; const idProp = this._idProp; const addOrUpdate = (item) => { const origId = item[idProp]; if (origId != null && this._data.has(origId)) { const fullItem = item; // it has an id, therefore it is a fullitem const oldItem = Object.assign({}, this._data.get(origId)); // update item const id = this._updateItem(fullItem); updatedIds.push(id); updatedData.push(fullItem); oldData.push(oldItem); } else { // add new item const id = this._addItem(item); addedIds.push(id); } }; if (Array.isArray(data)) { // Array for (let i = 0, len = data.length; i < len; i++) { if (data[i] && typeof data[i] === "object") { addOrUpdate(data[i]); } else { console.warn("Ignoring input item, which is not an object at index " + i); } } } else if (data && typeof data === "object") { // Single item addOrUpdate(data); } else { throw new Error("Unknown dataType"); } if (addedIds.length) { this._trigger("add", { items: addedIds }, senderId); } if (updatedIds.length) { const props = { items: updatedIds, oldData: oldData, data: updatedData }; // TODO: remove deprecated property 'data' some day //Object.defineProperty(props, 'data', { // 'get': (function() { // console.warn('Property data is deprecated. Use DataSet.get(ids) to retrieve the new data, use the oldData property on this object to get the old data'); // return updatedData; // }).bind(this) //}); this._trigger("update", props, senderId); } return addedIds.concat(updatedIds); } /** * Update existing items. When an item does not exist, an error will be thrown. * * @remarks * The provided properties will be deeply merged into the existing item. * When an item does not exist (id not present in the data set or absent), an error will be thrown and nothing will be changed. * * After the items are updated, the DataSet will trigger an event `update`. * When a `senderId` is provided, this id will be passed with the triggered event to all subscribers. * * ## Example * * ```javascript * // create a DataSet * const data = new vis.DataSet([ * { id: 1, text: 'item 1' }, * { id: 2, text: 'item 2' }, * { id: 3, text: 'item 3' }, * ]) * * // update items * const ids = data.update([ * { id: 2, text: 'item 2 (updated)' }, // works * // { id: 4, text: 'item 4 (new)' }, // would throw * // { text: 'item 4 (new)' }, // would also throw * ]) * * console.log(ids) // [2] * ``` * * @param data - Updates (the id and optionally other props) to the items in this data set. * @param senderId - Sender id. * * @returns updatedIds - The ids of the updated items. * * @throws When the supplied data is neither an item nor an array of items, when the ids are missing. */ updateOnly(data, senderId) { if (!Array.isArray(data)) { data = [data]; } const updateEventData = data .map((update) => { const oldData = this._data.get(update[this._idProp]); if (oldData == null) { throw new Error("Updating non-existent items is not allowed."); } return { oldData, update }; }) .map(({ oldData, update }) => { const id = oldData[this._idProp]; const updatedData = esnext.pureDeepObjectAssign(oldData, update); this._data.set(id, updatedData); return { id, oldData: oldData, updatedData, }; }); if (updateEventData.length) { const props = { items: updateEventData.map((value) => value.id), oldData: updateEventData.map((value) => value.oldData), data: updateEventData.map((value) => value.updatedData), }; // TODO: remove deprecated property 'data' some day //Object.defineProperty(props, 'data', { // 'get': (function() { // console.warn('Property data is deprecated. Use DataSet.get(ids) to retrieve the new data, use the oldData property on this object to get the old data'); // return updatedData; // }).bind(this) //}); this._trigger("update", props, senderId); return props.items; } else { return []; } } /** @inheritdoc */ get(first, second) { // @TODO: Woudn't it be better to split this into multiple methods? // parse the arguments let id = undefined; let ids = undefined; let options = undefined; if (isId(first)) { // get(id [, options]) id = first; options = second; } else if (Array.isArray(first)) { // get(ids [, options]) ids = first; options = second; } else { // get([, options]) options = first; } // determine the return type const returnType = options && options.returnType === "Object" ? "Object" : "Array"; // @TODO: WTF is this? Or am I missing something? // var returnType // if (options && options.returnType) { // var allowedValues = ['Array', 'Object'] // returnType = // allowedValues.indexOf(options.returnType) == -1 // ? 'Array' // : options.returnType // } else { // returnType = 'Array' // } // build options const filter = options && options.filter; const items = []; let item = undefined; let itemIds = undefined; let itemId = undefined; // convert items if (id != null) { // return a single item item = this._data.get(id); if (item && filter && !filter(item)) { item = undefined; } } else if (ids != null) { // return a subset of items for (let i = 0, len = ids.length; i < len; i++) { item = this._data.get(ids[i]); if (item != null && (!filter || filter(item))) { items.push(item); } } } else { // return all items itemIds = [...this._data.keys()]; for (let i = 0, len = itemIds.length; i < len; i++) { itemId = itemIds[i]; item = this._data.get(itemId); if (item != null && (!filter || filter(item))) { items.push(item); } } } // order the results if (options && options.order && id == undefined) { this._sort(items, options.order); } // filter fields of the items if (options && options.fields) { const fields = options.fields; if (id != undefined && item != null) { item = this._filterFields(item, fields); } else { for (let i = 0, len = items.length; i < len; i++) { items[i] = this._filterFields(items[i], fields); } } } // return the results if (returnType == "Object") { const result = {}; for (let i = 0, len = items.length; i < len; i++) { const resultant = items[i]; // @TODO: Shoudn't this be this._fieldId? // result[resultant.id] = resultant const id = resultant[this._idProp]; result[id] = resultant; } return result; } else { if (id != null) { // a single item return item !== null && item !== void 0 ? item : null; } else { // just return our array return items; } } } /** @inheritdoc */ getIds(options) { const data = this._data; const filter = options && options.filter; const order = options && options.order; const itemIds = [...data.keys()]; const ids = []; if (filter) { // get filtered items if (order) { // create ordered list const items = []; for (let i = 0, len = itemIds.length; i < len; i++) { const id = itemIds[i]; const item = this._data.get(id); if (item != null && filter(item)) { items.push(item); } } this._sort(items, order); for (let i = 0, len = items.length; i < len; i++) { ids.push(items[i][this._idProp]); } } else { // create unordered list for (let i = 0, len = itemIds.length; i < len; i++) { const id = itemIds[i]; const item = this._data.get(id); if (item != null && filter(item)) { ids.push(item[this._idProp]); } } } } else { // get all items if (order) { // create an ordered list const items = []; for (let i = 0, len = itemIds.length; i < len; i++) { const id = itemIds[i]; items.push(data.get(id)); } this._sort(items, order); for (let i = 0, len = items.length; i < len; i++) { ids.push(items[i][this._idProp]); } } else { // create unordered list for (let i = 0, len = itemIds.length; i < len; i++) { const id = itemIds[i]; const item = data.get(id); if (item != null) { ids.push(item[this._idProp]); } } } } return ids; } /** @inheritdoc */ getDataSet() { return this; } /** @inheritdoc */ forEach(callback, options) { const filter = options && options.filter; const data = this._data; const itemIds = [...data.keys()]; if (options && options.order) { // execute forEach on ordered list const items = this.get(options); for (let i = 0, len = items.length; i < len; i++) { const item = items[i]; const id = item[this._idProp]; callback(item, id); } } else { // unordered for (let i = 0, len = itemIds.length; i < len; i++) { const id = itemIds[i]; const item = this._data.get(id); if (item != null && (!filter || filter(item))) { callback(item, id); } } } } /** @inheritdoc */ map(callback, options) { const filter = options && options.filter; const mappedItems = []; const data = this._data; const itemIds = [...data.keys()]; // convert and filter items for (let i = 0, len = itemIds.length; i < len; i++) { const id = itemIds[i]; const item = this._data.get(id); if (item != null && (!filter || filter(item))) { mappedItems.push(callback(item, id)); } } // order items if (options && options.order) { this._sort(mappedItems, options.order); } return mappedItems; } /** * Filter the fields of an item. * * @param item - The item whose fields should be filtered. * @param fields - The names of the fields that will be kept. * * @typeParam K - Field name type. * * @returns The item without any additional fields. */ _filterFields(item, fields) { if (!item) { // item is null return item; } return (Array.isArray(fields) ? // Use the supplied array fields : // Use the keys of the supplied object Object.keys(fields)).reduce((filteredItem, field) => { filteredItem[field] = item[field]; return filteredItem; }, {}); } /** * Sort the provided array with items. * * @param items - Items to be sorted in place. * @param order - A field name or custom sort function. * * @typeParam T - The type of the items in the items array. */ _sort(items, order) { if (typeof order === "string") { // order by provided field name const name = order; // field name items.sort((a, b) => { // @TODO: How to treat missing properties? const av = a[name]; const bv = b[name]; return av > bv ? 1 : av < bv ? -1 : 0; }); } else if (typeof order === "function") { // order by sort function items.sort(order); } else { // TODO: extend order by an Object {field:string, direction:string} // where direction can be 'asc' or 'desc' throw new TypeError("Order must be a function or a string"); } } /** * Remove an item or multiple items by “reference” (only the id is used) or by id. * * The method ignores removal of non-existing items, and returns an array containing the ids of the items which are actually removed from the DataSet. * * After the items are removed, the DataSet will trigger an event `remove` for the removed items. When a `senderId` is provided, this id will be passed with the triggered event to all subscribers. * * ## Example * ```javascript * // create a DataSet * const data = new vis.DataSet([ * { id: 1, text: 'item 1' }, * { id: 2, text: 'item 2' }, * { id: 3, text: 'item 3' } * ]) * * // remove items * const ids =