improve dom observer with once option

This commit is contained in:
Rene
2021-07-18 00:33:21 +02:00
parent 0280190c1d
commit 55aa5b81c7
4 changed files with 49 additions and 58 deletions
@@ -1,21 +1,4 @@
import { import { each, noop, debounce, indexOf, isString, MutationObserverConstructor, isEmptyArray, on, attr, is, find, push } from 'support';
each,
noop,
debounce,
indexOf,
isString,
MutationObserverConstructor,
isEmptyArray,
on,
off,
attr,
is,
find,
push,
isUndefined,
} from 'support';
type StringNullUndefined = string | null | undefined;
type DOMContentObserverCallback = (contentChangedTroughEvent: boolean) => any; type DOMContentObserverCallback = (contentChangedTroughEvent: boolean) => any;
@@ -37,7 +20,7 @@ interface DOMTargetObserverOptions extends DOMObserverOptionsBase {
_ignoreTargetChange?: DOMObserverIgnoreTargetChange; // a function which will prevent marking certain attributes as changed if it returns true _ignoreTargetChange?: DOMObserverIgnoreTargetChange; // a function which will prevent marking certain attributes as changed if it returns true
} }
type ContentChangeArrayItem = [StringNullUndefined, StringNullUndefined] | null | undefined; type ContentChangeArrayItem = [string?, string?, boolean?] | null | undefined;
export type DOMObserverEventContentChange = Array<ContentChangeArrayItem> | false | null | undefined; export type DOMObserverEventContentChange = Array<ContentChangeArrayItem> | false | null | undefined;
@@ -74,25 +57,28 @@ export interface DOMObserver {
* @returns A object which contains a set of helper functions to destroy and update the observation of elements. * @returns A object which contains a set of helper functions to destroy and update the observation of elements.
*/ */
const createEventContentChange = (target: Element, eventContentChange: DOMObserverEventContentChange, callback: (...args: any) => any) => { const createEventContentChange = (target: Element, eventContentChange: DOMObserverEventContentChange, callback: (...args: any) => any) => {
let map: Map<Node, string> | undefined; let eventSet: Set<() => any> | undefined;
let onceSet: WeakMap<Node, 0> | undefined; // use WeakMap instead of WeakSet because of IE11 support
let destroyed = false;
const _destroy = () => { const _destroy = () => {
if (map) { destroyed = true;
map.forEach((eventName: string, elm: Node) => { if (eventSet) {
off(elm, eventName, callback); eventSet.forEach((offFn) => {
offFn();
}); });
map.clear(); eventSet.clear();
} }
}; };
const _updateElements = (getElements?: (selector: string) => Node[]) => { const _updateElements = (getElements?: (selector: string) => Node[]) => {
if (map && eventContentChange) { if (eventSet && onceSet && eventContentChange) {
const eventElmList = eventContentChange.reduce<Array<[Node[], string]>>((arr, item) => { const eventElmList = eventContentChange.reduce<Array<[Node[], string, boolean]>>((arr, item) => {
if (item) { if (item) {
const selector = item[0]; const selector = item[0];
const eventNames = item[1]; const eventNames = item[1];
const elements = eventNames && selector && (getElements ? getElements(selector) : find(selector, target)); const elements = eventNames && selector && (getElements ? getElements(selector) : find(selector, target));
if (elements && elements.length && eventNames && isString(eventNames)) { if (elements && elements.length && eventNames && isString(eventNames)) {
push(arr, [elements, eventNames.trim()], true); push(arr, [elements, eventNames.trim(), !!item[2]], true);
} }
} }
return arr; return arr;
@@ -101,25 +87,31 @@ const createEventContentChange = (target: Element, eventContentChange: DOMObserv
each(eventElmList, (item) => each(eventElmList, (item) =>
each(item[0], (elm) => { each(item[0], (elm) => {
const eventNames = item[1]; const eventNames = item[1];
const registredEventNames = map!.get(elm); const once = item[2];
const newEntry = isUndefined(registredEventNames);
const changingExistingEntry = !newEntry && eventNames !== registredEventNames;
const finalEventNames = changingExistingEntry ? `${registredEventNames} ${eventNames}` : eventNames;
if (changingExistingEntry) { if (once && !onceSet!.has(elm)) {
off(elm, registredEventNames!, callback); onceSet!.set(elm, 0);
on(
elm,
eventNames,
(event) => {
if (!destroyed) {
callback(event);
}
},
{ _once: once }
);
} else {
eventSet!.add(on(elm, eventNames, callback));
} }
map!.set(elm, finalEventNames);
on(elm, finalEventNames, callback);
}) })
); );
} }
}; };
if (eventContentChange) { if (eventContentChange) {
map = map || new Map<Node, string>(); eventSet = eventSet || new Set();
_destroy(); onceSet = onceSet || new WeakMap();
_updateElements(); _updateElements();
} }
+2 -2
View File
@@ -34,7 +34,7 @@ export interface OSOptions {
resize: ResizeBehavior; resize: ResizeBehavior;
paddingAbsolute: boolean; paddingAbsolute: boolean;
updating: { updating: {
elementEvents: Array<[string, string]> | null; elementEvents: Array<[string, string, boolean?]> | null;
attributes: string[] | null; attributes: string[] | null;
debounce: number | [number, number] | null; debounce: number | [number, number] | null;
}; };
@@ -137,7 +137,7 @@ const defaultOptionsWithTemplate: OptionsWithOptionsTemplate<OSOptions> = {
resize: ['none', resizeAllowedValues], // none || both || horizontal || vertical || n || b || h || v resize: ['none', resizeAllowedValues], // none || both || horizontal || vertical || n || b || h || v
paddingAbsolute: booleanFalseTemplate, // true || false paddingAbsolute: booleanFalseTemplate, // true || false
updating: { updating: {
elementEvents: [[['img', 'load']], arrayNullValues], // array of tuples || null elementEvents: [[['img', 'load', true]], arrayNullValues], // array of tuples || null
attributes: [null, arrayNullValues], attributes: [null, arrayNullValues],
debounce: [ debounce: [
[0, 33], [0, 33],
@@ -1,5 +1,5 @@
import { each, hasOwnProperty, keys, push, isEmptyObject } from 'support/utils'; import { each, hasOwnProperty, keys, push, isEmptyObject } from 'support/utils';
import { type, isArray, isUndefined, isPlainObject, isString } from 'support/utils/types'; import { type, isArray, isUndefined, isPlainObject, isString, isNumber, isBoolean } from 'support/utils/types';
import { PlainObject } from 'typings'; import { PlainObject } from 'typings';
export type OptionsObjectType = Record<string, unknown>; export type OptionsObjectType = Record<string, unknown>;
@@ -163,7 +163,8 @@ const validateRecursive = <T extends PlainObject>(
}); });
if (isValid) { if (isValid) {
const doStringifyComparison = isArray(optionsValue) || isPlainObject(optionsValue); const isPrimitiveArr = isArray(optionsValue) && !optionsValue.some((val) => !isNumber(val) && !isString(val) && !isBoolean(val));
const doStringifyComparison = isPrimitiveArr || isPlainObject(optionsValue);
if (doStringifyComparison ? stringify(optionsValue) !== stringify(optionsDiffValue) : optionsValue !== optionsDiffValue) { if (doStringifyComparison ? stringify(optionsValue) !== stringify(optionsDiffValue) : optionsValue !== optionsDiffValue) {
validatedOptions[prop] = optionsValue; validatedOptions[prop] = optionsValue;
} }
@@ -58,7 +58,7 @@ const startBtn: HTMLButtonElement | null = document.querySelector('#start');
const hostSelector = '.host'; const hostSelector = '.host';
const ignorePrefix = 'ignore'; const ignorePrefix = 'ignore';
const attrs = ['id', 'class', 'style', 'open']; const attrs = ['id', 'class', 'style', 'open'];
const contentChangeArr: Array<[string, string]> = [['img', 'load']]; const contentChangeArr: Array<[string?, string?, boolean?]> = [['img', 'load', true]];
const domTargetObserverObservations: DOMTargetObserverResult[] = []; const domTargetObserverObservations: DOMTargetObserverResult[] = [];
const domContentObserverObservations: DOMContentObserverResult[] = []; const domContentObserverObservations: DOMContentObserverResult[] = [];
@@ -106,7 +106,7 @@ const targetDomObserver = createDOMObserver(
} }
); );
const createContentDomOserver = (eventContentChange: Array<[string | null | undefined, string | null | undefined] | null | undefined>) => { const createContentDomOserver = (eventContentChange: Array<[string?, string?, boolean?] | null | undefined>) => {
return createDOMObserver( return createDOMObserver(
trargetContentElm!, trargetContentElm!,
true, true,
@@ -344,7 +344,10 @@ const addRemoveTargetContentBetweenElmsFn = async () => {
const addRemoveImgElmsFn = async () => { const addRemoveImgElmsFn = async () => {
const add = async () => { const add = async () => {
const img = new Image(1, 1); const img = new Image(1, 1);
img.src = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs='; img.src = 'www.something.com/something/sometest';
setTimeout(() => {
img.src = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=';
}, 250);
const { before, after, compare } = changedThrough(domContentObserverObservations); const { before, after, compare } = changedThrough(domContentObserverObservations);
const imgHolder = createDiv('img'); const imgHolder = createDiv('img');
@@ -380,7 +383,10 @@ const addRemoveImgElmsFn = async () => {
const { before, after, compare } = changedThrough(domContentObserverObservations); const { before, after, compare } = changedThrough(domContentObserverObservations);
const genImage = () => { const genImage = () => {
const img = new Image(1, 1); const img = new Image(1, 1);
img.src = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs='; img.src = 'www.something.com/something/sometest';
setTimeout(() => {
img.src = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=';
}, 250);
const imgHolder = createDiv('img'); const imgHolder = createDiv('img');
appendChildren(imgHolder, img); appendChildren(imgHolder, img);
@@ -418,7 +424,7 @@ const addRemoveImgElmsFn = async () => {
await addMultiple(); await addMultiple();
// remove load event from image test // remove load event from image test
const addChanged = async (newEventContentChange: Array<[string | null | undefined, string | null | undefined] | null | undefined>) => { const addChanged = async (newEventContentChange: Array<[string?, string?, boolean?] | null | undefined>) => {
contentDomObserver._destroy(); contentDomObserver._destroy();
contentDomObserver = createContentDomOserver(newEventContentChange); contentDomObserver = createContentDomOserver(newEventContentChange);
@@ -443,16 +449,7 @@ const addRemoveImgElmsFn = async () => {
contentDomObserver = createContentDomOserver(contentChangeArr); contentDomObserver = createContentDomOserver(contentChangeArr);
}; };
await addChanged([ await addChanged([['img', 'something'], ['img', 'something2'], ['img', ''], ['img', undefined], ['', ''], [undefined, undefined], null, undefined]);
['img', 'something'],
['img', 'something2'],
['img', null],
['img', undefined],
[null, null],
[undefined, undefined],
null,
undefined,
]);
await addChanged([]); await addChanged([]);
removeElements(document.querySelectorAll('.img')); removeElements(document.querySelectorAll('.img'));
@@ -632,6 +629,7 @@ const start = async () => {
await addRemoveImgElmsFn(); await addRemoveImgElmsFn();
/*
targetDomObserver._update(); targetDomObserver._update();
targetDomObserver._destroy(); targetDomObserver._destroy();
targetDomObserver._destroy(); targetDomObserver._destroy();
@@ -641,7 +639,7 @@ const start = async () => {
contentDomObserver._destroy(); contentDomObserver._destroy();
contentDomObserver._destroy(); contentDomObserver._destroy();
contentDomObserver._update(); contentDomObserver._update();
*/
setTestResult(true); setTestResult(true);
}; };