1
2
3
4
5
6
7
8
9
10
11
12
13
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.BrowserVersion;
21 import org.htmlunit.SgmlPage;
22 import org.htmlunit.WebClient;
23 import org.htmlunit.WebWindow;
24 import org.htmlunit.css.ComputedCssStyleDeclaration;
25 import org.htmlunit.css.StyleAttributes.Definition;
26 import org.htmlunit.html.DomDocumentFragment;
27 import org.htmlunit.html.DomElement;
28 import org.htmlunit.html.DomNode;
29 import org.htmlunit.html.DomText;
30 import org.htmlunit.html.HtmlElement;
31 import org.htmlunit.html.impl.SimpleRange;
32 import org.htmlunit.javascript.HtmlUnitScriptable;
33 import org.htmlunit.javascript.JavaScriptEngine;
34 import org.htmlunit.javascript.configuration.JsxClass;
35 import org.htmlunit.javascript.configuration.JsxConstant;
36 import org.htmlunit.javascript.configuration.JsxConstructor;
37 import org.htmlunit.javascript.configuration.JsxFunction;
38 import org.htmlunit.javascript.configuration.JsxGetter;
39 import org.htmlunit.javascript.host.DOMRect;
40 import org.htmlunit.javascript.host.DOMRectList;
41 import org.htmlunit.javascript.host.html.HTMLElement;
42
43
44
45
46
47
48
49
50
51
52
53
54 @JsxClass
55 public class Range extends AbstractRange {
56
57
58 @JsxConstant
59 public static final int START_TO_START = 0;
60
61
62 @JsxConstant
63 public static final int START_TO_END = 1;
64
65
66 @JsxConstant
67 public static final int END_TO_END = 2;
68
69
70 @JsxConstant
71 public static final int END_TO_START = 3;
72
73
74
75
76 public Range() {
77 super();
78 }
79
80
81
82
83 @Override
84 @JsxConstructor
85 public void jsConstructor() {
86 super.jsConstructor();
87 }
88
89
90
91
92
93 public Range(final Document document) {
94 super(document, document, 0, 0);
95 }
96
97 Range(final SimpleRange simpleRange) {
98 super(simpleRange.getStartContainer().getScriptableObject(),
99 simpleRange.getEndContainer().getScriptableObject(),
100 simpleRange.getStartOffset(),
101 simpleRange.getEndOffset());
102 }
103
104
105
106
107
108
109 @JsxFunction
110 public void setStart(final Node refNode, final int offset) {
111 if (refNode == null) {
112 throw JavaScriptEngine.reportRuntimeError("It is illegal to call Range.setStart() with a null node.");
113 }
114 internSetStartContainer(refNode);
115 internSetStartOffset(offset);
116 }
117
118
119
120
121
122 @JsxFunction
123 public void setStartAfter(final Node refNode) {
124 if (refNode == null) {
125 throw JavaScriptEngine.reportRuntimeError("It is illegal to call Range.setStartAfter() with a null node.");
126 }
127 internSetStartContainer(refNode.getParent());
128 internSetStartOffset(getPositionInContainer(refNode) + 1);
129 }
130
131
132
133
134
135 @JsxFunction
136 public void setStartBefore(final Node refNode) {
137 if (refNode == null) {
138 throw JavaScriptEngine.reportRuntimeError("It is illegal to call Range.setStartBefore() with a null node.");
139 }
140 internSetStartContainer(refNode.getParent());
141 internSetStartOffset(getPositionInContainer(refNode));
142 }
143
144 private static int getPositionInContainer(final Node refNode) {
145 int i = 0;
146 Node node = refNode;
147 while (node.getPreviousSibling() != null) {
148 node = node.getPreviousSibling();
149 i++;
150 }
151 return i;
152 }
153
154
155
156
157
158
159 @JsxFunction
160 public void setEnd(final Node refNode, final int offset) {
161 if (refNode == null) {
162 throw JavaScriptEngine.reportRuntimeError("It is illegal to call Range.setEnd() with a null node.");
163 }
164 internSetEndContainer(refNode);
165 internSetEndOffset(offset);
166 }
167
168
169
170
171
172 @JsxFunction
173 public void setEndAfter(final Node refNode) {
174 if (refNode == null) {
175 throw JavaScriptEngine.reportRuntimeError("It is illegal to call Range.setEndAfter() with a null node.");
176 }
177 internSetEndContainer(refNode.getParent());
178 internSetEndOffset(getPositionInContainer(refNode) + 1);
179 }
180
181
182
183
184
185 @JsxFunction
186 public void setEndBefore(final Node refNode) {
187 if (refNode == null) {
188 throw JavaScriptEngine.reportRuntimeError("It is illegal to call Range.setEndBefore() with a null node.");
189 }
190 internSetEndContainer(refNode.getParent());
191 internSetEndOffset(getPositionInContainer(refNode));
192 }
193
194
195
196
197
198 @JsxFunction
199 public void selectNodeContents(final Node refNode) {
200 internSetStartContainer(refNode);
201 internSetStartOffset(0);
202 internSetEndContainer(refNode);
203 internSetEndOffset(refNode.getChildNodes().getLength());
204 }
205
206
207
208
209
210 @JsxFunction
211 public void selectNode(final Node refNode) {
212 setStartBefore(refNode);
213 setEndAfter(refNode);
214 }
215
216
217
218
219
220 @JsxFunction
221 public void collapse(final boolean toStart) {
222 if (toStart) {
223 internSetEndContainer(internGetStartContainer());
224 internSetEndOffset(internGetStartOffset());
225 }
226 else {
227 internSetStartContainer(internGetEndContainer());
228 internSetStartOffset(internGetEndOffset());
229 }
230 }
231
232
233
234
235
236 @JsxGetter
237 public Object getCommonAncestorContainer() {
238 final HashSet<Node> startAncestors = new HashSet<>();
239 Node ancestor = internGetStartContainer();
240 while (ancestor != null) {
241 startAncestors.add(ancestor);
242 ancestor = ancestor.getParent();
243 }
244
245 ancestor = internGetEndContainer();
246 while (ancestor != null) {
247 if (startAncestors.contains(ancestor)) {
248 return ancestor;
249 }
250 ancestor = ancestor.getParent();
251 }
252
253 return JavaScriptEngine.UNDEFINED;
254 }
255
256
257
258
259
260
261
262
263 @JsxFunction
264 public HtmlUnitScriptable createContextualFragment(final String valueAsString) {
265 final SgmlPage page = internGetStartContainer().getDomNodeOrDie().getPage();
266 final DomDocumentFragment fragment = new DomDocumentFragment(page);
267 try {
268 final WebClient webClient = page.getWebClient();
269 webClient.getPageCreator().getHtmlParser()
270 .parseFragment(webClient, fragment,
271 internGetStartContainer().getDomNodeOrDie(), valueAsString, false);
272 }
273 catch (final Exception e) {
274 LogFactory.getLog(Range.class).error("Unexpected exception occurred in createContextualFragment", e);
275 throw JavaScriptEngine.reportRuntimeError("Unexpected exception occurred in createContextualFragment: "
276 + e.getMessage());
277 }
278
279 return fragment.getScriptableObject();
280 }
281
282
283
284
285
286 @JsxFunction
287 public HtmlUnitScriptable extractContents() {
288 try {
289 return getSimpleRange().extractContents().getScriptableObject();
290 }
291 catch (final IllegalStateException e) {
292 throw JavaScriptEngine.reportRuntimeError(e.getMessage());
293 }
294 }
295
296
297
298
299
300
301
302
303 @JsxFunction
304 public int compareBoundaryPoints(final int how, final Range sourceRange) {
305 final Node nodeForThis;
306 final int offsetForThis;
307 final int containingModifier;
308 if (START_TO_START == how || END_TO_START == how) {
309 nodeForThis = internGetStartContainer();
310 offsetForThis = internGetStartOffset();
311 containingModifier = 1;
312 }
313 else {
314 nodeForThis = internGetEndContainer();
315 offsetForThis = internGetEndOffset();
316 containingModifier = -1;
317 }
318
319 final Node nodeForOther;
320 final int offsetForOther;
321 if (START_TO_END == how || START_TO_START == how) {
322 nodeForOther = sourceRange.internGetStartContainer();
323 offsetForOther = sourceRange.internGetStartOffset();
324 }
325 else {
326 nodeForOther = sourceRange.internGetEndContainer();
327 offsetForOther = sourceRange.internGetEndOffset();
328 }
329
330 if (nodeForThis == nodeForOther) {
331 if (offsetForThis < offsetForOther) {
332 return -1;
333 }
334 else if (offsetForThis > offsetForOther) {
335 return 1;
336 }
337 return 0;
338 }
339
340 final byte nodeComparision = (byte) nodeForThis.compareDocumentPosition(nodeForOther);
341 if ((nodeComparision & Node.DOCUMENT_POSITION_CONTAINED_BY) != 0) {
342 return -1 * containingModifier;
343 }
344 else if ((nodeComparision & Node.DOCUMENT_POSITION_PRECEDING) != 0) {
345 return -1;
346 }
347
348
349 return 1;
350 }
351
352
353
354
355
356 @JsxFunction
357 public HtmlUnitScriptable cloneContents() {
358 try {
359 return getSimpleRange().cloneContents().getScriptableObject();
360 }
361 catch (final IllegalStateException e) {
362 throw JavaScriptEngine.reportRuntimeError(e.getMessage());
363 }
364 }
365
366
367
368
369 @JsxFunction
370 public void deleteContents() {
371 try {
372 getSimpleRange().deleteContents();
373 }
374 catch (final IllegalStateException e) {
375 throw JavaScriptEngine.reportRuntimeError(e.getMessage());
376 }
377 }
378
379
380
381
382
383
384 @JsxFunction
385 public void insertNode(final Node newNode) {
386 try {
387 getSimpleRange().insertNode(newNode.getDomNodeOrDie());
388 }
389 catch (final IllegalStateException e) {
390 throw JavaScriptEngine.reportRuntimeError(e.getMessage());
391 }
392 }
393
394
395
396
397
398 @JsxFunction
399 public void surroundContents(final Node newNode) {
400 try {
401 getSimpleRange().surroundContents(newNode.getDomNodeOrDie());
402 }
403 catch (final IllegalStateException e) {
404 throw JavaScriptEngine.reportRuntimeError(e.getMessage());
405 }
406 }
407
408
409
410
411
412 @JsxFunction
413 public Range cloneRange() {
414 try {
415 return new Range(getSimpleRange().cloneRange());
416 }
417 catch (final IllegalStateException e) {
418 throw JavaScriptEngine.reportRuntimeError(e.getMessage());
419 }
420 }
421
422
423
424
425 @JsxFunction
426 public void detach() {
427
428 }
429
430
431
432
433
434 @JsxFunction(functionName = "toString")
435 public String jsToString() {
436 try {
437 return getSimpleRange().toString();
438 }
439 catch (final IllegalStateException e) {
440 throw JavaScriptEngine.reportRuntimeError(e.getMessage());
441 }
442 }
443
444
445
446
447
448
449 @JsxFunction
450 public DOMRectList getClientRects() {
451 final DOMRectList rectList = new DOMRectList();
452 rectList.setParentScope(getParentScope());
453 rectList.setPrototype(getPrototype(rectList.getClass()));
454
455 try {
456
457 for (final DomNode node : getSimpleRange().containedNodes()) {
458 final HtmlUnitScriptable scriptable = node.getScriptableObject();
459 if (scriptable instanceof HTMLElement element) {
460 final DOMRect rect = element.getBoundingClientRect();
461 rect.setParentScope(getParentScope());
462 rect.setPrototype(getPrototype(rect.getClass()));
463 rectList.add(rect);
464 }
465 else if (node instanceof DomText domText) {
466 final DOMRect rect = getTextNodeRect(domText);
467 if (rect != null) {
468 rectList.add(rect);
469 }
470 }
471 }
472
473 return rectList;
474 }
475 catch (final IllegalStateException e) {
476 throw JavaScriptEngine.reportRuntimeError(e.getMessage());
477 }
478 }
479
480
481
482
483
484
485 @JsxFunction
486 public DOMRect getBoundingClientRect() {
487 final DOMRect rect = new DOMRect(Integer.MAX_VALUE, Integer.MAX_VALUE, 0, 0);
488 rect.setParentScope(getParentScope());
489 rect.setPrototype(getPrototype(rect.getClass()));
490
491 try {
492
493 for (final DomNode node : getSimpleRange().containedNodes()) {
494 final HtmlUnitScriptable scriptable = node.getScriptableObject();
495
496 DOMRect childRect = null;
497 if (scriptable instanceof HTMLElement element) {
498 childRect = element.getBoundingClientRect();
499 }
500 else if (node instanceof DomText domText) {
501 childRect = getTextNodeRect(domText);
502 }
503
504 if (childRect != null) {
505 rect.setX(Math.min(rect.getX(), childRect.getX()));
506 rect.setY(Math.min(rect.getY(), childRect.getY()));
507 rect.setWidth(Math.max(rect.getWidth(), childRect.getWidth()));
508 rect.setHeight(Math.max(rect.getHeight(), childRect.getHeight()));
509 }
510 }
511
512 return rect;
513 }
514 catch (final IllegalStateException e) {
515 throw JavaScriptEngine.reportRuntimeError(e.getMessage());
516 }
517 }
518
519 private DOMRect getTextNodeRect(final DomText node) {
520
521 final DomNode parent = node.getParentNode();
522 if (!(parent instanceof HtmlElement parentHtml)) {
523 return null;
524 }
525
526 final HTMLElement parentScriptable = parentHtml.getScriptableObject();
527 final DOMRect parentRect = parentScriptable.getBoundingClientRect();
528
529 final SgmlPage page = parent.getPage();
530 final WebWindow webWindow = page.getEnclosingWindow();
531 final ComputedCssStyleDeclaration style = webWindow.getComputedStyle((DomElement) parent, null);
532 final BrowserVersion browserVersion = page.getWebClient().getBrowserVersion();
533
534 final int fontHeight = browserVersion.getFontHeight(style.getStyleAttribute(Definition.FONT_SIZE, true));
535 final float pixelsPerChar = fontHeight / 1.8f;
536
537
538
539 float siblingOffset = 0;
540 for (final DomNode sibling : parent.getChildren()) {
541 if (sibling == node) {
542 break;
543 }
544 if (sibling instanceof DomText sibText) {
545
546 final String visible = sibText.getVisibleText().trim();
547 if (!visible.isEmpty()) {
548 siblingOffset += visible.length() * pixelsPerChar;
549 }
550 }
551 else if (sibling instanceof HtmlElement siblingElement) {
552 final ComputedCssStyleDeclaration sibStyle =
553 webWindow.getComputedStyle(siblingElement, null);
554 siblingOffset += sibStyle.getCalculatedWidth(true, true, true);
555 }
556 }
557
558 final boolean startIsThisNode = getSimpleRange().getStartContainer() == node;
559 final boolean endIsThisNode = getSimpleRange().getEndContainer() == node;
560
561 final int startChar = startIsThisNode ? getSimpleRange().getStartOffset() : 0;
562
563 final String visibleText = node.getVisibleText().trim();
564 final int endChar = endIsThisNode ? getSimpleRange().getEndOffset() : visibleText.length();
565
566 final double rectLeft = parentRect.getX() + siblingOffset + startChar * pixelsPerChar;
567 final double rectWidth = (endChar - startChar) * pixelsPerChar;
568
569 final DOMRect rect = new DOMRect(rectLeft, parentRect.getY(), rectWidth, fontHeight);
570 rect.setParentScope(getParentScope());
571 rect.setPrototype(getPrototype(rect.getClass()));
572 return rect;
573 }
574 }