import { TypeVoidFunction } from '../types/voidFunction';
import { EventEmitter } from '../event';
import { debounce as _debounce, noop } from '../functions';
import { IMutationObserverExtOption } from './types';
/**
* @typedef {String} MutationRecordType
* @alias MutationRecordType
* @memberof MutationObserver
* @see https://developer.mozilla.org/ko/docs/Web/API/MutationObserver#MutationObserverInit
* @property {String} childList
* @property {String} attributes
* @property {String} characterData
* @property {String} subtree
*/
// for jsdoc
/**
* @export
* @readonly
* @enum {MutationRecordType}
*/
export enum MutationRecordType {
childList = 'childList',
attributes = 'attributes',
characterData = 'characterData',
subtree = 'subtree'
}
/**
* MutationEvent Types
* @event MutationObserver#MUTATION_EVENTS
* @memberof MutationObserver
* @property {String} CHANGE_CHILD_LIST - childList 변경 시점
* @property {String} CHANGE_SUBTREE - subtree 변경 시점
* @property {String} CHANGE_ATTRIBUTES - attributes 변경 시점
* @property {String} CHANGE_CHARACTER_DATA - data 변경 시점
* @property {String} CHANGE - 기타 변경 시점
* @property {String} WILD_CARD - 모든 변경 시점
*/
// for jsdoc
/**
* @export
* @readonly
* @enum {MUTATION_EVENTS}
*/
export enum MUTATION_EVENTS {
CHANGE_CHILD_LIST = 'MUTATION_OBERSERVER-EVENTS-CHANGE_CHILD_LIST',
CHANGE_SUBTREE = 'MUTATION_OBERSERVER-EVENTS-CHANGE_SUBTREE',
CHANGE_ATTRIBUTES = 'MUTATION_OBERSERVER-EVENTS-CHANGE_ATTRIBUTES',
CHANGE_CHARACTER_DATA = 'MUTATION_OBERSERVER-EVENTS-CHANGE_CHARACTER_DATA',
CHANGE = 'MUTATION_OBERSERVER-EVENTS-CHANGE',
WILD_CARD = 'MUTATION_OBERSERVER-EVENTS-CHANGE_WILCD_CARD'
}
class PureMutationObserver {
private pure: any;
constructor(callback: MutationCallback) {
// TODO polyfill
// const MutationObserver = require('mutation-observer');
// this.pure = new MutationObserver(callback);
this.pure = new (window as any).MutationObserver(callback);
}
public observe(target: HTMLElement | any, options: MutationObserverInit): void {
this.pure.observe(target, options);
}
public disconnect(): void {
this.pure.disconnect();
}
}
/**
* target 으로 설정된 element 의 변화를 감지
<iframe
src="https://codesandbox.io/embed/nonollcode-snippet-9gko8?autoresize=1&expanddevtools=1&fontsize=14&hidenavigation=1&initialpath=%2Fobserver-MutationObserver.html&module=%2Fobserver-MutationObserver.html&theme=dark"
style="width:100%; height:500px; border:1px solid black; border-radius: 4px; overflow:hidden;"
title="@nonoll/code-snippet"
allow="geolocation; microphone; camera; midi; vr; accelerometer; gyroscope; payment; ambient-light-sensor; encrypted-media; usb"
sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"
></iframe>
* @export
* @class MutationObserver
* @alias observer/MutationObserver
* @extends EventEmitter
* @see https://developer.mozilla.org/ko/docs/Web/API/MutationObserver
* @see https://github.com/webmodules/mutation-observer
* @see https://developer.mozilla.org/ko/docs/Web/API/MutationObserver#MutationObserverInit
* @param {Partial<IMutationObserverExtOption>} option
* @param {HTMLElement} [option.target=null] target
* @param {Number} [option.debounce=300] debounce
* @param {Function|null} [option.callback={@link noop}] callback function
* @param {Object} [option.options={}]
* @param {Boolean} [option.options.childList]
* @param {Boolean} [option.options.attributes]
* @param {Boolean} [option.options.characterData]
* @param {Boolean} [option.options.subtree]
* @param {Boolean} [option.options.attributeOldValue]
* @param {Boolean} [option.options.characterDataOldValue]
* @param {Array.<String>} [option.options.attributeFilter] [MDN attributeFilter]{@link https://developer.mozilla.org/en-US/docs/Web/API/MutationObserverInit/attributeFilter}
* @example
import { MutationObserver, MUTATION_EVENTS } from '@nonoll/code-snippet/observer';
const createElement = ({ tag = 'div', id = '', style = '', value = '', text = '' }) => {
const doc = window.document;
const target = doc.createElement(tag);
target.setAttribute('id', id);
target.setAttribute('style', style);
target.setAttribute('value', value);
if (text) {
target.textContent = text;
}
return target;
}
let observer;
const forExample = () => {
if (observer) {
console.log('already example');
return;
}
const doc = window.document;
const target = createElement({ id: 'example_target', style: 'border: 1px solid red' });
doc.body.appendChild(target);
const options = {
childList: true,
subtree: true
};
observer = new MutationObserver({ target, options });
observer.on(MUTATION_EVENTS.WILD_CARD, (type, values) => {
console.log('wildCard', type, values);
}).on(MUTATION_EVENTS.CHANGE_CHILD_LIST, values => {
console.log('childList', values);
});
observer.attach();
const attachButton = createElement({ tag: 'button', text: 'observer attach' });
const detachButton = createElement({ tag: 'button', text: 'observer detach' });
const appendButton = createElement({ tag: 'button', text: 'append' });
doc.body.appendChild(attachButton);
doc.body.appendChild(detachButton);
doc.body.appendChild(appendButton);
attachButton.addEventListener('click', e => {
e.preventDefault();
console.log('attachButton clicked');
if (!observer) {
return;
}
observer.attach();
});
detachButton.addEventListener('click', e => {
e.preventDefault();
console.log('detachButton clicked');
if (!observer) {
return;
}
observer.detach();
});
appendButton.addEventListener('click', e => {
e.preventDefault();
console.log('appendButton clicked');
if (!observer) {
return;
}
const input = createElement({ tag: 'input', value: `${+new Date()}` });
target.appendChild(input);
});
}
forExample();
*/
export class MutationObserver extends EventEmitter {
private target: HTMLElement;
private callback: TypeVoidFunction;
private observer: PureMutationObserver;
private options: MutationObserverInit;
constructor({ target = null, debounce = 300, callback = noop, options = {} }: Partial<IMutationObserverExtOption>) {
super();
const defaultOptions = { childList: true, subtree: true };
this.target = target;
this.options = Object.assign({}, defaultOptions, options);
this.onMutationObserverListener = this.onMutationObserverListener.bind(this);
/* istanbul ignore next: for noop */
this.callback = callback || noop;
if (debounce) {
this.observer = new PureMutationObserver(_debounce(this.onMutationObserverListener, debounce) as MutationCallback);
} else {
this.observer = new PureMutationObserver(this.onMutationObserverListener);
}
}
/**
* 이벤트 감지 설정
* @returns {MutationObserver}
* @memberof MutationObserver
* @example
observer.attach();
*/
public attach(): MutationObserver {
this.observer.observe(this.target, this.options);
return this;
}
/**
* 이벤트 감지 등록
* @override
* @param {string} eventName
* @param {TypeVoidFunction} [listener={@link noop}]
* @param {*} [context]
* @memberof MutationObserver
* @listens MutationObserver#MUTATION_EVENTS
* @returns {MutationObserver}
*/
public on(eventName: string, /* istanbul ignore next: for noop */ listener: TypeVoidFunction = noop, context?: any): MutationObserver {
super.on(eventName, listener, context);
return this;
}
/**
* 변화 감지 이벤트 리스너
* @private
* @param {MutationRecord[]} mutations [MDN MutationRecord]{@link https://developer.mozilla.org/en-US/docs/Web/API/MutationRecord}
* @memberof MutationObserver
* @fires MutationObserver#MUTATION_EVENTS
*/
private onMutationObserverListener(mutations: MutationRecord[]): void {
mutations.forEach(mutation => {
const type = mutation.type as MutationRecordType;
const values = { type, mutation };
switch (type) {
case MutationRecordType.childList:
this.callback(values);
this.emit(MUTATION_EVENTS.CHANGE_CHILD_LIST, values);
this.emit(MUTATION_EVENTS.WILD_CARD, MUTATION_EVENTS.CHANGE_CHILD_LIST, values);
break;
case MutationRecordType.subtree:
this.callback(values);
this.emit(MUTATION_EVENTS.CHANGE_SUBTREE, values);
this.emit(MUTATION_EVENTS.WILD_CARD, MUTATION_EVENTS.CHANGE_SUBTREE, values);
break;
case MutationRecordType.attributes:
this.callback(values);
this.emit(MUTATION_EVENTS.CHANGE_ATTRIBUTES, values);
this.emit(MUTATION_EVENTS.WILD_CARD, MUTATION_EVENTS.CHANGE_ATTRIBUTES, values);
break;
case MutationRecordType.characterData:
this.callback(values);
this.emit(MUTATION_EVENTS.CHANGE_CHARACTER_DATA, values);
this.emit(MUTATION_EVENTS.WILD_CARD, MUTATION_EVENTS.CHANGE_CHARACTER_DATA, values);
break;
/* istanbul ignore next: for default */
default:
this.callback(values);
this.emit(MUTATION_EVENTS.CHANGE, type, values);
this.emit(MUTATION_EVENTS.WILD_CARD, type, values);
break;
}
});
}
/**
* 이벤트 감지 해제
* @returns {MutationObserver}
* @memberof MutationObserver
* @example
observer.detach();
*/
public detach(): MutationObserver {
if (this.observer) {
this.observer.disconnect();
}
return this;
}
/**
* destory
* @returns {MutationObserver}
* @memberof MutationObserver
* @example
observer.destroy();
*/
public destroy(): MutationObserver {
this.detach();
this.observer = null;
return this;
}
}