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.HashSet;
18  
19  import org.apache.commons.logging.LogFactory;
20  import org.htmlunit.SgmlPage;
21  import org.htmlunit.html.DomDocumentFragment;
22  import org.htmlunit.html.DomNode;
23  import org.htmlunit.html.impl.SimpleRange;
24  import org.htmlunit.javascript.HtmlUnitScriptable;
25  import org.htmlunit.javascript.JavaScriptEngine;
26  import org.htmlunit.javascript.configuration.JsxClass;
27  import org.htmlunit.javascript.configuration.JsxConstant;
28  import org.htmlunit.javascript.configuration.JsxConstructor;
29  import org.htmlunit.javascript.configuration.JsxFunction;
30  import org.htmlunit.javascript.configuration.JsxGetter;
31  import org.htmlunit.javascript.host.ClientRect;
32  import org.htmlunit.javascript.host.ClientRectList;
33  import org.htmlunit.javascript.host.Window;
34  import org.htmlunit.javascript.host.html.HTMLElement;
35  
36  /**
37   * The JavaScript object that represents a Range.
38   *
39   * @see <a href="http://www.xulplanet.com/references/objref/Range.html">XULPlanet</a>
40   * @see <a href="http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html">DOM-Level-2-Traversal-Range</a>
41   * @author Marc Guillemot
42   * @author Ahmed Ashour
43   * @author Daniel Gredler
44   * @author James Phillpotts
45   * @author Ronald Brill
46   */
47  @JsxClass
48  public class Range extends AbstractRange {
49  
50      /** Comparison mode for compareBoundaryPoints. */
51      @JsxConstant
52      public static final int START_TO_START = 0;
53  
54      /** Comparison mode for compareBoundaryPoints. */
55      @JsxConstant
56      public static final int START_TO_END = 1;
57  
58      /** Comparison mode for compareBoundaryPoints. */
59      @JsxConstant
60      public static final int END_TO_END = 2;
61  
62      /** Comparison mode for compareBoundaryPoints. */
63      @JsxConstant
64      public static final int END_TO_START = 3;
65  
66      /**
67       * Creates an instance.
68       */
69      public Range() {
70          super();
71      }
72  
73      /**
74       * JavaScript constructor.
75       */
76      @Override
77      @JsxConstructor
78      public void jsConstructor() {
79          super.jsConstructor();
80      }
81  
82      /**
83       * Creates a new instance.
84       * @param document the HTML document creating the range
85       */
86      public Range(final Document document) {
87          super(document, document, 0, 0);
88      }
89  
90      Range(final SimpleRange simpleRange) {
91          super(simpleRange.getStartContainer().getScriptableObject(),
92                  simpleRange.getEndContainer().getScriptableObject(),
93                  simpleRange.getStartOffset(),
94                  simpleRange.getEndOffset());
95      }
96  
97      /**
98       * Sets the attributes describing the start of a Range.
99       * @param refNode the reference node
100      * @param offset the offset value within the node
101      */
102     @JsxFunction
103     public void setStart(final Node refNode, final int offset) {
104         if (refNode == null) {
105             throw JavaScriptEngine.reportRuntimeError("It is illegal to call Range.setStart() with a null node.");
106         }
107         internSetStartContainer(refNode);
108         internSetStartOffset(offset);
109     }
110 
111     /**
112      * Sets the start of the range to be after the node.
113      * @param refNode the reference node
114      */
115     @JsxFunction
116     public void setStartAfter(final Node refNode) {
117         if (refNode == null) {
118             throw JavaScriptEngine.reportRuntimeError("It is illegal to call Range.setStartAfter() with a null node.");
119         }
120         internSetStartContainer(refNode.getParent());
121         internSetStartOffset(getPositionInContainer(refNode) + 1);
122     }
123 
124     /**
125      * Sets the start of the range to be before the node.
126      * @param refNode the reference node
127      */
128     @JsxFunction
129     public void setStartBefore(final Node refNode) {
130         if (refNode == null) {
131             throw JavaScriptEngine.reportRuntimeError("It is illegal to call Range.setStartBefore() with a null node.");
132         }
133         internSetStartContainer(refNode.getParent());
134         internSetStartOffset(getPositionInContainer(refNode));
135     }
136 
137     private static int getPositionInContainer(final Node refNode) {
138         int i = 0;
139         Node node = refNode;
140         while (node.getPreviousSibling() != null) {
141             node = node.getPreviousSibling();
142             i++;
143         }
144         return i;
145     }
146 
147     /**
148      * Sets the attributes describing the end of a Range.
149      * @param refNode the reference node
150      * @param offset the offset value within the node
151      */
152     @JsxFunction
153     public void setEnd(final Node refNode, final int offset) {
154         if (refNode == null) {
155             throw JavaScriptEngine.reportRuntimeError("It is illegal to call Range.setEnd() with a null node.");
156         }
157         internSetEndContainer(refNode);
158         internSetEndOffset(offset);
159     }
160 
161     /**
162      * Sets the end of the range to be after the node.
163      * @param refNode the reference node
164      */
165     @JsxFunction
166     public void setEndAfter(final Node refNode) {
167         if (refNode == null) {
168             throw JavaScriptEngine.reportRuntimeError("It is illegal to call Range.setEndAfter() with a null node.");
169         }
170         internSetEndContainer(refNode.getParent());
171         internSetEndOffset(getPositionInContainer(refNode) + 1);
172     }
173 
174     /**
175      * Sets the end of the range to be before the node.
176      * @param refNode the reference node
177      */
178     @JsxFunction
179     public void setEndBefore(final Node refNode) {
180         if (refNode == null) {
181             throw JavaScriptEngine.reportRuntimeError("It is illegal to call Range.setEndBefore() with a null node.");
182         }
183         internSetStartContainer(refNode.getParent());
184         internSetStartOffset(getPositionInContainer(refNode));
185     }
186 
187     /**
188      * Select the contents within a node.
189      * @param refNode Node to select from
190      */
191     @JsxFunction
192     public void selectNodeContents(final Node refNode) {
193         internSetStartContainer(refNode);
194         internSetStartOffset(0);
195         internSetEndContainer(refNode);
196         internSetEndOffset(refNode.getChildNodes().getLength());
197     }
198 
199     /**
200      * Selects a node and its contents.
201      * @param refNode the node to select
202      */
203     @JsxFunction
204     public void selectNode(final Node refNode) {
205         setStartBefore(refNode);
206         setEndAfter(refNode);
207     }
208 
209     /**
210      * Collapse a Range onto one of its boundaries.
211      * @param toStart if {@code true}, collapses the Range onto its start; else collapses it onto its end
212      */
213     @JsxFunction
214     public void collapse(final boolean toStart) {
215         if (toStart) {
216             internSetEndContainer(internGetStartContainer());
217             internSetEndOffset(internGetStartOffset());
218         }
219         else {
220             internSetStartContainer(internGetEndContainer());
221             internSetStartOffset(internGetEndOffset());
222         }
223     }
224 
225     /**
226      * Returns the deepest common ancestor container of the Range's two boundary points.
227      * @return the deepest common ancestor container of the Range's two boundary points
228      */
229     @JsxGetter
230     public Object getCommonAncestorContainer() {
231         final HashSet<Node> startAncestors = new HashSet<>();
232         Node ancestor = internGetStartContainer();
233         while (ancestor != null) {
234             startAncestors.add(ancestor);
235             ancestor = ancestor.getParent();
236         }
237 
238         ancestor = internGetEndContainer();
239         while (ancestor != null) {
240             if (startAncestors.contains(ancestor)) {
241                 return ancestor;
242             }
243             ancestor = ancestor.getParent();
244         }
245 
246         return JavaScriptEngine.UNDEFINED;
247     }
248 
249     /**
250      * Parses an HTML snippet.
251      * @param valueAsString text that contains text and tags to be converted to a document fragment
252      * @return a document fragment
253      * @see <a href="https://developer.mozilla.org/en-US/docs/DOM/range.createContextualFragment">Mozilla
254      * documentation</a>
255      */
256     @JsxFunction
257     public HtmlUnitScriptable createContextualFragment(final String valueAsString) {
258         final SgmlPage page = internGetStartContainer().getDomNodeOrDie().getPage();
259         final DomDocumentFragment fragment = new DomDocumentFragment(page);
260         try {
261             page.getWebClient().getPageCreator().getHtmlParser()
262                     .parseFragment(fragment, internGetStartContainer().getDomNodeOrDie(), valueAsString, false);
263         }
264         catch (final Exception e) {
265             LogFactory.getLog(Range.class).error("Unexpected exception occurred in createContextualFragment", e);
266             throw JavaScriptEngine.reportRuntimeError("Unexpected exception occurred in createContextualFragment: "
267                     + e.getMessage());
268         }
269 
270         return fragment.getScriptableObject();
271     }
272 
273     /**
274      * Moves this range's contents from the document tree into a document fragment.
275      * @return the new document fragment containing the range contents
276      */
277     @JsxFunction
278     public HtmlUnitScriptable extractContents() {
279         try {
280             return getSimpleRange().extractContents().getScriptableObject();
281         }
282         catch (final IllegalStateException e) {
283             throw JavaScriptEngine.reportRuntimeError(e.getMessage());
284         }
285     }
286 
287     /**
288      * Compares the boundary points of two Ranges.
289      * @param how a constant describing the comparison method
290      * @param sourceRange the Range to compare boundary points with this range
291      * @return -1, 0, or 1, indicating whether the corresponding boundary-point of range is respectively before,
292      *         equal to, or after the corresponding boundary-point of sourceRange.
293      */
294     @JsxFunction
295     public int compareBoundaryPoints(final int how, final Range sourceRange) {
296         final Node nodeForThis;
297         final int offsetForThis;
298         final int containingMoficator;
299         if (START_TO_START == how || END_TO_START == how) {
300             nodeForThis = internGetStartContainer();
301             offsetForThis = internGetStartOffset();
302             containingMoficator = 1;
303         }
304         else {
305             nodeForThis = internGetEndContainer();
306             offsetForThis = internGetEndOffset();
307             containingMoficator = -1;
308         }
309 
310         final Node nodeForOther;
311         final int offsetForOther;
312         if (START_TO_END == how || START_TO_START == how) {
313             nodeForOther = sourceRange.internGetStartContainer();
314             offsetForOther = sourceRange.internGetStartOffset();
315         }
316         else {
317             nodeForOther = sourceRange.internGetEndContainer();
318             offsetForOther = sourceRange.internGetEndOffset();
319         }
320 
321         if (nodeForThis == nodeForOther) {
322             if (offsetForThis < offsetForOther) {
323                 return -1;
324             }
325             else if (offsetForThis > offsetForOther) {
326                 return 1;
327             }
328             return 0;
329         }
330 
331         final byte nodeComparision = (byte) nodeForThis.compareDocumentPosition(nodeForOther);
332         if ((nodeComparision & Node.DOCUMENT_POSITION_CONTAINED_BY) != 0) {
333             return -1 * containingMoficator;
334         }
335         else if ((nodeComparision & Node.DOCUMENT_POSITION_PRECEDING) != 0) {
336             return -1;
337         }
338 
339         // TODO: handle other cases
340         return 1;
341     }
342 
343     /**
344      * Returns a clone of the range in a document fragment.
345      * @return a clone
346      */
347     @JsxFunction
348     public HtmlUnitScriptable cloneContents() {
349         try {
350             return getSimpleRange().cloneContents().getScriptableObject();
351         }
352         catch (final IllegalStateException e) {
353             throw JavaScriptEngine.reportRuntimeError(e.getMessage());
354         }
355     }
356 
357     /**
358      * Deletes the contents of the range.
359      */
360     @JsxFunction
361     public void deleteContents() {
362         try {
363             getSimpleRange().deleteContents();
364         }
365         catch (final IllegalStateException e) {
366             throw JavaScriptEngine.reportRuntimeError(e.getMessage());
367         }
368     }
369 
370     /**
371      * Inserts a new node at the beginning of the range. If the range begins at an offset, the node is split.
372      * @param newNode The node to insert
373      * @see <a href="https://developer.mozilla.org/en/DOM/range">https://developer.mozilla.org/en/DOM/range</a>
374      */
375     @JsxFunction
376     public void insertNode(final Node newNode) {
377         try {
378             getSimpleRange().insertNode(newNode.getDomNodeOrDie());
379         }
380         catch (final IllegalStateException e) {
381             throw JavaScriptEngine.reportRuntimeError(e.getMessage());
382         }
383     }
384 
385     /**
386      * Surrounds the contents of the range in a new node.
387      * @param newNode The node to surround the range in
388      */
389     @JsxFunction
390     public void surroundContents(final Node newNode) {
391         try {
392             getSimpleRange().surroundContents(newNode.getDomNodeOrDie());
393         }
394         catch (final IllegalStateException e) {
395             throw JavaScriptEngine.reportRuntimeError(e.getMessage());
396         }
397     }
398 
399     /**
400      * Returns a clone of the range.
401      * @return a clone of the range
402      */
403     @JsxFunction
404     public Range cloneRange() {
405         try {
406             return new Range(getSimpleRange().cloneRange());
407         }
408         catch (final IllegalStateException e) {
409             throw JavaScriptEngine.reportRuntimeError(e.getMessage());
410         }
411     }
412 
413     /**
414      * Releases Range from use to improve performance.
415      */
416     @JsxFunction
417     public void detach() {
418         // Java garbage collection should take care of this for us
419     }
420 
421     /**
422      * Returns the text of the Range.
423      * @return the text
424      */
425     @JsxFunction(functionName = "toString")
426     public String jsToString() {
427         try {
428             return getSimpleRange().toString();
429         }
430         catch (final IllegalStateException e) {
431             throw JavaScriptEngine.reportRuntimeError(e.getMessage());
432         }
433     }
434 
435     /**
436      * Retrieves a collection of rectangles that describes the layout of the contents of an object
437      * or range within the client. Each rectangle describes a single line.
438      * @return a collection of rectangles that describes the layout of the contents
439      */
440     @JsxFunction
441     public ClientRectList getClientRects() {
442         final Window w = getWindow();
443         final ClientRectList rectList = new ClientRectList();
444         rectList.setParentScope(w);
445         rectList.setPrototype(getPrototype(rectList.getClass()));
446 
447         try {
448             // simple impl for now
449             for (final DomNode node : getSimpleRange().containedNodes()) {
450                 final HtmlUnitScriptable scriptable = node.getScriptableObject();
451                 if (scriptable instanceof HTMLElement) {
452                     final ClientRect rect = new ClientRect(0, 0, 1, 1);
453                     rect.setParentScope(w);
454                     rect.setPrototype(getPrototype(rect.getClass()));
455                     rectList.add(rect);
456                 }
457             }
458 
459             return rectList;
460         }
461         catch (final IllegalStateException e) {
462             throw JavaScriptEngine.reportRuntimeError(e.getMessage());
463         }
464     }
465 
466     /**
467      * Returns an object that bounds the contents of the range.
468      * this a rectangle enclosing the union of the bounding rectangles for all the elements in the range.
469      * @return an object the bounds the contents of the range
470      */
471     @JsxFunction
472     public ClientRect getBoundingClientRect() {
473         final ClientRect rect = new ClientRect();
474         rect.setParentScope(getWindow());
475         rect.setPrototype(getPrototype(rect.getClass()));
476 
477         try {
478             // simple impl for now
479             for (final DomNode node : getSimpleRange().containedNodes()) {
480                 final HtmlUnitScriptable scriptable = node.getScriptableObject();
481                 if (scriptable instanceof HTMLElement) {
482                     final ClientRect childRect = ((HTMLElement) scriptable).getBoundingClientRect();
483                     rect.setTop(Math.min(rect.getTop(), childRect.getTop()));
484                     rect.setLeft(Math.min(rect.getLeft(), childRect.getLeft()));
485                     rect.setRight(Math.max(rect.getRight(), childRect.getRight()));
486                     rect.setBottom(Math.max(rect.getBottom(), childRect.getBottom()));
487                 }
488             }
489 
490             return rect;
491         }
492         catch (final IllegalStateException e) {
493             throw JavaScriptEngine.reportRuntimeError(e.getMessage());
494         }
495     }
496 }