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.CHROME;
18  import static com.gargoylesoftware.htmlunit.javascript.configuration.SupportedBrowser.EDGE;
19  import static com.gargoylesoftware.htmlunit.javascript.configuration.SupportedBrowser.FF;
20  
21  import java.util.ArrayList;
22  import java.util.List;
23  
24  import org.w3c.dom.ranges.Range;
25  
26  import com.gargoylesoftware.htmlunit.html.HtmlPage;
27  import com.gargoylesoftware.htmlunit.html.impl.SimpleRange;
28  import com.gargoylesoftware.htmlunit.javascript.SimpleScriptable;
29  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxClass;
30  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxConstructor;
31  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxFunction;
32  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxGetter;
33  
34  import net.sourceforge.htmlunit.corejs.javascript.Context;
35  
36  /**
37   * A JavaScript object for {@code Selection}.
38   *
39   * @see <a href="http://msdn2.microsoft.com/en-us/library/ms535869.aspx">MSDN Documentation</a>
40   * @see <a href="https://developer.mozilla.org/en/DOM/Selection">Gecko DOM Reference</a>
41   * @author Ahmed Ashour
42   * @author Daniel Gredler
43   * @author Frank Danek
44   */
45  @JsxClass
46  public class Selection extends SimpleScriptable {
47      private static final String TYPE_NONE = "None";
48      private static final String TYPE_CARET = "Caret";
49      private static final String TYPE_RANGE = "Range";
50  
51      private String type_ = TYPE_NONE;
52  
53      /**
54       * Creates an instance.
55       */
56      @JsxConstructor({CHROME, FF, EDGE})
57      public Selection() {
58      }
59  
60      /**
61       * {@inheritDoc}
62       */
63      @Override
64      public Object getDefaultValue(final Class<?> hint) {
65          if (getPrototype() != null && (String.class.equals(hint) || hint == null)) {
66              final StringBuilder sb = new StringBuilder();
67              for (final Range r : getRanges()) {
68                  sb.append(r.toString());
69              }
70              return sb.toString();
71          }
72          return super.getDefaultValue(hint);
73      }
74  
75      /**
76       * Returns the node in which the selection begins.
77       * @return the node in which the selection begins
78       */
79      @JsxGetter
80      public Node getAnchorNode() {
81          final Range last = getLastRange();
82          if (last == null) {
83              return null;
84          }
85          return (Node) getScriptableNullSafe(last.getStartContainer());
86      }
87  
88      /**
89       * Returns the number of characters that the selection's anchor is offset within the anchor node.
90       * @return the number of characters that the selection's anchor is offset within the anchor node
91       */
92      @JsxGetter
93      public int getAnchorOffset() {
94          final Range last = getLastRange();
95          if (last == null) {
96              return 0;
97          }
98          return last.getStartOffset();
99      }
100 
101     /**
102      * Returns the node in which the selection ends.
103      * @return the node in which the selection ends
104      */
105     @JsxGetter
106     public Node getFocusNode() {
107         final Range last = getLastRange();
108         if (last == null) {
109             return null;
110         }
111         return (Node) getScriptableNullSafe(last.getEndContainer());
112     }
113 
114     /**
115      * Returns the number of characters that the selection's focus is offset within the focus node.
116      * @return the number of characters that the selection's focus is offset within the focus node
117      */
118     @JsxGetter
119     public int getFocusOffset() {
120         final Range last = getLastRange();
121         if (last == null) {
122             return 0;
123         }
124         return last.getEndOffset();
125     }
126 
127     /**
128      * Returns a boolean indicating whether the selection's start and end points are at the same position.
129      * @return a boolean indicating whether the selection's start and end points are at the same position
130      */
131     @JsxGetter
132     public boolean isIsCollapsed() {
133         final List<Range> ranges = getRanges();
134         return ranges.isEmpty() || (ranges.size() == 1 && ranges.get(0).getCollapsed());
135     }
136 
137     /**
138      * Returns the number of ranges in the selection.
139      * @return the number of ranges in the selection
140      */
141     @JsxGetter
142     public int getRangeCount() {
143         return getRanges().size();
144     }
145 
146     /**
147      * Returns the type of selection (IE only).
148      * @return the type of selection
149      */
150     @JsxGetter(CHROME)
151     public String getType() {
152         return type_;
153     }
154 
155     /**
156      * Adds a range to the selection.
157      * @param range the range to add
158      */
159     @JsxFunction
160     public void addRange(final com.gargoylesoftware.htmlunit.javascript.host.dom.Range range) {
161         final SimpleRange rg = range.toW3C();
162         getRanges().add(rg);
163 
164         if (TYPE_CARET.equals(type_) && rg.getCollapsed()) {
165             return;
166         }
167         type_ = TYPE_RANGE;
168     }
169 
170     /**
171      * Removes a range from the selection.
172      * @param range the range to remove
173      */
174     @JsxFunction
175     public void removeRange(final com.gargoylesoftware.htmlunit.javascript.host.dom.Range range) {
176         getRanges().remove(range.toW3C());
177 
178         if (getRangeCount() < 1) {
179             type_ = TYPE_NONE;
180         }
181     }
182 
183     /**
184      * Removes all ranges from the selection.
185      */
186     @JsxFunction
187     public void removeAllRanges() {
188         getRanges().clear();
189 
190         type_ = TYPE_NONE;
191     }
192 
193     /**
194      * Returns the range at the specified index.
195      *
196      * @param index the index of the range to return
197      * @return the range at the specified index
198      */
199     @JsxFunction
200     public com.gargoylesoftware.htmlunit.javascript.host.dom.Range getRangeAt(final int index) {
201         final List<Range> ranges = getRanges();
202         if (index < 0 || index >= ranges.size()) {
203             throw Context.reportRuntimeError("Invalid range index: " + index);
204         }
205         final Range range = ranges.get(index);
206         final com.gargoylesoftware.htmlunit.javascript.host.dom.Range jsRange =
207             new com.gargoylesoftware.htmlunit.javascript.host.dom.Range(range);
208         jsRange.setParentScope(getWindow());
209         jsRange.setPrototype(getPrototype(com.gargoylesoftware.htmlunit.javascript.host.dom.Range.class));
210 
211         return jsRange;
212     }
213 
214     /**
215      * Collapses the current selection to a single point. The document is not modified.
216      * @param parentNode the caret location will be within this node
217      * @param offset the caret will be placed this number of characters from the beginning of the parentNode's text
218      */
219     @JsxFunction
220     public void collapse(final Node parentNode, final int offset) {
221         final List<Range> ranges = getRanges();
222         ranges.clear();
223         ranges.add(new SimpleRange(parentNode.getDomNodeOrDie(), offset));
224 
225         type_ = TYPE_CARET;
226     }
227 
228     /**
229      * Moves the anchor of the selection to the same point as the focus. The focus does not move.
230      */
231     @JsxFunction
232     public void collapseToEnd() {
233         final Range last = getLastRange();
234         if (last != null) {
235             final List<Range> ranges = getRanges();
236             ranges.clear();
237             ranges.add(last);
238             last.collapse(false);
239         }
240 
241         type_ = TYPE_CARET;
242     }
243 
244     /**
245      * Moves the focus of the selection to the same point at the anchor. The anchor does not move.
246      */
247     @JsxFunction
248     public void collapseToStart() {
249         final Range first = getFirstRange();
250         if (first != null) {
251             final List<Range> ranges = getRanges();
252             ranges.clear();
253             ranges.add(first);
254             first.collapse(true);
255         }
256 
257         type_ = TYPE_CARET;
258     }
259 
260     /**
261      * Cancels the current selection, sets the selection type to none.
262      */
263     @JsxFunction(CHROME)
264     public void empty() {
265         removeAllRanges();
266     }
267 
268     /**
269      * Moves the focus of the selection to a specified point. The anchor of the selection does not move.
270      * @param parentNode the node within which the focus will be moved
271      * @param offset the number of characters from the beginning of parentNode's text the focus will be placed
272      */
273     @JsxFunction({CHROME, FF})
274     public void extend(final Node parentNode, final int offset) {
275         final Range last = getLastRange();
276         if (last != null) {
277             last.setEnd(parentNode.getDomNodeOrDie(), offset);
278 
279             type_ = TYPE_RANGE;
280         }
281     }
282 
283     /**
284      * Adds all the children of the specified node to the selection. The previous selection is lost.
285      * @param parentNode all children of parentNode will be selected; parentNode itself is not part of the selection
286      */
287     @JsxFunction
288     public void selectAllChildren(final Node parentNode) {
289         final List<Range> ranges = getRanges();
290         ranges.clear();
291         final SimpleRange rg = new SimpleRange(parentNode.getDomNodeOrDie());
292         ranges.add(rg);
293 
294         if (rg.getCollapsed()) {
295             type_ = TYPE_CARET;
296         }
297         else {
298             type_ = TYPE_RANGE;
299         }
300     }
301 
302     /**
303      * Returns the current HtmlUnit DOM selection ranges.
304      * @return the current HtmlUnit DOM selection ranges
305      */
306     private List<Range> getRanges() {
307         final HtmlPage page = (HtmlPage) getWindow().getDomNodeOrDie();
308         return page.getSelectionRanges();
309     }
310 
311     /**
312      * Returns the first selection range in the current document, by document position.
313      * @return the first selection range in the current document, by document position
314      */
315     private Range getFirstRange() {
316         // avoid concurrent modification exception
317         final List<Range> ranges = new ArrayList<>(getRanges());
318 
319         Range first = null;
320         for (final Range range : ranges) {
321             if (first == null) {
322                 first = range;
323             }
324             else {
325                 final org.w3c.dom.Node firstStart = first.getStartContainer();
326                 final org.w3c.dom.Node rangeStart = range.getStartContainer();
327                 if ((firstStart.compareDocumentPosition(rangeStart) & Node.DOCUMENT_POSITION_PRECEDING) != 0) {
328                     first = range;
329                 }
330             }
331         }
332         return first;
333     }
334 
335     /**
336      * Returns the last selection range in the current document, by document position.
337      * @return the last selection range in the current document, by document position
338      */
339     private Range getLastRange() {
340         // avoid concurrent modification exception
341         final List<Range> ranges = new ArrayList<>(getRanges());
342 
343         Range last = null;
344         for (final Range range : ranges) {
345             if (last == null) {
346                 last = range;
347             }
348             else {
349                 final org.w3c.dom.Node lastStart = last.getStartContainer();
350                 final org.w3c.dom.Node rangeStart = range.getStartContainer();
351                 if ((lastStart.compareDocumentPosition(rangeStart) & Node.DOCUMENT_POSITION_FOLLOWING) != 0) {
352                     last = range;
353                 }
354             }
355         }
356         return last;
357     }
358 
359     /**
360      * Returns the scriptable object corresponding to the specified HtmlUnit DOM object.
361      * @param object the HtmlUnit DOM object whose scriptable object is to be returned (may be {@code null})
362      * @return the scriptable object corresponding to the specified HtmlUnit DOM object, or {@code null} if
363      *         <tt>object</tt> was {@code null}
364      */
365     private SimpleScriptable getScriptableNullSafe(final Object object) {
366         final SimpleScriptable scriptable;
367         if (object != null) {
368             scriptable = getScriptableFor(object);
369         }
370         else {
371             scriptable = null;
372         }
373         return scriptable;
374     }
375 }