View Javadoc
1   /*
2    * Copyright (c) 2002-2017 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * http://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package com.gargoylesoftware.htmlunit.javascript.host.dom;
16  
17  import static com.gargoylesoftware.htmlunit.javascript.configuration.SupportedBrowser.IE;
18  
19  import org.apache.commons.logging.Log;
20  import org.apache.commons.logging.LogFactory;
21  import org.w3c.dom.ranges.Range;
22  
23  import com.gargoylesoftware.htmlunit.html.HtmlPage;
24  import com.gargoylesoftware.htmlunit.html.impl.SelectableTextInput;
25  import com.gargoylesoftware.htmlunit.html.impl.SimpleRange;
26  import com.gargoylesoftware.htmlunit.javascript.SimpleScriptable;
27  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxClass;
28  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxFunction;
29  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxGetter;
30  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxSetter;
31  import com.gargoylesoftware.htmlunit.javascript.host.Element;
32  import com.gargoylesoftware.htmlunit.javascript.host.Window;
33  import com.gargoylesoftware.htmlunit.javascript.host.html.HTMLElement;
34  
35  import net.sourceforge.htmlunit.corejs.javascript.Context;
36  import net.sourceforge.htmlunit.corejs.javascript.Undefined;
37  
38  /**
39   * A JavaScript object for {@code TextRange} (IE only).
40   *
41   * @see <a href="http://msdn2.microsoft.com/en-us/library/ms535872.aspx">MSDN documentation (1)</a>
42   * @see <a href="http://msdn2.microsoft.com/en-us/library/ms533042.aspx">MSDN documentation (2)</a>
43   * @author Ahmed Ashour
44   * @author Marc Guillemot
45   * @author David Gileadi
46   */
47  @JsxClass(IE)
48  public class TextRange extends SimpleScriptable {
49  
50      private static final Log LOG = LogFactory.getLog(TextRange.class);
51  
52      /** The wrapped selection range. */
53      private Range range_;
54  
55      /**
56       * Default constructor used to build the prototype.
57       */
58      public TextRange() {
59          // Empty.
60      }
61  
62      /**
63       * Constructs a text range around the provided element.
64       * @param elt the element to wrap
65       */
66      public TextRange(final Element elt) {
67          range_ = new SimpleRange(elt.getDomNodeOrDie());
68      }
69  
70      /**
71       * Constructs a text range around the provided range.
72       * @param range the initial range
73       */
74      public TextRange(final Range range) {
75          range_ = range.cloneRange();
76      }
77  
78      /**
79       * Retrieves the text contained within the range.
80       * @return the text contained within the range
81       */
82      @JsxGetter
83      public String getText() {
84          return range_.toString();
85      }
86  
87      /**
88       * Sets the text contained within the range.
89       * @param text the text contained within the range
90       */
91      @JsxSetter
92      public void setText(final String text) {
93          if (range_.getStartContainer() == range_.getEndContainer()
94                  && range_.getStartContainer() instanceof SelectableTextInput) {
95              final SelectableTextInput input = (SelectableTextInput) range_.getStartContainer();
96              final String oldValue = input.getText();
97              input.setText(oldValue.substring(0, input.getSelectionStart()) + text
98                      + oldValue.substring(input.getSelectionEnd()));
99          }
100     }
101 
102     /**
103      * Retrieves the HTML fragment contained within the range.
104      * @return the HTML fragment contained within the range
105      */
106     @JsxGetter
107     public String getHtmlText() {
108         final org.w3c.dom.Node node = range_.getCommonAncestorContainer();
109         if (null == node) {
110             return "";
111         }
112         final HTMLElement element = (HTMLElement) getScriptableFor(node);
113         return element.getOuterHTML(); // TODO: not quite right, but good enough for now
114     }
115 
116     /**
117      * Duplicates this TextRange instance.
118      * @see <a href="http://msdn.microsoft.com/en-us/library/ms536416.aspx">MSDN documentation</a>
119      * @return a duplicate of this TextRange instance
120      */
121     @JsxFunction
122     public Object duplicate() {
123         final TextRange range = new TextRange(range_.cloneRange());
124         range.setParentScope(getParentScope());
125         range.setPrototype(getPrototype());
126         return range;
127     }
128 
129     /**
130      * Retrieves the parent element for the given text range.
131      * The parent element is the element that completely encloses the text in the range.
132      * If the text range spans text in more than one element, this method returns the smallest element that encloses
133      * all the elements. When you insert text into a range that spans multiple elements, the text is placed in the
134      * parent element rather than in any of the contained elements.
135      *
136      * @see <a href="http://msdn.microsoft.com/en-us/library/ms536654.aspx">MSDN doc</a>
137      * @return the parent element object if successful, or null otherwise.
138      */
139     @JsxFunction
140     public Node parentElement() {
141         final org.w3c.dom.Node parent = range_.getCommonAncestorContainer();
142         if (null == parent) {
143             if (null == range_.getStartContainer() || null == range_.getEndContainer()) {
144                 try {
145                     final Window window = (Window) getParentScope();
146                     final HtmlPage page = (HtmlPage) window.getDomNodeOrDie();
147                     return (Node) getScriptableFor(page.getBody());
148                 }
149                 catch (final Exception e) {
150                     // ok bad luck
151                 }
152             }
153             return null;
154         }
155         return (Node) getScriptableFor(parent);
156     }
157 
158     /**
159      * Collapses the range.
160      * @param toStart indicates if collapse should be done to the start
161      * @see <a href="http://msdn.microsoft.com/en-us/library/ms536371.aspx">MSDN doc</a>
162      */
163     @JsxFunction
164     public void collapse(final boolean toStart) {
165         range_.collapse(toStart);
166     }
167 
168     /**
169      * Makes the current range the active selection.
170      *
171      * @see <a href="http://msdn.microsoft.com/en-us/library/ms536735.aspx">MSDN doc</a>
172      */
173     @JsxFunction
174     public void select() {
175         final HtmlPage page = (HtmlPage) getWindow().getDomNodeOrDie();
176         page.setSelectionRange(range_);
177     }
178 
179     /**
180      * Changes the start position of the range.
181      * @param unit specifies the units to move
182      * @param count the number of units to move
183      * @return the number of units moved
184      */
185     @JsxFunction
186     public int moveStart(final String unit, final Object count) {
187         if (!"character".equals(unit)) {
188             LOG.warn("moveStart('" + unit + "') is not yet supported");
189             return 0;
190         }
191         int c = 1;
192         if (count != Undefined.instance) {
193             c = (int) Context.toNumber(count);
194         }
195         if (range_.getStartContainer() == range_.getEndContainer()
196                 && range_.getStartContainer() instanceof SelectableTextInput) {
197             final SelectableTextInput input = (SelectableTextInput) range_.getStartContainer();
198             c = constrainMoveBy(c, range_.getStartOffset(), input.getText().length());
199             range_.setStart(input, range_.getStartOffset() + c);
200         }
201         return c;
202     }
203 
204     /**
205      * Collapses the given text range and moves the empty range by the given number of units.
206      * @param unit specifies the units to move
207      * @param count the number of units to move
208      * @return the number of units moved
209      */
210     @JsxFunction
211     public int move(final String unit, final Object count) {
212         collapse(true);
213         return moveStart(unit, count);
214     }
215 
216     /**
217      * Changes the end position of the range.
218      * @param unit specifies the units to move
219      * @param count the number of units to move
220      * @return the number of units moved
221      */
222     @JsxFunction
223     public int moveEnd(final String unit, final Object count) {
224         if (!"character".equals(unit)) {
225             LOG.warn("moveEnd('" + unit + "') is not yet supported");
226             return 0;
227         }
228         int c = 1;
229         if (count != Undefined.instance) {
230             c = (int) Context.toNumber(count);
231         }
232         if (range_.getStartContainer() == range_.getEndContainer()
233                 && range_.getStartContainer() instanceof SelectableTextInput) {
234             final SelectableTextInput input = (SelectableTextInput) range_.getStartContainer();
235             c = constrainMoveBy(c, range_.getEndOffset(), input.getText().length());
236             range_.setEnd(input, range_.getEndOffset() + c);
237         }
238         return c;
239     }
240 
241     /**
242      * Moves the text range so that the start and end positions of the range encompass
243      * the text in the specified element.
244      * @param element the element to move to
245      * @see <a href="http://msdn.microsoft.com/en-us/library/ms536630.aspx">MSDN Documentation</a>
246      */
247     @JsxFunction
248     public void moveToElementText(final HTMLElement element) {
249         range_.selectNode(element.getDomNodeOrDie());
250     }
251 
252     /**
253      * Indicates if a range is contained in current one.
254      * @param other the other range
255      * @return {@code true} if <code>other</code> is contained within current range
256      * @see <a href="http://msdn.microsoft.com/en-us/library/ms536371.aspx">MSDN doc</a>
257      */
258     @JsxFunction
259     public boolean inRange(final TextRange other) {
260         final Range otherRange = other.range_;
261 
262         final org.w3c.dom.Node start = range_.getStartContainer();
263         final org.w3c.dom.Node otherStart = otherRange.getStartContainer();
264         if (otherStart == null) {
265             return false;
266         }
267         final short startComparison = start.compareDocumentPosition(otherStart);
268         final boolean startNodeBefore = startComparison == 0
269                 || (startComparison & Node.DOCUMENT_POSITION_CONTAINS) != 0
270                 || (startComparison & Node.DOCUMENT_POSITION_PRECEDING) != 0;
271         if (startNodeBefore && (start != otherStart || range_.getStartOffset() <= otherRange.getStartOffset())) {
272             final org.w3c.dom.Node end = range_.getEndContainer();
273             final org.w3c.dom.Node otherEnd = otherRange.getEndContainer();
274             final short endComparison = end.compareDocumentPosition(otherEnd);
275             final boolean endNodeAfter = endComparison == 0
276                     || (endComparison & Node.DOCUMENT_POSITION_CONTAINS) != 0
277                     || (endComparison & Node.DOCUMENT_POSITION_FOLLOWING) != 0;
278             if (endNodeAfter && (end != otherEnd || range_.getEndOffset() >= otherRange.getEndOffset())) {
279                 return true;
280             }
281         }
282 
283         return false;
284     }
285 
286     /**
287      * Sets the endpoint of the range based on the endpoint of another range..
288      * @param type end point transfer type. One of "StartToEnd", "StartToStart", "EndToStart" and "EndToEnd"
289      * @param other the other range
290      * @see <a href="http://msdn.microsoft.com/en-us/library/ms536745.aspx">MSDN doc</a>
291      */
292     @JsxFunction
293     public void setEndPoint(final String type, final TextRange other) {
294         final Range otherRange = other.range_;
295 
296         final org.w3c.dom.Node target;
297         final int offset;
298         if (type.endsWith("ToStart")) {
299             target = otherRange.getStartContainer();
300             offset = otherRange.getStartOffset();
301         }
302         else {
303             target = otherRange.getEndContainer();
304             offset = otherRange.getEndOffset();
305         }
306 
307         if (type.startsWith("Start")) {
308             range_.setStart(target, offset);
309         }
310         else {
311             range_.setEnd(target, offset);
312         }
313     }
314 
315     /**
316      * Constrain the given amount to move the range by to the limits of the given current offset and text length.
317      * @param moveBy the amount to move by
318      * @param current the current index
319      * @param textLength the text length
320      * @return the moveBy amount constrained to the text length
321      */
322     protected int constrainMoveBy(int moveBy, final int current, final int textLength) {
323         final int to = current + moveBy;
324         if (to < 0) {
325             moveBy -= to;
326         }
327         else if (to >= textLength) {
328             moveBy -= to - textLength;
329         }
330         return moveBy;
331     }
332 
333     /**
334      * Retrieves a bookmark (opaque string) that can be used with {@link #moveToBookmark}
335      * to return to the same range.
336      * The current implementation return empty string
337      * @return the bookmark
338      */
339     @JsxFunction
340     public String getBookmark() {
341         return "";
342     }
343 
344     /**
345      * Moves to a bookmark.
346      * The current implementation does nothing
347      * @param bookmark the bookmark
348      * @return {@code false}
349      */
350     @JsxFunction
351     public boolean moveToBookmark(final String bookmark) {
352         return false;
353     }
354 
355     /**
356      * Compares an end point of a TextRange object with an end point of another range.
357      * @param how how to compare
358      * @param sourceRange the other range
359      * @return the result
360      */
361     @JsxFunction
362     public int compareEndPoints(final String how, final TextRange sourceRange) {
363         final org.w3c.dom.Node start;
364         final org.w3c.dom.Node otherStart;
365         switch (how) {
366             case "StartToEnd":
367                 start = range_.getStartContainer();
368                 otherStart = sourceRange.range_.getEndContainer();
369                 break;
370 
371             case "StartToStart":
372                 start = range_.getStartContainer();
373                 otherStart = sourceRange.range_.getStartContainer();
374                 break;
375 
376             case "EndToStart":
377                 start = range_.getEndContainer();
378                 otherStart = sourceRange.range_.getStartContainer();
379                 break;
380 
381             default:
382                 start = range_.getEndContainer();
383                 otherStart = sourceRange.range_.getEndContainer();
384                 break;
385         }
386         if (start == null || otherStart == null) {
387             return 0;
388         }
389         return start.compareDocumentPosition(otherStart);
390     }
391 }