/*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.xwiki.gwt.user.client.ui.rta.cmd.internal;
import java.util.ArrayList;
import java.util.List;
import org.xwiki.gwt.dom.client.Range;
import org.xwiki.gwt.user.client.ui.rta.RichTextArea;
import com.google.gwt.dom.client.Node;
/**
* Overwrites {@link InsertHTMLExecutableImpl} with a custom implementation for Internet Explorer in order to ensure the
* caret remains in the right position after the selected content is deleted. IE sometimes moves the caret inside a
* sibling node when an entire node is deleted, instead of keeping the caret been the nodes.
*
* @version $Id: 5f289dd7a8eec8c8bfc2e8daf2a0eb8462e37dac $
*/
public class InsertHTMLExecutableImplIE extends InsertHTMLExecutableImpl
{
/**
* {@inheritDoc}
* <p>
* Ensures the caret remains in the right position after the selected content is deleted.
* </p>
*
* @see InsertHTMLExecutable#deleteSelection(RichTextArea)
*/
@Override
protected Range deleteSelection(RichTextArea rta)
{
// If the start container is badly detected then this method might fail to adjust the caret position.
Node node = rta.getDocument().getSelection().getRangeAt(0).getStartContainer();
// Save the list of ancestors and their child nodes before deleting the selected content in order to be able to
// detect which is the first deleted node. The first deleted node is not always the node where the selection
// starts. In some cases (e.g. the entire content of an element is selected) Internet Explorer deletes more than
// what is selected. Internet Explorer also has the habit of deleting sibling elements that are not visible and
// normalizing sibling text nodes.
List<Node> ancestors = new ArrayList<Node>();
// Here we don't use a map because a runtime exception is thrown when the key is a node (JavaScript object).
List<List<Node>> children = new ArrayList<List<Node>>();
while (node != null) {
ancestors.add(node);
children.add(getChildren(node));
node = node.getParentNode();
}
// Delete the selected content.
Range range = super.deleteSelection(rta);
// Look for the first ancestor that hasn't been affected by the delete command.
int containerIndex = getFirstAttachedAncestorIndex(ancestors);
// Look for the first child node that has been deleted.
int offset = getFirstDeletedChildIndex(ancestors.get(containerIndex), children.get(containerIndex));
if (offset >= 0) {
range = rta.getDocument().createRange();
range.setStart(ancestors.get(containerIndex), offset);
range.collapse(true);
}
return range;
}
/**
* Looks for the first ancestor that hasn't been affected by the delete command.
*
* @param ancestors the path from a node to the root element as it was before deleting the selected content
* @return the index of the first node in the given list of ancestors that is still attached to the document
*/
private int getFirstAttachedAncestorIndex(List<Node> ancestors)
{
for (int i = ancestors.size() - 1; i > 0; i--) {
Node expectedParent = ancestors.get(i);
Node actualParent = null;
try {
actualParent = ancestors.get(i - 1).getParentNode();
} catch (Exception e) {
// Accessing the properties of a deleted node can trigger sometimes and exception.
// Keep actualParent null.
}
if (actualParent != expectedParent) {
return i;
}
}
return ancestors.size() > 0 ? 0 : -1;
}
/**
* Copy the list of child nodes of the given parent node.
*
* @param parent a DOM node
* @return the list of child nodes of the given parent node
*/
private List<Node> getChildren(Node parent)
{
List<Node> children = new ArrayList<Node>(parent.getChildNodes().getLength());
Node child = parent.getFirstChild();
while (child != null) {
children.add(child);
child = child.getNextSibling();
}
return children;
}
/**
* Looks for the first child node that doesn't have the expected parent.
*
* @param expectedParent the expected parent
* @param children the list of child nodes before the delete command was executed
* @return the index of the first deleted child node or {@code -1} of none of the child nodes has been deleted
*/
private int getFirstDeletedChildIndex(Node expectedParent, List<Node> children)
{
for (int i = 0; i < children.size(); i++) {
Node actualParent = null;
try {
actualParent = children.get(i).getParentNode();
} catch (Exception e) {
// Accessing the properties of a deleted node can trigger sometimes and exception.
// Keep actualParent null.
}
if (actualParent != expectedParent) {
return i;
}
}
// None of the child nodes has been deleted.
return -1;
}
}