Skip to content

Commit

Permalink
fix #520 Add the ability to listen to attribute changes for an element
Browse files Browse the repository at this point in the history
  • Loading branch information
vegegoku committed Nov 12, 2024
1 parent 3e19bcd commit 17b98db
Show file tree
Hide file tree
Showing 9 changed files with 210 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -40,8 +39,8 @@
public class MenuTarget implements HasMeta<MenuTarget> {

private final Element targetElement;
private AttachDetachCallback targetDetachObserver;
private AttachDetachCallback targetAttachObserver;
private MutationObserverCallback targetDetachObserver;
private MutationObserverCallback targetAttachObserver;
private final Map<String, ComponentMeta> metaObjects = new HashMap<>();

/**
Expand Down Expand Up @@ -77,7 +76,7 @@ public DominoElement<Element> getTargetElement() {
*
* @param targetDetachObserver the observer callback
*/
void setTargetDetachObserver(AttachDetachCallback targetDetachObserver) {
void setTargetDetachObserver(MutationObserverCallback targetDetachObserver) {
this.targetDetachObserver = targetDetachObserver;
}

Expand All @@ -86,7 +85,7 @@ void setTargetDetachObserver(AttachDetachCallback targetDetachObserver) {
*
* @return the observer callback
*/
AttachDetachCallback getTargetDetachObserver() {
MutationObserverCallback getTargetDetachObserver() {
return targetDetachObserver;
}

Expand All @@ -95,7 +94,7 @@ AttachDetachCallback getTargetDetachObserver() {
*
* @param targetDetachObserver the observer callback
*/
void setTargetAttachObserver(AttachDetachCallback targetAttachObserver) {
void setTargetAttachObserver(MutationObserverCallback targetAttachObserver) {
this.targetAttachObserver = targetAttachObserver;
}

Expand All @@ -104,7 +103,7 @@ void setTargetAttachObserver(AttachDetachCallback targetAttachObserver) {
*
* @return the observer callback
*/
AttachDetachCallback getTargetAttachObserver() {
MutationObserverCallback getTargetAttachObserver() {
return targetAttachObserver;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<MutationRecord> 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<MutationRecord> ceinit = CustomEventInit.create();
ceinit.setDetail(record);
DominoElement<Element> element = elements.elementOf(Js.<Element>uncheckedCast(record.target));
String type = ObserverEventType.attributeType(element);

CustomEvent<MutationRecord> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -118,6 +119,7 @@ public abstract class BaseDominoElement<E extends Element, T extends IsElement<E

public static String ATTACH_UID_KEY = "dui-on-attach-uid";
public static String DETACH_UID_KEY = "dui-on-detach-uid";
public static String ATTRIBUTE_CHANGE_UID_KEY = "dui-on-attribute-change-uid";

@Editor.Ignore protected T element;
/** A unique identifier for this DOM element. */
Expand Down Expand Up @@ -145,10 +147,13 @@ public abstract class BaseDominoElement<E extends Element, T extends IsElement<E
protected WavesSupport wavesSupport;

/** A list of attach observers for this DOM element. */
private List<AttachDetachCallback> attachObservers = new ArrayList<>();
private List<MutationObserverCallback> attachObservers = new ArrayList<>();

/** A list of detach observers for this DOM element. */
private List<AttachDetachCallback> detachObservers = new ArrayList<>();
private List<MutationObserverCallback> detachObservers = new ArrayList<>();

/** A list of detach observers for this DOM element. */
private Map<String, List<MutationObserverCallback>> attributesObservers = new HashMap<>();

/** Optional ResizeObserver for this DOM element. */
private Optional<ResizeObserver> resizeObserverOptional = Optional.empty();
Expand All @@ -175,6 +180,7 @@ public abstract class BaseDominoElement<E extends Element, T extends IsElement<E

private EventListener attachEventListener;
private EventListener detachEventListener;
private EventListener attributeChangeEventListener;
private final List<Consumer<T>> onBeforeRemoveHandlers = new ArrayList<>();
private final List<Consumer<T>> onRemoveHandlers = new ArrayList<>();
private final Map<String, ComponentMeta> metaObjects = new HashMap<>();
Expand Down Expand Up @@ -655,11 +661,11 @@ public T withPopover(ChildHandler<T, Popover> 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());
Expand All @@ -672,21 +678,74 @@ 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.
*
* @param callback The observer to be registered.
* @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());
Expand All @@ -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();
Expand All @@ -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;
}
Expand All @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ private static void onElementsAppended(MutationRecord record) {
List<DominoElement<Element>> 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));
Expand All @@ -118,7 +118,7 @@ private static void onElementsAppended(MutationRecord record) {
child -> {
CustomEventInit<MutationRecord> 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<MutationRecord> event = new CustomEvent<>(type, ceinit);
Expand All @@ -139,7 +139,7 @@ private static void onElementsRemoved(MutationRecord record) {
List<DominoElement<Element>> 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));
Expand All @@ -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<MutationRecord> ceinit = CustomEventInit.create();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand All @@ -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();
Expand Down
Loading

0 comments on commit 17b98db

Please sign in to comment.