/* * Copyright 2011 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.google.gwt.dom.client.Element; import com.google.gwt.dom.client.Node; import com.google.gwt.dom.client.Text; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.ui.Widget; import java.util.ArrayList; import java.util.List; /** * Represents a bounding box, and methods for finding the bounding box of * elements and ranges. * * @author John Kozura */ public class HtmlBBox { private int m_x, m_y, m_width, m_height; /** * Create a bounding box based on x/y and width/height * * @param x * @param y * @param width * @param height */ public HtmlBBox(int x, int y, int width, int height) { m_x = x; m_y = y; m_width = width; m_height = height; } /** * Create a location based on an element's bounding box * * @param ele Element to initialize bounding box with */ public HtmlBBox(Element ele) { this(ele.getAbsoluteLeft(), ele.getAbsoluteTop(), ele.getOffsetWidth(), ele.getOffsetHeight()); } /** * Expand this box by the given bounding box * * @param bb box to use to expand this one */ public void expand(HtmlBBox bb) { if (bb != null) { m_x = Math.min(m_x, bb.getAbsoluteLeft()); m_y = Math.min(m_y, bb.getAbsoluteTop()); m_width = Math.max(getAbsoluteRight(), bb.getAbsoluteRight()) - m_x; m_height = Math.max(getAbsoluteBottom(), bb.getAbsoluteBottom()) - m_y; } } /** * Create a bounding box the size of the given element * * @param ele Element to create bounding box around * @return a new bounding box */ public static HtmlBBox getBoundingBox(Element ele) { return new HtmlBBox(ele); } /** * Create a bounding box the size of the given range * * @param range Range to create bounding box around * @return a new bounding box */ public static HtmlBBox getBoundingBox(Range rng) { HtmlBBox res; if (rng.getStartPoint().getTextNode() == rng.getEndPoint().getTextNode()) { res = getBoundingBox(rng.getStartPoint().getTextNode(), rng.getStartPoint().getOffset(), rng.getEndPoint().getOffset()); } else { res = getBoundingBox(rng.getStartPoint(), true); res.expand(getBoundingBox(rng.getEndPoint(), false)); // Make sure the range encompasses all of the text nodes within // the range as well List<Text> texts = rng.getSelectedTextElements(); for (int i = 1; i < (texts.size() - 1); i++) { res.expand(getBoundingBox(texts.get(i))); } } return res; } /** * Create a bounding box the size of the given text node. Note that this * temporarily modifies the document to surround this node with a Span. * * @param textNode Text to create bounding box around * @return a new bounding box */ public static HtmlBBox getBoundingBox(Text textNode) { Element el = DOM.createSpan(); surround(textNode, el); HtmlBBox res = new HtmlBBox(el); unSurround(el); return res; } /** * Create a bounding box around the single character at the offset given * within a text node. If the offset is at the end of the text, the * bounding box is a point. Temporarily modifies the document as indicated * in getBoundingBox(textNode, offset1, offset2) * * @param textNode Text node to find character in * @param offset offset of the character * @return a new bounding box */ public static HtmlBBox getBoundingBox(Text textNode, int offset) { return getBoundingBox(textNode, offset, (offset == textNode.getLength()) ? offset : offset + 1); } /** * Create a bounding box the size of the text between the two offsets of * the given textNode. Note that this temporarily modifies the document * to excise the sub-text into its own span element, which is then used * to generate the bounding box. * * @param textNode Text to create bounding box around * @param offset1 Starting offset to get bounding box from * @param offset2 Ending offset to get bounding box from * @return a new bounding box */ public static HtmlBBox getBoundingBox(Text textNode, int offset1, int offset2) { HtmlBBox res; String text = textNode.getData(); int len = text.length(); if ((offset1 == 0) && (offset2 == len)) { // Shortcut return getBoundingBox(textNode); } if ((offset2 > len) || (offset1 > offset2)) { return null; } // If this is a cursor, we still need to outline one character boolean isCursor = (offset1 == offset2); boolean posRight = false; if (isCursor) { if (offset1 == len) { offset1--; posRight = true; } else { offset2++; } } // Create 2 or 3 spans of this text, so we can measure List<Element> nodes = new ArrayList<Element>(3); Element tmpSpan, measureSpan; if (offset1 > 0) { // First tmpSpan = DOM.createSpan(); tmpSpan.setInnerHTML(text.substring(0, offset1)); nodes.add(tmpSpan); } // Middle, the one we measure measureSpan = DOM.createSpan(); measureSpan.setInnerHTML(text.substring(offset1, offset2)); nodes.add(measureSpan); if (offset2 < (len - 1)) { // Last tmpSpan = DOM.createSpan(); tmpSpan.setInnerHTML(text.substring(offset2 + 1)); nodes.add(tmpSpan); } Node parent = textNode.getParentNode(); for (Node node : nodes) { parent.insertBefore(node, textNode); } parent.removeChild(textNode); if (isCursor) { // Just make a 0-width version, depending on left or right res = new HtmlBBox(measureSpan.getAbsoluteLeft() + (posRight ? measureSpan.getOffsetWidth() : 0), measureSpan.getAbsoluteTop(), 0, measureSpan.getOffsetHeight()); } else { res = new HtmlBBox(measureSpan); } parent.insertBefore(textNode, nodes.get(0)); for (Node node : nodes) { parent.removeChild(node); } return res; } /** * Create a bounding box around the single character at the rangeEndPoint * given. If the offset is at the end of the text, the * bounding box is a point. Temporarily modifies the document as indicated * in getBoundingBox(textNode, offset1, offset2) * * @param endPoint End point to find character in * @return a new bounding box */ public static HtmlBBox getBoundingBox(RangeEndPoint endPoint) { return getBoundingBox(endPoint.getTextNode(), endPoint.getOffset()); } /** * Create a bounding box around the text of the rangeEndPoint specified, * either to the end or the beginning of the endPoint's text node. * Temporarily modifies the document as indicated in * getBoundingBox(textNode, offset1, offset2) * * @param endPoint End point to find character in * @param asStart Whether to get text from here to end (true) or from start * to here (false) * @return a new bounding box */ public static HtmlBBox getBoundingBox(RangeEndPoint endPoint, boolean asStart) { return getBoundingBox(endPoint.getTextNode(), asStart ? endPoint.getOffset() : 0, asStart ? endPoint.getTextNode().getLength() : endPoint.getOffset()); } /** * Create a bounding box around a widget. * * @param wid Widget to get bounding box of * @return a new bounding box */ public static HtmlBBox getBoundingBox(Widget wid) { return getBoundingBox(wid.getElement()); } /** * Some random DOM utility functions */ /** * Determine the index of a node within its parent * * @param child A node to determine the index of * @return index of the node, or -1 on failure */ public static int getChildIndex(Node child) { int res = -1; Node parent = child.getParentNode(); if (parent != null) { for (int i = 0; i < parent.getChildCount(); i++) { if (child == parent.getChild(i)) { res = i; break; } } } return res; } /** * Move all children of this element up into its place, and remove the * element. * * @param parent element to replace with its children */ public static void unSurround(Element parent) { Node superParent = parent.getParentNode(); Node child; while ((child = parent.getFirstChild()) != null) { parent.removeChild(child); superParent.insertBefore(child, parent); } superParent.removeChild(parent); } /** * Move a node inside of a parent element, maintaining it within the DOM * * @param toChild Node to make into a child * @param newParent Element to make into a parent in its place */ public static void surround(Node toChild, Element newParent) { toChild.getParentElement().insertBefore(newParent, toChild); newParent.appendChild(toChild); } public int getAbsoluteLeft() { return m_x; } public int getAbsoluteTop() { return m_y; } public int getAbsoluteRight() { return m_x + m_width; } public int getAbsoluteBottom() { return m_y + m_height; } public int getOffsetWidth() { return m_width; } public int getOffsetHeight() { return m_height; } public int getCenterX() { return m_x + m_width / 2; } public int getCenterY() { return m_y + m_height / 2; } @Override public boolean equals(Object o) { try { HtmlBBox cmp = (HtmlBBox) o; return (cmp.getAbsoluteLeft() == m_x) && (cmp.getAbsoluteTop() == m_y) && (cmp.getOffsetHeight() == m_height) && (cmp.getOffsetWidth() == m_width); } catch (Exception ex) { } return false; } }