browser-pageActions.js (30797B)
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 var BrowserPageActions = { 6 _panelNode: null, 7 /** 8 * The main page action button in the urlbar (DOM node) 9 */ 10 get mainButtonNode() { 11 delete this.mainButtonNode; 12 return (this.mainButtonNode = document.getElementById("pageActionButton")); 13 }, 14 15 /** 16 * The main page action panel DOM node (DOM node) 17 */ 18 get panelNode() { 19 // Lazy load the page action panel the first time we need to display it 20 if (!this._panelNode) { 21 this.initializePanel(); 22 } 23 delete this.panelNode; 24 return (this.panelNode = this._panelNode); 25 }, 26 27 /** 28 * The panelmultiview node in the main page action panel (DOM node) 29 */ 30 get multiViewNode() { 31 delete this.multiViewNode; 32 return (this.multiViewNode = document.getElementById( 33 "pageActionPanelMultiView" 34 )); 35 }, 36 37 /** 38 * The main panelview node in the main page action panel (DOM node) 39 */ 40 get mainViewNode() { 41 delete this.mainViewNode; 42 return (this.mainViewNode = document.getElementById( 43 "pageActionPanelMainView" 44 )); 45 }, 46 47 /** 48 * The vbox body node in the main panelview node (DOM node) 49 */ 50 get mainViewBodyNode() { 51 delete this.mainViewBodyNode; 52 return (this.mainViewBodyNode = this.mainViewNode.querySelector( 53 ".panel-subview-body" 54 )); 55 }, 56 57 /** 58 * Inits. Call to init. 59 */ 60 init() { 61 this.placeAllActionsInUrlbar(); 62 this._onPanelShowing = this._onPanelShowing.bind(this); 63 }, 64 65 _onPanelShowing() { 66 this.initializePanel(); 67 for (let action of PageActions.actionsInPanel(window)) { 68 let buttonNode = this.panelButtonNodeForActionID(action.id); 69 action.onShowingInPanel(buttonNode); 70 } 71 }, 72 73 placeLazyActionsInPanel() { 74 let actions = this._actionsToLazilyPlaceInPanel; 75 this._actionsToLazilyPlaceInPanel = []; 76 for (let action of actions) { 77 this._placeActionInPanelNow(action); 78 } 79 }, 80 81 // Actions placed in the panel aren't actually placed until the panel is 82 // subsequently opened. 83 _actionsToLazilyPlaceInPanel: [], 84 85 /** 86 * Places all registered actions in the urlbar. 87 */ 88 placeAllActionsInUrlbar() { 89 let urlbarActions = PageActions.actionsInUrlbar(window); 90 for (let action of urlbarActions) { 91 this.placeActionInUrlbar(action); 92 } 93 this._updateMainButtonAttributes(); 94 }, 95 96 /** 97 * Initializes the panel if necessary. 98 */ 99 initializePanel() { 100 // Lazy load the page action panel the first time we need to display it 101 if (!this._panelNode) { 102 let template = document.getElementById("pageActionPanelTemplate"); 103 template.replaceWith(template.content); 104 this._panelNode = document.getElementById("pageActionPanel"); 105 this._panelNode.addEventListener("popupshowing", this._onPanelShowing); 106 } 107 108 for (let action of PageActions.actionsInPanel(window)) { 109 this.placeActionInPanel(action); 110 } 111 this.placeLazyActionsInPanel(); 112 }, 113 114 /** 115 * Adds or removes as necessary DOM nodes for the given action. 116 * 117 * @param action (PageActions.Action, required) 118 * The action to place. 119 */ 120 placeAction(action) { 121 this.placeActionInPanel(action); 122 this.placeActionInUrlbar(action); 123 this._updateMainButtonAttributes(); 124 }, 125 126 /** 127 * Adds or removes as necessary DOM nodes for the action in the panel. 128 * 129 * @param action (PageActions.Action, required) 130 * The action to place. 131 */ 132 placeActionInPanel(action) { 133 if (this._panelNode && this.panelNode.state != "closed") { 134 this._placeActionInPanelNow(action); 135 } else { 136 // This method may be called for the same action more than once 137 // (e.g. when an extension does call pageAction.show/hidden to 138 // enable or disable its own pageAction and we will have to 139 // update the urlbar overflow panel accordingly). 140 // 141 // Ensure we don't add the same actions more than once (otherwise we will 142 // not remove all the entries in _removeActionFromPanel). 143 if ( 144 this._actionsToLazilyPlaceInPanel.findIndex(a => a.id == action.id) >= 0 145 ) { 146 return; 147 } 148 // Lazily place the action in the panel the next time it opens. 149 this._actionsToLazilyPlaceInPanel.push(action); 150 } 151 }, 152 153 _placeActionInPanelNow(action) { 154 if (action.shouldShowInPanel(window)) { 155 this._addActionToPanel(action); 156 } else { 157 this._removeActionFromPanel(action); 158 } 159 }, 160 161 _addActionToPanel(action) { 162 let id = this.panelButtonNodeIDForActionID(action.id); 163 let node = document.getElementById(id); 164 if (node) { 165 return; 166 } 167 this._maybeNotifyBeforePlacedInWindow(action); 168 node = this._makePanelButtonNodeForAction(action); 169 node.id = id; 170 let insertBeforeNode = this._getNextNode(action, false); 171 this.mainViewBodyNode.insertBefore(node, insertBeforeNode); 172 this.updateAction(action, null, { 173 panelNode: node, 174 }); 175 this._updateActionDisabledInPanel(action, node); 176 action.onPlacedInPanel(node); 177 this._addOrRemoveSeparatorsInPanel(); 178 }, 179 180 _removeActionFromPanel(action) { 181 let lazyIndex = this._actionsToLazilyPlaceInPanel.findIndex( 182 a => a.id == action.id 183 ); 184 if (lazyIndex >= 0) { 185 this._actionsToLazilyPlaceInPanel.splice(lazyIndex, 1); 186 } 187 let node = this.panelButtonNodeForActionID(action.id); 188 if (!node) { 189 return; 190 } 191 node.remove(); 192 if (action.getWantsSubview(window)) { 193 let panelViewNodeID = this._panelViewNodeIDForActionID(action.id, false); 194 let panelViewNode = document.getElementById(panelViewNodeID); 195 if (panelViewNode) { 196 panelViewNode.remove(); 197 } 198 } 199 this._addOrRemoveSeparatorsInPanel(); 200 }, 201 202 _addOrRemoveSeparatorsInPanel() { 203 let actions = PageActions.actionsInPanel(window); 204 let ids = [ 205 PageActions.ACTION_ID_BUILT_IN_SEPARATOR, 206 PageActions.ACTION_ID_TRANSIENT_SEPARATOR, 207 ]; 208 for (let id of ids) { 209 let sep = actions.find(a => a.id == id); 210 if (sep) { 211 this._addActionToPanel(sep); 212 } else { 213 let node = this.panelButtonNodeForActionID(id); 214 if (node) { 215 node.remove(); 216 } 217 } 218 } 219 }, 220 221 _updateMainButtonAttributes() { 222 this.mainButtonNode.toggleAttribute( 223 "multiple-children", 224 PageActions.actions.length > 1 225 ); 226 }, 227 228 /** 229 * Returns the node before which an action's node should be inserted. 230 * 231 * @param action (PageActions.Action, required) 232 * The action that will be inserted. 233 * @param forUrlbar (bool, required) 234 * True if you're inserting into the urlbar, false if you're inserting 235 * into the panel. 236 * @return (DOM node, maybe null) The DOM node before which to insert the 237 * given action. Null if the action should be inserted at the end. 238 */ 239 _getNextNode(action, forUrlbar) { 240 let actions = forUrlbar 241 ? PageActions.actionsInUrlbar(window) 242 : PageActions.actionsInPanel(window); 243 let index = actions.findIndex(a => a.id == action.id); 244 if (index < 0) { 245 return null; 246 } 247 for (let i = index + 1; i < actions.length; i++) { 248 let node = forUrlbar 249 ? this.urlbarButtonNodeForActionID(actions[i].id) 250 : this.panelButtonNodeForActionID(actions[i].id); 251 if (node) { 252 return node; 253 } 254 } 255 return null; 256 }, 257 258 _maybeNotifyBeforePlacedInWindow(action) { 259 if (!this._isActionPlacedInWindow(action)) { 260 action.onBeforePlacedInWindow(window); 261 } 262 }, 263 264 _isActionPlacedInWindow(action) { 265 if (this.panelButtonNodeForActionID(action.id)) { 266 return true; 267 } 268 let urlbarNode = this.urlbarButtonNodeForActionID(action.id); 269 return urlbarNode && !urlbarNode.hidden; 270 }, 271 272 _makePanelButtonNodeForAction(action) { 273 if (action.__isSeparator) { 274 let node = document.createXULElement("toolbarseparator"); 275 return node; 276 } 277 let buttonNode = document.createXULElement("toolbarbutton"); 278 buttonNode.classList.add( 279 "subviewbutton", 280 "subviewbutton-iconic", 281 "pageAction-panel-button" 282 ); 283 if (action.isBadged) { 284 buttonNode.setAttribute("badged", "true"); 285 } 286 buttonNode.setAttribute("actionid", action.id); 287 buttonNode.addEventListener("command", event => { 288 this.doCommandForAction(action, event, buttonNode); 289 }); 290 return buttonNode; 291 }, 292 293 _makePanelViewNodeForAction(action, forUrlbar) { 294 let panelViewNode = document.createXULElement("panelview"); 295 panelViewNode.id = this._panelViewNodeIDForActionID(action.id, forUrlbar); 296 panelViewNode.classList.add("PanelUI-subView"); 297 let bodyNode = document.createXULElement("vbox"); 298 bodyNode.id = panelViewNode.id + "-body"; 299 bodyNode.classList.add("panel-subview-body"); 300 panelViewNode.appendChild(bodyNode); 301 return panelViewNode; 302 }, 303 304 /** 305 * Shows or hides a panel for an action. You can supply your own panel; 306 * otherwise one is created. 307 * 308 * @param action (PageActions.Action, required) 309 * The action for which to toggle the panel. If the action is in the 310 * urlbar, then the panel will be anchored to it. Otherwise, a 311 * suitable anchor will be used. 312 * @param panelNode (DOM node, optional) 313 * The panel to use. This method takes a hands-off approach with 314 * regard to your panel in terms of attributes, styling, etc. 315 * @param event (DOM event, optional) 316 * The event which triggered this panel. 317 */ 318 togglePanelForAction(action, panelNode = null, event = null) { 319 let aaPanelNode = this.activatedActionPanelNode; 320 if (panelNode) { 321 // Note that this particular code path will not prevent the panel from 322 // opening later if PanelMultiView.showPopup was called but the panel has 323 // not been opened yet. 324 if (panelNode.state != "closed") { 325 PanelMultiView.hidePopup(panelNode); 326 return; 327 } 328 if (aaPanelNode) { 329 PanelMultiView.hidePopup(aaPanelNode); 330 } 331 } else if (aaPanelNode) { 332 PanelMultiView.hidePopup(aaPanelNode); 333 return; 334 } else { 335 panelNode = this._makeActivatedActionPanelForAction(action); 336 } 337 338 // Hide the main panel before showing the action's panel. 339 PanelMultiView.hidePopup(this.panelNode); 340 341 let anchorNode = this.panelAnchorNodeForAction(action); 342 PanelMultiView.openPopup(panelNode, anchorNode, { 343 position: "bottomright topright", 344 triggerEvent: event, 345 }).catch(console.error); 346 }, 347 348 _makeActivatedActionPanelForAction(action) { 349 let panelNode = document.createXULElement("panel"); 350 panelNode.id = this._activatedActionPanelID; 351 panelNode.classList.add("cui-widget-panel", "panel-no-padding"); 352 panelNode.setAttribute("actionID", action.id); 353 panelNode.setAttribute("role", "group"); 354 panelNode.setAttribute("type", "arrow"); 355 panelNode.setAttribute("flip", "slide"); 356 panelNode.setAttribute("noautofocus", "true"); 357 panelNode.setAttribute("tabspecific", "true"); 358 359 let panelViewNode = null; 360 let iframeNode = null; 361 362 if (action.getWantsSubview(window)) { 363 let multiViewNode = document.createXULElement("panelmultiview"); 364 panelViewNode = this._makePanelViewNodeForAction(action, true); 365 multiViewNode.setAttribute("mainViewId", panelViewNode.id); 366 multiViewNode.appendChild(panelViewNode); 367 panelNode.appendChild(multiViewNode); 368 } else if (action.wantsIframe) { 369 iframeNode = document.createXULElement("iframe"); 370 iframeNode.setAttribute("type", "content"); 371 panelNode.appendChild(iframeNode); 372 } 373 374 let popupSet = document.getElementById("mainPopupSet"); 375 popupSet.appendChild(panelNode); 376 panelNode.addEventListener( 377 "popuphidden", 378 () => { 379 PanelMultiView.removePopup(panelNode); 380 }, 381 { once: true } 382 ); 383 384 if (iframeNode) { 385 panelNode.addEventListener( 386 "popupshowing", 387 () => { 388 action.onIframeShowing(iframeNode, panelNode); 389 }, 390 { once: true } 391 ); 392 panelNode.addEventListener( 393 "popupshown", 394 () => { 395 iframeNode.focus(); 396 }, 397 { once: true } 398 ); 399 panelNode.addEventListener( 400 "popuphiding", 401 () => { 402 action.onIframeHiding(iframeNode, panelNode); 403 }, 404 { once: true } 405 ); 406 panelNode.addEventListener( 407 "popuphidden", 408 () => { 409 action.onIframeHidden(iframeNode, panelNode); 410 }, 411 { once: true } 412 ); 413 } 414 415 if (panelViewNode) { 416 action.onSubviewPlaced(panelViewNode); 417 panelNode.addEventListener( 418 "popupshowing", 419 () => { 420 action.onSubviewShowing(panelViewNode); 421 }, 422 { once: true } 423 ); 424 } 425 426 return panelNode; 427 }, 428 429 /** 430 * Returns the node in the urlbar to which popups for the given action should 431 * be anchored. If the action is null, a sensible anchor is returned. 432 * 433 * @param action (PageActions.Action, optional) 434 * The action you want to anchor. 435 * @param event (DOM event, optional) 436 * This is used to display the feedback panel on the right node when 437 * the command can be invoked from both the main panel and another 438 * location, such as an activated action panel or a button. 439 * @return (DOM node) The node to which the action should be anchored. 440 */ 441 panelAnchorNodeForAction(action, event) { 442 if (event && event.target.closest("panel") == this.panelNode) { 443 return this.mainButtonNode; 444 } 445 446 // Try each of the following nodes in order, using the first that's visible. 447 let potentialAnchorNodes = [ 448 document.getElementById(action?.anchorIDOverride), 449 document.getElementById( 450 action && this.urlbarButtonNodeIDForActionID(action.id) 451 ), 452 document.getElementById(this.mainButtonNode.id), 453 document.getElementById("identity-icon"), 454 ]; 455 for (let node of potentialAnchorNodes) { 456 if (node && !node.hidden) { 457 let bounds = window.windowUtils.getBoundsWithoutFlushing(node); 458 if (bounds.height > 0 && bounds.width > 0) { 459 return node; 460 } 461 } 462 } 463 let id = action ? action.id : "<no action>"; 464 throw new Error(`PageActions: No anchor node for ${id}`); 465 }, 466 467 get activatedActionPanelNode() { 468 return document.getElementById(this._activatedActionPanelID); 469 }, 470 471 get _activatedActionPanelID() { 472 return "pageActionActivatedActionPanel"; 473 }, 474 475 /** 476 * Adds or removes as necessary a DOM node for the given action in the urlbar. 477 * 478 * @param action (PageActions.Action, required) 479 * The action to place. 480 */ 481 placeActionInUrlbar(action) { 482 let id = this.urlbarButtonNodeIDForActionID(action.id); 483 let node = document.getElementById(id); 484 485 if (!action.shouldShowInUrlbar(window)) { 486 if (node) { 487 if (action.__urlbarNodeInMarkup) { 488 node.hidden = true; 489 } else { 490 node.remove(); 491 } 492 } 493 return; 494 } 495 496 let newlyPlaced = false; 497 if (action.__urlbarNodeInMarkup) { 498 this._maybeNotifyBeforePlacedInWindow(action); 499 // Allow the consumer to add the node in response to the 500 // onBeforePlacedInWindow notification. 501 node = document.getElementById(id); 502 if (!node) { 503 return; 504 } 505 newlyPlaced = node.hidden; 506 node.hidden = false; 507 } else if (!node) { 508 newlyPlaced = true; 509 this._maybeNotifyBeforePlacedInWindow(action); 510 node = this._makeUrlbarButtonNode(action); 511 node.id = id; 512 } 513 514 if (!newlyPlaced) { 515 return; 516 } 517 518 let insertBeforeNode = this._getNextNode(action, true); 519 this.mainButtonNode.parentNode.insertBefore(node, insertBeforeNode); 520 this.updateAction(action, null, { 521 urlbarNode: node, 522 }); 523 action.onPlacedInUrlbar(node); 524 }, 525 526 _makeUrlbarButtonNode(action) { 527 let buttonNode = document.createXULElement("hbox"); 528 buttonNode.classList.add("urlbar-page-action"); 529 if (action.extensionID) { 530 buttonNode.classList.add("urlbar-addon-page-action"); 531 } 532 buttonNode.setAttribute("actionid", action.id); 533 buttonNode.setAttribute("role", "button"); 534 let commandHandler = event => { 535 this.doCommandForAction(action, event, buttonNode); 536 }; 537 buttonNode.addEventListener("click", commandHandler); 538 buttonNode.addEventListener("keypress", commandHandler); 539 540 let imageNode = document.createXULElement("image"); 541 imageNode.classList.add("urlbar-icon"); 542 buttonNode.appendChild(imageNode); 543 return buttonNode; 544 }, 545 546 /** 547 * Removes all the DOM nodes of the given action. 548 * 549 * @param action (PageActions.Action, required) 550 * The action to remove. 551 */ 552 removeAction(action) { 553 this._removeActionFromPanel(action); 554 this._removeActionFromUrlbar(action); 555 action.onRemovedFromWindow(window); 556 this._updateMainButtonAttributes(); 557 }, 558 559 _removeActionFromUrlbar(action) { 560 let node = this.urlbarButtonNodeForActionID(action.id); 561 if (node) { 562 node.remove(); 563 } 564 }, 565 566 /** 567 * Updates the DOM nodes of an action to reflect either a changed property or 568 * all properties. 569 * 570 * @param action (PageActions.Action, required) 571 * The action to update. 572 * @param propertyName (string, optional) 573 * The name of the property to update. If not given, then DOM nodes 574 * will be updated to reflect the current values of all properties. 575 * @param opts (object, optional) 576 * - panelNode: The action's node in the panel to update. 577 * - urlbarNode: The action's node in the urlbar to update. 578 * - value: If a property name is passed, this argument may contain 579 * its current value, in order to prevent a further look-up. 580 */ 581 updateAction(action, propertyName = null, opts = {}) { 582 let anyNodeGiven = "panelNode" in opts || "urlbarNode" in opts; 583 let panelNode = anyNodeGiven 584 ? opts.panelNode || null 585 : this.panelButtonNodeForActionID(action.id); 586 let urlbarNode = anyNodeGiven 587 ? opts.urlbarNode || null 588 : this.urlbarButtonNodeForActionID(action.id); 589 let value = opts.value || undefined; 590 if (propertyName) { 591 this[this._updateMethods[propertyName]]( 592 action, 593 panelNode, 594 urlbarNode, 595 value 596 ); 597 } else { 598 for (let name of ["iconURL", "title", "tooltip", "wantsSubview"]) { 599 this[this._updateMethods[name]](action, panelNode, urlbarNode, value); 600 } 601 } 602 }, 603 604 _updateMethods: { 605 disabled: "_updateActionDisabled", 606 iconURL: "_updateActionIconURL", 607 title: "_updateActionLabeling", 608 tooltip: "_updateActionTooltip", 609 wantsSubview: "_updateActionWantsSubview", 610 }, 611 612 _updateActionDisabled( 613 action, 614 panelNode, 615 urlbarNode, 616 disabled = action.getDisabled(window) 617 ) { 618 // Extension page actions should behave like a transient action, 619 // and be hidden from the urlbar overflow menu if they 620 // are disabled (as in the urlbar when the overflow menu isn't available) 621 // 622 // TODO(Bug 1704139): as a follow up we may look into just set on all 623 // extension pageActions `_transient: true`, at least once we sunset 624 // the proton preference and we don't need the pre-Proton behavior anymore, 625 // and remove this special case. 626 const isProtonExtensionAction = action.extensionID; 627 628 if (action.__transient || isProtonExtensionAction) { 629 this.placeActionInPanel(action); 630 } else { 631 this._updateActionDisabledInPanel(action, panelNode, disabled); 632 } 633 this.placeActionInUrlbar(action); 634 }, 635 636 _updateActionDisabledInPanel( 637 action, 638 panelNode, 639 disabled = action.getDisabled(window) 640 ) { 641 if (panelNode) { 642 if (disabled) { 643 panelNode.setAttribute("disabled", "true"); 644 } else { 645 panelNode.removeAttribute("disabled"); 646 } 647 } 648 }, 649 650 _updateActionIconURL( 651 action, 652 panelNode, 653 urlbarNode, 654 properties = action.getIconProperties(window) 655 ) { 656 for (let [prop, value] of Object.entries(properties)) { 657 if (panelNode) { 658 panelNode.style.setProperty(prop, value); 659 } 660 if (urlbarNode) { 661 urlbarNode.style.setProperty(prop, value); 662 } 663 } 664 }, 665 666 _updateActionLabeling( 667 action, 668 panelNode, 669 urlbarNode, 670 title = action.getTitle(window) 671 ) { 672 if (panelNode) { 673 panelNode.setAttribute("label", title); 674 } 675 if (urlbarNode) { 676 urlbarNode.setAttribute("aria-label", title); 677 // tooltiptext falls back to the title, so update it too if necessary. 678 let tooltip = action.getTooltip(window); 679 if (!tooltip) { 680 urlbarNode.setAttribute("tooltiptext", title); 681 } 682 } 683 }, 684 685 _updateActionTooltip( 686 action, 687 panelNode, 688 urlbarNode, 689 tooltip = action.getTooltip(window) 690 ) { 691 if (urlbarNode) { 692 if (!tooltip) { 693 tooltip = action.getTitle(window); 694 } 695 if (tooltip) { 696 urlbarNode.setAttribute("tooltiptext", tooltip); 697 } 698 } 699 }, 700 701 _updateActionWantsSubview( 702 action, 703 panelNode, 704 urlbarNode, 705 wantsSubview = action.getWantsSubview(window) 706 ) { 707 if (!panelNode) { 708 return; 709 } 710 let panelViewID = this._panelViewNodeIDForActionID(action.id, false); 711 let panelViewNode = document.getElementById(panelViewID); 712 panelNode.classList.toggle("subviewbutton-nav", wantsSubview); 713 if (!wantsSubview) { 714 if (panelViewNode) { 715 panelViewNode.remove(); 716 } 717 return; 718 } 719 if (!panelViewNode) { 720 panelViewNode = this._makePanelViewNodeForAction(action, false); 721 this.multiViewNode.appendChild(panelViewNode); 722 action.onSubviewPlaced(panelViewNode); 723 } 724 }, 725 726 doCommandForAction(action, event, buttonNode) { 727 if (event && event.type == "click" && event.button != 0) { 728 return; 729 } 730 if (event && event.type == "keypress") { 731 if (event.key != " " && event.key != "Enter") { 732 return; 733 } 734 event.stopPropagation(); 735 } 736 // If we're in the panel, open a subview inside the panel: 737 // Note that we can't use this.panelNode.contains(buttonNode) here 738 // because of XBL boundaries breaking Element.contains. 739 if ( 740 action.getWantsSubview(window) && 741 buttonNode && 742 buttonNode.closest("panel") == this.panelNode 743 ) { 744 let panelViewNodeID = this._panelViewNodeIDForActionID(action.id, false); 745 let panelViewNode = document.getElementById(panelViewNodeID); 746 action.onSubviewShowing(panelViewNode); 747 this.multiViewNode.showSubView(panelViewNode, buttonNode); 748 return; 749 } 750 // Otherwise, hide the main popup in case it was open: 751 PanelMultiView.hidePopup(this.panelNode); 752 753 let aaPanelNode = this.activatedActionPanelNode; 754 if (!aaPanelNode || aaPanelNode.getAttribute("actionID") != action.id) { 755 action.onCommand(event, buttonNode); 756 } 757 if (action.getWantsSubview(window) || action.wantsIframe) { 758 this.togglePanelForAction(action, null, event); 759 } 760 }, 761 762 /** 763 * Returns the action for a node. 764 * 765 * @param node (DOM node, required) 766 * A button DOM node, either one that's shown in the page action panel 767 * or the urlbar. 768 * @return (PageAction.Action) If the node has a related action and the action 769 * is not a separator, then the action is returned. Otherwise null is 770 * returned. 771 */ 772 actionForNode(node) { 773 if (!node) { 774 return null; 775 } 776 let actionID = this._actionIDForNodeID(node.id); 777 let action = PageActions.actionForID(actionID); 778 if (!action) { 779 // When a page action is clicked, `node` will be an ancestor of 780 // a node corresponding to an action. `node` will be the page action node 781 // itself when a page action is selected with the keyboard. That's because 782 // the semantic meaning of page action is on an hbox that contains an 783 // <image>. 784 for (let n = node.parentNode; n && !action; n = n.parentNode) { 785 if (n.id == "page-action-buttons" || n.localName == "panelview") { 786 // We reached the page-action-buttons or panelview container. 787 // Stop looking; no action was found. 788 break; 789 } 790 actionID = this._actionIDForNodeID(n.id); 791 action = PageActions.actionForID(actionID); 792 } 793 } 794 return action && !action.__isSeparator ? action : null; 795 }, 796 797 /** 798 * The given action's top-level button in the main panel. 799 * 800 * @param actionID (string, required) 801 * The action ID. 802 * @return (DOM node) The action's button in the main panel. 803 */ 804 panelButtonNodeForActionID(actionID) { 805 return document.getElementById(this.panelButtonNodeIDForActionID(actionID)); 806 }, 807 808 /** 809 * The ID of the given action's top-level button in the main panel. 810 * 811 * @param actionID (string, required) 812 * The action ID. 813 * @return (string) The ID of the action's button in the main panel. 814 */ 815 panelButtonNodeIDForActionID(actionID) { 816 return `pageAction-panel-${actionID}`; 817 }, 818 819 /** 820 * The given action's button in the urlbar. 821 * 822 * @param actionID (string, required) 823 * The action ID. 824 * @return (DOM node) The action's urlbar button node. 825 */ 826 urlbarButtonNodeForActionID(actionID) { 827 return document.getElementById( 828 this.urlbarButtonNodeIDForActionID(actionID) 829 ); 830 }, 831 832 /** 833 * The ID of the given action's button in the urlbar. 834 * 835 * @param actionID (string, required) 836 * The action ID. 837 * @return (string) The ID of the action's urlbar button node. 838 */ 839 urlbarButtonNodeIDForActionID(actionID) { 840 let action = PageActions.actionForID(actionID); 841 if (action && action.urlbarIDOverride) { 842 return action.urlbarIDOverride; 843 } 844 return `pageAction-urlbar-${actionID}`; 845 }, 846 847 // The ID of the given action's panelview. 848 _panelViewNodeIDForActionID(actionID, forUrlbar) { 849 let placementID = forUrlbar ? "urlbar" : "panel"; 850 return `pageAction-${placementID}-${actionID}-subview`; 851 }, 852 853 // The ID of the action corresponding to the given top-level button in the 854 // panel or button in the urlbar. 855 _actionIDForNodeID(nodeID) { 856 if (!nodeID) { 857 return null; 858 } 859 let match = nodeID.match(/^pageAction-(?:panel|urlbar)-(.+)$/); 860 if (match) { 861 return match[1]; 862 } 863 // Check all the urlbar ID overrides. 864 for (let action of PageActions.actions) { 865 if (action.urlbarIDOverride && action.urlbarIDOverride == nodeID) { 866 return action.id; 867 } 868 } 869 return null; 870 }, 871 872 /** 873 * Call this when the main page action button in the urlbar is activated. 874 * 875 * @param event (DOM event, required) 876 * The click or whatever event. 877 */ 878 mainButtonClicked(event) { 879 event.stopPropagation(); 880 if ( 881 // On mac, ctrl-click will send a context menu event from the widget, so 882 // we don't want to bring up the panel when ctrl key is pressed. 883 (event.type == "mousedown" && 884 (event.button != 0 || 885 (AppConstants.platform == "macosx" && event.ctrlKey))) || 886 (event.type == "keypress" && 887 event.charCode != KeyEvent.DOM_VK_SPACE && 888 event.keyCode != KeyEvent.DOM_VK_RETURN) 889 ) { 890 return; 891 } 892 893 // If the activated-action panel is open and anchored to the main button, 894 // close it. 895 let panelNode = this.activatedActionPanelNode; 896 if (panelNode && panelNode.anchorNode.id == this.mainButtonNode.id) { 897 PanelMultiView.hidePopup(panelNode); 898 return; 899 } 900 901 if (this.panelNode.state == "open") { 902 PanelMultiView.hidePopup(this.panelNode); 903 } else if (this.panelNode.state == "closed") { 904 this.showPanel(event); 905 } 906 }, 907 908 /** 909 * Show the page action panel 910 * 911 * @param event (DOM event, optional) 912 * The event that triggers showing the panel. (such as a mouse click, 913 * if the user clicked something to open the panel) 914 */ 915 showPanel(event = null) { 916 this.panelNode.hidden = false; 917 PanelMultiView.openPopup(this.panelNode, this.mainButtonNode, { 918 position: "bottomright topright", 919 triggerEvent: event, 920 }).catch(console.error); 921 }, 922 923 /** 924 * Call this on the context menu's popupshowing event. 925 * 926 * @param event (DOM event, required) 927 * The popupshowing event. 928 * @param popup (DOM node, required) 929 * The context menu popup DOM node. 930 */ 931 async onContextMenuShowing(event, popup) { 932 if (event.target != popup) { 933 return; 934 } 935 936 let action = this.actionForNode(popup.triggerNode); 937 // Only extension actions provide a context menu. 938 if (!action?.extensionID) { 939 this._contextAction = null; 940 event.preventDefault(); 941 return; 942 } 943 this._contextAction = action; 944 945 let removeExtension = popup.querySelector(".removeExtensionItem"); 946 let { extensionID } = this._contextAction; 947 let addon = extensionID && (await AddonManager.getAddonByID(extensionID)); 948 removeExtension.hidden = !addon; 949 if (addon) { 950 removeExtension.disabled = !( 951 addon.permissions & AddonManager.PERM_CAN_UNINSTALL 952 ); 953 } 954 }, 955 956 /** 957 * Call this from the menu item in the context menu that opens about:addons. 958 */ 959 openAboutAddonsForContextAction() { 960 if (!this._contextAction) { 961 return; 962 } 963 let action = this._contextAction; 964 this._contextAction = null; 965 966 let viewID = "addons://detail/" + encodeURIComponent(action.extensionID); 967 window.BrowserAddonUI.openAddonsMgr(viewID); 968 }, 969 970 /** 971 * Call this from the menu item in the context menu that removes an add-on. 972 */ 973 removeExtensionForContextAction() { 974 if (!this._contextAction) { 975 return; 976 } 977 let action = this._contextAction; 978 this._contextAction = null; 979 980 BrowserAddonUI.removeAddon(action.extensionID, "pageAction"); 981 }, 982 983 _contextAction: null, 984 985 /** 986 * Call this on tab switch or when the current <browser>'s location changes. 987 */ 988 onLocationChange() { 989 for (let action of PageActions.actions) { 990 action.onLocationChange(window); 991 } 992 }, 993 }; 994 995 // built-in actions below ////////////////////////////////////////////////////// 996 997 // bookmark 998 BrowserPageActions.bookmark = { 999 onShowingInPanel(buttonNode) { 1000 if (buttonNode.label == "null") { 1001 BookmarkingUI.updateBookmarkPageMenuItem(); 1002 } 1003 }, 1004 1005 onCommand(event) { 1006 PanelMultiView.hidePopup(BrowserPageActions.panelNode); 1007 BookmarkingUI.onStarCommand(event); 1008 }, 1009 };