diff --git a/packages/common/src/core/__tests__/slickCore.spec.ts b/packages/common/src/core/__tests__/slickCore.spec.ts index 1effd0468..c1b93fd41 100644 --- a/packages/common/src/core/__tests__/slickCore.spec.ts +++ b/packages/common/src/core/__tests__/slickCore.spec.ts @@ -415,32 +415,6 @@ describe('SlickCore file', () => { }); describe('Utils', () => { - describe('isPlainObject', () => { - it('should be falsy when object contains prototype methods', () => { - const l = console.log; - const obj = { - method: () => l("method in obj") - }; - const obj2: any = { hello: 'world' }; - obj2.__proto__ = obj; - - expect(Utils.isPlainObject(obj2)).toBeFalsy(); - }); - - it('should be truthy when object does not contains any prototype', () => { - const obj2: any = { hello: 'world' }; - obj2.__proto__ = null; - - expect(Utils.isPlainObject(obj2)).toBeTruthy(); - }); - - it('should be truthy when object is a regular object without methods', () => { - const obj2 = { hello: 'world' }; - - expect(Utils.isPlainObject(obj2)).toBeTruthy(); - }); - }); - describe('storage() function', () => { it('should be able to store an object and retrieve it later', () => { const div = document.createElement('div'); @@ -489,105 +463,6 @@ describe('SlickCore file', () => { }); }); - describe('extend() function', () => { - it('should be able to make a perfect deep copy of an object', () => { - const callback = () => console.log('hello'); - const obj1 = { hello: { sender: 'me', target: 'world' }, deeper: { children: ['abc', 'cde'], callback } }; - const obj2 = Utils.extend(true, {}, obj1, { another: 'prop' }); - - expect(obj2).toEqual({ hello: { sender: 'me', target: 'world' }, deeper: { children: ['abc', 'cde'], callback }, another: 'prop' }); - }); - - it('should be able to make a perfect deep copy of an object that includes String() and Boolean() constructors', () => { - const callback = () => console.log('hello'); - const obj1 = { hello: { sender: 'me', target: String(123), valid: Boolean(null) }, deeper: { children: ['abc', 'cde'], callback } }; - const obj2 = Utils.extend(true, {}, obj1, { another: 'prop' }); - - expect(obj2).toEqual({ hello: { sender: 'me', target: '123', valid: false }, deeper: { children: ['abc', 'cde'], callback }, another: 'prop' }); - }); - - it('should be able to make a deep copy of an object and changing new object prop should not affect input object', () => { - const obj1 = { hello: { sender: 'me', target: 'world' }, deeper: { children: ['abc', 'cde'] } }; - const obj2 = Utils.extend(true, {}, obj1, { another: 'prop' }); - obj2.hello.target = 'mum'; - - expect(obj1).toEqual({ hello: { sender: 'me', target: 'world' }, deeper: { children: ['abc', 'cde'] } }); - expect(obj2).toEqual({ hello: { sender: 'me', target: 'mum' }, deeper: { children: ['abc', 'cde'] }, another: 'prop' }); - }); - - it('should assume an extended object when passing true boolean but ommitting empty object as target, so changing output object will impact input object as well', () => { - const obj1 = { hello: { sender: 'me', target: 'world' }, deeper: { children: ['abc', 'cde'] } }; - const obj2 = Utils.extend(true, obj1, { another: 'prop' }); - obj2.hello.target = 'mum'; - - expect(obj1).toEqual({ hello: { sender: 'me', target: 'mum' }, deeper: { children: ['abc', 'cde'] }, another: 'prop' }); - expect(obj2).toEqual({ hello: { sender: 'me', target: 'mum' }, deeper: { children: ['abc', 'cde'] }, another: 'prop' }); - }); - - it('should assume an extended object when ommitting true boolean, so changing output object will impact input object as well', () => { - const obj1 = { hello: { sender: 'me', target: 'world' }, deeper: { children: ['abc', 'cde'] } }; - const obj2 = Utils.extend(obj1, { another: { age: 20 } }); - obj2.hello.target = 'mum'; - - expect(obj1).toEqual({ hello: { sender: 'me', target: 'mum' }, deeper: { children: ['abc', 'cde'] }, another: { age: 20 } }); - expect(obj2).toEqual({ hello: { sender: 'me', target: 'mum' }, deeper: { children: ['abc', 'cde'] }, another: { age: 20 } }); - }); - - it('should return same object when passing input object twice', () => { - const obj1 = { hello: { sender: 'me', target: 'world' }, deeper: { children: ['abc', 'cde'] } }; - const obj2 = Utils.extend(true, {}, obj1, obj1); - - expect(obj2).toEqual(obj1); - }); - - it('should do a deep copy of an array of objects with properties having objects and changing object property should not affect original object', () => { - const obj1 = { firstName: 'John', lastName: 'Doe', address: { zip: 123456 } }; - const obj2 = { firstName: 'Jane', lastName: 'Doe', address: { zip: 222222 } }; - const arr1 = [obj1, obj2]; - const arr2 = Utils.extend(true, [], arr1); - arr2[0].address.zip = 888888; - arr2[1].address.zip = 999999; - - expect(arr1[0].address.zip).toBe(123456); - expect(arr1[1].address.zip).toBe(222222); - expect(arr2[0].address.zip).toBe(888888); - expect(arr2[1].address.zip).toBe(999999); - }); - - it('should return same object when passing only a single object', () => { - expect(Utils.extend({ hello: 'world' })).toEqual({ hello: 'world' }); - }); - - it('should expect Symbol to be converted to Object', () => { - const sym1 = Symbol("foo"); - const sym2 = Symbol("bar"); - - expect(Utils.extend(sym1, sym2, { hello: 'world' })).toEqual({ hello: 'world' }); - }); - - it('should be able to make a copy of an object with prototype', () => { - const l = console.log; - const method = () => l("method in obj"); - const obj = { - method - }; - const obj2: any = { hello: 'world' }; - obj2.__proto__ = obj; - - const obj1 = { hello: { sender: 'me', target: 'world' }, deeper: { children: ['abc', 'cde'] } }; - const obj3 = Utils.extend(obj1, obj2); - - expect(obj3).toEqual({ hello: 'world', deeper: { children: ['abc', 'cde'] }, method }); - }); - }); - - describe('noop() function', () => { - it('should return empty function', () => { - expect(typeof Utils.noop).toBe('function'); - expect(Utils.noop()).toBeUndefined(); - }); - }); - describe('height() function', () => { it('should return null when calling without a valid element', () => { const result = Utils.height(null as any); diff --git a/packages/common/src/core/slickCore.ts b/packages/common/src/core/slickCore.ts index 3b68fc8ba..a2071fd80 100644 --- a/packages/common/src/core/slickCore.ts +++ b/packages/common/src/core/slickCore.ts @@ -7,8 +7,6 @@ * @module Core * @namespace Slick */ -import { isDefined } from '@slickgrid-universal/utils'; - import { MergeTypes } from '../enums/index'; import type { CSSStyleDeclarationWritable, EditController } from '../interfaces'; @@ -557,13 +555,6 @@ export class SlickEditorLock { } export class Utils { - // jQuery's extend - private static getProto = Object.getPrototypeOf; - private static class2type: any = {}; - private static toString = Utils.class2type.toString; - private static hasOwn = Utils.class2type.hasOwnProperty; - private static fnToString = Utils.hasOwn.toString; - private static ObjectFunctionString = Utils.fnToString.call(Object); public static storage = { // https://stackoverflow.com/questions/29222027/vanilla-alternative-to-jquery-data-function-any-native-javascript-alternati _storage: new WeakMap(), @@ -589,81 +580,6 @@ export class Utils { } }; - public static isFunction(obj: any) { - return typeof obj === 'function' && typeof obj.nodeType !== 'number' && typeof obj.item !== 'function'; - } - - public static isPlainObject(obj: any) { - if (!obj || Utils.toString.call(obj) !== '[object Object]') { - return false; - } - - const proto = Utils.getProto(obj); - if (!proto) { - return true; - } - const Ctor = Utils.hasOwn.call(proto, 'constructor') && proto.constructor; - return typeof Ctor === 'function' && Utils.fnToString.call(Ctor) === Utils.ObjectFunctionString; - } - - public static extend(...args: any[]): T { - // eslint-disable-next-line one-var - let options, name, src, copy, copyIsArray, clone; - let target = args[0]; - let i = 1; - let deep = false; - const length = args.length; - - if (target === true) { - deep = target; - target = args[i] || {}; - i++; - } else { - target = target || {}; - } - if (typeof target !== 'object' && !Utils.isFunction(target)) { - target = {}; // Symbol and others will be converted to Object - } - if (length === 1) { - return args[0]; - } - /* istanbul ignore if */ - if (i === length) { - // @ts-ignore - // eslint-disable-next-line @typescript-eslint/no-this-alias - target = this; - i--; - } - for (; i < length; i++) { - if (isDefined(options = args[i])) { - for (name in options) { - copy = options[name]; - /* istanbul ignore if */ - if (name === '__proto__' || target === copy) { - continue; - } - if (deep && copy && (Utils.isPlainObject(copy) || (copyIsArray = Array.isArray(copy)))) { - src = target[name]; - if (copyIsArray && !Array.isArray(src)) { - clone = []; - } else if (!copyIsArray && !Utils.isPlainObject(src)) { - clone = {}; - } else { - clone = src; - } - copyIsArray = false; - target[name] = Utils.extend(deep, clone, copy); - } else if (copy !== undefined) { - target[name] = copy; - } - } - } - } - return target as T; - } - - public static noop() { } - public static height(el: HTMLElement, value?: number | string): number | void { if (!el) { return; diff --git a/packages/common/src/core/slickDataview.ts b/packages/common/src/core/slickDataview.ts index 5293b9687..6c74edebc 100644 --- a/packages/common/src/core/slickDataview.ts +++ b/packages/common/src/core/slickDataview.ts @@ -1,6 +1,6 @@ /* eslint-disable no-new-func */ /* eslint-disable no-bitwise */ -import { isDefined } from '@slickgrid-universal/utils'; +import { extend, isDefined } from '@slickgrid-universal/utils'; import { SlickGroupItemMetadataProvider } from '../extensions/slickGroupItemMetadataProvider'; import type { @@ -26,7 +26,6 @@ import { SlickGroup, SlickGroupTotals, SlickNonDataItem, - Utils, } from './slickCore'; import type { SlickGrid } from './slickGrid'; @@ -137,7 +136,7 @@ export class SlickDataView implements CustomD this.onSelectedRowIdsChanged = new SlickEvent('onSelectedRowIdsChanged', externalPubSub); this.onSetItemsCalled = new SlickEvent('onSetItemsCalled', externalPubSub); - this._options = Utils.extend(true, {}, this.defaults, options); + this._options = extend(true, {}, this.defaults, options); } /** @@ -394,7 +393,7 @@ export class SlickDataView implements CustomD this.groupingInfos = ((groupingInfo instanceof Array) ? groupingInfo : [groupingInfo]) as any; for (let i = 0; i < this.groupingInfos.length; i++) { - const gi = this.groupingInfos[i] = Utils.extend(true, {}, this.groupingInfoDefaults, this.groupingInfos[i]); + const gi = this.groupingInfos[i] = extend(true, {}, this.groupingInfoDefaults, this.groupingInfos[i]); gi.getterIsAFn = typeof gi.getter === 'function'; // pre-compile accumulator loops @@ -1319,7 +1318,7 @@ export class SlickDataView implements CustomD return; } - const previousPagingInfo = Utils.extend(true, {}, this.getPagingInfo()); + const previousPagingInfo = extend(true, {}, this.getPagingInfo()); const countBefore = this.rows.length; const totalRowsBefore = this.totalRows; diff --git a/packages/common/src/core/slickGrid.ts b/packages/common/src/core/slickGrid.ts index bebaa01c7..b710ede1c 100644 --- a/packages/common/src/core/slickGrid.ts +++ b/packages/common/src/core/slickGrid.ts @@ -2,7 +2,7 @@ import Sortable, { SortableEvent } from 'sortablejs'; import DOMPurify from 'dompurify'; import { BindingEventService } from '@slickgrid-universal/binding'; -import { createDomElement, emptyElement, getInnerSize, getOffset, insertAfterElement, isDefined, isPrimitiveOrHTML } from '@slickgrid-universal/utils'; +import { createDomElement, emptyElement, extend, getInnerSize, getOffset, insertAfterElement, isDefined, isPrimitiveOrHTML } from '@slickgrid-universal/utils'; import { type BasePubSub, @@ -588,7 +588,7 @@ export class SlickGrid = Column, O e if (!options) { this._options = {} as O; } Utils.applyDefaults(this._options, this._defaults); } else { - this._options = Utils.extend(true, {}, this._defaults, options); + this._options = extend(true, {}, this._defaults, options); } this.scrollThrottle = this.actionThrottle(this.render.bind(this), this._options.scrollRenderThrottling as number); this.maxSupportedCssHeight = this.maxSupportedCssHeight || this.getMaxSupportedCssHeight(); @@ -3002,7 +3002,7 @@ export class SlickGrid = Column, O e if (this._options.mixinDefaults) { Utils.applyDefaults(m, this._columnDefaults); } else { - m = this.columns[i] = Utils.extend({}, this._columnDefaults, m); + m = this.columns[i] = extend({}, this._columnDefaults, m); } this.columnsById[m.id] = i; @@ -3076,8 +3076,8 @@ export class SlickGrid = Column, O e this.handleScroll(); // trigger scroll to realign column headers as well } - const originalOptions = Utils.extend(true, {}, this._options); - this._options = Utils.extend(this._options, newOptions); + const originalOptions = extend(true, {}, this._options); + this._options = extend(this._options, newOptions); this.trigger(this.onSetOptions, { optionsBefore: originalOptions, optionsAfter: this._options }); this.internal_setOptions(suppressRender, suppressColumnSet, suppressSetOverflow); @@ -4339,7 +4339,7 @@ export class SlickGrid = Column, O e // add new rows & missing cells in existing rows if (this.lastRenderedScrollLeft !== this.scrollLeft) { if (this.hasFrozenRows) { - const renderedFrozenRows = Utils.extend(true, {}, rendered); + const renderedFrozenRows = extend(true, {}, rendered); if (this._options.frozenBottom) { renderedFrozenRows.top = this.actualFrozenRow; diff --git a/packages/common/src/extensions/slickGroupItemMetadataProvider.ts b/packages/common/src/extensions/slickGroupItemMetadataProvider.ts index 2d1e9ca09..264ef33ac 100644 --- a/packages/common/src/extensions/slickGroupItemMetadataProvider.ts +++ b/packages/common/src/extensions/slickGroupItemMetadataProvider.ts @@ -1,6 +1,6 @@ -import { createDomElement } from '@slickgrid-universal/utils'; +import { createDomElement, extend } from '@slickgrid-universal/utils'; -import { SlickEventHandler, Utils as SlickUtils, type SlickDataView, SlickGroup, type SlickGrid } from '../core/index'; +import { SlickEventHandler, type SlickDataView, SlickGroup, type SlickGrid } from '../core/index'; import type { Column, DOMEvent, @@ -42,7 +42,7 @@ export class SlickGroupItemMetadataProvider { constructor(inputOptions?: GroupItemMetadataProviderOption) { this._eventHandler = new SlickEventHandler(); - this._options = SlickUtils.extend(true, {}, this._defaults, inputOptions); + this._options = extend(true, {}, this._defaults, inputOptions); } /** Getter of the SlickGrid Event Handler */ diff --git a/packages/common/src/services/filter.service.ts b/packages/common/src/services/filter.service.ts index 3e0f14d58..cfd0e959b 100644 --- a/packages/common/src/services/filter.service.ts +++ b/packages/common/src/services/filter.service.ts @@ -1,7 +1,6 @@ import { BasePubSubService } from '@slickgrid-universal/event-pub-sub'; -import { deepCopy, stripTags } from '@slickgrid-universal/utils'; +import { deepCopy, extend, stripTags } from '@slickgrid-universal/utils'; import { dequal } from 'dequal/lite'; -import { Utils as SlickUtils } from '../core/index'; import { Constants } from '../constants'; import { FilterConditions, getParsedSearchTermsByFieldType } from './../filter-conditions/index'; @@ -1168,7 +1167,7 @@ export class FilterService { // event might have been created as a CustomEvent (e.g. CompoundDateFilter), without being a valid SlickEventData, // if so we will create a new SlickEventData and merge it with that CustomEvent to avoid having SlickGrid errors - const eventData = ((event && typeof (event as any).isPropagationStopped !== 'function') ? SlickUtils.extend({}, new SlickEventData(), event) : event); + const eventData = ((event && typeof (event as any).isPropagationStopped !== 'function') ? extend({}, new SlickEventData(), event) : event); // trigger an event only if Filters changed or if ENTER key was pressed const eventKey = (event as KeyboardEvent)?.key; diff --git a/packages/utils/src/__tests__/domUtils.spec.ts b/packages/utils/src/__tests__/domUtils.spec.ts index fe27b4a36..6ccf05484 100644 --- a/packages/utils/src/__tests__/domUtils.spec.ts +++ b/packages/utils/src/__tests__/domUtils.spec.ts @@ -6,6 +6,7 @@ import { destroyAllElementProps, emptyElement, findFirstAttribute, + findWidthOrDefault, getHTMLFromFragment, getOffsetRelativeToParent, getStyleProp, @@ -13,10 +14,12 @@ import { getInnerSize, htmlEncode, htmlEntityDecode, + htmlEncodeWithPadding, + insertAfterElement, } from '../domUtils'; describe('Service/domUtilies', () => { - describe('calculateAvailableSpace method', () => { + describe('calculateAvailableSpace() method', () => { const div = document.createElement('div'); div.innerHTML = `
  • Item 1
  • Item 2
`; document.body.appendChild(div); @@ -48,7 +51,7 @@ describe('Service/domUtilies', () => { }); }); - describe('createDomElement method', () => { + describe('createDomElement() method', () => { it('should create a DOM element via the method to equal a regular DOM element', () => { const div = document.createElement('div'); div.className = 'red bold'; @@ -66,7 +69,7 @@ describe('Service/domUtilies', () => { expect(div.outerHTML).toBe('
some text
'); }); - it('should display a warning when trying to use innerHTML via the method', () => { + it('should display a warning when trying to use innerHTML via the method', () => { const consoleWarnSpy = jest.spyOn(global.console, 'warn').mockReturnValue(); createDomElement('div', { className: 'red bold', innerHTML: '' }); @@ -87,7 +90,7 @@ describe('Service/domUtilies', () => { }) }) - describe('emptyElement method', () => { + describe('emptyElement() method', () => { const div = document.createElement('div'); div.innerHTML = `
  • Item 1
  • Item 2
`; document.body.appendChild(div); @@ -99,7 +102,7 @@ describe('Service/domUtilies', () => { }); }); - describe('findFirstAttribute method', () => { + describe('findFirstAttribute() method', () => { const div = document.createElement('div'); div.innerHTML = `
  • Item 1
  • Item 2
`; document.body.appendChild(div); @@ -120,6 +123,20 @@ describe('Service/domUtilies', () => { }); }); + describe('findWidthOrDefault() method', () => { + it('should return default value when input is null', () => { + expect(findWidthOrDefault(null, '20px')).toBe('20px'); + }); + + it('should return default value when input is undefined', () => { + expect(findWidthOrDefault(undefined, '20px')).toBe('20px'); + }); + + it('should return value in pixel when input is a number', () => { + expect(findWidthOrDefault(33, '20px')).toBe('33px'); + }); + }); + describe('getHTMLFromFragment() method', () => { it('should return innerHTML from fragment', () => { const div = document.createElement('div'); @@ -157,7 +174,7 @@ describe('Service/domUtilies', () => { }); }); - describe('getElementOffsetRelativeToParent method', () => { + describe('getElementOffsetRelativeToParent() method', () => { const parentDiv = document.createElement('div'); const childDiv = document.createElement('div'); parentDiv.innerHTML = ``; @@ -184,7 +201,7 @@ describe('Service/domUtilies', () => { }); }); - describe('getOffset method', () => { + describe('getOffset() method', () => { const div = document.createElement('div'); div.innerHTML = ``; document.body.appendChild(div); @@ -239,7 +256,7 @@ describe('Service/domUtilies', () => { }); }); - describe('htmlEncode method', () => { + describe('htmlEncode() method', () => { it('should return a encoded HTML string', () => { const result = htmlEncode(`
Something
`); expect(result).toBe(`<div class="color: blue">Something</div>`); @@ -251,7 +268,7 @@ describe('Service/domUtilies', () => { }); }); - describe('htmlEntityDecode method', () => { + describe('htmlEntityDecode() method', () => { it('should be able to decode HTML entity of an HTML string', () => { const result = htmlEntityDecode(`<div>a</div>`); expect(result).toBe(`
a
`); @@ -262,4 +279,34 @@ describe('Service/domUtilies', () => { expect(result).toBe(`Sam's 🚀🦄 español`); }); }); + + describe('htmlEncodeWithPadding() method', () => { + it('should return 2 spaces HTML encoded when input is empty', () => { + const result = htmlEncodeWithPadding('', 2); + expect(result).toBe(`  `); + }); + + it('should be able to encore HTML entity to an encoded HTML string', () => { + const result = htmlEncodeWithPadding(`
some text
`, 2); + expect(result).toBe(`<div>some text</div>`); + }); + }); + + describe('insertAfterElement() method', () => { + it('should insert span3 after span1', () => { + const div = document.createElement('div'); + div.className = 'div-one'; + const span1 = document.createElement('span'); + span1.className = 'span-one'; + const span2 = document.createElement('span'); + span2.className = 'span-two'; + const span3 = document.createElement('span'); + span3.className = 'span-three'; + div.appendChild(span1); + div.appendChild(span2); + + insertAfterElement(span1, span3); + expect(div.outerHTML).toBe(`
`); + }); + }); }); diff --git a/packages/utils/src/__tests__/nodeExtend.spec.ts b/packages/utils/src/__tests__/nodeExtend.spec.ts new file mode 100644 index 000000000..28bb59fe1 --- /dev/null +++ b/packages/utils/src/__tests__/nodeExtend.spec.ts @@ -0,0 +1,720 @@ +import { extend } from '../nodeExtend'; + +// port of all the node-extend tests +// original tests can be found at: https://github.com/justmoon/node-extend/blob/main/test/index.js + +describe('extend()', () => { + const str = 'me a test'; + const integer = 10; + const arr = [1, 'what', new Date(81, 8, 4)]; + const date = new Date(81, 4, 13); + + const Foo = function () { }; + + const obj = { + str, + integer, + arr, + date, + constructor: 'fake', + isPrototypeOf: 'not a function', + foo: new Foo() + }; + + const deep = { + ori: obj, + layer: { + integer: 10, + str: 'str', + date: new Date(84, 5, 12), + arr: [101, 'dude', new Date(82, 10, 4)], + deep: { + str: obj.str, + integer, + arr: obj.arr, + date: new Date(81, 7, 4) + } + } + }; + + describe('missing arguments', () => { + test('missing first argument is second argument', () => expect(extend(undefined, { a: 1 })).toEqual({ a: 1 })); + test('missing second argument is first argument', () => expect(extend({ a: 1 })).toEqual({ a: 1 })); + test('deep: missing first argument is second argument', () => expect(extend(true, undefined, { a: 1 })).toEqual({ a: 1 })); + test('deep: missing second argument is first argument', () => expect(extend(true, { a: 1 })).toEqual({ a: 1 })); + test('no arguments is object', () => expect(extend()).toEqual({})); + }); + + describe('merge string with string', () => { + const ori = 'what u gonna say'; + const target = extend(ori, str); + const expectedTarget = { + 0: 'm', + 1: 'e', + 2: ' ', + 3: 'a', + 4: ' ', + 5: 't', + 6: 'e', + 7: 's', + 8: 't' + }; + + test('original string 1 is unchanged', () => expect(ori).toBe('what u gonna say')); + test('original string 2 is unchanged', () => expect(str).toBe('me a test')); + test('string + string is merged object form of string', () => expect(target).toEqual(expectedTarget)); + }); + + describe('merge string with number', () => { + const ori = 'what u gonna say'; + const target = extend(ori, 10); + + test('original string is unchanged', () => expect(ori).toBe('what u gonna say')); + test('string + number is empty object', () => expect(target).toEqual({})); + }); + + describe('merge string with array', () => { + const ori = 'what u gonna say'; + const target = extend(ori, arr); + + test('original string is unchanged', () => expect(ori).toBe('what u gonna say')); + test('array is unchanged', () => expect(arr).toEqual([1, 'what', new Date(81, 8, 4)])); + test('string + array is array', () => expect(target).toEqual({ + 0: 1, + 1: 'what', + 2: new Date(81, 8, 4) + })); + }); + + describe('merge string with date', () => { + const ori = 'what u gonna say'; + const target = extend(ori, date); + + const testDate = new Date(81, 4, 13); + test('original string is unchanged', () => expect(ori).toBe('what u gonna say')); + test('date is unchanged', () => expect(date).toEqual(testDate)); + // test('string + date is date', () => expect(target).toEqual(testDate)); + test('string + date is empty object', () => expect(target).toEqual({})); + }); + + describe('merge string with obj', () => { + const ori = 'what u gonna say'; + const target = extend(ori, obj); + const testObj = { + str: 'me a test', + integer: 10, + arr: [1, 'what', new Date(81, 8, 4)], + date: new Date(81, 4, 13), + constructor: 'fake', + isPrototypeOf: 'not a function', + foo: new Foo() + }; + + test('original string is unchanged', () => expect(ori).toBe('what u gonna say')); + test('original obj is unchanged', () => expect(obj).toEqual(testObj)); + test('string + obj is obj', () => expect(target).toEqual(testObj)); + }); + + describe('merge number with string', () => { + const ori = 20; + const target = extend(ori, str); + + test('number is unchanged', () => expect(ori).toBe(20)); + test('string is unchanged', () => expect(str).toBe('me a test')); + test('number + string is object form of string', () => expect(target).toEqual({ + 0: 'm', + 1: 'e', + 2: ' ', + 3: 'a', + 4: ' ', + 5: 't', + 6: 'e', + 7: 's', + 8: 't' + })); + }); + + describe('merge number with number', () => { + test('number + number is empty object', () => expect(extend(20, 10)).toEqual({})); + }); + + describe('merge number with array', () => { + const target = extend(20, arr); + + test('array is unchanged', () => expect(arr).toEqual([1, 'what', new Date(81, 8, 4)])); + test('number + arr is object with array contents', () => expect(target).toEqual({ + 0: 1, + 1: 'what', + 2: new Date(81, 8, 4) + })); + }); + + describe('merge number with date', () => { + const target = extend(20, date); + const testDate = new Date(81, 4, 13); + + test('original date is unchanged', () => expect(date).toEqual(testDate)); + // test('number + date is date', () => expect(target).toEqual(testDate)); + test('number + date is empty object', () => expect(target).toEqual({})); + }); + + describe('merge number with object', () => { + const target = extend(20, obj); + const testObj = { + str: 'me a test', + integer: 10, + arr: [1, 'what', new Date(81, 8, 4)], + date: new Date(81, 4, 13), + constructor: 'fake', + isPrototypeOf: 'not a function', + foo: new Foo() + }; + + test('obj is unchanged', () => expect(obj).toEqual(testObj)); + test('number + obj is obj', () => expect(target).toEqual(testObj)); + }); + + describe('merge array with string', () => { + const ori = [1, 2, 3, 4, 5, 6]; + const target = extend(ori, str); + + test('array is changed to be an array of string chars', () => expect(ori).toEqual(str.split(''))); + test('string is unchanged', () => expect(str).toBe('me a test')); + test('array + string is object form of string', () => expect(target).toMatchObject({ + 0: 'm', + 1: 'e', + 2: ' ', + 3: 'a', + 4: ' ', + 5: 't', + 6: 'e', + 7: 's', + 8: 't' + })); + }); + + describe('merge array with number', () => { + const ori = [1, 2, 3, 4, 5, 6]; + const target = extend(ori, 10); + + test('array is unchanged', () => expect(ori).toEqual([1, 2, 3, 4, 5, 6])); + test('array + number is array', () => expect(target).toEqual(ori)); + }); + + describe('merge array with array', () => { + const ori = [1, 2, 3, 4, 5, 6]; + const target = extend(ori, arr); + const testDate = new Date(81, 8, 4); + const expectedTarget = [1, 'what', testDate, 4, 5, 6]; + + test('array + array merges arrays; changes first array', () => expect(ori).toEqual(expectedTarget)); + test('second array is unchanged', () => expect(arr).toEqual([1, 'what', testDate])); + test('array + array is merged array', () => expect(target).toEqual(expectedTarget)); + }); + + describe('merge array with date', () => { + const ori = [1, 2, 3, 4, 5, 6]; + const target = extend(ori, date); + const testDate = new Date(81, 4, 13); + const testArray = [1, 2, 3, 4, 5, 6]; + + test('array is unchanged', () => expect(ori).toEqual(testArray)); + test('date is unchanged', () => expect(date).toEqual(testDate)); + test('array + date is array', () => expect(target).toEqual(testArray)); + }); + + describe('merge array with object', () => { + const ori: any = [1, 2, 3, 4, 5, 6]; + const target = extend(ori, obj); + const testObject = { + str: 'me a test', + integer: 10, + arr: [1, 'what', new Date(81, 8, 4)], + date: new Date(81, 4, 13), + constructor: 'fake', + isPrototypeOf: 'not a function', + foo: new Foo() + }; + + test('obj is unchanged', () => expect(obj).toEqual(testObject)); + test('array has proper length', () => expect(ori.length).toBe(6)); + test('array has obj.str property', () => expect(ori.str).toBe(obj.str)); + test('array has obj.integer property', () => expect(ori.integer).toBe(obj.integer)); + test('array has obj.arr property', () => expect(ori.arr).toEqual(obj.arr)); + test('array has obj.date property', () => expect(ori.date).toBe(obj.date)); + + test('target has proper length', () => expect(target.length).toBe(6)); + test('target has obj.str property', () => expect(target.str).toBe(obj.str)); + test('target has obj.integer property', () => expect(target.integer).toBe(obj.integer)); + test('target has obj.arr property', () => expect(target.arr).toEqual(obj.arr)); + test('target has obj.date property', () => expect(target.date).toBe(obj.date)); + }); + + describe('merge date with string', () => { + const ori = new Date(81, 9, 20); + const target = extend(ori, str); + const testObject = { + 0: 'm', + 1: 'e', + 2: ' ', + 3: 'a', + 4: ' ', + 5: 't', + 6: 'e', + 7: 's', + 8: 't' + }; + + test('date is changed to object form of string', () => expect(ori).toMatchObject(testObject)); + test('string is unchanged', () => expect(str).toBe('me a test')); + test('date + string is object form of string', () => expect(target).toMatchObject(testObject)); + }); + + describe('merge date with number', () => { + const ori = new Date(81, 9, 20); + const target = extend(ori, 10); + + // test('date is changed to empty object', () => expect(ori).toEqual({})); + // test('date + number is empty object', () => expect(target).toEqual({})); + test('date is unchanged', () => expect(ori).toEqual(ori)); + test('date + number is date', () => expect(target).toEqual(ori)); + }); + + describe('merge date with array', () => { + const ori = new Date(81, 9, 20); + const target = extend(ori, arr); + const testDate = new Date(81, 9, 20); + const testArray = [1, 'what', new Date(81, 8, 4)]; + + test('date is unchanged', () => expect(ori).toEqual(testDate)); + test('array is unchanged', () => expect(arr).toEqual(testArray)); + test('date + array is date', () => expect(target).toEqual(testDate)); + }); + + describe('merge date with date', () => { + const ori = new Date(81, 9, 20); + const target = extend(ori, date); + + // test('date is empty object', () => expect(ori).toEqual({})); + // test('date + date is empty object', () => expect(target).toEqual({})); + test('date + date is date', () => expect(target).toEqual(ori)); + }); + + describe('merge date with object', () => { + const ori = new Date(81, 9, 20); + const target = extend(ori, obj); + const testDate = new Date(81, 8, 4); + const testObject = { + str: 'me a test', + integer: 10, + arr: [1, 'what', testDate], + date: new Date(81, 4, 13), + constructor: 'fake', + isPrototypeOf: 'not a function', + foo: new Foo() + }; + + test('original object is unchanged', () => expect(obj).toMatchObject(testObject)); + test('date becomes original object', () => expect(ori).toMatchObject(testObject)); + test('date + object is object', () => expect(target).toMatchObject(testObject)); + }); + + describe('merge object with string', () => { + const testDate = new Date(81, 7, 26); + const ori = { + str: 'no shit', + integer: 76, + arr: [1, 2, 3, 4], + date: testDate + }; + const target = extend(ori, str); + const testObj = { + 0: 'm', + 1: 'e', + 2: ' ', + 3: 'a', + 4: ' ', + 5: 't', + 6: 'e', + 7: 's', + 8: 't', + str: 'no shit', + integer: 76, + arr: [1, 2, 3, 4], + date: testDate + }; + + test('original object updated', () => expect(ori).toEqual(testObj)); + test('string is unchanged', () => expect(str).toBe('me a test')); + test('object + string is object + object form of string', () => expect(target).toEqual(testObj)); + }); + + describe('merge object with number', () => { + const ori = { + str: 'no shit', + integer: 76, + arr: [1, 2, 3, 4], + date: new Date(81, 7, 26) + }; + const testObject = { + str: 'no shit', + integer: 76, + arr: [1, 2, 3, 4], + date: new Date(81, 7, 26) + }; + const target = extend(ori, 10); + + test('object is unchanged', () => expect(ori).toEqual(testObject)); + test('object + number is object', () => expect(target).toEqual(testObject)); + }); + + describe('merge object with array', () => { + const ori = { + str: 'no shit', + integer: 76, + arr: [1, 2, 3, 4], + date: new Date(81, 7, 26) + }; + const target = extend(ori, arr); + const testObject = { + 0: 1, + 1: 'what', + 2: new Date(81, 8, 4), + str: 'no shit', + integer: 76, + arr: [1, 2, 3, 4], + date: new Date(81, 7, 26) + }; + + test('original object is merged', () => expect(ori).toEqual(testObject)); + test('array is unchanged', () => expect(arr).toEqual([1, 'what', testObject[2]])); + test('object + array is merged object', () => expect(target).toEqual(testObject)); + }); + + describe('merge object with date', () => { + const ori = { + str: 'no shit', + integer: 76, + arr: [1, 2, 3, 4], + date: new Date(81, 7, 26) + }; + const target = extend(ori, date); + const testObject = { + str: 'no shit', + integer: 76, + arr: [1, 2, 3, 4], + date: new Date(81, 7, 26) + }; + + test('original object is unchanged', () => expect(ori).toEqual(testObject)); + test('date is unchanged', () => expect(date).toEqual(new Date(81, 4, 13))); + test('object + date is object', () => expect(target).toEqual(testObject)); + }); + + describe('merge object with object', () => { + const ori = { + str: 'no shit', + integer: 76, + arr: [1, 2, 3, 4], + date: new Date(81, 7, 26), + foo: 'bar' + }; + const target = extend(ori, obj); + const expectedObj = { + str: 'me a test', + integer: 10, + arr: [1, 'what', new Date(81, 8, 4)], + date: new Date(81, 4, 13), + constructor: 'fake', + isPrototypeOf: 'not a function', + foo: new Foo() + }; + const expectedTarget = { + str: 'me a test', + integer: 10, + arr: [1, 'what', new Date(81, 8, 4)], + date: new Date(81, 4, 13), + constructor: 'fake', + isPrototypeOf: 'not a function', + foo: new Foo() + }; + + test('obj is unchanged', () => expect(obj).toEqual(expectedObj)); + test('original has been merged', () => expect(ori).toEqual(expectedTarget)); + test('object + object is merged object', () => expect(target).toEqual(expectedTarget)); + }); + + describe('deep clone', () => { + const ori = { + str: 'no shit', + integer: 76, + arr: [1, 2, 3, 4], + date: new Date(81, 7, 26), + layer: { deep: { integer: 42 } } + }; + const target = extend(true, ori, deep); + + test('original object is merged', () => expect(ori).toEqual({ + str: 'no shit', + integer: 76, + arr: [1, 2, 3, 4], + date: new Date(81, 7, 26), + ori: { + str: 'me a test', + integer: 10, + arr: [1, 'what', new Date(81, 8, 4)], + date: new Date(81, 4, 13), + constructor: 'fake', + isPrototypeOf: 'not a function', + foo: new Foo() + }, + layer: { + integer: 10, + str: 'str', + date: new Date(84, 5, 12), + arr: [101, 'dude', new Date(82, 10, 4)], + deep: { + str: 'me a test', + integer: 10, + arr: [1, 'what', new Date(81, 8, 4)], + date: new Date(81, 7, 4) + } + } + })); + + test('deep is unchanged', () => expect(deep).toEqual({ + ori: { + str: 'me a test', + integer: 10, + arr: [1, 'what', new Date(81, 8, 4)], + date: new Date(81, 4, 13), + constructor: 'fake', + isPrototypeOf: 'not a function', + foo: new Foo() + }, + layer: { + integer: 10, + str: 'str', + date: new Date(84, 5, 12), + arr: [101, 'dude', new Date(82, 10, 4)], + deep: { + str: 'me a test', + integer: 10, + arr: [1, 'what', new Date(81, 8, 4)], + date: new Date(81, 7, 4) + } + } + })); + + test('deep + object + object is deeply merged object', () => expect(target).toEqual({ + str: 'no shit', + integer: 76, + arr: [1, 2, 3, 4], + date: new Date(81, 7, 26), + ori: { + str: 'me a test', + integer: 10, + arr: [1, 'what', new Date(81, 8, 4)], + date: new Date(81, 4, 13), + constructor: 'fake', + isPrototypeOf: 'not a function', + foo: new Foo() + }, + layer: { + integer: 10, + str: 'str', + date: new Date(84, 5, 12), + arr: [101, 'dude', new Date(82, 10, 4)], + deep: { + str: 'me a test', + integer: 10, + arr: [1, 'what', new Date(81, 8, 4)], + date: new Date(81, 7, 4) + } + } + })); + + // ----- NEVER USE EXTEND WITH THE ABOVE SITUATION ------------------------------ + }); + + describe('deep clone - change target property', () => { + const ori = { + str: 'no shit', + integer: 76, + arr: [1, 2, 3, 4], + date: new Date(81, 7, 26), + layer: { deep: { integer: 42 } } + }; + const target = extend(true, ori, deep); + target.layer.deep = 339; + + test('deep is unchanged after setting target property', () => expect(deep).toEqual({ + ori: { + str: 'me a test', + integer: 10, + arr: [1, 'what', new Date(81, 8, 4)], + date: new Date(81, 4, 13), + constructor: 'fake', + isPrototypeOf: 'not a function', + foo: new Foo() + }, + layer: { + integer: 10, + str: 'str', + date: new Date(84, 5, 12), + arr: [101, 'dude', new Date(82, 10, 4)], + deep: { + str: 'me a test', + integer: 10, + arr: [1, 'what', new Date(81, 8, 4)], + date: new Date(81, 7, 4) + } + } + })); + // ----- NEVER USE EXTEND WITH THE ABOVE SITUATION ------------------------------ + }); + + describe('deep clone; arrays are merged', () => { + const defaults = { arr: [1, 2, 3] }; + const override = { arr: ['x'] }; + const expectedTarget = { arr: ['x', 2, 3] }; + + const target = extend(true, defaults, override); + + test('arrays are merged', () => expect(target).toEqual(expectedTarget)); + }); + + describe('deep clone === false; objects merged normally', () => { + const defaults = { a: 1 }; + const override = { a: 2 }; + const target = extend(false, defaults, override); + test('deep === false handled normally', () => expect(target).toEqual(override)); + }); + + describe('pass in null; should create a valid object', () => { + const override = { a: 1 }; + const target = extend(null, override); + test('null object handled normally', () => expect(target).toEqual(override)); + }); + + describe('works without Array.isArray', () => { + const savedIsArray = Array.isArray; + Array.isArray = false as any; // don't delete, to preserve enumerability + const target = []; + const source = [1, [2], { 3: true }]; + + test('It works without Array.isArray', () => expect( + extend(true, target, source)).toEqual( + [1, [2], { 3: true }] + )); + Array.isArray = savedIsArray; + }); + + describe('non-object target', () => { + test('non-object', () => expect(extend(3.14, { a: 'b' })).toEqual({ a: 'b' })); + test('non-object', () => expect(extend(true, 3.14, { a: 'b' })).toEqual({ a: 'b' })); + }); + + describe('__proto__ is merged as an own property', () => { + const malicious = { fred: 1 }; + Object.defineProperty(malicious, '__proto__', { value: { george: 1 }, enumerable: true }); + const target: any = {}; + extend(true, target, malicious); + + test('falsy target prop', () => expect(target.george).toBeFalsy()); + test('truthy prototype', () => expect(Object.prototype.hasOwnProperty.call(target, '__proto__')).toBeTruthy()); + test('prototype has a value set', () => expect(Object.getOwnPropertyDescriptor(target, '__proto__')!.value).toEqual({ george: 1 })); + }); +}); + +describe('extend() - extra tests', () => { + it('should be able to make a perfect deep copy of an object', () => { + const callback = () => console.log('hello'); + const obj1 = { hello: { sender: 'me', target: 'world' }, deeper: { children: ['abc', 'cde'], callback } }; + const obj2 = extend(true, {}, obj1, { another: 'prop' }); + + expect(obj2).toEqual({ hello: { sender: 'me', target: 'world' }, deeper: { children: ['abc', 'cde'], callback }, another: 'prop' }); + }); + + it('should be able to make a perfect deep copy of an object that includes String() and Boolean() constructors', () => { + const callback = () => console.log('hello'); + const obj1 = { hello: { sender: 'me', target: String(123), valid: Boolean(null) }, deeper: { children: ['abc', 'cde'], callback } }; + const obj2 = extend(true, {}, obj1, { another: 'prop' }); + + expect(obj2).toEqual({ hello: { sender: 'me', target: '123', valid: false }, deeper: { children: ['abc', 'cde'], callback }, another: 'prop' }); + }); + + it('should be able to make a deep copy of an object and changing new object prop should not affect input object', () => { + const obj1 = { hello: { sender: 'me', target: 'world' }, deeper: { children: ['abc', 'cde'] } }; + const obj2 = extend(true, {}, obj1, { another: 'prop' }); + obj2.hello.target = 'mum'; + + expect(obj1).toEqual({ hello: { sender: 'me', target: 'world' }, deeper: { children: ['abc', 'cde'] } }); + expect(obj2).toEqual({ hello: { sender: 'me', target: 'mum' }, deeper: { children: ['abc', 'cde'] }, another: 'prop' }); + }); + + it('should assume an extended object when passing true boolean but ommitting empty object as target, so changing output object will impact input object as well', () => { + const obj1 = { hello: { sender: 'me', target: 'world' }, deeper: { children: ['abc', 'cde'] } }; + const obj2 = extend(true, obj1, { another: 'prop' }); + obj2.hello.target = 'mum'; + + expect(obj1).toEqual({ hello: { sender: 'me', target: 'mum' }, deeper: { children: ['abc', 'cde'] }, another: 'prop' }); + expect(obj2).toEqual({ hello: { sender: 'me', target: 'mum' }, deeper: { children: ['abc', 'cde'] }, another: 'prop' }); + }); + + it('should assume an extended object when ommitting true boolean, so changing output object will impact input object as well', () => { + const obj1 = { hello: { sender: 'me', target: 'world' }, deeper: { children: ['abc', 'cde'] } }; + const obj2 = extend(obj1, { another: { age: 20 } }); + obj2.hello.target = 'mum'; + + expect(obj1).toEqual({ hello: { sender: 'me', target: 'mum' }, deeper: { children: ['abc', 'cde'] }, another: { age: 20 } }); + expect(obj2).toEqual({ hello: { sender: 'me', target: 'mum' }, deeper: { children: ['abc', 'cde'] }, another: { age: 20 } }); + }); + + it('should return same object when passing input object twice', () => { + const obj1 = { hello: { sender: 'me', target: 'world' }, deeper: { children: ['abc', 'cde'] } }; + const obj2 = extend(true, {}, obj1, obj1); + + expect(obj2).toEqual(obj1); + }); + + it('should do a deep copy of an array of objects with properties having objects and changing object property should not affect original object', () => { + const obj1 = { firstName: 'John', lastName: 'Doe', address: { zip: 123456 } }; + const obj2 = { firstName: 'Jane', lastName: 'Doe', address: { zip: 222222 } }; + const arr1 = [obj1, obj2]; + const arr2 = extend(true, [], arr1); + arr2[0].address.zip = 888888; + arr2[1].address.zip = 999999; + + expect(arr1[0].address.zip).toBe(123456); + expect(arr1[1].address.zip).toBe(222222); + expect(arr2[0].address.zip).toBe(888888); + expect(arr2[1].address.zip).toBe(999999); + }); + + it('should return same object when passing only a single object', () => { + expect(extend({ hello: 'world' })).toEqual({ hello: 'world' }); + }); + + it('should expect Symbol to be converted to Object', () => { + const sym1 = Symbol("foo"); + const sym2 = Symbol("bar"); + + expect(extend(sym1, sym2, { hello: 'world' })).toEqual({ hello: 'world' }); + }); + + it('should be able to make a copy of an object with prototype', () => { + const l = console.log; + const method = () => l("method in obj"); + const obj = { + method + }; + const obj2: any = { hello: 'world' }; + obj2.__proto__ = obj; + + const obj1 = { hello: { sender: 'me', target: 'world' }, deeper: { children: ['abc', 'cde'] } }; + const obj3 = extend(obj1, obj2); + + expect(obj3).toEqual({ hello: 'world', deeper: { children: ['abc', 'cde'] }, method }); + }); +}); \ No newline at end of file diff --git a/packages/utils/src/domUtils.ts b/packages/utils/src/domUtils.ts index 395521490..b314676ff 100644 --- a/packages/utils/src/domUtils.ts +++ b/packages/utils/src/domUtils.ts @@ -181,7 +181,7 @@ export function findFirstAttribute(inputElm: Element | null | undefined, attribu * @param {Number | String} defaultValue [defaultValue=auto] - optional default value or use "auto" when nothing is provided * @returns {String} string output */ -export function findWidthOrDefault(inputWidth?: number | string, defaultValue = 'auto'): string { +export function findWidthOrDefault(inputWidth?: number | string | null, defaultValue = 'auto'): string { return (/^[0-9]+$/i.test(`${inputWidth}`) ? `${+(inputWidth as number)}px` : inputWidth as string) || defaultValue; } diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 150b3eb02..59c437ec7 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1,4 +1,5 @@ export * from './domUtils'; +export * from './nodeExtend'; export * from './stripTagsUtil'; export * from './types'; export * from './utils'; \ No newline at end of file diff --git a/packages/utils/src/nodeExtend.ts b/packages/utils/src/nodeExtend.ts new file mode 100644 index 000000000..f51b44129 --- /dev/null +++ b/packages/utils/src/nodeExtend.ts @@ -0,0 +1,131 @@ +/* eslint-disable guard-for-in */ +/** + * This extend function is a reimplementation of the npm package `extend` (also named `node-extend`). + * The reason for the reimplementation was mostly because the original project is not ESM compatible + * and written with old ES6 IIFE syntax, the goal was to reimplement and fix these old syntax and build problems. + * e.g. it used `var` everywhere, it used `arguments` to get function arguments, ... + * + * The previous lib can be found here at this Github link: + * https://github.com/justmoon/node-extend + * With an MIT licence that and can be found at + * https://github.com/justmoon/node-extend/blob/main/LICENSE + */ + +const hasOwn = Object.prototype.hasOwnProperty; +const toStr = Object.prototype.toString; +const defineProperty = Object.defineProperty; +const gOPD = Object.getOwnPropertyDescriptor; + +const isArray = function isArray(arr: any) { + if (typeof Array.isArray === 'function') { + return Array.isArray(arr); + } + /* istanbul ignore next */ + return toStr.call(arr) === '[object Array]'; +}; + +const isPlainObject = function isPlainObject(obj: any) { + if (!obj || toStr.call(obj) !== '[object Object]') { + return false; + } + + const hasOwnConstructor = hasOwn.call(obj, 'constructor'); + const hasIsPrototypeOf = obj.constructor && obj.constructor.prototype && hasOwn.call(obj.constructor.prototype, 'isPrototypeOf'); + // Not own constructor property must be Object + if (obj.constructor && !hasOwnConstructor && !hasIsPrototypeOf) { + return false; + } + + // Own properties are enumerated firstly, so to speed up, if last one is own, then all properties are own. + let key; + for (key in obj) { /**/ } + + return typeof key === 'undefined' || hasOwn.call(obj, key); +}; + +// If name is '__proto__', and Object.defineProperty is available, define __proto__ as an own property on target +const setProperty = function setProperty(target: any, options: any) { + if (defineProperty && options.name === '__proto__') { + defineProperty(target, options.name, { + enumerable: true, + configurable: true, + value: options.newValue, + writable: true + }); + } else { + target[options.name] = options.newValue; + } +}; + +// Return undefined instead of __proto__ if '__proto__' is not an own property +const getProperty = function getProperty(obj: any, name: any) { + if (name === '__proto__') { + if (!hasOwn.call(obj, name)) { + return void 0; + } else if (gOPD) { + // In early versions of node, obj['__proto__'] is buggy when obj has __proto__ as an own property. Object.getOwnPropertyDescriptor() works. + return gOPD(obj, name)!.value; + } + } + + return obj[name]; +}; + +export function extend(...args: any[]): T { + let options; + let name; + let src; + let copy; + let copyIsArray; + let clone; + let target = args[0]; + let i = 1; + const length = args.length; + let deep = false; + + // Handle a deep copy situation + if (typeof target === 'boolean') { + deep = target; + target = args[1] || {}; + // skip the boolean and the target + i = 2; + } + if (target === null || target === undefined || (typeof target !== 'object' && typeof target !== 'function')) { + target = {}; + } + + for (; i < length; ++i) { + options = args[i]; + // Only deal with non-null/undefined values + if (options !== null && options !== undefined) { + // Extend the base object + for (name in options) { + src = getProperty(target, name); + copy = getProperty(options, name); + + // Prevent never-ending loop + if (target !== copy) { + // Recurse if we're merging plain objects or arrays + if (deep && copy && (isPlainObject(copy) || (copyIsArray = isArray(copy)))) { + if (copyIsArray) { + copyIsArray = false; + clone = src && isArray(src) ? src : []; + } else { + clone = src && isPlainObject(src) ? src : {}; + } + + // Never move original objects, clone them + setProperty(target, { name, newValue: extend(deep, clone, copy) }); + + // Don't bring in undefined values + } else if (typeof copy !== 'undefined') { + setProperty(target, { name, newValue: copy }); + } + } + } + } + } + + // Return the modified object + return target; +}; \ No newline at end of file diff --git a/packages/vanilla-bundle/src/components/slick-vanilla-grid-bundle.ts b/packages/vanilla-bundle/src/components/slick-vanilla-grid-bundle.ts index f0d72f9b2..45894ca19 100644 --- a/packages/vanilla-bundle/src/components/slick-vanilla-grid-bundle.ts +++ b/packages/vanilla-bundle/src/components/slick-vanilla-grid-bundle.ts @@ -48,7 +48,6 @@ import { deepCopy, emptyElement, unsubscribeAll, - Utils as SlickUtils, SlickEventHandler, SlickDataView, SlickGrid @@ -57,6 +56,7 @@ import { EventNamingStyle, EventPubSubService } from '@slickgrid-universal/event import { SlickEmptyWarningComponent } from '@slickgrid-universal/empty-warning-component'; import { SlickFooterComponent } from '@slickgrid-universal/custom-footer-component'; import { SlickPaginationComponent } from '@slickgrid-universal/pagination-component'; +import { extend } from '@slickgrid-universal/utils'; import { SlickerGridInstance } from '../interfaces/slickerGridInstance.interface'; import { UniversalContainerService } from '../services/universalContainer.service'; @@ -209,7 +209,7 @@ export class SlickVanillaGridBundle { // if we already have grid options, when grid was already initialized, we'll merge with those options // else we'll merge with global grid options if (this.slickGrid?.getOptions) { - mergedOptions = (SlickUtils.extend(true, {} as GridOption, this.slickGrid.getOptions() as GridOption, options)) as GridOption; + mergedOptions = (extend(true, {} as GridOption, this.slickGrid.getOptions() as GridOption, options)) as GridOption; } else { mergedOptions = this.mergeGridOptions(options); } @@ -674,7 +674,7 @@ export class SlickVanillaGridBundle { } mergeGridOptions(gridOptions: GridOption) { - const options = SlickUtils.extend(true, {}, GlobalGridOptions, gridOptions); + const options = extend(true, {}, GlobalGridOptions, gridOptions); // also make sure to show the header row if user have enabled filtering if (options.enableFiltering && !options.showHeaderRow) { diff --git a/packages/vanilla-force-bundle/src/vanilla-force-bundle.ts b/packages/vanilla-force-bundle/src/vanilla-force-bundle.ts index fe75cdf05..2c8d32078 100644 --- a/packages/vanilla-force-bundle/src/vanilla-force-bundle.ts +++ b/packages/vanilla-force-bundle/src/vanilla-force-bundle.ts @@ -19,13 +19,14 @@ import type { TranslaterService, TreeDataService, } from '@slickgrid-universal/common'; -import { GlobalGridOptions, Utils as SlickUtils } from '@slickgrid-universal/common'; +import { GlobalGridOptions } from '@slickgrid-universal/common'; import { EventPubSubService } from '@slickgrid-universal/event-pub-sub'; import { ExcelExportService } from '@slickgrid-universal/excel-export'; import { SlickCompositeEditorComponent } from '@slickgrid-universal/composite-editor-component'; import { SlickEmptyWarningComponent } from '@slickgrid-universal/empty-warning-component'; import { SlickCustomTooltip } from '@slickgrid-universal/custom-tooltip-plugin'; import { TextExportService } from '@slickgrid-universal/text-export'; +import { extend } from '@slickgrid-universal/utils'; import { SlickVanillaGridBundle, UniversalContainerService } from '@slickgrid-universal/vanilla-bundle'; import { SalesforceGlobalGridOptions } from './salesforce-global-grid-options'; @@ -74,7 +75,7 @@ export class VanillaForceGridBundle extends SlickVanillaGridBundle { mergeGridOptions(gridOptions: GridOption) { const extraOptions = (gridOptions.useSalesforceDefaultGridOptions || (this._gridOptions?.useSalesforceDefaultGridOptions)) ? SalesforceGlobalGridOptions : {}; - const options = SlickUtils.extend(true, {}, GlobalGridOptions, extraOptions, gridOptions); + const options = extend(true, {}, GlobalGridOptions, extraOptions, gridOptions); // also make sure to show the header row if user have enabled filtering if (options.enableFiltering && !options.showHeaderRow) {