UNPKG

hadron-document

Version:
1,150 lines (1,149 loc) 37.9 kB
'use strict'; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ElementList = exports.Element = exports.DEFAULT_VISIBLE_ELEMENTS = exports.ElementEvents = exports.DATE_FORMAT = void 0; exports.isInternalFieldPath = isInternalFieldPath; exports.isValueExpandable = isValueExpandable; const eventemitter3_1 = __importDefault(require("eventemitter3")); const lodash_1 = require("lodash"); const object_generator_1 = __importDefault(require("./object-generator")); const hadron_type_checker_1 = __importStar(require("hadron-type-checker")); const bson_1 = require("bson"); const date_1 = __importDefault(require("./editor/date")); const element_events_1 = require("./element-events"); Object.defineProperty(exports, "ElementEvents", { enumerable: true, get: function () { return element_events_1.ElementEvents; } }); const utils_1 = require("./utils"); const document_events_1 = require("./document-events"); exports.DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss.SSS'; /** * Id field constant. */ const ID = '_id'; /** * Is this the path to a field that is used internally by the * MongoDB driver or server and not for user consumption? */ function isInternalFieldPath(path) { return typeof path === 'string' && /^__safeContent__($|\.)/.test(path); } /** * Types that are not editable. */ const UNEDITABLE_TYPES = [ 'Binary', 'Code', 'MinKey', 'MaxKey', 'Timestamp', 'BSONRegExp', 'Undefined', 'Null', 'DBRef', ]; /** * Type guard to check if a value is a BSON Binary. */ function isBinary(value) { return (0, hadron_type_checker_1.getBsonType)(value) === 'Binary'; } exports.DEFAULT_VISIBLE_ELEMENTS = 25; function isValueExpandable(value) { return (0, lodash_1.isPlainObject)(value) || (0, lodash_1.isArray)(value); } /** * Represents an element in a document. */ class Element extends eventemitter3_1.default { uuid; key; currentKey; value; currentValue; added; removed; elements; // With Arrays and Objects we store the value in the // `elements` instead of currentValue. We use `originalExpandableValue` // to store the original value that was passed in. This is necessary // to be able to revert to the original value when the user cancels // any changes. originalExpandableValue; parent; type; currentType; level; currentTypeValid; invalidTypeMessage; decrypted; expanded = false; maxVisibleElementsCount = exports.DEFAULT_VISIBLE_ELEMENTS; /** * Cancel any modifications to the element. */ cancel() { if (this.elements) { // Cancel will remove elements from iterator, clone it before iterating // otherwise we will skip items for (const element of Array.from(this.elements)) { element.cancel(); } } if (this.isModified()) { this.revert(); } } /** * Create the element. * * @param key - The key. * @param value - The value. * @param parent - The parent element. * @param added - Is the element a new 'addition'? */ constructor(key, value, parent = null, added = false) { super(); this.uuid = new bson_1.UUID().toHexString(); this.key = key; this.currentKey = key; this.parent = parent; this.added = added; this.removed = false; this.type = hadron_type_checker_1.default.type(value); this.currentType = this.type; this.level = this._getLevel(); this.setValid(); // Make sure that all values that element will hold onto will be explicit // bson types: convert JavaScript numbers to either Int32 or Double if (typeof value === 'number') { value = hadron_type_checker_1.default.cast(value, hadron_type_checker_1.default.type(value)); } if (isValueExpandable(value)) { // NB: Important to set `originalExpandableValue` first as element // generation will depend on it this.originalExpandableValue = value; this.elements = this._generateElements(value); } else { this.value = value; this.currentValue = value; } // The AutoEncrypter marks decrypted entries in a document // for us with a special symbol. We opt into this behavior // in the devtools-connect package. let parentValue; if (this.parent) { parentValue = this.parent.isRoot() ? this.parent.doc : this.parent.originalExpandableValue; } const parentDecryptedKeys = parentValue && parentValue[Symbol.for('@@mdb.decryptedKeys')]; this.decrypted = (parentDecryptedKeys || []) .map(String) .includes(String(key)); } get nextElement() { return this.parent?.elements?.findNext(this); } get previousElement() { return this.parent?.elements?.findPrevious(this); } _getLevel() { let level = -1; let parent = this.parent; while (parent) { level++; parent = parent.parent; } return level; } /** * Edit the element. * * @param value - The new value. */ edit(value) { this.currentType = hadron_type_checker_1.default.type(value); if (isValueExpandable(value) && !isValueExpandable(this.currentValue)) { this.currentValue = null; this.elements = this._generateElements(value); } else if (!isValueExpandable(value) && this.elements) { this.currentValue = value; this.elements = undefined; } else { // if they are both integer types and equal if made the previous type, don't update. this.currentValue = value; } this.setValid(); this._bubbleUp(element_events_1.ElementEvents.Edited, this); } preserveType(otherElement) { switch (this.currentType) { case 'Object': { if (!this.elements) { return; } for (const child of this.elements) { const otherChild = otherElement.get(child.currentKey); if (!otherChild) { continue; } child.preserveType(otherChild); } break; } case 'Int32': { const otherType = otherElement.currentType; if (otherType === 'Double') { this.changeType('Double'); } if (otherType === 'Int64') { this.changeType('Int64'); } break; } default: // NOTE: Purposefully leaving Array elements alone because deciding // whether to turn on Int32 into a Double or Int64 or not is complex and // error-prone. Also leaving every other type alone. break; } } /** * Adjust this element's type based on the collection's existing schema. * Only converts Int32 → Double or Int32 → Int64 when the schema indicates * the field should be one of those types. This prevents unintended type * narrowing when inserting new documents (e.g. 5.0 becoming Int32 when * the field is typically Double). * * @param schemaFields - A map of dotted field paths to objects containing * a `type` property with their schema type(s). Type values use * mongodb-schema naming: 'Double', 'Int32', 'Long'. * @param parentPath - The dotted path prefix for nested elements. */ preserveTypeFromSchema(schemaFields, parentPath) { const fieldPath = parentPath ? `${parentPath}.${String(this.currentKey)}` : String(this.currentKey); if (this.currentType === 'Object' && this.elements) { for (const child of this.elements) { child.preserveTypeFromSchema(schemaFields, fieldPath); } return; } // NOTE: Purposefully leaving Array elements alone because deciding // whether to turn an Int32 into a Double or Int64 or not is complex // and error-prone. Also leaving every other non-Int32 type alone. if (this.currentType !== 'Int32') { return; } const schemaType = schemaFields[fieldPath]?.type; if (!schemaType) { return; } const types = Array.isArray(schemaType) ? schemaType : [schemaType]; // Prefer Double over Long/Int64 when both are present in the schema, // since Double is the more common floating-point representation. if (types.includes('Double')) { this.changeType('Double'); } else if (types.includes('Long') || types.includes('Int64')) { this.changeType('Int64'); } } changeType(newType) { if (newType === 'Object') { this._convertToEmptyObject(); } else if (newType === 'Array') { this._convertToEmptyArray(); } else { try { if (newType === 'Date') { const editor = new date_1.default(this); editor.edit(this.generateObject()); editor.complete(); } else if ((0, hadron_type_checker_1.isUUIDType)(newType) && (0, hadron_type_checker_1.isUUIDType)(this.currentType) && isBinary(this.currentValue)) { // Special handling for converting between UUID types // We need to use the source type to properly decode the binary const convertedBinary = (0, hadron_type_checker_1.convertBinaryUUID)(this.currentValue, this.currentType, newType); this.edit(convertedBinary); this.currentType = newType; this._bubbleUp(element_events_1.ElementEvents.Edited, this); } else { this.edit(hadron_type_checker_1.default.cast(this.generateObject(), newType)); // For UUID types, explicitly set the currentType since TypeChecker.type() // may not return the specific UUID type for legacy UUIDs (subtype 3) if ((0, hadron_type_checker_1.isUUIDType)(newType)) { this.currentType = newType; // Fire another event to notify the UI of the type change this._bubbleUp(element_events_1.ElementEvents.Edited, this); } } } catch (e) { this.setInvalid(this.currentValue, newType, e.message); } } } getRoot() { let parent = this.parent; while (parent?.parent) { parent = parent.parent; } return parent; } /** * Get an element by its key. * * @param key - The key name. * * @returns The element. */ get(key) { return this.elements?.get(key); } /** * Get an element by its index. * * @param i - The index. * * @returns The element. */ at(i) { return this.elements?.at(i); } /** * Rename the element. Update the parent's mapping if available. * * @param key - The new key. */ rename(key) { this.currentKey = key; this._bubbleUp(element_events_1.ElementEvents.Edited, this); } /** * Generate the javascript object for this element. * * @returns The javascript object. */ generateObject(options) { if (this.currentType === 'Array') { return object_generator_1.default.generateArray(this.elements, options); } if (this.currentType === 'Object') { return object_generator_1.default.generate(this.elements, options); } return this.currentValue; } /** * Generate the javascript object representing the original values * for this element (pre-element removal, renaming, editing). * * @returns The javascript object. */ generateOriginalObject(options) { if (this.type === 'Array') { const originalElements = this._generateElements(this.originalExpandableValue); return object_generator_1.default.generateOriginalArray(originalElements, options); } if (this.type === 'Object') { const originalElements = this._generateElements(this.originalExpandableValue); return object_generator_1.default.generateOriginal(originalElements, options); } return this.value; } /** * Generate the Extended JSON string representation of this element. * * @returns The Extended JSON string. */ toEJSON(source = 'current', options = {}) { const generated = source === 'original' ? this.generateOriginalObject() : this.generateObject(); return (0, utils_1.objectToIdiomaticEJSON)(generated, options); } /** * Insert an element after the provided element. If this element is an array, * then ignore the key specified by the caller and use the correct index. * Update the keys of the rest of the elements in the LinkedList. * * @param element - The element to insert after. * @param key - The key. * @param value - The value. * * @returns The new element. */ insertAfter(element, key, value) { if (!this.elements) { throw new Error('Cannot insert values on non-array/non-object elements'); } const newElement = this.elements.insertAfter(element, key, value); newElement._bubbleUp(element_events_1.ElementEvents.Added, newElement, this); if (newElement && this.elements.at(this.maxVisibleElementsCount - 1) === element) { this.maxVisibleElementsCount++; } this.emitVisibleElementsChanged(); return newElement; } /** * Add a new element to this element. * * @param| Number} key - The element key. * @param value - The value. * * @returns The new element. */ insertEnd(key, value) { if (!this.elements) { throw new Error('Cannot insert values on non-array/non-object elements'); } const newElement = this.elements.insertEnd(key, value, true); this._bubbleUp(element_events_1.ElementEvents.Added, newElement); this.emitVisibleElementsChanged(); return newElement; } /** * Insert a placeholder element at the end of the element. * * @returns The placeholder element. */ insertPlaceholder() { // When adding a placeholder value to an array we default to the type // of the last value currently in the array. Otherwise empty string. const placeholderValue = this.currentType === 'Array' && this.elements?.lastElement ? (0, utils_1.getDefaultValueForType)(this.elements?.lastElement.currentType) : ''; return this.insertEnd('', placeholderValue); } insertSiblingPlaceholder() { // When adding a sibling placeholder value to an array we default the // new values' type to the preceding element's type to hopefully make // it so folks don't have to change the type later. Otherwise empty string. const placeholderValue = this.parent?.currentType === 'Array' ? (0, utils_1.getDefaultValueForType)(this.currentType) : ''; return this.parent.insertAfter(this, '', placeholderValue); } /** * Is the element a newly added element * * @returns If the element is newly added. */ isAdded() { return this.added || !!this.parent?.isAdded(); } /** * Is the element blank? * * @returns If the element is blank. */ isBlank() { return this.currentKey === '' && this.currentValue === ''; } /** * Does the element have a valid value for the current type? * * @returns If the value is valid. */ isCurrentTypeValid() { return !!this.currentTypeValid; } /** * Set the element as valid. */ setValid() { this.currentTypeValid = true; this.invalidTypeMessage = undefined; this._bubbleUp(element_events_1.ElementEvents.Valid, this); } /** * Set the element as invalid. * * @param value - The value. * @param newType - The new type. * @param message - The error message. */ setInvalid(value, newType, message) { this.currentValue = value; this.currentType = newType; this.currentTypeValid = false; this.invalidTypeMessage = message; this._bubbleUp(element_events_1.ElementEvents.Invalid, this); } /** * Determine if the key is a duplicate. * * @param value - The value to check. * * @returns If the key is a duplicate. */ isDuplicateKey(value) { if (value === '') { return false; } for (const element of this.parent?.elements ?? []) { if (element.uuid !== this.uuid && element.currentKey === value) { return true; } } return false; } /** * Determine if the element is edited - returns true if * the key or value changed. Does not count array values whose keys have * changed as edited. * * @returns If the element is edited. */ isEdited() { return ((this.isRenamed() || !this._valuesEqual() || this.type !== this.currentType) && !this.isAdded()); } /** * Check for value equality. * @returns If the value is equal. */ _valuesEqual() { if (this.currentType === 'Date' && (0, lodash_1.isString)(this.currentValue)) { return (0, lodash_1.isEqual)(this.value, new Date(this.currentValue)); } else if (this.currentType === 'ObjectId' && (0, lodash_1.isString)(this.currentValue)) { return this._isObjectIdEqual(); } return (0, lodash_1.isEqual)(this.value, this.currentValue); } _isObjectIdEqual() { try { return this.value.toHexString() === this.currentValue; } catch { return false; } } _isExpandable() { return this.currentType === 'Array' || this.currentType === 'Object'; } /** * Is the element the last in the elements. * * @returns If the element is last. */ isLast() { return this.parent?.elements?.lastElement === this; } /** * Determine if the element was explicitly renamed by the user. * * @returns If the element was explicitly renamed by the user. */ isRenamed() { if (!this.parent || this.parent.isRoot() || this.parent.currentType === 'Object') { return this.key !== this.currentKey; } return false; } /** * Determine if the element was renamed, potentially as part * of moving array elements. * * @returns If the element was renamed, explicitly or implicitly. */ hasChangedKey() { return this.key !== this.currentKey; } /** * Can changes to the element be reverted? * * @returns If the element can be reverted. */ isRevertable() { return this.isEdited() || this.isRemoved(); } /** * Can the element be removed? * * @returns If the element can be removed. */ isRemovable() { return !this.parent.isRemoved(); } /** * Can no action be taken on the element * * @returns If no action can be taken. */ isNotActionable() { return (((this.key === ID || this.isInternalField()) && !this.isAdded()) || !this.isRemovable()); } /** * Determine if the value has been decrypted via CSFLE. * * Warning: This does *not* apply to the children of decrypted elements! * This only returns true for the exact field that was decrypted. * * a: Object * \-- b: Object <- decrypted * \-- c: number * * a.isValueDecrypted() === false * a.get('b')?.isValueDecrypted() === true * a.get('b')?.get('c')?.isValueDecrypted() === true * * @returns If the value was encrypted on the server and is now decrypted. */ isValueDecrypted() { return this.decrypted; } /** * Detemine if this value or any of its children were marked * as having been decrypted with CSFLE. * * Warning: This does *not* apply to the children of decrypted elements! * This only returns true for the exact field that was decrypted * and its parents. * * a: Object * \-- b: Object <- decrypted * \-- c: number * * a.containsDecryptedChildren() === true * a.get('b')?.containsDecryptedChildren() === true * a.get('b')?.get('c')?.containsDecryptedChildren() === false * * @returns If any child of this element has been decrypted directly. */ containsDecryptedChildren() { if (this.isValueDecrypted()) { return true; } for (const element of this.elements || []) { if (element.containsDecryptedChildren()) { return true; } } return false; } /** * Determine if the value is editable. * * @returns If the value is editable. */ isValueEditable() { // UUID types are editable even though Binary is in UNEDITABLE_TYPES const isCurrentTypeUUID = (0, hadron_type_checker_1.isUUIDType)(this.currentType); // Also check for Binary values that are actually UUIDs (subtype 3 or 4) // This handles the case where currentType is 'Binary' but it's actually a UUID const isBinaryUUIDValue = this.currentType === 'Binary' && isBinary(this.currentValue) && (this.currentValue.sub_type === bson_1.Binary.SUBTYPE_UUID || (this.currentValue.sub_type === bson_1.Binary.SUBTYPE_UUID_OLD && this.currentValue.buffer.length === 16)); return (this._isKeyLegallyEditable() && (isCurrentTypeUUID || isBinaryUUIDValue || !UNEDITABLE_TYPES.includes(this.currentType))); } /** * Determine if the key of the parent element is editable. * * @returns If the parent's key is editable. */ isParentEditable() { if (this.parent && !this.parent.isRoot()) { return this.parent._isKeyLegallyEditable(); } return true; } _isKeyLegallyEditable() { return (this.isParentEditable() && (this.isAdded() || ((this.currentKey !== ID || !this.parent?.isRoot()) && !this.isInternalField()))); } /** * Determine if the key is editable. * * @returns If the key is editable. */ isKeyEditable() { return this._isKeyLegallyEditable() && !this.containsDecryptedChildren(); } /** * Is this a field that is used internally by the MongoDB driver or server * and not for user consumption? * * @returns */ isInternalField() { if (!this.parent) { return false; } if (!this.parent.isRoot() && this.parent.isInternalField()) { return true; } if (this.parent.isRoot() && isInternalFieldPath(this.currentKey)) { return true; } return false; } /** * Determine if the element is modified at all. * * @returns If the element is modified. */ isModified() { if (this.elements) { for (const element of this.elements) { if (element.isModified()) { return true; } } } return this.isAdded() || this.isEdited() || this.isRemoved(); } /** * Is the element flagged for removal? * * @returns If the element is flagged for removal. */ isRemoved() { return this.removed; } /** * Are any immediate children of this element flagged for removal? * * @returns If any immediate children of this element are flagged for removal. */ hasAnyRemovedChild() { if (this.elements) { for (const element of this.elements) { if (element.isRemoved()) { return true; } } } return false; } /** * Elements themselves are not the root. * * @returns Always false. */ isRoot() { return false; } /** * Flag the element for removal. */ remove() { this.revert(); this.removed = true; if (this.parent) { this._bubbleUp(element_events_1.ElementEvents.Removed, this, this.parent); this.emitVisibleElementsChanged(this.parent); } } /** * Revert the changes to the element. */ revert() { if (this.isAdded()) { this.parent?.elements?.remove(this); this._bubbleUp(element_events_1.ElementEvents.Removed, this, this.parent); if (this.parent) { this.emitVisibleElementsChanged(this.parent); } delete this.parent; } else { if (this.originalExpandableValue) { this.elements = this._generateElements(this.originalExpandableValue); this.currentValue = undefined; } else { if (this.currentValue === null && this.value !== null) { delete this.elements; } else { this._removeAddedElements(); } this.currentValue = this.value; } this.currentKey = this.key; this.currentType = this.type; this.removed = false; } this.setValid(); this._bubbleUp(element_events_1.ElementEvents.Reverted, this); } /** * Expands the target element and optionally its children as well. * Document.expand is when we would want to expand the children otherwise we * will most expand the element itself. */ expand(expandChildren = false) { if (!this._isExpandable()) { return; } this.expanded = true; if (expandChildren && this.elements) { for (const element of this.elements) { element.expand(expandChildren); } } this.emit(element_events_1.ElementEvents.Expanded, this); this.emitVisibleElementsChanged(); } /** * Collapses only the target element */ collapse() { if (!this._isExpandable()) { return; } this.expanded = false; if (this.elements) { for (const element of this.elements) { element.collapse(); } } this.emit(element_events_1.ElementEvents.Collapsed, this); this.emitVisibleElementsChanged(); } /** * Fire and bubble up the event. * * @param evt - The event. * @paramdata - Optional. */ _bubbleUp(evt, ...data) { this.emit(evt, ...data); const element = this.parent; if (element) { if (element.isRoot()) { element.emit(evt, ...data); } else { element._bubbleUp(evt, ...data); } } } /** * Convert this element to an empty object. */ _convertToEmptyObject() { this.edit({}); this.insertPlaceholder(); } /** * Convert to an empty array. */ _convertToEmptyArray() { this.edit([]); this.insertPlaceholder(); } /** * Is the element empty? * * @param element - The element to check. * * @returns If the element is empty. */ _isElementEmpty(element) { return !!element && element.isAdded() && element.isBlank(); } /** * Generates a sequence of child elements. * * @param object - The object to generate from. * * @returns The elements. */ _generateElements(object) { return new ElementList(this, object); } /** * Removes the added elements from the element. */ _removeAddedElements() { if (this.elements) { for (const element of this.elements) { if (element.isAdded()) { this.elements.remove(element); } } } } getVisibleElements() { if (!this.elements || !this.expanded) { return []; } return [...this.elements].slice(0, this.maxVisibleElementsCount); } setMaxVisibleElementsCount(newCount) { if (!this._isExpandable()) { return; } this.maxVisibleElementsCount = newCount; this.emitVisibleElementsChanged(); } getTotalVisibleElementsCount() { if (!this.elements || !this.expanded) { return 0; } return this.getVisibleElements().reduce((totalVisibleChildElements, element) => { return (totalVisibleChildElements + 1 + element.getTotalVisibleElementsCount()); }, 0); } findUUIDs() { let subtype4Count = 0; let subtype3Count = 0; if (!this.elements) { return { subtype4Count, subtype3Count }; } for (const element of this.elements) { if (element.currentType === 'Binary') { if (element.currentValue.sub_type === 4) { subtype4Count++; } if (element.currentValue.sub_type === 3) { subtype3Count++; } } else if (element.currentType === 'Object' || element.currentType === 'Array') { const { subtype3Count: sub3, subtype4Count: sub4 } = element.findUUIDs(); subtype3Count += sub3; subtype4Count += sub4; } } return { subtype4Count, subtype3Count }; } emitVisibleElementsChanged(targetElement = this) { if (targetElement.isRoot()) { targetElement.emit(document_events_1.DocumentEvents.VisibleElementsChanged, targetElement); } else if (targetElement.expanded) { targetElement._bubbleUp(element_events_1.ElementEvents.VisibleElementsChanged, targetElement, targetElement.getRoot()); } } /** * @deprecated Use ElementEvents import instead */ static get Events() { return element_events_1.ElementEvents; } } exports.Element = Element; /** * Represents a doubly linked list. */ class ElementList { elements; parent; constructor(parent, originalDoc) { this.parent = parent; this.elements = Object.entries(originalDoc ?? {}).map(([k, v]) => { return new Element(this.isArray() ? parseInt(k, 10) : k, v, parent, parent.isRoot() ? parent.cloned : false); }); } isArray() { return this.parent.currentType === 'Array'; } get size() { return this.elements.length; } at(index) { return this.elements[index]; } get(key) { return this.elements.find((el) => { return el.currentKey === key; }); } some(predicate) { return this.elements.some(predicate); } every(predicate) { return this.elements.every(predicate); } get firstElement() { return this.elements[0]; } get lastElement() { return this.elements[this.elements.length - 1]; } /** * Insert data after the provided element. * * @param afterElement - The element to insert after. * @param key - The element key. * @param value - The element value. * @param added - If the element is new. * * @returns The inserted element. */ insertAfter(afterElement, key, value, added = true) { let newElement; let newElementIdx = -1; for (const [idx, el] of this.elements.entries()) { if (afterElement === el) { newElementIdx = idx + 1; newElement = new Element(this.isArray() ? newElementIdx : key, value, this.parent, added); continue; } if (newElement && this.isArray()) { el.currentKey = idx + 1; } } if (newElement) { this.elements.splice(newElementIdx, 0, newElement); } return newElement; } /** * Insert data before the provided element. * * @param beforeElement - The element to insert before. * @param key - The element key. * @param value - The element value. * @param added - If the element is new. * * @returns The inserted element. */ insertBefore(beforeElement, key, value, added = true) { let newElement; let newElementIdx = -1; for (const [idx, el] of this.elements.entries()) { if (beforeElement === el) { newElementIdx = idx; newElement = new Element(this.isArray() ? newElementIdx : key, value, this.parent, added); } if (newElement && this.isArray()) { el.currentKey = idx + 1; } } if (newElement) { this.elements.splice(newElementIdx, 0, newElement); } return newElement; } /** * Insert data at the beginning of the list. * * @param key - The element key. * @param value - The element value. * @param added - If the element is new. * * @returns The data element. */ insertBeginning(key, value, added = true) { const newElement = new Element(this.isArray() ? 0 : key, value, this.parent, added); if (this.isArray()) { this.elements.forEach((el) => { el.currentKey += 1; }); } this.elements.unshift(newElement); return newElement; } /** * Insert data at the end of the list. * * @param key - The element key. * @param value - The element value. * @param added - If the element is new. * * @returns The data element. */ insertEnd(key, value, added = true) { const newElement = new Element(this.isArray() ? this.elements.length : key, value, this.parent, added); this.elements.push(newElement); return newElement; } /** * Remove the element from the list. * * @param removeElement - The element to remove. * * @returns The list with the element removed. */ remove(removeElement) { let removeIdx = -1; for (const [idx, el] of this.elements.entries()) { if (el === removeElement) { removeIdx = idx; continue; } if (removeIdx !== -1 && this.isArray()) { el.currentKey -= 1; } } if (removeIdx !== -1) { this.elements.splice(removeIdx, 1); } return this; } findNext(el) { const idx = this.elements.indexOf(el); return idx !== -1 ? this.elements[idx + 1] : undefined; } findPrevious(el) { const idx = this.elements.indexOf(el); return idx !== -1 ? this.elements[idx - 1] : undefined; } *[Symbol.iterator]() { yield* this.elements; } } exports.ElementList = ElementList; //# sourceMappingURL=element.js.map