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