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