/*
* Copyright 2010 John Kozura
*
* 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 com.bfr.client.selection.impl;
import com.bfr.client.selection.Range;
import com.bfr.client.selection.RangeEndPoint;
import com.bfr.client.selection.Selection;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Node;
import com.google.gwt.dom.client.Text;
/**
* Generic implementation of the Range object, using the W3C standard
* implemented by Firefox, Safari, and Opera.
*
* @author John Kozura
*/
public class RangeImpl {
// For convenience of maintaining JS range objects
public static class JSRange extends JavaScriptObject {
protected JSRange() {
}
}
/**
* Reads an object's property as an integer value.
*
* @param object The object
* @param propertyName The name of the property being read
* @return The value
*/
public native static int getIntProp(JavaScriptObject object,
String propertyName)
/*-{
var value = object[ propertyName ];
return value;
}-*/;
/**
* Reads an object given a property and returns it as a JavaScriptObject
*
* @param object
* @param propertyName
* @return the object
*/
private native static JavaScriptObject getProperty(JavaScriptObject object,
String propertyName)
/*-{
var value = object[ propertyName ];
return value || null;
}-*/;
;
/**
* Make a copy of the given js range; the new JS range is decoupled from any
* changes.
*
* @param range a js range to copy
* @return a full copy of the range
*/
public native JSRange cloneRange(JSRange range)
/*-{
return range.cloneRange();
}-*/;
/**
* Collapse a JS range object to the start or end point
*
* @param range js range to collapse
* @param start if true, collapse to start, otherwise to end
*/
public native void collapse(JSRange range, boolean start)
/*-{
range.collapse(start);
}-*/;
/**
* Compare endpoints of 2 ranges, returning -1, 0, or 1 depending on whether
* the compare endpoint comes before, at, or after the range endpoint.
*
* @param range range to compare against
* @param compare range to compare
* @param how a constant to choose which endpoint of each range to compare,
* i.e. Range.START_TO_END
* @return -1, 0, or 1 depending on order of the 2 ranges
*/
public native int compareBoundaryPoint(JSRange range,
JSRange compare,
short how)
/*-{
return range.compareBoundaryPoints(compare, how);
}-*/;
/**
* Copy the contents of the range into the given element, including any
* tags needed to make it complete. The DOM is not changed.
*
* @param range js range to copy contents out of.
* @param copyInto an element to copy these contents into
*/
public native void copyContents(JSRange range, Element copyInto)
/*-{
copyInto.appendChild(range.cloneContents());
}-*/;
/**
* Create an empty JS range from a document
*
* @param doc DOM document
* @return a new empty JS range
*/
public native JSRange createFromDocument(Document doc)
/*-{
return doc.createRange();
}-*/;
/**
* Create a JS range with the given endpoints
*
* @param startPoint Start text of the selection
* @param startOffset offset into start text
* @param endPoint End text of the selection
* @param endOffset offset into end text
* @return A javascript object of this range
*/
public native JSRange createRange(Document doc,
Text startPoint,
int startOffset,
Text endPoint,
int endOffset)
/*-{
var range = doc.createRange();
range.setStart(startPoint, startOffset);
range.setEnd(endPoint, endOffset);
return range;
}-*/;
/**
* Remove the contents of the js range from the DOM
*
* @param range js range to remove
*/
public native void deleteContents(JSRange range)
/*-{
range.deleteContents();
}-*/;
/**
* Extract the contents of the range into the given element, removing them
* from the DOM. Any tags needed to make the contents complete are included.
* Element object ids are not maintained.
*
* @param range js range to extract contents from
* @param copyInto an element to extract these contents into
*/
public native void extractContents(JSRange range, Element copyInto)
/*-{
copyInto.appendChild(range.extractContents());
}-*/;
/**
* Fill the start and end point of a Range object, using the javascript
* range.
*
* @param fillRange range object to set the endpoints of
*/
public void fillRangePoints(Range fillRange) {
JSRange jsRange = fillRange._getJSRange();
Node startNode = getProperty(jsRange, Selection.START_NODE).cast();
int startOffset = getIntProp(jsRange, Selection.START_OFFSET);
RangeEndPoint startPoint = findTextPoint(startNode, startOffset);
Node endNode = getProperty(jsRange, Selection.END_NODE).cast();
int endOffset = getIntProp(jsRange, Selection.END_OFFSET);
RangeEndPoint endPoint = findTextPoint(endNode, endOffset);
fillRange._setRange(startPoint, endPoint);
}
/**
* Get lowest common ancestor element of the given js range
*
* @param range js range to get ancestor element of
* @return the lowest element that completely encompasses the range
*/
public native Element getCommonAncestor(JSRange range)
/*-{
return range.commonAncestorContainer;
}-*/;
/**
* Get the complete html fragment enclosed by this range. Ensures that all
* opening and closing tags are included.
*
* @param range js range to get the html of
* @return an html string of the range
*/
public native String getHtmlText(JSRange range)
/*-{
var parent = range.startContainer.ownerDocument.createElement("span");
this.@com.bfr.client.selection.impl.RangeImpl::copyContents(Lcom/bfr/client/selection/impl/RangeImpl$JSRange;Lcom/google/gwt/dom/client/Element;)(range, parent);
return parent.innerHTML;
}-*/;
/**
* Get the pure text that is included in a js range
*
* @param range js range to get the text of
* @return string of the range's text
*/
public native String getText(JSRange range)
/*-{
return range.toString();
}-*/;
/**
* Surround the contents of the range with the given element, and put the
* element in their place. Any tags needed to make the contents complete
* are included. Element object ids are not maintained.
*
* @param range js range to surround with this element
* @param copyInto element to surround the range's contents with
*/
public native void surroundContents(JSRange range, Element copyInto)
/*-{
copyInto.appendChild(range.extractContents());
range.insertNode(copyInto);
}-*/;
/**
* If the found range is not on a text node, this finds the cooresponding
* text node to where the selection is. If it is on a text node, just
* directly creates the endpoint from it.
*
* @param node node returned as an endpoint of a range
* @param offset offset returned to the endpoint of a range
* @return A range end point with a proper (or null) text node
*/
private RangeEndPoint findTextPoint(Node node, int offset) {
RangeEndPoint res;
if (node.getNodeType() == Node.TEXT_NODE) {
res = new RangeEndPoint((Text) node, offset);
} else {
// search backwards unless this is after the last node
boolean dir = offset >= node.getChildCount();
Node child = (node.getChildCount() == 0) ? node :
node.getChild(dir ? offset - 1 : offset);
// Get the previous/next text node
Text text = Range.getAdjacentTextElement(child, dir);
if (text == null) {
// If we didn't find a text node in the preferred direction,
// try the other direction
dir = !dir;
text = Range.getAdjacentTextElement(child, dir);
}
res = new RangeEndPoint(text, dir);
}
return res;
}
}