overlayHelpers.mjs (13545B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 // An autoselection smaller than these will be ignored entirely: 6 const MIN_DETECT_ABSOLUTE_HEIGHT = 10; 7 const MIN_DETECT_ABSOLUTE_WIDTH = 30; 8 // An autoselection smaller than these will not be preferred: 9 const MIN_DETECT_HEIGHT = 30; 10 const MIN_DETECT_WIDTH = 100; 11 // An autoselection bigger than either of these will be ignored: 12 let MAX_DETECT_HEIGHT = 700; 13 let MAX_DETECT_WIDTH = 1000; 14 15 const doNotAutoselectTags = { 16 H1: true, 17 H2: true, 18 H3: true, 19 H4: true, 20 H5: true, 21 H6: true, 22 }; 23 24 /** 25 * Gets the rect for an element if getBoundingClientRect exists 26 * 27 * @param ele The element to get the rect from 28 * @returns The bounding client rect of the element or null 29 */ 30 function getBoundingClientRect(ele) { 31 if (!ele.getBoundingClientRect) { 32 return null; 33 } 34 35 return ele.getBoundingClientRect(); 36 } 37 38 export function setMaxDetectHeight(maxHeight) { 39 MAX_DETECT_HEIGHT = maxHeight; 40 } 41 42 export function setMaxDetectWidth(maxWidth) { 43 MAX_DETECT_WIDTH = maxWidth; 44 } 45 46 /** 47 * This function will try to get an element from a given point in the doc. 48 * This function is recursive because when sending a message to the 49 * ScreenshotsHelper, the ScreenshotsHelper will call into this function. 50 * This only occurs when the element at the given point is an iframe. 51 * 52 * If the element is an iframe, we will send a message to the ScreenshotsHelper 53 * actor in the correct context to get the element at the given point. 54 * The message will return the "getBestRectForElement" for the element at the 55 * given point. 56 * 57 * If the element is not an iframe, then we will just return the element. 58 * 59 * @param {number} x The x coordinate 60 * @param {number} y The y coordinate 61 * @param {Document} doc The document 62 * @returns {object} 63 * ele: The element for a given point (x, y) 64 * rect: The rect for the given point if ele is an iframe 65 * otherwise null 66 */ 67 export async function getElementFromPoint(x, y, doc) { 68 let ele = null; 69 let rect = null; 70 try { 71 ele = doc.elementFromPoint(x, y); 72 // if the element is an iframe, we need to send a message to that browsing context 73 // to get the coordinates of the element in the iframe 74 if (doc.defaultView.HTMLIFrameElement.isInstance(ele)) { 75 let actor = 76 ele.browsingContext.parentWindowContext.windowGlobalChild.getActor( 77 "ScreenshotsHelper" 78 ); 79 rect = await actor.sendQuery( 80 "ScreenshotsHelper:GetElementRectFromPoint", 81 { 82 x: x + ele.ownerGlobal.mozInnerScreenX, 83 y: y + ele.ownerGlobal.mozInnerScreenY, 84 bcId: ele.browsingContext.id, 85 } 86 ); 87 88 if (rect) { 89 rect = { 90 left: rect.left - ele.ownerGlobal.mozInnerScreenX, 91 right: rect.right - ele.ownerGlobal.mozInnerScreenX, 92 top: rect.top - ele.ownerGlobal.mozInnerScreenY, 93 bottom: rect.bottom - ele.ownerGlobal.mozInnerScreenY, 94 }; 95 } 96 } else if (ele.openOrClosedShadowRoot) { 97 while (ele.openOrClosedShadowRoot) { 98 let shadowEle = ele.openOrClosedShadowRoot.elementFromPoint(x, y); 99 if (shadowEle) { 100 ele = shadowEle; 101 } else { 102 break; 103 } 104 } 105 } 106 } catch (e) { 107 console.error(e); 108 } 109 110 return { ele, rect }; 111 } 112 113 /** 114 * This function takes an element and finds a suitable rect to draw the hover box on 115 * 116 * @param {Element} ele The element to find a suitale rect of 117 * @param {Document} doc The current document 118 * @returns A suitable rect or null 119 */ 120 export function getBestRectForElement(ele, doc) { 121 let lastRect; 122 let lastNode; 123 let rect; 124 let attemptExtend = false; 125 let node = ele; 126 while (node) { 127 rect = getBoundingClientRect(node); 128 if (!rect) { 129 rect = lastRect; 130 break; 131 } 132 if (rect.width < MIN_DETECT_WIDTH || rect.height < MIN_DETECT_HEIGHT) { 133 // Avoid infinite loop for elements with zero or nearly zero height, 134 // like non-clearfixed float parents with or without borders. 135 break; 136 } 137 if (rect.width > MAX_DETECT_WIDTH || rect.height > MAX_DETECT_HEIGHT) { 138 // Then the last rectangle is better 139 rect = lastRect; 140 attemptExtend = true; 141 break; 142 } 143 if (rect.width >= MIN_DETECT_WIDTH && rect.height >= MIN_DETECT_HEIGHT) { 144 if (!doNotAutoselectTags[node.tagName]) { 145 break; 146 } 147 } 148 lastRect = rect; 149 lastNode = node; 150 node = node.parentNode; 151 } 152 if (rect && node) { 153 const evenBetter = evenBetterElement(node, doc); 154 if (evenBetter) { 155 node = lastNode = evenBetter; 156 rect = getBoundingClientRect(evenBetter); 157 attemptExtend = false; 158 } 159 } 160 if (rect && attemptExtend) { 161 let extendNode = lastNode.nextSibling; 162 while (extendNode) { 163 if (extendNode.nodeType === doc.ELEMENT_NODE) { 164 break; 165 } 166 extendNode = extendNode.nextSibling; 167 if (!extendNode) { 168 const parentNode = lastNode.parentNode; 169 for (let i = 0; i < parentNode.childNodes.length; i++) { 170 if (parentNode.childNodes[i] === lastNode) { 171 extendNode = parentNode.childNodes[i + 1]; 172 } 173 } 174 } 175 } 176 if (extendNode) { 177 const extendRect = getBoundingClientRect(extendNode); 178 let x = Math.min(rect.x, extendRect.x); 179 let y = Math.min(rect.y, extendRect.y); 180 let width = Math.max(rect.right, extendRect.right) - x; 181 let height = Math.max(rect.bottom, extendRect.bottom) - y; 182 const combinedRect = new DOMRect(x, y, width, height); 183 if ( 184 combinedRect.width <= MAX_DETECT_WIDTH && 185 combinedRect.height <= MAX_DETECT_HEIGHT 186 ) { 187 rect = combinedRect; 188 } 189 } 190 } 191 192 if ( 193 rect && 194 (rect.width < MIN_DETECT_ABSOLUTE_WIDTH || 195 rect.height < MIN_DETECT_ABSOLUTE_HEIGHT) 196 ) { 197 rect = null; 198 } 199 200 return rect; 201 } 202 203 /** 204 * This finds a better element by looking for elements with role article 205 * 206 * @param {Element} node The currently hovered node 207 * @param {Document} doc The current document 208 * @returns A better node or null 209 */ 210 function evenBetterElement(node, doc) { 211 let el = node.parentNode; 212 const ELEMENT_NODE = doc.ELEMENT_NODE; 213 while (el && el.nodeType === ELEMENT_NODE) { 214 if (!el.getAttribute) { 215 return null; 216 } 217 if (el.getAttribute("role") === "article") { 218 const rect = getBoundingClientRect(el); 219 if (!rect) { 220 return null; 221 } 222 if (rect.width <= MAX_DETECT_WIDTH && rect.height <= MAX_DETECT_HEIGHT) { 223 return el; 224 } 225 return null; 226 } 227 el = el.parentNode; 228 } 229 return null; 230 } 231 232 export class Region { 233 #x1; 234 #x2; 235 #y1; 236 #y2; 237 #xOffset; 238 #yOffset; 239 #windowDimensions; 240 241 constructor(windowDimensions) { 242 this.resetDimensions(); 243 this.#windowDimensions = windowDimensions; 244 } 245 246 /** 247 * Sets the dimensions if the given dimension is defined. 248 * Otherwise will reset the dimensions 249 * 250 * @param {object} dims The new region dimensions 251 * { 252 * left: new left dimension value or undefined 253 * top: new top dimension value or undefined 254 * right: new right dimension value or undefined 255 * bottom: new bottom dimension value or undefined 256 * } 257 */ 258 set dimensions(dims) { 259 if (dims == null) { 260 this.resetDimensions(); 261 return; 262 } 263 264 if (dims.left != null) { 265 this.left = dims.left; 266 } 267 if (dims.top != null) { 268 this.top = dims.top; 269 } 270 if (dims.right != null) { 271 this.right = dims.right; 272 } 273 if (dims.bottom != null) { 274 this.bottom = dims.bottom; 275 } 276 } 277 278 get dimensions() { 279 return { 280 left: this.left, 281 top: this.top, 282 right: this.right, 283 bottom: this.bottom, 284 width: this.width, 285 height: this.height, 286 }; 287 } 288 289 get isRegionValid() { 290 return this.#x1 + this.#x2 + this.#y1 + this.#y2 > 0; 291 } 292 293 resetDimensions() { 294 this.#x1 = 0; 295 this.#x2 = 0; 296 this.#y1 = 0; 297 this.#y2 = 0; 298 this.#xOffset = 0; 299 this.#yOffset = 0; 300 } 301 302 /** 303 * Sort the coordinates so x1 < x2 and y1 < y2 304 */ 305 sortCoords() { 306 if (this.#x1 > this.#x2) { 307 [this.#x1, this.#x2] = [this.#x2, this.#x1]; 308 } 309 if (this.#y1 > this.#y2) { 310 [this.#y1, this.#y2] = [this.#y2, this.#y1]; 311 } 312 } 313 314 /** 315 * The region should never appear outside the document so the region will 316 * be shifted if the region is outside the page's width or height. 317 */ 318 shift() { 319 let didShift = false; 320 let xDiff = this.right - this.#windowDimensions.scrollWidth; 321 if (xDiff > 0) { 322 this.left -= xDiff; 323 this.right -= xDiff; 324 325 didShift = true; 326 } 327 328 let yDiff = this.bottom - this.#windowDimensions.scrollHeight; 329 if (yDiff > 0) { 330 this.top -= yDiff; 331 this.bottom -= yDiff; 332 333 didShift = true; 334 } 335 336 return didShift; 337 } 338 339 /** 340 * The diagonal distance of the region 341 */ 342 get distance() { 343 return Math.sqrt(Math.pow(this.width, 2) + Math.pow(this.height, 2)); 344 } 345 346 get xOffset() { 347 return this.#xOffset; 348 } 349 set xOffset(val) { 350 this.#xOffset = val; 351 } 352 353 get yOffset() { 354 return this.#yOffset; 355 } 356 set yOffset(val) { 357 this.#yOffset = val; 358 } 359 360 get top() { 361 return Math.min(this.#y1, this.#y2); 362 } 363 set top(val) { 364 this.#y1 = Math.min(this.#windowDimensions.scrollHeight, Math.max(0, val)); 365 } 366 367 get left() { 368 return Math.min(this.#x1, this.#x2); 369 } 370 set left(val) { 371 this.#x1 = Math.min(this.#windowDimensions.scrollWidth, Math.max(0, val)); 372 } 373 374 get right() { 375 return Math.max(this.#x1, this.#x2); 376 } 377 set right(val) { 378 this.#x2 = Math.min(this.#windowDimensions.scrollWidth, Math.max(0, val)); 379 } 380 381 get bottom() { 382 return Math.max(this.#y1, this.#y2); 383 } 384 set bottom(val) { 385 this.#y2 = Math.min(this.#windowDimensions.scrollHeight, Math.max(0, val)); 386 } 387 388 get width() { 389 return Math.abs(this.#x2 - this.#x1); 390 } 391 get height() { 392 return Math.abs(this.#y2 - this.#y1); 393 } 394 395 get x1() { 396 return this.#x1; 397 } 398 get x2() { 399 return this.#x2; 400 } 401 get y1() { 402 return this.#y1; 403 } 404 get y2() { 405 return this.#y2; 406 } 407 } 408 409 export class WindowDimensions { 410 #clientHeight = null; 411 #clientWidth = null; 412 #scrollHeight = null; 413 #scrollWidth = null; 414 #scrollX = null; 415 #scrollY = null; 416 #scrollMinX = null; 417 #scrollMinY = null; 418 #scrollMaxX = null; 419 #scrollMaxY = null; 420 #devicePixelRatio = null; 421 422 set dimensions(dimensions) { 423 if (dimensions.clientHeight != null) { 424 this.#clientHeight = dimensions.clientHeight; 425 } 426 if (dimensions.clientWidth != null) { 427 this.#clientWidth = dimensions.clientWidth; 428 } 429 if (dimensions.scrollHeight != null) { 430 this.#scrollHeight = dimensions.scrollHeight; 431 } 432 if (dimensions.scrollWidth != null) { 433 this.#scrollWidth = dimensions.scrollWidth; 434 } 435 if (dimensions.scrollX != null) { 436 this.#scrollX = dimensions.scrollX; 437 } 438 if (dimensions.scrollY != null) { 439 this.#scrollY = dimensions.scrollY; 440 } 441 if (dimensions.scrollMinX != null) { 442 this.#scrollMinX = dimensions.scrollMinX; 443 } 444 if (dimensions.scrollMinY != null) { 445 this.#scrollMinY = dimensions.scrollMinY; 446 } 447 if (dimensions.scrollMaxX != null) { 448 this.#scrollMaxX = dimensions.scrollMaxX; 449 } 450 if (dimensions.scrollMaxY != null) { 451 this.#scrollMaxY = dimensions.scrollMaxY; 452 } 453 if (dimensions.devicePixelRatio != null) { 454 this.#devicePixelRatio = dimensions.devicePixelRatio; 455 } 456 } 457 458 get dimensions() { 459 return { 460 clientHeight: this.clientHeight, 461 clientWidth: this.clientWidth, 462 scrollHeight: this.scrollHeight, 463 scrollWidth: this.scrollWidth, 464 scrollX: this.scrollX, 465 scrollY: this.scrollY, 466 pageScrollX: this.pageScrollX, 467 pageScrollY: this.pageScrollY, 468 scrollMinX: this.scrollMinX, 469 scrollMinY: this.scrollMinY, 470 scrollMaxX: this.scrollMaxX, 471 scrollMaxY: this.scrollMaxY, 472 devicePixelRatio: this.devicePixelRatio, 473 }; 474 } 475 476 get clientWidth() { 477 return this.#clientWidth; 478 } 479 480 get clientHeight() { 481 return this.#clientHeight; 482 } 483 484 get scrollWidth() { 485 return this.#scrollWidth; 486 } 487 488 get scrollHeight() { 489 return this.#scrollHeight; 490 } 491 492 get scrollX() { 493 return this.#scrollX - this.scrollMinX; 494 } 495 496 get pageScrollX() { 497 return this.#scrollX; 498 } 499 500 get scrollY() { 501 return this.#scrollY - this.scrollMinY; 502 } 503 504 get pageScrollY() { 505 return this.#scrollY; 506 } 507 508 get scrollMinX() { 509 return this.#scrollMinX; 510 } 511 512 get scrollMinY() { 513 return this.#scrollMinY; 514 } 515 516 get scrollMaxX() { 517 return this.#scrollMaxX; 518 } 519 520 get scrollMaxY() { 521 return this.#scrollMaxY; 522 } 523 524 get devicePixelRatio() { 525 return this.#devicePixelRatio; 526 } 527 528 isInViewport(rect) { 529 // eslint-disable-next-line no-shadow 530 let { left, top, right, bottom } = rect; 531 532 if ( 533 left > this.scrollX + this.clientWidth || 534 right < this.scrollX || 535 top > this.scrollY + this.clientHeight || 536 bottom < this.scrollY 537 ) { 538 return false; 539 } 540 return true; 541 } 542 543 reset() { 544 this.#clientHeight = 0; 545 this.#clientWidth = 0; 546 this.#scrollHeight = 0; 547 this.#scrollWidth = 0; 548 this.#scrollX = 0; 549 this.#scrollY = 0; 550 this.#scrollMinX = 0; 551 this.#scrollMinY = 0; 552 this.#scrollMaxX = 0; 553 this.#scrollMaxY = 0; 554 } 555 }