/* * 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; import com.bfr.client.selection.impl.RangeImpl; import com.google.gwt.core.client.GWT; 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; import java.util.ArrayList; import java.util.List; /** * Implements a text range in a Document, everything between two RangeEndPoints. * Works with both a (browser dependent) javascript range object, and with * the java RangeEndPoint objects, building one or the other as needed on * demand. * * @author John Kozura */ public class Range { // For use in compareBoundaryPoint, which end points to compare public static final short START_TO_START = 0; public static final short START_TO_END = 1; public static final short END_TO_END = 2; public static final short END_TO_START = 3; private static RangeImpl c_impl = (RangeImpl) GWT.create(RangeImpl.class); /** * Returns the next adjacent text node in the given direction. Will move * down the hierarchy, then through siblings, then up, looking for the first * text node. * <p/> * This could be non-statically included in the Node class * * @param current An element to start the search from, can be any type * of node. * @param forward whether to search forward or backward * @return the next (previous) text node, or null if no more */ public static Text getAdjacentTextElement(Node current, boolean forward) { Text res = getAdjacentTextElement(current, null, forward, false); return res; } /** * Returns the next adjacent text node in the given direction. Will move * down the hierarchy (if traversingUp is not set), then through siblings, * then up (but not past topMostNode), looking for the first node * <p/> * This could be non-statically included in the Node class * * @param current An element to start the search from, can be any type * of node. * @param topMostNode A node that this will traverse no higher than * @param forward whether to search forward or backward * @param traversingUp if true, will not look at the children of this element * @return the next (previous) text node, or null if no more */ public static Text getAdjacentTextElement(Node current, Node topMostNode, boolean forward, boolean traversingUp) { Text res = null; Node node; // If traversingUp, then the children have already been processed if (!traversingUp) { if (current.getChildCount() > 0) { node = forward ? current.getFirstChild() : current.getLastChild(); if (node.getNodeType() == Node.TEXT_NODE) { res = (Text) node; } else { // Depth first traversal, the recursive call deals with // siblings res = getAdjacentTextElement(node, topMostNode, forward, false); } } } if (res == null) { node = forward ? current.getNextSibling() : current.getPreviousSibling(); // Traverse siblings if (node != null) { if (node.getNodeType() == Node.TEXT_NODE) { res = (Text) node; } else { // Depth first traversal, the recursive call deals with // siblings res = getAdjacentTextElement(node, topMostNode, forward, false); } } } // Go up and over if still not found if ((res == null) && (current != topMostNode)) { node = current.getParentNode(); // Stop at document (technically could stop at "html" tag) if ((node != null) && (node.getNodeType() != Node.DOCUMENT_NODE)) { res = getAdjacentTextElement(node, topMostNode, forward, true); } } return res; } /** * Returns all text nodes between (and including) two arbitrary text nodes. * Caller must ensure startNode comes before endNode. * * @param startNode start node to traverse * @param endNode end node to finish traversal * @return A list of all text nodes between these two text nodes */ public static List<Text> getSelectedTextElements(Text startNode, Text endNode) { List<Text> res = new ArrayList<Text>(); Text current = startNode; while ((current != null) && (current != endNode)) { res.add(current); current = getAdjacentTextElement(current, null, true, false); } if (current == null) { // With the old way this could have been backwards, but should not // happen now, so this is an error res = null; } else { res.add(current); } return res; } // Starting point of this range private RangeEndPoint m_startPoint; // Ending point of this range private RangeEndPoint m_endPoint; // The document this range is contained within. private Document m_document; // The javascript rendering of this range private RangeImpl.JSRange m_range; // The points as they were last time m_range was computed private RangeEndPoint m_lastStartPoint; private RangeEndPoint m_lastEndPoint; /** * Creates an empty range on this document * * @param doc Document to create an empty range in */ public Range(Document doc) { setDocument(doc); } /** * Creates a range that encompasses the given element * * @param element Element to create a range around */ public Range(Element element) { setRange(element); } /** * Creates a range that is a cursor at the given location * * @param cursorPoint a single point to make a cursor range */ public Range(RangeEndPoint cursorPoint) { setCursor(cursorPoint); } /** * Create a range that extends between the given points. Caller must * ensure that end comes after start * * @param startPoint start point of the new range * @param endPoint end point of the new range */ public Range(RangeEndPoint startPoint, RangeEndPoint endPoint) { setRange(startPoint, endPoint); } /** * Internal method for creating a range from a JS object * * @param document * @param rangeObj */ Range(Document document, RangeImpl.JSRange rangeObj) { m_document = document; m_range = rangeObj; } /** * Internal function for retrieving the range, external callers should NOT * USE THIS * * @return */ public RangeImpl.JSRange _getJSRange() { return m_range; } /** * Internal call to set the range, which skips some checks and settings; * this SHOULD NOT be used externally. * * @param startPoint * @param endPoint */ public void _setRange(RangeEndPoint startPoint, RangeEndPoint endPoint) { m_document = startPoint == null ? null : startPoint.getNode().getOwnerDocument(); m_startPoint = startPoint; m_endPoint = endPoint; } /** * Collapses the range into a cursor, either to the start or end point * * @param start if true, cursor is the start point, otherwise the end point */ public void collapse(boolean start) { if (m_range != null) { c_impl.collapse(m_range, start); m_startPoint = null; } else if (start) { m_endPoint = m_startPoint; } else { m_startPoint = m_endPoint; } } /** * Compares an endpoint of this range with an endpoint in another range, * returning -1, 0, or 1 depending whether the comparison endpoint comes * before, at, or after this endpoint. how is a constant determining which * endpoints to compare, for example Range.START_TO_START. * * @param compare Range to compare against this one. * @param how constant indicating which endpoints to compare * @return -1, 0, or 1 indicating relative order of the endpoints */ public int compareBoundaryPoint(Range compare, short how) { ensureRange(); compare.ensureRange(); return c_impl.compareBoundaryPoint(m_range, getJSRange(), how); } /** * Make a copy of the contents of this range, into the given element. All * tags required to make the range complete will be included * * @param copyInto an element to copy the contents into, ie * DOM.createSpanElement() */ public void copyContents(Element copyInto) { ensureRange(); c_impl.copyContents(m_range, copyInto); } /** * Remove the contents of this range from the DOM. */ public void deleteContents() { ensureRange(); c_impl.deleteContents(m_range); } public boolean equals(Object obj) { boolean res = false; try { Range cmp = (Range) obj; ensureEndPoints(); cmp.ensureEndPoints(); res = (cmp == this) || (m_startPoint.equals(cmp.getStartPoint()) && m_endPoint.equals(cmp.getEndPoint())); } catch (Exception ex) { } return res; } /** * Place the contents of this range into a new SPAN element, removing them * from the DOM. All tags required to make the range complete will be * included. This does not preserve the element object ids of the contents. * * @return a new SPAN element unattached to the DOM, containing the range * contents. */ public Element extractContents() { Element res = m_document.createSpanElement(); extractContents(res); return res; } /** * Place the contents of this range into the given element, removing them * from the DOM. All tags required to make the range complete will be * included. This does not preserve the element object ids of the contents. * * @param copyInto an element to extract the contents into, ie * DOM.createSpanElement() */ public void extractContents(Element copyInto) { ensureRange(); c_impl.extractContents(m_range, copyInto); } /** * Get the element that is the lowest common ancestor of both ends of the * range. In other words, the smallest element that includes the range. * * @return the element that completely encompasses this range */ public Element getCommonAncestor() { ensureRange(); return c_impl.getCommonAncestor(m_range); } /** * Gets a single point of the cursor location if this is a cursor, otherwise * returns null. * * @return the single point if this is a cursor and not a selection */ public RangeEndPoint getCursor() { return isCursor() ? m_startPoint : null; } /** * Get the DOM Document this range is within * * @return document this range is in */ public Document getDocument() { return m_document; } /** * Get the end point of the range. Not a copy, so changing this alters * the range. * * @return the end point object */ public RangeEndPoint getEndPoint() { ensureEndPoints(); return m_endPoint; } /** * Gets an HTML string represnting all elements enclosed by this range. * * @return An html string of this range */ public String getHtmlText() { ensureRange(); return c_impl.getHtmlText(m_range); } /** * Get the JS object representing this range. Since it is highly browser * dependent, it is not recommended to operate on this * * @return JavaScriptObject representing this range */ public RangeImpl.JSRange getJSRange() { ensureRange(); return m_range; } /** * Returns a list of all text elements that are part of this range, in order. * * @return all elements in this range */ public List<Text> getSelectedTextElements() { return getSelectedTextElements(m_startPoint.getTextNode(), m_endPoint.getTextNode()); } /** * Get the start point of the range. Not a copy, so changing this alters * the range. * * @return the start point object */ public RangeEndPoint getStartPoint() { ensureEndPoints(); return m_startPoint; } /** * Gets the plain text that is enclosed by this range * * @return A string of the text in this range */ public String getText() { ensureRange(); return c_impl.getText(m_range); } /** * Returns whether this is a cursor, ie the start and end point are equal * * @return true if start == end */ public boolean isCursor() { ensureEndPoints(); return m_startPoint.equals(m_endPoint); } /** * Minimize the number of text nodes included in this range. If the start * point is at the end of a text node, move it to the beginning of the * next text node; vice versa for the end point. The result should ensure * no text nodes with 0 included characters. */ public void minimizeTextNodes() { ensureEndPoints(); m_startPoint.minimizeBoundaryTextNodes(true); m_endPoint.minimizeBoundaryTextNodes(false); } /** * TODO NOT IMPLEMENTED YET * Move the end points to encompass a boundary type, such as a word. * * @param topMostNode a Node not to traverse above, or null * @param type unit to move boundary by, such as RangeEndPoint.MOVE_WORD */ public void moveToBoundary(Node topMostNode, short type) { ensureEndPoints(); m_startPoint.move(false, topMostNode, null, type, 1); m_endPoint.move(true, topMostNode, null, type, 1); } /** * Sets the range to a point cursor. * * @param cursorPoint A single endpoint to create a cursor range at */ public void setCursor(RangeEndPoint cursorPoint) { setRange(cursorPoint, cursorPoint); } /** * Sets just the end point of the range. New endPoint must reside within * the same document as the current startpoint, and must occur after it. * * @param startPoint New start point for this range */ public void setEndPoint(RangeEndPoint endPoint) { assert ((m_startPoint != null) || (endPoint.getNode().getOwnerDocument() == m_document)); m_endPoint = endPoint; m_range = null; } /** * Sets the range to encompass the given element. May not work around * non-text containing elements. * * @param element Element to surround by this range * @return whether a range can be placed around this element. */ public boolean setRange(Element element) { Text firstText = getAdjacentTextElement(element, element, true, false); Text lastText = getAdjacentTextElement(element, element, false, false); if ((firstText == null) || (lastText == null)) { return false; } setRange(new RangeEndPoint(firstText, 0), new RangeEndPoint(lastText, lastText.getLength())); return true; } /** * Set the range to be between the two given points. Both points must be * within the same document, and end must come after start. * * @param startPoint Start point to set the range to * @param endPoint End point to set the range to */ public void setRange(RangeEndPoint startPoint, RangeEndPoint endPoint) { assert (startPoint.getNode().getOwnerDocument() == endPoint.getNode().getOwnerDocument()); _setRange(startPoint, endPoint); m_range = null; } /** * Sets just the start point of the range. New startPoint must reside within * the same document as the current endpoint, and must occur before it. * * @param startPoint New start point for this range */ public void setStartPoint(RangeEndPoint startPoint) { assert ((m_endPoint != null) && (startPoint.getNode().getOwnerDocument() == m_document)); m_startPoint = startPoint; m_range = null; } /** * Surround all of the contents of the range with a new SPAN element, which * replaces the content in the DOM. All tags required to make the range * complete are included in the child content. This does not preserve the * element object ids of the contents. The range will surround the new * element after this operation. * * @return The new span element that now surround the contents */ public Element surroundContents() { Element res = m_document.createSpanElement(); surroundContents(res); return res; } /** * Surround all of the contents of the range with the given element, which * replaces the content in the DOM. All tags required to make the range * complete are included in the child content. This does not preserve the * element object ids of the contents. The range will surround this * element after this operation. * * @param copyInto an element to place the contents into, which will replace * them in the DOM after this operation */ public void surroundContents(Element copyInto) { ensureRange(); c_impl.surroundContents(m_range, copyInto); setRange(copyInto); } /** * Ensure the end points exists and are consisent with the javascript range */ void ensureEndPoints() { if ((m_startPoint == null) || (m_endPoint == null)) { c_impl.fillRangePoints(this); setupLastEndpoints(); } } /** * Ensure the javascript range exists and is consistent with the end points */ void ensureRange() { if (rangeNeedsUpdate()) { m_range = c_impl.createRange(m_document, m_startPoint.getTextNode(), m_startPoint.getOffset(), m_endPoint.getTextNode(), m_endPoint.getOffset()); setupLastEndpoints(); } } private boolean rangeNeedsUpdate() { return (m_range == null) || ((m_startPoint != null) && ((m_lastStartPoint == null) || !m_lastStartPoint.equals(m_startPoint) || (m_lastEndPoint == null) || !m_lastEndPoint.equals(m_endPoint))); } private void setupLastEndpoints() { m_lastStartPoint = new RangeEndPoint(m_startPoint); m_lastEndPoint = new RangeEndPoint(m_endPoint); } /** * Set the document this range is contained within * * @param doc document to set */ private void setDocument(Document doc) { if (m_document != doc) { m_document = doc; m_range = c_impl.createFromDocument(doc); } } }