dom/handle.js

'use strict';

/**
 * @author schukai GmbH
 */

import {Monster, Base} from '../types/base.js';
import {Stack} from "../types/stack.js";
import {validateInstance} from "../types/validate.js";
import {ProxyObserver} from "../types/proxyobserver.js";
import {Observer} from "../types/observer.js";
import {getGlobalFunction, getGlobalObject} from "../types/global.js";
import {isInstance} from "../types/is.js";
import {ATTRIBUTEPREFIX} from "./assembler.js"
import {ID} from "../types/id.js";

/**
 * @private
 * @type {Symbol}
 */
const MONSTERDOMHANDLE = Symbol('MonsterHandle');

/**
 * you can call the method via the monster namespace `new Monster.DOM.Handle()`.
 *
 * ```
 * <script type="module">
 * import {Monster} from 'https://cdn.jsdelivr.net/npm/@schukai/monster@1.6.0/dist/modules/dom/handle.js';
 * console.log(new Monster.DOM.Handle())
 * </script>
 * ```
 *
 * Alternatively, you can also integrate this function individually.
 *
 * ```
 * <script type="module">
 * import {Handle} from 'https://cdn.jsdelivr.net/npm/@schukai/monster@1.6.0/dist/modules/dom/handle.js';
 * console.log(new Handle())
 * </script>
 * ```
 *
 * @since 1.6.0
 * @copyright schukai GmbH
 * @memberOf Monster.DOM
 */
class Handle extends Base {
    /**
     *
     * @param {ProxyObserver} data
     */
    constructor(data) {
        super();

        let self = this;

        self.id = new ID();

        validateInstance(data, ProxyObserver);

        this.data = data

        this.mutationObserver = createMutationObserver.call(this);
        this.dataObserver = createDataObserver.call(this);
        this.data.attachObserver(this.dataObserver);

        this.nodes = new WeakSet
        this.updates = new Stack();

    }

    /**
     * @return {void}
     */
    update() {
        return;
    }

    /**
     *
     * @param {HTMLElement|Document} node
     * @return {Handle}
     */
    remove(node) {

        if (isInstance(node, getGlobalFunction('Document'))) {
            node = node.firstElementChild
        }

        validateInstance(node, getGlobalFunction('HTMLElement'))

        if (!this.nodes.has(node)) {
            return this;
        }

        this.mutationObserver.disconnect(node);

        delete node.dataset[MONSTERDOMHANDLE]
        node.removeAttribute(ATTRIBUTEPREFIX + "handler");

        return this;

    }

    /**
     *
     * @param {HTMLElement|Document} node
     * @return {Handle}
     */
    append(node) {

        if (isInstance(node, getGlobalFunction('Document'))) {
            node = node.firstElementChild
        }

        validateInstance(node, getGlobalFunction('HTMLElement'))

        if (this.nodes.has(node)) {
            return this;
        }

        node.dataset[MONSTERDOMHANDLE] = this;
        node.setAttribute(ATTRIBUTEPREFIX + "handler", true);

        this.mutationObserver.observe(node, {
            attributes: true,
            childList: true,
            subtree: true,
            characterData: true,
            characterDataOldValue: true,
            attributeOldValue: true
        });

        this.nodes.add(node);

        return this;

    }
}

/**
 *
 * @private
 * @return {Observer}
 */
function createDataObserver() {
    const self = this;

    return new Observer(() => {
        self.update();
    });
}

/**
 *
 * @private
 * @return {MutationObserver}
 */
function createMutationObserver() {

    const self = this;

    /**
     * @private
     * @type {MutationObserver}
     */
    const MutationObserver = getGlobalFunction('MutationObserver');

    // @link https://developer.mozilla.org/en/docs/Web/API/MutationObserver
    return new MutationObserver((mutationsList, observer) => {

            for (const mutation of mutationsList) {
                self.updates.push(mutation);
            }

            self.update();
        }
    )

}

/**
 * get the handle of a node
 *
 * if a node is specified without a handler, a recursive search upwards is performed until the corresponding
 * handle is found, or undefined is returned.
 *
 * you can call the method via the monster namespace `Monster.DOM.getHandleFromNode()`.
 *
 * ```
 * <script type="module">
 * import {Monster} from 'https://cdn.jsdelivr.net/npm/@schukai/monster@1.6.0/dist/modules/dom/handle.js';
 * console.log(Monster.DOM.getHandleFromNode())
 * </script>
 * ```
 *
 * Alternatively, you can also integrate this function individually.
 *
 * ```
 * <script type="module">
 * import {getHandleFromNode} from 'https://cdn.jsdelivr.net/npm/@schukai/monster@1.6.0/dist/modules/dom/handle.js';
 * console.log(getHandleFromNode())
 * </script>
 * ```
 *
 * @param {Node} node
 * @return {Handle|undefined}
 * @since 1.6.0
 * @copyright schukai GmbH
 * @memberOf Monster/DOM
 * @throws {TypeError} value is not an instance of Node
 */
function getHandleFromNode(node) {
    validateInstance(node, getGlobalFunction('Node'));

    let handle = node.dataset?.[MONSTERDOMHANDLE];
    if (handle === undefined) {
        let parentNode = node?.['parentNode'];
        if (isInstance(parentNode, getGlobalFunction('Node'))) {
            return getHandleFromNode(parentNode)
        }
    }

    return handle;
}

Monster.assignToNamespace('Monster.DOM', getHandleFromNode, Handle);
export {Monster, getHandleFromNode, Handle}