All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jboss.elemento.BodyObserver Maven / Gradle / Ivy

There is a newer version: 1.7.0
Show newest version
/*
 *  Copyright 2023 Red Hat
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package org.jboss.elemento;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.jboss.elemento.logger.Logger;

import elemental2.core.JsArray;
import elemental2.dom.HTMLElement;
import elemental2.dom.MutationObserver;
import elemental2.dom.MutationObserverInit;
import elemental2.dom.MutationRecord;
import jsinterop.base.Js;

import static elemental2.dom.DomGlobal.document;
import static java.util.stream.Collectors.toList;
import static org.jboss.elemento.Elements.asHtmlElement;
import static org.jboss.elemento.Elements.htmlElements;
import static org.jboss.elemento.Elements.stream;
import static org.jboss.elemento.logger.Level.DEBUG;

final class BodyObserver {

    private static final String ATTACH_UID_KEY = "on-attach-uid";
    private static final String DETACH_UID_KEY = "on-detach-uid";

    private static final List detachObservers = new ArrayList<>();
    private static final List attachObservers = new ArrayList<>();
    private static final Logger logger = Logger.getLogger(BodyObserver.class.getName());
    private static boolean ready = false;

    // ------------------------------------------------------ api

    /**
     * Registers the element and calls the callback when the element is attached to the DOM.
     */
    static void addAttachObserver(HTMLElement element, ObserverCallback callback) {
        if (!ready) {
            startObserving();
        }
        String id = Id.unique("a");
        attachObservers.add(createObserver(id, element, ATTACH_UID_KEY, callback));
        if (logger.isEnabled(DEBUG)) {
            logger.debug("Add attach observer %s for %o %s", id, element, count());
        }
    }

    static void removeAttachObserver(HTMLElement element) {
        element.removeAttribute(ATTACH_UID_KEY);
    }

    /**
     * Registers the element and calls the callback when the element is detached from the DOM.
     */
    static void addDetachObserver(HTMLElement element, ObserverCallback callback) {
        if (!ready) {
            startObserving();
        }
        String id = Id.unique("d");
        detachObservers.add(createObserver(id, element, DETACH_UID_KEY, callback));
        if (logger.isEnabled(DEBUG)) {
            logger.debug("Add detach observer %s for %o %s", id, element, count());
        }
    }

    static void removeDetachObserver(HTMLElement element) {
        element.removeAttribute(DETACH_UID_KEY);
    }

    // ------------------------------------------------------ internal

    private static void startObserving() {
        MutationObserver mutationObserver = new MutationObserver((mutationRecords, observer) -> {
            MutationRecord[] records = Js.uncheckedCast(mutationRecords);
            // noinspection ConstantValue,DataFlowIssue
            for (MutationRecord record : records) {
                if (record.removedNodes.length != 0) {
                    onElementsRemoved(record);
                }
                if (record.addedNodes.length != 0) {
                    onElementsAppended(record);
                }
            }
            return null;
        });

        MutationObserverInit mutationObserverInit = MutationObserverInit.create();
        mutationObserverInit.setChildList(true);
        mutationObserverInit.setSubtree(true);
        if (document.body == null) {
            logger.error("Cannot start observing elements. Document is not ready yet!");
        } else {
            logger.debug("Start observing elements");
            mutationObserver.observe(document.body, mutationObserverInit);
            ready = true;
        }
    }

    private static void onElementsAppended(MutationRecord record) {
        List elements = stream(record.addedNodes)
                .filter(htmlElements())
                .map(asHtmlElement())
                .collect(toList());

        if (!elements.isEmpty()) {
            for (Iterator iterator = attachObservers.iterator(); iterator.hasNext(); ) {
                ElementObserver eo = iterator.next();
                if (eo.element == null) {
                    iterator.remove();
                    if (logger.isEnabled(DEBUG)) {
                        logger.debug("Remove attach observer %s w/o element %s", eo.id, count());
                    }
                } else {
                    if (elements.contains(eo.element)
                            || isChildOfObservedElement(elements, ATTACH_UID_KEY, eo.id)) {
                        if (logger.isEnabled(DEBUG)) {
                            logger.debug("Call attach callback %s for %o", eo.id, eo.element);
                        }
                        eo.callback.onObserved(record);
                        removeId(eo.element, ATTACH_UID_KEY, eo.id);
                        iterator.remove();
                        if (logger.isEnabled(DEBUG)) {
                            logger.debug("Remove attach observer %s for %o %s", eo.id, eo.element, count());
                        }
                    }
                }
            }
        }
    }

    private static void onElementsRemoved(MutationRecord record) {
        List elements = stream(record.removedNodes)
                .filter(htmlElements())
                .map(asHtmlElement())
                .collect(toList());

        if (!elements.isEmpty()) {
            for (Iterator iterator = detachObservers.iterator(); iterator.hasNext(); ) {
                ElementObserver eo = iterator.next();
                if (eo.element == null) {
                    iterator.remove();
                    if (logger.isEnabled(DEBUG)) {
                        logger.debug("Remove detach observer %s w/o element %s", eo.id, count());
                    }
                } else {
                    if (elements.contains(eo.element)
                            || isChildOfObservedElement(elements, DETACH_UID_KEY, eo.id)) {
                        if (logger.isEnabled(DEBUG)) {
                            logger.debug("Call detach callback %s for %o", eo.id, eo.element);
                        }
                        eo.callback.onObserved(record);
                        removeId(eo.element, DETACH_UID_KEY, eo.id);
                        iterator.remove();
                        if (logger.isEnabled(DEBUG)) {
                            logger.debug("Remove detach observer %s for %o %s", eo.id, eo.element, count());
                        }
                    }
                }
            }
        }
    }

    private static boolean isChildOfObservedElement(List elements, String attribute, String id) {
        for (HTMLElement element : elements) {
            // The use of the right attribute selector is important here!
            // Multiple attach/detach IDs can be present on an element.

            // The selector "~=" matches elements whose value is exactly the value
            // or contains the value in its (space-separated) list of values.

            // The selector "*=" must not be used!
            // It matches elements whose value contains the value anywhere within the string.
            if (element.querySelector("[" + attribute + "~='" + id + "']") != null) {
                return true;
            }
        }
        return false;
    }

    private static ElementObserver createObserver(String id, HTMLElement element, String attribute, ObserverCallback callback) {
        addId(element, attribute, id);
        return new ElementObserver(id, element, callback);
    }

    private static void addId(HTMLElement element, String attribute, String id) {
        JsArray ids = ids(element, attribute);
        ids.push(id);
        element.setAttribute(attribute, ids.join(" "));
    }

    private static void removeId(HTMLElement element, String attribute, String id) {
        JsArray ids = ids(element, attribute);
        int index = ids.indexOf(id);
        if (index != -1) {
            ids.splice(index, 1);
        }
        if (ids.length == 0) {
            element.removeAttribute(attribute);
        } else {
            element.setAttribute(attribute, ids.join(" "));
        }
    }

    private static JsArray ids(HTMLElement element, String attribute) {
        JsArray ids = new JsArray<>();
        if (element.hasAttribute(attribute)) {
            String value = element.getAttribute(attribute);
            if (!value.trim().isEmpty()) {
                for (String id : value.split(" ")) {
                    ids.push(id);
                }
            }
        }
        return ids;
    }

    private static String count() {
        return "(a:" + attachObservers.size() + "|d:" + detachObservers.size() + ")";
    }


    private static final class ElementObserver {

        private final String id;
        private final HTMLElement element;
        private final ObserverCallback callback;

        private ElementObserver(String id, HTMLElement element, ObserverCallback callback) {
            this.id = id;
            this.element = element;
            this.callback = callback;
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy