diff --git a/domino-ui/src/main/java/org/dominokit/domino/ui/forms/TimeBox.java b/domino-ui/src/main/java/org/dominokit/domino/ui/forms/TimeBox.java index 59237b8e4..c777a360b 100644 --- a/domino-ui/src/main/java/org/dominokit/domino/ui/forms/TimeBox.java +++ b/domino-ui/src/main/java/org/dominokit/domino/ui/forms/TimeBox.java @@ -160,7 +160,8 @@ public TimeBox(Date date, DateTimeFormatInfo dateTimeFormatInfo) { if (parseStrict) { invalidate(getLabels().timePickerInvalidTimeFormat(value)); } - DomGlobal.console.warn("Unable to parse date value " + value); + DomGlobal.console.warn( + "Unable to parse date value " + value + ", for pattern : " + pattern); } } }); diff --git a/domino-ui/src/main/java/org/dominokit/domino/ui/menu/MenuTarget.java b/domino-ui/src/main/java/org/dominokit/domino/ui/menu/MenuTarget.java index 2295d4322..2bbf261f2 100644 --- a/domino-ui/src/main/java/org/dominokit/domino/ui/menu/MenuTarget.java +++ b/domino-ui/src/main/java/org/dominokit/domino/ui/menu/MenuTarget.java @@ -15,16 +15,15 @@ */ package org.dominokit.domino.ui.menu; -import static org.dominokit.domino.ui.utils.Domino.*; import static org.dominokit.domino.ui.utils.ElementsFactory.elements; import elemental2.dom.Element; import java.util.HashMap; import java.util.Map; -import org.dominokit.domino.ui.utils.AttachDetachCallback; import org.dominokit.domino.ui.utils.ComponentMeta; import org.dominokit.domino.ui.utils.DominoElement; import org.dominokit.domino.ui.utils.HasMeta; +import org.dominokit.domino.ui.utils.MutationObserverCallback; /** * Represents a target for the menu in the UI. This class wraps a target DOM {@link Element} to be @@ -40,8 +39,8 @@ public class MenuTarget implements HasMeta { private final Element targetElement; - private AttachDetachCallback targetDetachObserver; - private AttachDetachCallback targetAttachObserver; + private MutationObserverCallback targetDetachObserver; + private MutationObserverCallback targetAttachObserver; private final Map metaObjects = new HashMap<>(); /** @@ -77,7 +76,7 @@ public DominoElement getTargetElement() { * * @param targetDetachObserver the observer callback */ - void setTargetDetachObserver(AttachDetachCallback targetDetachObserver) { + void setTargetDetachObserver(MutationObserverCallback targetDetachObserver) { this.targetDetachObserver = targetDetachObserver; } @@ -86,7 +85,7 @@ void setTargetDetachObserver(AttachDetachCallback targetDetachObserver) { * * @return the observer callback */ - AttachDetachCallback getTargetDetachObserver() { + MutationObserverCallback getTargetDetachObserver() { return targetDetachObserver; } @@ -95,7 +94,7 @@ AttachDetachCallback getTargetDetachObserver() { * * @param targetDetachObserver the observer callback */ - void setTargetAttachObserver(AttachDetachCallback targetAttachObserver) { + void setTargetAttachObserver(MutationObserverCallback targetAttachObserver) { this.targetAttachObserver = targetAttachObserver; } @@ -104,7 +103,7 @@ void setTargetAttachObserver(AttachDetachCallback targetAttachObserver) { * * @return the observer callback */ - AttachDetachCallback getTargetAttachObserver() { + MutationObserverCallback getTargetAttachObserver() { return targetAttachObserver; } diff --git a/domino-ui/src/main/java/org/dominokit/domino/ui/utils/AttributesObserver.java b/domino-ui/src/main/java/org/dominokit/domino/ui/utils/AttributesObserver.java new file mode 100644 index 000000000..483ea743c --- /dev/null +++ b/domino-ui/src/main/java/org/dominokit/domino/ui/utils/AttributesObserver.java @@ -0,0 +1,96 @@ +/* + * Copyright © 2019 Dominokit + * + * 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 + * + * http://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.dominokit.domino.ui.utils; + +import static elemental2.dom.DomGlobal.document; +import static org.dominokit.domino.ui.utils.ElementsFactory.elements; + +import elemental2.core.JsArray; +import elemental2.dom.CustomEvent; +import elemental2.dom.CustomEventInit; +import elemental2.dom.Element; +import elemental2.dom.MutationObserver; +import elemental2.dom.MutationObserverInit; +import elemental2.dom.MutationRecord; +import jsinterop.base.Js; + +/** + * The {@code BodyObserver} class is responsible for observing mutations in the document's body. It + * tracks the addition and removal of elements with specific attributes and dispatches events + * accordingly. + */ +final class AttributesObserver { + + private static boolean ready = false; + private static boolean paused = false; + private static MutationObserver mutationObserver; + + private AttributesObserver() {} + + /** + * Pauses the observer for a specified action and resumes it afterward. + * + * @param handler The action to perform while the observer is paused. + */ + static void pauseFor(Runnable handler) { + mutationObserver.disconnect(); + try { + handler.run(); + } finally { + observe(); + } + } + + /** Starts observing mutations in the document's body. */ + static void startObserving() { + if (!ready) { + mutationObserver = + new MutationObserver( + (JsArray records, MutationObserver observer) -> { + if (!paused) { + MutationRecord[] recordsArray = + Js.uncheckedCast(records.asArray(new MutationRecord[records.length])); + for (MutationRecord record : recordsArray) { + if ("attributes".equalsIgnoreCase(record.type)) { + onElementAttributesChanged(record); + } + } + } + return null; + }); + + observe(); + ready = true; + } + } + + private static void onElementAttributesChanged(MutationRecord record) { + CustomEventInit ceinit = CustomEventInit.create(); + ceinit.setDetail(record); + DominoElement element = elements.elementOf(Js.uncheckedCast(record.target)); + String type = ObserverEventType.attributeType(element); + + CustomEvent event = new CustomEvent<>(type, ceinit); + element.element().dispatchEvent(event); + } + + private static void observe() { + MutationObserverInit mutationObserverInit = MutationObserverInit.create(); + mutationObserverInit.setSubtree(true); + mutationObserverInit.setAttributes(true); + mutationObserver.observe(document.body, mutationObserverInit); + } +} diff --git a/domino-ui/src/main/java/org/dominokit/domino/ui/utils/BaseDominoElement.java b/domino-ui/src/main/java/org/dominokit/domino/ui/utils/BaseDominoElement.java index e3741a9c5..6ab2198cf 100644 --- a/domino-ui/src/main/java/org/dominokit/domino/ui/utils/BaseDominoElement.java +++ b/domino-ui/src/main/java/org/dominokit/domino/ui/utils/BaseDominoElement.java @@ -31,6 +31,7 @@ import elemental2.dom.EventListener; import elemental2.dom.EventTarget; import elemental2.dom.HTMLElement; +import elemental2.dom.MutationRecord; import elemental2.dom.Node; import elemental2.dom.NodeList; import java.util.ArrayList; @@ -118,6 +119,7 @@ public abstract class BaseDominoElement attachObservers = new ArrayList<>(); + private List attachObservers = new ArrayList<>(); /** A list of detach observers for this DOM element. */ - private List detachObservers = new ArrayList<>(); + private List detachObservers = new ArrayList<>(); + + /** A list of detach observers for this DOM element. */ + private Map> attributesObservers = new HashMap<>(); /** Optional ResizeObserver for this DOM element. */ private Optional resizeObserverOptional = Optional.empty(); @@ -175,6 +180,7 @@ public abstract class BaseDominoElement> onBeforeRemoveHandlers = new ArrayList<>(); private final List> onRemoveHandlers = new ArrayList<>(); private final Map metaObjects = new HashMap<>(); @@ -655,11 +661,11 @@ public T withPopover(ChildHandler handler) { /** * Registers an observer to be notified when this element is attached to the DOM. * - * @param attachDetachCallback The observer to be registered. + * @param mutationObserverCallback The observer to be registered. * @return The modified DOM element. */ @Editor.Ignore - public T onAttached(AttachDetachCallback attachDetachCallback) { + public T onAttached(MutationObserverCallback mutationObserverCallback) { if (isNull(this.attachEventListener)) { if (!hasAttribute(ATTACH_UID_KEY)) { setAttribute(ATTACH_UID_KEY, DominoId.unique()); @@ -672,13 +678,66 @@ public T onAttached(AttachDetachCallback attachDetachCallback) { }; this.element .element() - .addEventListener(AttachDetachEventType.attachedType(this), this.attachEventListener); + .addEventListener(ObserverEventType.attachedType(this), this.attachEventListener); } - attachObservers.add(attachDetachCallback); + attachObservers.add(mutationObserverCallback); ElementUtil.startObserving(); return element; } + /** + * Registers an observer to be notified when this element is attached to the DOM. + * + * @param mutationObserverCallback The observer to be registered. + * @return The modified DOM element. + */ + @Editor.Ignore + public T onAttributeChange(MutationObserverCallback mutationObserverCallback) { + return onAttributeChange("*", mutationObserverCallback); + } + + /** + * Registers an observer to be notified when this element is attached to the DOM. + * + * @param mutationObserverCallback The observer to be registered. + * @return The modified DOM element. + */ + @Editor.Ignore + public T onAttributeChange(String attribute, MutationObserverCallback mutationObserverCallback) { + if (isNull(this.attributeChangeEventListener)) { + if (!hasAttribute(ATTRIBUTE_CHANGE_UID_KEY)) { + setAttribute(ATTRIBUTE_CHANGE_UID_KEY, DominoId.unique()); + } + this.attributeChangeEventListener = + evt -> { + CustomEvent cevent = Js.uncheckedCast(evt); + MutationRecord record = Js.uncheckedCast(cevent.detail); + + Optional.ofNullable(attributesObservers.get("*")) + .ifPresent( + mutationObserverCallbacks -> { + mutationObserverCallbacks.forEach( + callback -> callback.onObserved(Js.uncheckedCast(cevent.detail))); + }); + + Optional.ofNullable(attributesObservers.get(record.attributeName)) + .ifPresent( + mutationObserverCallbacks -> { + mutationObserverCallbacks.forEach( + callback -> callback.onObserved(Js.uncheckedCast(cevent.detail))); + }); + }; + String type = ObserverEventType.attributeType(this); + this.element.element().addEventListener(type, this.attributeChangeEventListener); + } + if (!attributesObservers.containsKey(attribute)) { + attributesObservers.put(attribute, new ArrayList<>()); + } + attributesObservers.get(attribute).add(mutationObserverCallback); + ElementUtil.startObservingAttributes(); + return element; + } + /** * Registers an observer to be notified when this element is detached from the DOM. * @@ -686,7 +745,7 @@ public T onAttached(AttachDetachCallback attachDetachCallback) { * @return The modified DOM element. */ @Editor.Ignore - public T onDetached(AttachDetachCallback callback) { + public T onDetached(MutationObserverCallback callback) { if (isNull(this.detachEventListener)) { if (!hasAttribute(DETACH_UID_KEY)) { setAttribute(DETACH_UID_KEY, DominoId.unique()); @@ -699,7 +758,7 @@ public T onDetached(AttachDetachCallback callback) { }; this.element .element() - .addEventListener(AttachDetachEventType.detachedType(this), this.detachEventListener); + .addEventListener(ObserverEventType.detachedType(this), this.detachEventListener); } detachObservers.add(callback); ElementUtil.startObserving(); @@ -713,7 +772,7 @@ public T onDetached(AttachDetachCallback callback) { * @param callback The observer to be removed. * @return The modified DOM element. */ - public T removeAttachObserver(AttachDetachCallback callback) { + public T removeAttachObserver(MutationObserverCallback callback) { attachObservers.remove(callback); return element; } @@ -725,7 +784,7 @@ public T removeAttachObserver(AttachDetachCallback callback) { * @param callback The observer to be removed. * @return The modified DOM element. */ - public T removeDetachObserver(AttachDetachCallback callback) { + public T removeDetachObserver(MutationObserverCallback callback) { detachObservers.remove(callback); return element; } diff --git a/domino-ui/src/main/java/org/dominokit/domino/ui/utils/BodyObserver.java b/domino-ui/src/main/java/org/dominokit/domino/ui/utils/BodyObserver.java index e3479b497..fd7de3c15 100644 --- a/domino-ui/src/main/java/org/dominokit/domino/ui/utils/BodyObserver.java +++ b/domino-ui/src/main/java/org/dominokit/domino/ui/utils/BodyObserver.java @@ -107,7 +107,7 @@ private static void onElementsAppended(MutationRecord record) { List> childElements = elements.elementOf(element).querySelectorAll("[" + ATTACH_UID_KEY + "]"); if (element.hasAttribute(ATTACH_UID_KEY)) { - String type = AttachDetachEventType.attachedType(elements.elementOf(element)); + String type = ObserverEventType.attachedType(elements.elementOf(element)); if (!processed.contains(type)) { processed.add(type); element.dispatchEvent(new CustomEvent<>(type)); @@ -118,7 +118,7 @@ private static void onElementsAppended(MutationRecord record) { child -> { CustomEventInit ceinit = CustomEventInit.create(); ceinit.setDetail(record); - String type = AttachDetachEventType.attachedType(elements.elementOf(child)); + String type = ObserverEventType.attachedType(elements.elementOf(child)); if (!processed.contains(type)) { processed.add(type); CustomEvent event = new CustomEvent<>(type, ceinit); @@ -139,7 +139,7 @@ private static void onElementsRemoved(MutationRecord record) { List> childElements = elements.elementOf(element).querySelectorAll("[" + DETACH_UID_KEY + "]"); if (element.hasAttribute(DETACH_UID_KEY)) { - String type = AttachDetachEventType.detachedType(elements.elementOf(element)); + String type = ObserverEventType.detachedType(elements.elementOf(element)); if (!processed.contains(type)) { processed.add(type); element.dispatchEvent(new Event(type)); @@ -148,7 +148,7 @@ private static void onElementsRemoved(MutationRecord record) { childElements.forEach( child -> { - String type = AttachDetachEventType.detachedType(elements.elementOf(child)); + String type = ObserverEventType.detachedType(elements.elementOf(child)); if (!processed.contains(type)) { processed.add(type); CustomEventInit ceinit = CustomEventInit.create(); diff --git a/domino-ui/src/main/java/org/dominokit/domino/ui/utils/ElementObserver.java b/domino-ui/src/main/java/org/dominokit/domino/ui/utils/ElementObserver.java index ab54a22a3..be2f7255e 100644 --- a/domino-ui/src/main/java/org/dominokit/domino/ui/utils/ElementObserver.java +++ b/domino-ui/src/main/java/org/dominokit/domino/ui/utils/ElementObserver.java @@ -15,8 +15,6 @@ */ package org.dominokit.domino.ui.utils; -import static org.dominokit.domino.ui.utils.Domino.*; - import elemental2.dom.HTMLElement; /** An interface for observing changes in an HTML element. */ @@ -41,7 +39,7 @@ public interface ElementObserver { * * @return The callback to execute. */ - AttachDetachCallback callback(); + MutationObserverCallback callback(); /** Removes the element observer, detaching it from the observed element. */ void remove(); diff --git a/domino-ui/src/main/java/org/dominokit/domino/ui/utils/ElementUtil.java b/domino-ui/src/main/java/org/dominokit/domino/ui/utils/ElementUtil.java index b8516802d..2da2ab797 100644 --- a/domino-ui/src/main/java/org/dominokit/domino/ui/utils/ElementUtil.java +++ b/domino-ui/src/main/java/org/dominokit/domino/ui/utils/ElementUtil.java @@ -17,7 +17,6 @@ package org.dominokit.domino.ui.utils; import static java.util.Objects.nonNull; -import static org.dominokit.domino.ui.utils.Domino.*; import static org.dominokit.domino.ui.utils.ElementsFactory.elements; import elemental2.dom.*; @@ -138,7 +137,7 @@ public static boolean isEscapeKey(KeyboardEvent keyboardEvent) { * null. */ public static Optional onAttach( - HTMLElement element, AttachDetachCallback callback) { + HTMLElement element, MutationObserverCallback callback) { if (element != null) { elements.elementOf(element).onAttached(callback); } @@ -154,6 +153,15 @@ public static void withBodyObserverPaused(Runnable handler) { BodyObserver.pauseFor(handler); } + /** + * Pauses the body observer to prevent it from triggering unnecessary events. + * + * @param handler The runnable to be executed while the observer is paused. + */ + public static void withAttributesObserverPaused(Runnable handler) { + AttributesObserver.pauseFor(handler); + } + /** * Registers an observer to be notified when an IsElement is attached to the DOM. * @@ -163,7 +171,7 @@ public static void withBodyObserverPaused(Runnable handler) { * null. */ public static Optional onAttach( - IsElement element, AttachDetachCallback callback) { + IsElement element, MutationObserverCallback callback) { if (element != null) { elements.elementOf(element).onAttached(callback); } @@ -175,6 +183,11 @@ public static void startObserving() { BodyObserver.startObserving(); } + /** Starts observing the body for elements attributes changes events. */ + public static void startObservingAttributes() { + AttributesObserver.startObserving(); + } + /** * Registers an observer to be notified when an HTMLElement is detached from the DOM. * @@ -184,7 +197,7 @@ public static void startObserving() { * null. */ public static Optional onDetach( - HTMLElement element, AttachDetachCallback callback) { + HTMLElement element, MutationObserverCallback callback) { if (element != null) { elements.elementOf(element).onDetached(callback); } @@ -200,7 +213,7 @@ public static Optional onDetach( * null. */ public static Optional onDetach( - IsElement element, AttachDetachCallback callback) { + IsElement element, MutationObserverCallback callback) { if (element != null) { elements.elementOf(element).onDetached(callback); } diff --git a/domino-ui/src/main/java/org/dominokit/domino/ui/utils/AttachDetachCallback.java b/domino-ui/src/main/java/org/dominokit/domino/ui/utils/MutationObserverCallback.java similarity index 91% rename from domino-ui/src/main/java/org/dominokit/domino/ui/utils/AttachDetachCallback.java rename to domino-ui/src/main/java/org/dominokit/domino/ui/utils/MutationObserverCallback.java index 5dbf28329..463083402 100644 --- a/domino-ui/src/main/java/org/dominokit/domino/ui/utils/AttachDetachCallback.java +++ b/domino-ui/src/main/java/org/dominokit/domino/ui/utils/MutationObserverCallback.java @@ -15,13 +15,11 @@ */ package org.dominokit.domino.ui.utils; -import static org.dominokit.domino.ui.utils.Domino.*; - import elemental2.dom.MutationRecord; /** A functional interface for attaching and detaching callback methods to observe DOM mutations. */ @FunctionalInterface -public interface AttachDetachCallback { +public interface MutationObserverCallback { /** * Invoked when observed DOM mutations occur. diff --git a/domino-ui/src/main/java/org/dominokit/domino/ui/utils/AttachDetachEventType.java b/domino-ui/src/main/java/org/dominokit/domino/ui/utils/ObserverEventType.java similarity index 77% rename from domino-ui/src/main/java/org/dominokit/domino/ui/utils/AttachDetachEventType.java rename to domino-ui/src/main/java/org/dominokit/domino/ui/utils/ObserverEventType.java index 8dcd040c1..ffe8fe0bd 100644 --- a/domino-ui/src/main/java/org/dominokit/domino/ui/utils/AttachDetachEventType.java +++ b/domino-ui/src/main/java/org/dominokit/domino/ui/utils/ObserverEventType.java @@ -16,7 +16,7 @@ package org.dominokit.domino.ui.utils; /** A utility class for generating event types related to attaching and detaching elements. */ -public class AttachDetachEventType { +public class ObserverEventType { /** * Generates an event type for an attached element. @@ -37,4 +37,15 @@ public static String attachedType(HasAttributes element) { public static String detachedType(HasAttributes element) { return "dui-detached-" + element.getAttribute(BaseDominoElement.DETACH_UID_KEY); } + + /** + * Generates an event type for a detached element. + * + * @param element The element that has been detached. + * @return A string representing the event type for detached elements. + */ + public static String attributeType(HasAttributes element) { + return "dui-attribute-change-" + + element.getAttribute(BaseDominoElement.ATTRIBUTE_CHANGE_UID_KEY); + } }