/* * 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.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; /** * IE implementation of range, which emulates the methods of the W3C standard. * IE's range object doesn't have any methods for directly getting/setting * the end points to structural elements of the DOM, so we have to incrementally * search/modify ranges to intiut this. * * @author John Kozura */ public class RangeImplIE6 extends RangeImpl { private static final String[] BOUNDARY_STRINGS = { "StartToStart", "StartToEnd", "EndToEnd", "EndToStart" }; // Used for deleting/replacing values of a range private static final String REPLACING_STRING = "DeL3EteTh1s"; private static Document m_lastDocument = null; private static Element m_testElement; @Override public native JSRange cloneRange(JSRange range) /*-{ return range.duplicate(); }-*/; @Override public int compareBoundaryPoint(JSRange range, JSRange compare, short how) { return compareBoundaryPoint(range, compare, BOUNDARY_STRINGS[how]); } public native int compareBoundaryPoint(JSRange range, JSRange compare, String how) /*-{ return range.compareEndPoints(how, compare); }-*/; /** * For IE, do this by copying the HTML string * * @see com.bfr.client.selection.impl.RangeImpl#copyContents(com.bfr.client.selection.impl.RangeImpl.JSRange, com.google.gwt.dom.client.Element) */ @Override public void copyContents(JSRange range, Element copyInto) { copyInto.setInnerHTML(getHtmlText(range)); } @Override public native RangeImpl.JSRange createFromDocument(Document doc) /*-{ return doc.body.createTextRange(); }-*/; @Override public JSRange createRange(Document doc, Text startPoint, int startOffset, Text endPoint, int endOffset) { /* * IE code to make this work - create a range on the start and the end * point, then move the end point to include the second */ JSRange res = createRangeOnText(startPoint, startOffset); if (startPoint == endPoint) { // Shortcut if the start and end texts are the same moveEndCharacter(res, endOffset - startOffset); } else { JSRange endRange = createRangeOnText(endPoint, endOffset); moveRangePoint(res, endRange, BOUNDARY_STRINGS[Range.END_TO_END]); } return res; } /** * IE has no function for doing this with a range vs with a selection, so * instead use pasteHTML, then remove the resulting element. * * @see com.bfr.client.selection.impl.RangeImpl#deleteContents(com.bfr.client.selection.impl.RangeImpl.JSRange) */ @Override public void deleteContents(JSRange range) { Text txt = placeholdRange(range); if (txt != null) { txt.removeFromParent(); } } @Override public void extractContents(JSRange range, Element copyInto) { copyContents(range, copyInto); deleteContents(range); } @Override public void fillRangePoints(Range range) { JSRange selRange = range._getJSRange(); if (selRange == null) { return; } RangeEndPoint start = getRangeEndPoint(range, selRange, true); RangeEndPoint end = getRangeEndPoint(range, selRange, false); canonicalize(start, end); range._setRange(start, end); } /** * Place to put any final checks and corrections to result in a consistent * cursor. Either of the range points passed may be modified by this * function. * * @param start Start range point to check * @param end End range point to check */ private void canonicalize(RangeEndPoint start, RangeEndPoint end) { if (start != null) { // This checks if the cursor is at the end of one text range, and // the beginning of the next, as IE will do this for adjacent nodes.. if ((start.getTextNode() != null) && (start.getTextNode().getNextSibling() == end.getTextNode()) && (start.getTextNode().getLength() == start.getOffset()) && (end.getOffset() == 0)) { end.setTextNode(start.getTextNode(), false); } } } @Override public native Element getCommonAncestor(JSRange range) /*-{ return range.parentElement(); }-*/; @Override public native String getHtmlText(JSRange range) /*-{ return range.htmlText; }-*/; @Override public native String getText(JSRange range) /*-{ return range.text; }-*/; /** * For IE, do this by copying the contents, then creating a dummy element * and replacing it with this element. * * @see com.bfr.client.selection.impl.RangeImpl#surroundContents(com.bfr.client.selection.impl.RangeImpl.JSRange, com.google.gwt.dom.client.Element) */ @Override public void surroundContents(JSRange range, Element copyInto) { copyContents(range, copyInto); Text txt = placeholdRange(range); if (txt != null) { txt.getParentElement().replaceChild(copyInto, txt); } } private native void collapseRange(JSRange range, boolean start) /*-{ range.collapse(start); }-*/; /** * Create a 0-width js range on the first text element of this parent. * * @param parent * @return */ private native JSRange createRangeOnFirst(Element parent) /*-{ var res = parent.ownerDocument.body.createTextRange(); res.moveToElementText(parent); res.collapse(true); return res; }-*/; /** * Create a new range with a range that has its start and end point within * the given text and at the given offset. This emulates capabilities of * the W3C standard.. * * @param setText * @param offset * @return */ private JSRange createRangeOnText(Text setText, int offset) { Element parent = setText.getParentElement(); JSRange res = createRangeOnFirst(parent); Element testElement = getTestElement(parent.getOwnerDocument()); // Can't directly select the text, but we can select a fake element // before it, then move the selection... try { parent.insertBefore(testElement, setText); moveToElementText(res, testElement); moveCharacter(res, offset); } finally { // Ensure the test element gets removed from the document testElement.removeFromParent(); } return res; } /** * Get the IE start or end point of the given range, have to search for it * to find it properly. * * @param range used to get the document * @param selRange the selection we are getting the point of * @param start whether to get the start or end point * @return RangeEndPoint representing this, or null on error */ private RangeEndPoint getRangeEndPoint(Range range, JSRange selRange, boolean start) { RangeEndPoint res = null; // Create a cursor at either the beginning or end of the range, to // get that point's immediate parent JSRange checkRange = cloneRange(selRange); collapseRange(checkRange, start); Element parent = getCommonAncestor(checkRange); String compareFcn = BOUNDARY_STRINGS[start ? Range.START_TO_START : Range.END_TO_END]; Node compNode; // Test element we move around the document to check relative selection Element testElement = getTestElement(range.getDocument()); try { // Move the test element backwards past nodes until we are in front // of the desired range endpoint for (compNode = parent.getLastChild(); compNode != null; compNode = testElement.getPreviousSibling()) { parent.insertBefore(testElement, compNode); moveToElementText(checkRange, testElement); if (compareBoundaryPoint(checkRange, selRange, compareFcn) <= 0) { break; } } if (compNode == null) { // Sometimes selection at beginning of a span causes a fail compNode = testElement.getNextSibling(); } if (compNode == null) { } else if (compNode.getNodeType() == Node.ELEMENT_NODE) { // We only represent text elements right now, so if this is not // then go find one. Check if the desired selection is at the // beginning or end of this element, first select the entire // element to determine whether the endpoint is at the // beginning or the end of it, ie whether to look forward or // backward. testElement.removeFromParent(); moveToElementText(checkRange, (Element) compNode); int cmp = compareBoundaryPoint(checkRange, selRange, compareFcn); boolean dir = (cmp == 0) ? !start : (cmp < 0); Text closest = Range.getAdjacentTextElement(compNode, parent, dir, true); if (closest == null) { dir = !dir; closest = Range.getAdjacentTextElement(compNode, parent, dir, true); } if (closest != null) { // Found a text node in one direction or the other res = new RangeEndPoint(closest, dir ? 0 : closest.getLength()); } } else { // Get the proper offset, move the end of the check range to the // boundary of the actual range and get its length moveRangePoint(checkRange, selRange, BOUNDARY_STRINGS[start ? Range.END_TO_START : Range.END_TO_END]); res = new RangeEndPoint((Text) compNode, getText(checkRange).length()); } } catch (Exception ex) { GWT.log("Failed to find IE selection", ex); } finally { // Make sure this gets removed from the document no matter what testElement.removeFromParent(); } return (res == null) ? new RangeEndPoint() : res; } private Element getTestElement(Document document) { // Create an element to search for the cursor with, cache it so we // don't create a ton of these unnecessarily if (document != m_lastDocument) { m_lastDocument = document; m_testElement = m_lastDocument.createDivElement(); } return m_testElement; } /** * Move both the start and end point of this range */ private native int moveCharacter(JSRange range, int chars) /*-{ return range.move("character", chars); }-*/; /** * Move just the end point of this range */ private native int moveEndCharacter(JSRange range, int chars) /*-{ return range.moveEnd("character", chars); }-*/; private native void moveRangePoint(JSRange range, JSRange moveTo, String how) /*-{ range.setEndPoint(how, moveTo); }-*/; private native void moveToElementText(JSRange range, Element element) /*-{ range.moveToElementText(element); }-*/; private native void placeholdPaste(JSRange range, String str) /*-{ range.pasteHTML(str); }-*/; /** * Since there's no good delete for an arbitrary range, simply replace it * with this text that nobody would use, then go find it so we can * delete or replace it in other functions. This depends on IE creating a * single text element that includes exactly this string (and no user also * has this exact text on their page..) * <p/> * An alternative but far more complicated method would be to try to do * this via setting the selection, doing the delete/replace, and then * restoring the selection. * * @param range The range to replace with a text node * @return the text node that replaced the contents of range */ private Text placeholdRange(JSRange range) { // Paranoid, include a random number to reduce chance this string // would occur in the text.. String replaceString = REPLACING_STRING + (int) (Integer.MAX_VALUE * Math.random()); Element parent = getCommonAncestor(range); placeholdPaste(range, replaceString); Text res; for (res = Range.getAdjacentTextElement(parent, true); res != null; res = Range.getAdjacentTextElement(res, true)) { if (replaceString.equals(res.getData())) { break; } } return res; } }