/*
* Copyright 2015 cruxframework.org.
*
* 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.cruxframework.crux.core.client.html5.api;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.core.client.ScriptInjector;
import com.google.gwt.dom.client.Node;
import com.google.gwt.resources.client.ExternalTextResource;
import com.google.gwt.resources.client.ResourceCallback;
import com.google.gwt.resources.client.ResourceException;
import com.google.gwt.resources.client.TextResource;
/**
* MutationObserver provides developers a way to react to changes in a DOM.
* It is designed as a replacement for Mutation Events defined in the DOM3 Events specification.
* @author Thiago da Rosa de Bustamante
*/
public class MutationObserver extends JavaScriptObject
{
/**
* Default constructor
*/
protected MutationObserver(){}
/**
* Stops the MutationObserver instance from receiving notifications of DOM mutations.
* Until the observe() method is used again, observer's callback will not be invoked.
*/
public final native void disconnect()/*-{
this.disconnect();
}-*/;
/**
* Observe the given node for modifications
*
* Adding an observer to an element is just like addEventListener, if you observe the element
* multiple times it does not make a difference. Meaning if you observe element twice, the
* observe callback does not fire twice, nor will you have to run disconnect() twice. In other words,
* once an element is observed, observing it again with the same observer instance will do nothing.
* However if the callback object is different it will of course add another observer to it.
* @param node the node to be monitored.
* @param children if true, start monitoring changes on children list.
* @param attributes if true, start monitoring changes on node attributes.
* @param characterData if true, start monitoring changes on node data.
*/
public final native void observe(Node node, boolean children, boolean attributes, boolean characterData)/*-{
this.observe(node, {'attributes': attributes, 'childList': children, 'characterData': characterData});
}-*/;
/**
* Observe the given node for modifications on its attributes.
* @param node the node to be monitored.
*/
public final void observeAttributes(Node node)
{
observe(node, false, true, false);
}
/**
* Observe the given node for modifications on its children.
* @param node the node to be monitored.
*/
public final void observeChildren(Node node)
{
observe(node, true, false, false);
}
/**
* Verify if the current browser supports the MutationObserver API.
* @return true if supported.
*/
public static native boolean isSupported()/*-{
return !!$wnd.MutationObserver;
}-*/;
/**
* Verify if the current browser supports the WeakMap API.
* @return true if supported.
*/
public static native boolean isWeakMapSupported()/*-{
return !!$wnd.WeakMap;
}-*/;
/**
* If current browser supports the MutationObserver API, create a new Observer.
* @param callback A handler for the mutations.
* @return the MutationObserver
*/
public static void load(final Callback callback, final LoadCallback loadCallback)
{
if (isSupported())
{
loadCallback.onLoaded(create(callback));
}
else
{
if(!isWeakMapSupported())
{
injectPolyfill(Polyfill.INSTANCE.weakMap(), new ResourceCallback<TextResource>()
{
@Override
public void onSuccess(TextResource resource)
{
injectPolyfill(Polyfill.INSTANCE.mutationObserver(), new ResourceCallback<TextResource>()
{
@Override
public void onSuccess(TextResource resource)
{
loadCallback.onLoaded(create(callback));
}
@Override
public void onError(ResourceException e)
{
loadCallback.onError(e.getMessage());
}
});
}
@Override
public void onError(ResourceException e)
{
loadCallback.onError(e.getMessage());
}
});
}
else
{
injectPolyfill(Polyfill.INSTANCE.mutationObserver(), new ResourceCallback<TextResource>()
{
@Override
public void onSuccess(TextResource resource)
{
loadCallback.onLoaded(create(callback));
}
@Override
public void onError(ResourceException e)
{
loadCallback.onError(e.getMessage());
}
});
}
}
}
private static void injectPolyfill(ExternalTextResource resource, final ResourceCallback<TextResource> resourceCallback)
{
try
{
resource.getText(new ResourceCallback<TextResource>()
{
@Override
public void onError(ResourceException e)
{
resourceCallback.onError(e);
}
@Override
public void onSuccess(final TextResource resource)
{
ScriptInjector.fromString(resource.getText()).setWindow(ScriptInjector.TOP_WINDOW).setRemoveTag(true).inject();
Scheduler.get().scheduleDeferred(new ScheduledCommand()
{
@Override
public void execute()
{
resourceCallback.onSuccess(resource);
}
});
}
});
}
catch (ResourceException e)
{
resourceCallback.onError(e);
}
}
protected static native MutationObserver create(Callback callback)/*-{
return new $wnd.MutationObserver(function (mutations, observer) {
callback.@org.cruxframework.crux.core.client.html5.api.MutationObserver.Callback::onChanged(Lcom/google/gwt/core/client/JsArray;Lorg/cruxframework/crux/core/client/html5/api/MutationObserver;)(mutations, observer);
});
}-*/;
/**
* Define an handler for mutations observed.
* @author Thiago da Rosa de Bustamante
*
*/
public static interface Callback
{
/**
* The observer will call this method when a mutation occurs on the observed node.
* @param mutations An array containing all the mutation records.
* @param observer the MutationObserver instance.
*/
void onChanged(JsArray<MutationRecord> mutations, MutationObserver observer);
}
/**
* Interface used to load a new MutationObserver object.
* @author Thiago da Rosa de Bustamante
*
*/
public static interface LoadCallback
{
/**
* Called when MutationObserver is not supported on browser
* @param message error message
*/
void onError(String message);
/**
* Called when a MutationObserver is created successfully
* @param mutationObserver observer created
*/
void onLoaded(MutationObserver mutationObserver);
}
/**
* MutationRecord is the object that will be passed to the observer's callback.
* @author Thiago da Rosa de Bustamante
*
*/
public static class MutationRecord extends JavaScriptObject
{
/**
* Default Constructor
*/
protected MutationRecord() {}
/**
* Return the nodes added. Will be an empty NodeList if no nodes were added.
* @return the nodes added.
*/
public final native JsArray<Node> getAddedNodes()/*-{
return this.addedNodes;
}-*/;
/**
* Returns the local name of the changed attribute, or null.
* @return the attribute local name.
*/
public final native String getAttributeName()/*-{
return this.attributeName;
}-*/;
/**
* Returns the namespace of the changed attribute, or null.
* @return the attribute namespace
*/
public final native String getAttributeNamespace()/*-{
return this.attributeNamespace;
}-*/;
/**
* Return the next sibling of the added or removed nodes, or null.
* @return the next sibling node.
*/
public final native Node getNextSibling()/*-{
return this.nextSibling;
}-*/;
/**
* The return value depends on the type. For attributes, it is the value of the changed
* attribute before the change. For characterData, it is the data of the changed node
* before the change. For childList, it is null.
* @return the value before the mutation occurs.
*/
public final native String getOldValue()/*-{
return this.oldValue;
}-*/;
/**
* Return the previous sibling of the added or removed nodes, or null.
* @return the previous sibling node.
*/
public final native Node getPreviousSibling()/*-{
return this.previousSibling;
}-*/;
/**
* Return the nodes removed. Will be an empty NodeList if no nodes were removed.
* @return the nodes removed.
*/
public final native JsArray<Node> getRemovedNodes()/*-{
return this.removedNodes;
}-*/;
/**
* Returns the node the mutation affected, depending on the type. For attributes, it is
* the element whose attribute changed. For characterData, it is the CharacterData node.
* For childList, it is the node whose children changed.
* @return the node affected.
*/
public final native Node getTarget()/*-{
return this.target;
}-*/;
/**
* Returns attributes if the mutation was an attribute mutation, characterData if
* it was a mutation to a CharacterData node, and childList if it was a mutation
* to the tree of nodes.
* @return mutation type.
*/
public final native String getType()/*-{
return this.type;
}-*/;
}
}