/**
* Copyright 2010 Google Inc.
*
* 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.waveprotocol.wave.client.editor.extract;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Node;
import com.google.gwt.user.client.Command;
import org.waveprotocol.wave.client.common.util.SignalEvent;
import org.waveprotocol.wave.client.common.util.UserAgent;
import org.waveprotocol.wave.client.editor.EditorStaticDeps;
import org.waveprotocol.wave.client.editor.content.ContentElement;
import org.waveprotocol.wave.client.editor.content.ContentRange;
import org.waveprotocol.wave.client.editor.impl.NodeManager;
import org.waveprotocol.wave.client.editor.selection.content.SelectionHelper;
import org.waveprotocol.wave.client.scheduler.CommandQueue;
import org.waveprotocol.wave.model.util.CollectionUtils;
import org.waveprotocol.wave.model.util.StringSet;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* Class for handling DOM mutation. This currently reverts all unexpected DOM
* mutations without causing the red flash.
*
* NOTE(user): Consider trigger red flash for unexpected DOM mutations other
* than Apple+B
*
*/
public final class DOMMutationExtractor {
private static final StringSet DOM_EVENTS_IGNORE = CollectionUtils.createStringSet();
private final CommandQueue deferredCommands;
private final SelectionHelper passiveSelectionHelper;
/**
* List of content elements queued for revert.
*/
private final List<ContentElement> toRevert = new ArrayList<ContentElement>();
/**
* Is set to true while we are reverting.
*/
private boolean isReverting;
/**
* The content range prior to DOM mutation event.
*/
private ContentRange previousContentRange;
static {
for (String e : new String[] {"DOMSubtreeModified", "DOMNodeRemovedFromDocument",
"DOMNodeInsertedIntoDocument", "DOMAttrModified", "DOMCharacterDataModified",
"DOMElementNameChanged", "DOMAttributeNameChanged"}) {
DOM_EVENTS_IGNORE.add(e);
}
}
private final Command revertCmd = new Command() {
public void execute() {
isReverting = true;
domMutationLog("nodes to revert: " + toRevert.size());
for (ContentElement e : toRevert) {
revertElement(e);
}
toRevert.clear();
// Set the selection to the end.
if (previousContentRange != null) {
passiveSelectionHelper.setSelectionPoints(previousContentRange.getFirst(),
previousContentRange.getSecond());
}
isReverting = false;
}
private void revertElement(ContentElement e) {
if (e == null || !e.isContentAttached()) {
return;
}
e.revertImplementation();
}
};
/**
* Creates a DOMMutationExtractor.
*/
public DOMMutationExtractor(CommandQueue deferredCommands,
SelectionHelper passiveSelectionHelper) {
this.deferredCommands = deferredCommands;
this.passiveSelectionHelper = passiveSelectionHelper;
}
/**
* Handles DOM mutation events.
* @param event
* @param contentRange last known selection
*/
public void handleDOMMutation(SignalEvent event, ContentRange contentRange) {
// Early exit if non-safari or non-mac
if (!(UserAgent.isSafari() && UserAgent.isMac())) {
return;
}
// We don't care about DOMMutations that we generate while we are reverting.
if (isReverting) {
return;
}
previousContentRange = contentRange;
Node n = event.getTarget();
if (n.getNodeType() == Node.ELEMENT_NODE) {
Element e = Element.as(event.getTarget());
if (DOM_EVENTS_IGNORE.contains(event.getType())) {
// ignore
return;
} else if (event.getType().equals("DOMNodeInserted") && handleDOMNodeInserted(e)) {
return;
} else if (event.getType().equals("DOMNodeRemoved") && handleDOMNodeRemoved(e)) {
return;
}
}
return;
}
private boolean handleDOMNodeInserted(final Element e) {
if (e.getTagName().equals("SPAN") && e.hasAttribute("class")
&& e.getAttribute("class").equals("Apple-style-span")) {
scheduleElementForRevert(e.getParentElement());
}
return true;
}
private boolean handleDOMNodeRemoved(final Element e) {
if (e.getTagName().equals("B")) {
scheduleElementForRevert(e.getParentElement());
}
return false;
}
private void scheduleElementForRevert(Element e) {
// We get the back reference of the target's parent, because even if this
// element is just inserted, the parent should have a corresponding content node.
final ContentElement content = NodeManager.getBackReference(e);
if (content == null) {
return;
}
Iterator<ContentElement> i = toRevert.iterator();
while (i.hasNext()) {
ContentElement current = i.next();
if (isAncestorOf(current, content)) {
// Ancestor has already been scheduled for revert, we can return early.
return;
} else if (isAncestorOf(content, current)){
// We no longer need to revert his node, as we will revert the ancestor
i.remove();
}
}
toRevert.add(content);
schedule();
}
private void schedule() {
deferredCommands.addCommand(revertCmd);
}
private void domMutationLog(String msg) {
EditorStaticDeps.logger.trace().log("DOMMutationExtractor: " + msg);
}
/**
* Returns true iff a is b or an ancestor of b
* @param a
* @param b
*/
public static boolean isAncestorOf(ContentElement a, ContentElement b) {
while (b != null) {
if (a == b) {
return true;
}
b = b.getParentElement();
}
return false;
}
}