highlighter-test-actor.js (27307B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 /* exported HighlighterTestActor, HighlighterTestFront */ 5 6 "use strict"; 7 8 // A helper actor for testing highlighters. 9 // ⚠️ This should only be used for getting data for objects using CanvasFrameAnonymousContentHelper, 10 // that we can't get directly from tests. 11 const { 12 getRect, 13 getAdjustedQuads, 14 } = require("resource://devtools/shared/layout/utils.js"); 15 16 // Set up a dummy environment so that EventUtils works. We need to be careful to 17 // pass a window object into each EventUtils method we call rather than having 18 // it rely on the |window| global. 19 const EventUtils = {}; 20 EventUtils.window = {}; 21 EventUtils.parent = {}; 22 /* eslint-disable camelcase */ 23 EventUtils._EU_Ci = Ci; 24 EventUtils._EU_Cc = Cc; 25 /* eslint-disable camelcase */ 26 Services.scriptloader.loadSubScript( 27 "chrome://mochikit/content/tests/SimpleTest/EventUtils.js", 28 EventUtils 29 ); 30 31 // We're an actor so we don't run in the browser test environment, so 32 // we need to import TestUtils manually despite what the linter thinks. 33 // eslint-disable-next-line mozilla/no-redeclare-with-import-autofix 34 const { TestUtils } = ChromeUtils.importESModule( 35 "resource://testing-common/TestUtils.sys.mjs" 36 ); 37 38 const protocol = require("resource://devtools/shared/protocol.js"); 39 const { Arg, RetVal } = protocol; 40 41 const dumpn = msg => { 42 dump(msg + "\n"); 43 }; 44 45 /** 46 * Get the instance of CanvasFrameAnonymousContentHelper used by a given 47 * highlighter actor. 48 * The instance provides methods to get/set attributes/text/style on nodes of 49 * the highlighter, inserted into the nsCanvasFrame. 50 * 51 * @see /devtools/server/actors/highlighters.js 52 * @param {string} actorID 53 */ 54 function getHighlighterCanvasFrameHelper(conn, actorID) { 55 // Retrieve the CustomHighlighterActor by its actorID: 56 const actor = conn.getActor(actorID); 57 if (!actor) { 58 return null; 59 } 60 61 // Retrieve the sub class instance specific to each highlighter type: 62 let highlighter = actor.instance; 63 64 // SelectorHighlighter and TabbingOrderHighlighter can hold multiple highlighters. 65 // For now, only retrieve the first highlighter. 66 if ( 67 highlighter._highlighters && 68 Array.isArray(highlighter._highlighters) && 69 highlighter._highlighters.length 70 ) { 71 highlighter = highlighter._highlighters[0]; 72 } 73 74 // Now, `highlighter` should be a final highlighter class, exposing 75 // `CanvasFrameAnonymousContentHelper` via a `markup` attribute. 76 if (highlighter.markup) { 77 return highlighter.markup; 78 } 79 80 // Here we didn't find any highlighter; it can happen if the actor is a 81 // FontsHighlighter (which does not use a CanvasFrameAnonymousContentHelper). 82 return null; 83 } 84 85 var highlighterTestSpec = protocol.generateActorSpec({ 86 typeName: "highlighterTest", 87 88 events: { 89 "highlighter-updated": {}, 90 }, 91 92 methods: { 93 getHighlighterAttribute: { 94 request: { 95 nodeID: Arg(0, "string"), 96 name: Arg(1, "string"), 97 actorID: Arg(2, "string"), 98 }, 99 response: { 100 value: RetVal("string"), 101 }, 102 }, 103 getHighlighterBoundingClientRect: { 104 request: { 105 nodeID: Arg(0, "string"), 106 actorID: Arg(1, "string"), 107 }, 108 response: { 109 value: RetVal("json"), 110 }, 111 }, 112 getHighlighterComputedStyle: { 113 request: { 114 nodeID: Arg(0, "string"), 115 property: Arg(1, "string"), 116 actorID: Arg(2, "string"), 117 }, 118 response: { 119 value: RetVal("string"), 120 }, 121 }, 122 getHighlighterNodeTextContent: { 123 request: { 124 nodeID: Arg(0, "string"), 125 actorID: Arg(1, "string"), 126 }, 127 response: { 128 value: RetVal("string"), 129 }, 130 }, 131 getSelectorHighlighterBoxNb: { 132 request: { 133 highlighter: Arg(0, "string"), 134 }, 135 response: { 136 value: RetVal("number"), 137 }, 138 }, 139 changeHighlightedNodeWaitForUpdate: { 140 request: { 141 name: Arg(0, "string"), 142 value: Arg(1, "string"), 143 actorID: Arg(2, "string"), 144 }, 145 response: {}, 146 }, 147 registerOneTimeHighlighterUpdate: { 148 request: { 149 actorID: Arg(0, "string"), 150 }, 151 response: {}, 152 }, 153 getNodeRect: { 154 request: { 155 selector: Arg(0, "string"), 156 }, 157 response: { 158 value: RetVal("json"), 159 }, 160 }, 161 getTextNodeRect: { 162 request: { 163 parentSelector: Arg(0, "string"), 164 childNodeIndex: Arg(1, "number"), 165 }, 166 response: { 167 value: RetVal("json"), 168 }, 169 }, 170 isPausedDebuggerOverlayVisible: { 171 request: {}, 172 response: { 173 value: RetVal("boolean"), 174 }, 175 }, 176 clickPausedDebuggerOverlayButton: { 177 request: { 178 id: Arg(0, "string"), 179 }, 180 response: {}, 181 }, 182 isEyeDropperVisible: { 183 request: {}, 184 response: { 185 value: RetVal("boolean"), 186 }, 187 }, 188 getEyeDropperElementAttribute: { 189 request: { 190 elementId: Arg(0, "string"), 191 attributeName: Arg(1, "string"), 192 }, 193 response: { 194 value: RetVal("string"), 195 }, 196 }, 197 getEyeDropperColorValue: { 198 request: {}, 199 response: { 200 value: RetVal("string"), 201 }, 202 }, 203 getTabbingOrderHighlighterData: { 204 request: {}, 205 response: { 206 value: RetVal("json"), 207 }, 208 }, 209 }, 210 }); 211 212 class HighlighterTestActor extends protocol.Actor { 213 constructor(conn, targetActor) { 214 super(conn, highlighterTestSpec); 215 216 this.targetActor = targetActor; 217 } 218 219 get content() { 220 return this.targetActor.window; 221 } 222 223 /** 224 * Helper to retrieve a DOM element. 225 * 226 * @param {string | Array} selector Either a regular selector string 227 * or a selector array. If an array, each item, except the last one 228 * are considered matching an iframe, so that we can query element 229 * within deep iframes. 230 */ 231 _querySelector(selector) { 232 let document = this.content.document; 233 if (Array.isArray(selector)) { 234 const fullSelector = selector.join(" >> "); 235 while (selector.length > 1) { 236 const str = selector.shift(); 237 const iframe = document.querySelector(str); 238 if (!iframe) { 239 throw new Error( 240 'Unable to find element with selector "' + 241 str + 242 '"' + 243 " (full selector:" + 244 fullSelector + 245 ")" 246 ); 247 } 248 if (!iframe.contentWindow) { 249 throw new Error( 250 "Iframe selector doesn't target an iframe \"" + 251 str + 252 '"' + 253 " (full selector:" + 254 fullSelector + 255 ")" 256 ); 257 } 258 document = iframe.contentWindow.document; 259 } 260 selector = selector.shift(); 261 } 262 const node = document.querySelector(selector); 263 if (!node) { 264 throw new Error( 265 'Unable to find element with selector "' + selector + '"' 266 ); 267 } 268 return node; 269 } 270 271 /** 272 * Get a value for a given attribute name, on one of the elements of the box 273 * model highlighter, given its ID. 274 * 275 * @param {string} nodeID The full ID of the element to get the attribute for 276 * @param {string} name The name of the attribute to get 277 * @param {string} actorID The highlighter actor ID 278 * @return {string} The value, if found, null otherwise 279 */ 280 getHighlighterAttribute(nodeID, name, actorID) { 281 const helper = getHighlighterCanvasFrameHelper(this.conn, actorID); 282 283 if (!helper) { 284 throw new Error(`Highlighter not found`); 285 } 286 287 return helper.getAttributeForElement(nodeID, name); 288 } 289 290 /** 291 * Get the bounding client rect for an highlighter element, given its ID. 292 * 293 * @param {string} nodeID The full ID of the element to get the DOMRect for 294 * @param {string} actorID The highlighter actor ID 295 * @return {DOMRect} The value, if found, null otherwise 296 */ 297 getHighlighterBoundingClientRect(nodeID, actorID) { 298 const helper = getHighlighterCanvasFrameHelper(this.conn, actorID); 299 300 if (!helper) { 301 throw new Error(`Highlighter not found`); 302 } 303 304 return helper.getBoundingClientRect(nodeID); 305 } 306 307 /** 308 * Get the computed style for a given property, on one of the elements of the 309 * box model highlighter, given its ID. 310 * 311 * @param {string} nodeID The full ID of the element to get the attribute for 312 * @param {string} property The name of the property 313 * @param {string} actorID The highlighter actor ID 314 * @return {string} The computed style of the property 315 */ 316 getHighlighterComputedStyle(nodeID, property, actorID) { 317 const helper = getHighlighterCanvasFrameHelper(this.conn, actorID); 318 319 if (!helper) { 320 throw new Error(`Highlighter not found`); 321 } 322 323 return helper.getElement(nodeID).computedStyle.getPropertyValue(property); 324 } 325 326 /** 327 * Get the textcontent of one of the elements of the box model highlighter, 328 * given its ID. 329 * 330 * @param {string} nodeID The full ID of the element to get the attribute for 331 * @param {string} actorID The highlighter actor ID 332 * @return {string} The textcontent value 333 */ 334 getHighlighterNodeTextContent(nodeID, actorID) { 335 let value; 336 const helper = getHighlighterCanvasFrameHelper(this.conn, actorID); 337 if (helper) { 338 value = helper.getTextContentForElement(nodeID); 339 } 340 return value; 341 } 342 343 /** 344 * Get the number of box-model highlighters created by the SelectorHighlighter 345 * 346 * @param {string} actorID The highlighter actor ID 347 * @return {number} The number of box-model highlighters created, or null if the 348 * SelectorHighlighter was not found. 349 */ 350 getSelectorHighlighterBoxNb(actorID) { 351 const highlighter = this.conn.getActor(actorID); 352 const { _highlighter: h } = highlighter; 353 if (!h || !h._highlighters) { 354 return null; 355 } 356 return h._highlighters.length; 357 } 358 359 /** 360 * Subscribe to the box-model highlighter's update event, modify an attribute of 361 * the currently highlighted node and send a message when the highlighter has 362 * updated. 363 * 364 * @param {string} the name of the attribute to be changed 365 * @param {string} the new value for the attribute 366 * @param {string} actorID The highlighter actor ID 367 */ 368 changeHighlightedNodeWaitForUpdate(name, value, actorID) { 369 return new Promise(resolve => { 370 const highlighter = this.conn.getActor(actorID); 371 const { _highlighter: h } = highlighter; 372 373 h.once("updated", resolve); 374 375 h.currentNode.setAttribute(name, value); 376 }); 377 } 378 379 /** 380 * Register a one-time "updated" event listener. 381 * The method does not wait for the "updated" event itself so the response can be sent 382 * back and the client would know the event listener is properly set. 383 * A separate event, "highlighter-updated", will be emitted when the highlighter updates. 384 * 385 * @param {string} actorID The highlighter actor ID 386 */ 387 registerOneTimeHighlighterUpdate(actorID) { 388 const { _highlighter } = this.conn.getActor(actorID); 389 _highlighter.once("updated").then(() => this.emit("highlighter-updated")); 390 391 // Return directly so the client knows the event listener is set 392 } 393 394 async getNodeRect(selector) { 395 const node = this._querySelector(selector); 396 return getRect(this.content, node, this.content); 397 } 398 399 async getTextNodeRect(parentSelector, childNodeIndex) { 400 const parentNode = this._querySelector(parentSelector); 401 const node = parentNode.childNodes[childNodeIndex]; 402 return getAdjustedQuads(this.content, node)[0].bounds; 403 } 404 405 /** 406 * @returns {PausedDebuggerOverlay} The paused overlay instance 407 */ 408 _getPausedDebuggerOverlay() { 409 // We use `_pauseOverlay` since it's the cached value; `pauseOverlay` is a getter that 410 // will create the overlay when called (if it does not exist yet). 411 return this.targetActor?.threadActor?._pauseOverlay; 412 } 413 414 isPausedDebuggerOverlayVisible() { 415 const pauseOverlay = this._getPausedDebuggerOverlay(); 416 if (!pauseOverlay) { 417 return false; 418 } 419 420 const root = pauseOverlay.getElement("paused-dbg-root"); 421 const toolbar = pauseOverlay.getElement("paused-dbg-toolbar"); 422 423 return ( 424 !root.hasAttribute("hidden") && 425 root.getAttribute("overlay") == "true" && 426 !toolbar.hasAttribute("hidden") && 427 !!toolbar.getTextContent() 428 ); 429 } 430 431 /** 432 * Simulates a click on a button of the debugger pause overlay. 433 * 434 * @param {string} id: The id of the element (e.g. "paused-dbg-resume-button"). 435 */ 436 async clickPausedDebuggerOverlayButton(id) { 437 const pauseOverlay = this._getPausedDebuggerOverlay(); 438 if (!pauseOverlay) { 439 return; 440 } 441 442 // Because the highlighter markup elements live inside an anonymous content frame which 443 // does not expose an API to dispatch events to them, we can't directly dispatch 444 // events to the nodes themselves. 445 // We're directly calling `handleEvent` on the pause overlay, which is the mouse events 446 // listener callback on the overlay. 447 pauseOverlay.handleEvent({ type: "mousedown", target: { id } }); 448 } 449 450 /** 451 * @returns {EyeDropper} 452 */ 453 _getEyeDropper() { 454 const form = this.targetActor.form(); 455 const inspectorActor = this.conn._getOrCreateActor(form.inspectorActor); 456 return inspectorActor?._eyeDropper; 457 } 458 459 isEyeDropperVisible() { 460 const eyeDropper = this._getEyeDropper(); 461 if (!eyeDropper) { 462 return false; 463 } 464 465 return ( 466 eyeDropper.getElement("eye-dropper-root").getAttribute("hidden") !== 467 "true" 468 ); 469 } 470 471 getEyeDropperElementAttribute(elementId, attributeName) { 472 const eyeDropper = this._getEyeDropper(); 473 if (!eyeDropper) { 474 return null; 475 } 476 477 return eyeDropper.getElement(elementId).getAttribute(attributeName); 478 } 479 480 async getEyeDropperColorValue() { 481 const eyeDropper = this._getEyeDropper(); 482 if (!eyeDropper) { 483 return null; 484 } 485 486 // It might happen that while the eyedropper isn't hidden anymore, the color-value 487 // is not set yet. 488 const color = await TestUtils.waitForCondition(() => { 489 const colorValueElement = eyeDropper.getElement( 490 "eye-dropper-color-value" 491 ); 492 const textContent = colorValueElement.getTextContent(); 493 return textContent; 494 }, "Couldn't get a non-empty text content for the color-value element"); 495 496 return color; 497 } 498 499 /** 500 * Get the TabbingOrderHighlighter for the associated targetActor 501 * 502 * @returns {TabbingOrderHighlighter} 503 */ 504 _getTabbingOrderHighlighter() { 505 const form = this.targetActor.form(); 506 const accessibilityActor = this.conn._getOrCreateActor( 507 form.accessibilityActor 508 ); 509 510 if (!accessibilityActor) { 511 return null; 512 } 513 // We use `_tabbingOrderHighlighter` since it's the cached value; `tabbingOrderHighlighter` 514 // is a getter that will create the highlighter when called (if it does not exist yet). 515 return accessibilityActor.walker?._tabbingOrderHighlighter; 516 } 517 518 /** 519 * Get a representation of the NodeTabbingOrderHighlighters created by the 520 * TabbingOrderHighlighter of a given targetActor. 521 * 522 * @returns {Array<string>} An array which will contain as many entry as they are 523 * NodeTabbingOrderHighlighters displayed. 524 * Each item will be of the form `nodename[#id]: index`. 525 * For example: 526 * [ 527 * `button#top-btn-1 : 1`, 528 * `html : 2`, 529 * `button#iframe-btn-1 : 3`, 530 * `button#iframe-btn-2 : 4`, 531 * `button#top-btn-2 : 5`, 532 * ] 533 */ 534 getTabbingOrderHighlighterData() { 535 const highlighter = this._getTabbingOrderHighlighter(); 536 if (!highlighter) { 537 return []; 538 } 539 540 const nodeTabbingOrderHighlighters = [ 541 ...highlighter._highlighter._highlighters.values(), 542 ].filter(h => !h.getElement("tabbing-order-root").hasAttribute("hidden")); 543 544 return nodeTabbingOrderHighlighters.map(h => { 545 let nodeStr = h.currentNode.nodeName.toLowerCase(); 546 if (h.currentNode.id) { 547 nodeStr = `${nodeStr}#${h.currentNode.id}`; 548 } 549 return `${nodeStr} : ${h.getElement("tabbing-order-root").getTextContent()}`; 550 }); 551 } 552 } 553 exports.HighlighterTestActor = HighlighterTestActor; 554 555 class HighlighterTestFront extends protocol.FrontClassWithSpec( 556 highlighterTestSpec 557 ) { 558 constructor(client, targetFront, parentFront) { 559 super(client, targetFront, parentFront); 560 this.formAttributeName = "highlighterTestActor"; 561 // The currently active highlighter is obtained by calling a custom getter 562 // provided manually after requesting TestFront. See `getHighlighterTestFront(toolbox)` 563 this._highlighter = null; 564 } 565 566 /** 567 * Override the highlighter getter with a custom method that returns 568 * the currently active highlighter instance. 569 * 570 * @param {Function|Highlighter} _customHighlighterGetter 571 */ 572 set highlighter(_customHighlighterGetter) { 573 this._highlighter = _customHighlighterGetter; 574 } 575 576 /** 577 * The currently active highlighter instance. 578 * If there is a custom getter for the highlighter, return its result. 579 * 580 * @return {Highlighter|null} 581 */ 582 get highlighter() { 583 return typeof this._highlighter === "function" 584 ? this._highlighter() 585 : this._highlighter; 586 } 587 588 /* eslint-disable max-len */ 589 changeHighlightedNodeWaitForUpdate(name, value, highlighter) { 590 /* eslint-enable max-len */ 591 return super.changeHighlightedNodeWaitForUpdate( 592 name, 593 value, 594 (highlighter || this.highlighter).actorID 595 ); 596 } 597 598 /** 599 * Get the value of an attribute on one of the highlighter's node. 600 * 601 * @param {string} nodeID The Id of the node in the highlighter. 602 * @param {string} name The name of the attribute. 603 * @param {object} highlighter Optional custom highlighter to target 604 * @return {string} value 605 */ 606 getHighlighterNodeAttribute(nodeID, name, highlighter) { 607 return this.getHighlighterAttribute( 608 nodeID, 609 name, 610 (highlighter || this.highlighter).actorID 611 ); 612 } 613 614 getHighlighterNodeTextContent(nodeID, highlighter) { 615 return super.getHighlighterNodeTextContent( 616 nodeID, 617 (highlighter || this.highlighter).actorID 618 ); 619 } 620 621 /** 622 * Get the computed style of a property on one of the highlighter's node. 623 * 624 * @param {string} nodeID The Id of the node in the highlighter. 625 * @param {string} property The name of the property. 626 * @param {object} highlighter Optional custom highlighter to target 627 * @return {string} value 628 */ 629 getHighlighterComputedStyle(nodeID, property, highlighter) { 630 return super.getHighlighterComputedStyle( 631 nodeID, 632 property, 633 (highlighter || this.highlighter).actorID 634 ); 635 } 636 637 /** 638 * Is the highlighter currently visible on the page? 639 */ 640 async isHighlighting() { 641 // Once the highlighter is hidden, the reference to it is lost. 642 // Assume it is not highlighting. 643 if (!this.highlighter) { 644 return false; 645 } 646 647 try { 648 const hidden = await this.getHighlighterNodeAttribute( 649 "box-model-elements", 650 "hidden" 651 ); 652 return hidden === null; 653 } catch (e) { 654 if (e.message.match(/Highlighter not found/)) { 655 return false; 656 } 657 throw e; 658 } 659 } 660 661 /** 662 * Get the current rect of the border region of the box-model highlighter 663 */ 664 async getSimpleBorderRect() { 665 const { border } = await this.getBoxModelStatus(); 666 const { p1, p2, p4 } = border.points; 667 668 return { 669 top: p1.y, 670 left: p1.x, 671 width: p2.x - p1.x, 672 height: p4.y - p1.y, 673 }; 674 } 675 676 /** 677 * Get the current positions and visibility of the various box-model highlighter 678 * elements. 679 */ 680 async getBoxModelStatus() { 681 const isVisible = await this.isHighlighting(); 682 683 const ret = { 684 visible: isVisible, 685 }; 686 687 for (const region of ["margin", "border", "padding", "content"]) { 688 const points = await this._getPointsForRegion(region); 689 const visible = await this._isRegionHidden(region); 690 ret[region] = { points, visible }; 691 } 692 693 ret.guides = {}; 694 for (const guide of ["top", "right", "bottom", "left"]) { 695 ret.guides[guide] = await this._getGuideStatus(guide); 696 } 697 698 return ret; 699 } 700 701 /** 702 * Check that the box-model highlighter is currently highlighting the node matching the 703 * given selector. 704 * 705 * @param {string} selector 706 * @return {boolean} 707 */ 708 async assertHighlightedNode(selector) { 709 const rect = await this.getNodeRect(selector); 710 return this.isNodeRectHighlighted(rect); 711 } 712 713 /** 714 * Check that the box-model highlighter is currently highlighting the text node that can 715 * be found at a given index within the list of childNodes of a parent element matching 716 * the given selector. 717 * 718 * @param {string} parentSelector 719 * @param {number} childNodeIndex 720 * @return {boolean} 721 */ 722 async assertHighlightedTextNode(parentSelector, childNodeIndex) { 723 const rect = await this.getTextNodeRect(parentSelector, childNodeIndex); 724 return this.isNodeRectHighlighted(rect); 725 } 726 727 /** 728 * Check that the box-model highlighter is currently highlighting the given rect. 729 * 730 * @param {object} rect 731 * @return {boolean} 732 */ 733 async isNodeRectHighlighted({ left, top, width, height }) { 734 const { visible, border } = await this.getBoxModelStatus(); 735 let points = border.points; 736 if (!visible) { 737 return false; 738 } 739 740 // Check that the node is within the box model 741 const right = left + width; 742 const bottom = top + height; 743 744 // Converts points dictionnary into an array 745 const list = []; 746 for (let i = 1; i <= 4; i++) { 747 const p = points["p" + i]; 748 list.push([p.x, p.y]); 749 } 750 points = list; 751 752 // Check that each point of the node is within the box model 753 return ( 754 isInside([left, top], points) && 755 isInside([right, top], points) && 756 isInside([right, bottom], points) && 757 isInside([left, bottom], points) 758 ); 759 } 760 761 /** 762 * Get the coordinate (points attribute) from one of the polygon elements in the 763 * box model highlighter. 764 */ 765 async _getPointsForRegion(region) { 766 const d = await this.getHighlighterNodeAttribute( 767 "box-model-" + region, 768 "d" 769 ); 770 771 if (!d) { 772 return null; 773 } 774 775 const polygons = d.match(/M[^M]+/g); 776 if (!polygons) { 777 return null; 778 } 779 780 const points = polygons[0] 781 .trim() 782 .split(" ") 783 .map(i => { 784 return i.replace(/M|L/, "").split(","); 785 }); 786 787 return { 788 p1: { 789 x: parseFloat(points[0][0]), 790 y: parseFloat(points[0][1]), 791 }, 792 p2: { 793 x: parseFloat(points[1][0]), 794 y: parseFloat(points[1][1]), 795 }, 796 p3: { 797 x: parseFloat(points[2][0]), 798 y: parseFloat(points[2][1]), 799 }, 800 p4: { 801 x: parseFloat(points[3][0]), 802 y: parseFloat(points[3][1]), 803 }, 804 }; 805 } 806 807 /** 808 * Is a given region polygon element of the box-model highlighter currently 809 * hidden? 810 */ 811 async _isRegionHidden(region) { 812 const value = await this.getHighlighterNodeAttribute( 813 "box-model-" + region, 814 "hidden" 815 ); 816 return value !== null; 817 } 818 819 async _getGuideStatus(location) { 820 const id = "box-model-guide-" + location; 821 822 const hidden = await this.getHighlighterNodeAttribute(id, "hidden"); 823 const x1 = await this.getHighlighterNodeAttribute(id, "x1"); 824 const y1 = await this.getHighlighterNodeAttribute(id, "y1"); 825 const x2 = await this.getHighlighterNodeAttribute(id, "x2"); 826 const y2 = await this.getHighlighterNodeAttribute(id, "y2"); 827 828 return { 829 visible: !hidden, 830 x1, 831 y1, 832 x2, 833 y2, 834 }; 835 } 836 837 /** 838 * Get the coordinates of the rectangle that is defined by the 4 guides displayed 839 * in the toolbox box-model highlighter. 840 * 841 * @return {object} Null if at least one guide is hidden. Otherwise an object 842 * with p1, p2, p3, p4 properties being {x, y} objects. 843 */ 844 async getGuidesRectangle() { 845 const tGuide = await this._getGuideStatus("top"); 846 const rGuide = await this._getGuideStatus("right"); 847 const bGuide = await this._getGuideStatus("bottom"); 848 const lGuide = await this._getGuideStatus("left"); 849 850 if ( 851 !tGuide.visible || 852 !rGuide.visible || 853 !bGuide.visible || 854 !lGuide.visible 855 ) { 856 return null; 857 } 858 859 return { 860 p1: { x: lGuide.x1, y: tGuide.y1 }, 861 p2: { x: +rGuide.x1 + 1, y: tGuide.y1 }, 862 p3: { x: +rGuide.x1 + 1, y: +bGuide.y1 + 1 }, 863 p4: { x: lGuide.x1, y: +bGuide.y1 + 1 }, 864 }; 865 } 866 867 /** 868 * Get the "d" attribute value for one of the box-model highlighter's region 869 * <path> elements, and parse it to a list of points. 870 * 871 * @param {string} region The box model region name. 872 * @param {Front} highlighter The front of the highlighter. 873 * @return {object} The object returned has the following form: 874 * - d {String} the d attribute value 875 * - points {Array} an array of all the polygons defined by the path. Each box 876 * is itself an Array of points, themselves being [x,y] coordinates arrays. 877 */ 878 async getHighlighterRegionPath(region, highlighter) { 879 const d = await this.getHighlighterNodeAttribute( 880 `box-model-${region}`, 881 "d", 882 highlighter 883 ); 884 if (!d) { 885 return { d: null }; 886 } 887 888 const polygons = d.match(/M[^M]+/g); 889 if (!polygons) { 890 return { d }; 891 } 892 893 const points = []; 894 for (const polygon of polygons) { 895 points.push( 896 polygon 897 .trim() 898 .split(" ") 899 .map(i => { 900 return i.replace(/M|L/, "").split(","); 901 }) 902 ); 903 } 904 905 return { d, points }; 906 } 907 } 908 protocol.registerFront(HighlighterTestFront); 909 /** 910 * Check whether a point is included in a polygon. 911 * Taken and tweaked from: 912 * https://github.com/iominh/point-in-polygon-extended/blob/master/src/index.js#L30-L85 913 * 914 * @param {Array} point [x,y] coordinates 915 * @param {Array} polygon An array of [x,y] points 916 * @return {boolean} 917 */ 918 function isInside(point, polygon) { 919 if (polygon.length === 0) { 920 return false; 921 } 922 923 // Reduce the length of the fractional part because this is likely to cause errors when 924 // the point is on the edge of the polygon. 925 point = point.map(n => n.toFixed(2)); 926 polygon = polygon.map(p => p.map(n => n.toFixed(2))); 927 928 const n = polygon.length; 929 const newPoints = polygon.slice(0); 930 newPoints.push(polygon[0]); 931 let wn = 0; 932 933 // loop through all edges of the polygon 934 for (let i = 0; i < n; i++) { 935 // Accept points on the edges 936 const r = isLeft(newPoints[i], newPoints[i + 1], point); 937 if (r === 0) { 938 return true; 939 } 940 if (newPoints[i][1] <= point[1]) { 941 if (newPoints[i + 1][1] > point[1] && r > 0) { 942 wn++; 943 } 944 } else if (newPoints[i + 1][1] <= point[1] && r < 0) { 945 wn--; 946 } 947 } 948 if (wn === 0) { 949 dumpn(JSON.stringify(point) + " is outside of " + JSON.stringify(polygon)); 950 } 951 // the point is outside only when this winding number wn===0, otherwise it's inside 952 return wn !== 0; 953 } 954 955 function isLeft(p0, p1, p2) { 956 const l = 957 (p1[0] - p0[0]) * (p2[1] - p0[1]) - (p2[0] - p0[0]) * (p1[1] - p0[1]); 958 return l; 959 }