package com.project.shared.client.html5.impl;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Node;
import com.google.gwt.dom.client.NodeList;
import com.google.gwt.user.client.DOM;
import com.project.shared.client.html5.Range;
import com.project.shared.data.Pair;
import com.project.shared.data.funcs.Func;
public class RangeUtils
{
public static void applyToNodesInRange(Range range, Func.Action<Element> func)
{
HashMap<Node, Boolean> nodeContainmentMap = getNodeContainmentMap(range);
Element startElem = null;
Element endElem = null;
Pair<Node, Integer> startPoint = new Pair<Node,Integer>(range.getStartContainer(), range.getStartOffset());
Pair<Node, Integer> endPoint = new Pair<Node,Integer>(range.getEndContainer(), range.getEndOffset());
// Now modify the tree in-place, and at the same time remember the elements that define the range
// after the modification.
for (Map.Entry<Node, Boolean> entry : nodeContainmentMap.entrySet())
{
Element elem = wrapIncludedPart(startPoint, endPoint, entry.getKey(), entry.getValue());
if (null != elem) {
func.apply(elem);
}
if (startPoint.getA() == entry.getKey()) {
startElem = elem;
}
if (endPoint.getA() == entry.getKey()) {
endElem = elem;
}
}
// RangeImpl updatedRange = RangeImpl.create();
// if ((null != startElem) && (null != endElem)) {
// updatedRange.setStartBefore(startElem);
// updatedRange.setEndAfter(endElem);
// }
// return updatedRange;
if ((null != startElem) && (null != endElem)) {
range.setStartBefore(startElem);
range.setEndAfter(endElem);
}
}
/**
* Returns the set of nodes that are covered by the given range. For each node key, the boolean value indicates
* whether the node is fully (true) or partially (false) contained in the range.
*/
public static HashMap<Node, Boolean> getNodeContainmentMap(Range range) {
ArrayList<Node> descendants = new ArrayList<Node>();
descendants.add(range.getCommonAncestorContainer());
HashMap<Node, Boolean> nodeInclusionMap = new HashMap<Node, Boolean>();
while (descendants.size() > 0) {
for (Node descendant : descendants.toArray(new Node[0]))
{
descendants.remove(descendant);
addNodeChildren(descendant, descendants);
if (descendant.getNodeType() != Node.TEXT_NODE) {
continue;
}
int startOffsetCompare = range.comparePoint(descendant, 0);
int endOffsetCompare = range.comparePoint(descendant, Math.max(0, descendant.getNodeValue().length() - 1));
boolean startContained = 0 == startOffsetCompare;
boolean endContained = 0 == endOffsetCompare;
boolean midContained = (-1 == startOffsetCompare) && (1 == endOffsetCompare);
boolean isFullyContained = startContained && endContained;
boolean isPartiallyContained = startContained || endContained || midContained;
if (isPartiallyContained) {
//logNode("Checking descendant with offset compare value: " + startOffsetCompare, descendant);
// If we change the DOM while iterating here, the range.comparePoint method may return wrong results?
// that's why we add to a map and later split the elements appropriately
nodeInclusionMap.put(descendant, isFullyContained);
}
}
}
return nodeInclusionMap;
}
private static Element wrapIncludedPart(Pair<Node, Integer> startPoint, Pair<Node, Integer> endPoint, Node descendant, boolean fullyContained)
{
Element elem = null;
Node startNode = startPoint.getA();
Node endNode = endPoint.getA();
int startOffset = startPoint.getB();
int endOffset = endPoint.getB();
if (startNode == descendant) {
if (startNode == endNode) {
elem = wrapSplitTextNode(startNode, startOffset, endOffset).mid;
}
else {
elem = wrapSplitTextNode(startNode, startOffset, startNode.getNodeValue().length()).mid;
}
}
else if (endNode == descendant) {
if (startNode != endNode) {
elem = wrapSplitTextNode(endNode, endOffset, endNode.getNodeValue().length()).pre;
}
else {
elem = null;
}
}
else if (fullyContained) {
if (descendant.getNodeType() == Node.TEXT_NODE) {
elem = wrapSplitTextNode(descendant, 0, descendant.getNodeValue().length()).parent;
}
else {
elem = Element.as(descendant);
}
}
return elem;
}
private static void addNodeChildren(Node commonAncestor, ArrayList<Node> descendants)
{
NodeList<Node> ancestorChildren = commonAncestor.getChildNodes();
for (int i = 0 ; i < ancestorChildren.getLength(); i++)
{
Node item = ancestorChildren.getItem(i);
//logNode("Adding child: ", item);
descendants.add(item);
}
}
// private static void logNode(String message, Node item)
// {
// Logger.log(message + " - Node: " + item.toString() + " : " + item.getNodeValue());
// if (item.getNodeType() == Node.ELEMENT_NODE) {
// Element elem = Element.as(item);
// Logger.log(" Node is an Element:" + elem.getString());
// }
// }
public static class SplitElement {
public SplitElement(Element parent, Element pre, Element mid, Element post)
{
super();
this.parent = parent;
this.pre = pre;
this.mid = mid;
this.post = post;
}
Element parent;
Element pre;
Element mid;
Element post;
}
public static SplitElement wrapSplitTextNode(Node textNode, int startOffset, int endOffset)
{
com.google.gwt.user.client.Element wrapperSpan = DOM.createSpan();
String text = textNode.getNodeValue();
boolean endOffsetBeyondLength = text.length() <= endOffset;
com.google.gwt.user.client.Element prePartSpan = null;
com.google.gwt.user.client.Element midPartSpan = null;
com.google.gwt.user.client.Element postPartSpan = null;
if (startOffset > 0) {
prePartSpan = DOM.createSpan();
prePartSpan.setInnerText(text.substring(0, startOffset));
wrapperSpan.appendChild(prePartSpan);
}
midPartSpan = DOM.createSpan();
midPartSpan.setInnerText(text.substring(startOffset, endOffset));
wrapperSpan.appendChild(midPartSpan);
if (false == endOffsetBeyondLength) {
postPartSpan = DOM.createSpan();
postPartSpan.setInnerText(text.substring(endOffset));
wrapperSpan.appendChild(postPartSpan);
}
textNode.getParentNode().replaceChild(wrapperSpan, textNode);
return new SplitElement(wrapperSpan, prePartSpan, midPartSpan, postPartSpan);
}
}