tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

places-menupopup.js (21389B)


      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 "use strict";
      6 
      7 /* import-globals-from controller.js */
      8 
      9 // On Wayland when D&D source popup is closed,
     10 // D&D operation is canceled by window manager.
     11 function closingPopupEndsDrag(popup) {
     12  if (!popup.isWaylandPopup) {
     13    return false;
     14  }
     15  if (popup.isWaylandDragSource) {
     16    return true;
     17  }
     18  for (let childPopup of popup.querySelectorAll("menu > menupopup")) {
     19    if (childPopup.isWaylandDragSource) {
     20      return true;
     21    }
     22  }
     23  return false;
     24 }
     25 
     26 // This is loaded into all XUL windows. Wrap in a block to prevent
     27 // leaking to window scope.
     28 {
     29  /**
     30   * This class handles the custom element for the places popup menu.
     31   */
     32  class MozPlacesPopup extends MozElements.MozMenuPopup {
     33    constructor() {
     34      super();
     35 
     36      const event_names = [
     37        "DOMMenuItemActive",
     38        "DOMMenuItemInactive",
     39        "dragstart",
     40        "drop",
     41        "dragover",
     42        "dragleave",
     43        "dragend",
     44      ];
     45      for (let event_name of event_names) {
     46        this.addEventListener(event_name, this);
     47      }
     48    }
     49 
     50    get markup() {
     51      return `
     52      <html:link rel="stylesheet" href="chrome://global/skin/global.css" />
     53      <hbox part="drop-indicator-container">
     54        <vbox part="drop-indicator-bar" hidden="true">
     55          <image part="drop-indicator"/>
     56        </vbox>
     57        <arrowscrollbox class="menupopup-arrowscrollbox" flex="1" orient="vertical"
     58                        exportparts="scrollbox: arrowscrollbox-scrollbox"
     59                        smoothscroll="false" part="arrowscrollbox content">
     60          <html:slot/>
     61        </arrowscrollbox>
     62      </hbox>
     63    `;
     64    }
     65 
     66    connectedCallback() {
     67      if (this.delayConnectedCallback()) {
     68        return;
     69      }
     70 
     71      /**
     72       * Sub-menus should be opened when the mouse drags over them, and closed
     73       * when the mouse drags off.  The overFolder object manages opening and
     74       * closing of folders when the mouse hovers.
     75       */
     76      this._overFolder = {
     77        _self: this,
     78        _folder: {
     79          elt: null,
     80          openTimer: null,
     81          hoverTime: 350,
     82          closeTimer: null,
     83        },
     84        _closeMenuTimer: null,
     85 
     86        get elt() {
     87          return this._folder.elt;
     88        },
     89        set elt(val) {
     90          this._folder.elt = val;
     91        },
     92 
     93        get openTimer() {
     94          return this._folder.openTimer;
     95        },
     96        set openTimer(val) {
     97          this._folder.openTimer = val;
     98        },
     99 
    100        get hoverTime() {
    101          return this._folder.hoverTime;
    102        },
    103        set hoverTime(val) {
    104          this._folder.hoverTime = val;
    105        },
    106 
    107        get closeTimer() {
    108          return this._folder.closeTimer;
    109        },
    110        set closeTimer(val) {
    111          this._folder.closeTimer = val;
    112        },
    113 
    114        get closeMenuTimer() {
    115          return this._closeMenuTimer;
    116        },
    117        set closeMenuTimer(val) {
    118          this._closeMenuTimer = val;
    119        },
    120 
    121        setTimer: function OF__setTimer(aTime) {
    122          var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
    123          timer.initWithCallback(this, aTime, timer.TYPE_ONE_SHOT);
    124          return timer;
    125        },
    126 
    127        notify: function OF__notify(aTimer) {
    128          // Function to process all timer notifications.
    129 
    130          if (aTimer == this._folder.openTimer) {
    131            // Timer to open a submenu that's being dragged over.
    132            this._folder.elt.lastElementChild.setAttribute(
    133              "autoopened",
    134              "true"
    135            );
    136            this._folder.elt.lastElementChild.openPopup();
    137            this._folder.openTimer = null;
    138          } else if (aTimer == this._folder.closeTimer) {
    139            // Timer to close a submenu that's been dragged off of.
    140            // Only close the submenu if the mouse isn't being dragged over any
    141            // of its child menus.
    142            var draggingOverChild =
    143              PlacesControllerDragHelper.draggingOverChildNode(
    144                this._folder.elt
    145              );
    146            if (draggingOverChild) {
    147              this._folder.elt = null;
    148            }
    149            this.clear();
    150 
    151            // Close any parent folders which aren't being dragged over.
    152            // (This is necessary because of the above code that keeps a folder
    153            // open while its children are being dragged over.)
    154            if (!draggingOverChild && !closingPopupEndsDrag(this._self)) {
    155              this.closeParentMenus();
    156            }
    157          } else if (aTimer == this.closeMenuTimer) {
    158            // Timer to close this menu after the drag exit.
    159            var popup = this._self;
    160            // if we are no more dragging we can leave the menu open to allow
    161            // for better D&D bookmark organization
    162            var hidePopup =
    163              PlacesControllerDragHelper.getSession() &&
    164              !PlacesControllerDragHelper.draggingOverChildNode(
    165                popup.parentNode
    166              );
    167            if (hidePopup) {
    168              if (!closingPopupEndsDrag(popup)) {
    169                popup.hidePopup();
    170                // Close any parent menus that aren't being dragged over;
    171                // otherwise they'll stay open because they couldn't close
    172                // while this menu was being dragged over.
    173                this.closeParentMenus();
    174              } else if (popup.isWaylandDragSource) {
    175                // Postpone popup hide until drag end on Wayland.
    176                this._closeMenuTimer = this.setTimer(this.hoverTime);
    177              }
    178            }
    179          }
    180        },
    181 
    182        //  Helper function to close all parent menus of this menu,
    183        //  as long as none of the parent's children are currently being
    184        //  dragged over.
    185        closeParentMenus: function OF__closeParentMenus() {
    186          var popup = this._self;
    187          var parent = popup.parentNode;
    188          while (parent) {
    189            if (parent.localName == "menupopup" && parent._placesNode) {
    190              if (
    191                PlacesControllerDragHelper.draggingOverChildNode(
    192                  parent.parentNode
    193                )
    194              ) {
    195                break;
    196              }
    197              parent.hidePopup();
    198            }
    199            parent = parent.parentNode;
    200          }
    201        },
    202 
    203        //  The mouse is no longer dragging over the stored menubutton.
    204        //  Close the menubutton, clear out drag styles, and clear all
    205        //  timers for opening/closing it.
    206        clear: function OF__clear() {
    207          if (this._folder.elt && this._folder.elt.lastElementChild) {
    208            var popup = this._folder.elt.lastElementChild;
    209            if (
    210              !popup.hasAttribute("dragover") &&
    211              !closingPopupEndsDrag(popup)
    212            ) {
    213              popup.hidePopup();
    214            }
    215            // remove menuactive style
    216            this._folder.elt.removeAttribute("_moz-menuactive");
    217            this._folder.elt = null;
    218          }
    219          if (this._folder.openTimer) {
    220            this._folder.openTimer.cancel();
    221            this._folder.openTimer = null;
    222          }
    223          if (this._folder.closeTimer) {
    224            this._folder.closeTimer.cancel();
    225            this._folder.closeTimer = null;
    226          }
    227        },
    228      };
    229    }
    230 
    231    get _indicatorBar() {
    232      if (!this.__indicatorBar) {
    233        this.__indicatorBar = this.shadowRoot.querySelector(
    234          "[part=drop-indicator-bar]"
    235        );
    236      }
    237      return this.__indicatorBar;
    238    }
    239 
    240    /**
    241     * This is the view that manages the popup.
    242     *
    243     * @see {@link PlacesUIUtils.getViewForNode}
    244     * @returns {DOMNode}
    245     */
    246    get _rootView() {
    247      if (!this.__rootView) {
    248        this.__rootView = PlacesUIUtils.getViewForNode(this);
    249      }
    250      return this.__rootView;
    251    }
    252 
    253    /**
    254     * Check if we should hide the drop indicator for the target
    255     *
    256     * @param {object} aEvent
    257     *   The event associated with the drop.
    258     * @returns {boolean}
    259     */
    260    _hideDropIndicator(aEvent) {
    261      let target = aEvent.target;
    262 
    263      // Don't draw the drop indicator outside of markers or if current
    264      // node is not a Places node.
    265      let betweenMarkers =
    266        this._startMarker.compareDocumentPosition(target) &
    267          Node.DOCUMENT_POSITION_FOLLOWING &&
    268        this._endMarker.compareDocumentPosition(target) &
    269          Node.DOCUMENT_POSITION_PRECEDING;
    270 
    271      // Hide the dropmarker if current node is not a Places node.
    272      return !(target && target._placesNode && betweenMarkers);
    273    }
    274 
    275    /**
    276     * This function returns information about where to drop when
    277     * dragging over this popup insertion point
    278     *
    279     * @param {object} aEvent
    280     *   The event associated with the drop.
    281     * @returns {object|null}
    282     *   The associated drop point information.
    283     */
    284    _getDropPoint(aEvent) {
    285      // Can't drop if the menu isn't a folder
    286      let resultNode = this._placesNode;
    287 
    288      if (
    289        !PlacesUtils.nodeIsFolderOrShortcut(resultNode) ||
    290        this._rootView.controller.disallowInsertion(resultNode)
    291      ) {
    292        return null;
    293      }
    294 
    295      var dropPoint = { ip: null, folderElt: null };
    296 
    297      // The element we are dragging over
    298      let elt = aEvent.target;
    299      if (elt.localName == "menupopup") {
    300        elt = elt.parentNode;
    301      }
    302 
    303      let eventY = aEvent.clientY;
    304      let { y: eltY, height: eltHeight } = elt.getBoundingClientRect();
    305 
    306      if (!elt._placesNode) {
    307        // If we are dragging over a non places node drop at the end.
    308        dropPoint.ip = new PlacesInsertionPoint({
    309          parentGuid: PlacesUtils.getConcreteItemGuid(resultNode),
    310        });
    311        // We can set folderElt if we are dropping over a static menu that
    312        // has an internal placespopup.
    313        let isMenu =
    314          elt.localName == "menu" ||
    315          (elt.localName == "toolbarbutton" &&
    316            elt.getAttribute("type") == "menu");
    317        if (
    318          isMenu &&
    319          elt.lastElementChild &&
    320          elt.lastElementChild.hasAttribute("placespopup")
    321        ) {
    322          dropPoint.folderElt = elt;
    323        }
    324        return dropPoint;
    325      }
    326 
    327      let tagName = PlacesUtils.nodeIsTagQuery(elt._placesNode)
    328        ? elt._placesNode.title
    329        : null;
    330      if (
    331        (PlacesUtils.nodeIsFolderOrShortcut(elt._placesNode) &&
    332          !PlacesUIUtils.isFolderReadOnly(elt._placesNode)) ||
    333        PlacesUtils.nodeIsTagQuery(elt._placesNode)
    334      ) {
    335        // This is a folder or a tag container.
    336        if (eventY - eltY < eltHeight * 0.2) {
    337          // If mouse is in the top part of the element, drop above folder.
    338          dropPoint.ip = new PlacesInsertionPoint({
    339            parentGuid: PlacesUtils.getConcreteItemGuid(resultNode),
    340            orientation: Ci.nsITreeView.DROP_BEFORE,
    341            tagName,
    342            dropNearNode: elt._placesNode,
    343          });
    344          return dropPoint;
    345        } else if (eventY - eltY < eltHeight * 0.8) {
    346          // If mouse is in the middle of the element, drop inside folder.
    347          dropPoint.ip = new PlacesInsertionPoint({
    348            parentGuid: PlacesUtils.getConcreteItemGuid(elt._placesNode),
    349            tagName,
    350          });
    351          dropPoint.folderElt = elt;
    352          return dropPoint;
    353        }
    354      } else if (eventY - eltY <= eltHeight / 2) {
    355        // This is a non-folder node or a readonly folder.
    356        // If the mouse is above the middle, drop above this item.
    357        dropPoint.ip = new PlacesInsertionPoint({
    358          parentGuid: PlacesUtils.getConcreteItemGuid(resultNode),
    359          orientation: Ci.nsITreeView.DROP_BEFORE,
    360          tagName,
    361          dropNearNode: elt._placesNode,
    362        });
    363        return dropPoint;
    364      }
    365 
    366      // Drop below the item.
    367      dropPoint.ip = new PlacesInsertionPoint({
    368        parentGuid: PlacesUtils.getConcreteItemGuid(resultNode),
    369        orientation: Ci.nsITreeView.DROP_AFTER,
    370        tagName,
    371        dropNearNode: elt._placesNode,
    372      });
    373      return dropPoint;
    374    }
    375 
    376    _cleanupDragDetails() {
    377      // Called on dragend and drop.
    378      PlacesControllerDragHelper.currentDropTarget = null;
    379      this._rootView._draggedElt = null;
    380      this.removeAttribute("dragover");
    381      this.removeAttribute("dragstart");
    382      this._indicatorBar.hidden = true;
    383    }
    384 
    385    on_DOMMenuItemActive(event) {
    386      if (super.on_DOMMenuItemActive) {
    387        super.on_DOMMenuItemActive(event);
    388      }
    389 
    390      let elt = event.target;
    391      if (elt.parentNode != this) {
    392        return;
    393      }
    394 
    395      if (window.XULBrowserWindow) {
    396        let placesNode = elt._placesNode;
    397 
    398        var linkURI;
    399        if (placesNode && PlacesUtils.nodeIsURI(placesNode)) {
    400          linkURI = placesNode.uri;
    401        } else if (elt.hasAttribute("targetURI")) {
    402          linkURI = elt.getAttribute("targetURI");
    403        }
    404 
    405        if (linkURI) {
    406          window.XULBrowserWindow.setOverLink(linkURI);
    407        }
    408      }
    409    }
    410 
    411    on_DOMMenuItemInactive(event) {
    412      let elt = event.target;
    413      if (elt.parentNode != this) {
    414        return;
    415      }
    416 
    417      if (window.XULBrowserWindow) {
    418        window.XULBrowserWindow.setOverLink("");
    419      }
    420    }
    421 
    422    on_dragstart(event) {
    423      let elt = event.target;
    424      if (!elt._placesNode) {
    425        return;
    426      }
    427 
    428      let draggedElt = elt._placesNode;
    429 
    430      // Force a copy action if parent node is a query or we are dragging a
    431      // not-removable node.
    432      if (!this._rootView.controller.canMoveNode(draggedElt)) {
    433        event.dataTransfer.effectAllowed = "copyLink";
    434      }
    435 
    436      // Activate the view and cache the dragged element.
    437      this._rootView._draggedElt = draggedElt;
    438      this._rootView.controller.setDataTransfer(event);
    439      this.setAttribute("dragstart", "true");
    440      event.stopPropagation();
    441    }
    442 
    443    on_drop(event) {
    444      PlacesControllerDragHelper.currentDropTarget = event.target;
    445 
    446      let dropPoint = this._getDropPoint(event);
    447      if (dropPoint && dropPoint.ip) {
    448        PlacesControllerDragHelper.onDrop(
    449          dropPoint.ip,
    450          event.dataTransfer
    451        ).catch(console.error);
    452        event.preventDefault();
    453      }
    454 
    455      this._cleanupDragDetails();
    456      event.stopPropagation();
    457    }
    458 
    459    on_dragover(event) {
    460      PlacesControllerDragHelper.currentDropTarget = event.target;
    461      let dt = event.dataTransfer;
    462 
    463      let dropPoint = this._getDropPoint(event);
    464      if (
    465        !dropPoint ||
    466        !dropPoint.ip ||
    467        !PlacesControllerDragHelper.canDrop(dropPoint.ip, dt)
    468      ) {
    469        this._indicatorBar.hidden = true;
    470        event.stopPropagation();
    471        return;
    472      }
    473 
    474      // Mark this popup as being dragged over.
    475      this.setAttribute("dragover", "true");
    476 
    477      if (dropPoint.folderElt) {
    478        // We are dragging over a folder.
    479        // _overFolder should take the care of opening it on a timer.
    480        if (
    481          this._overFolder.elt &&
    482          this._overFolder.elt != dropPoint.folderElt
    483        ) {
    484          // We are dragging over a new folder, let's clear old values
    485          this._overFolder.clear();
    486        }
    487        if (!this._overFolder.elt) {
    488          this._overFolder.elt = dropPoint.folderElt;
    489          // Create the timer to open this folder.
    490          this._overFolder.openTimer = this._overFolder.setTimer(
    491            this._overFolder.hoverTime
    492          );
    493        }
    494        // Since we are dropping into a folder set the corresponding style.
    495        dropPoint.folderElt.setAttribute("_moz-menuactive", true);
    496      } else {
    497        // We are not dragging over a folder.
    498        // Clear out old _overFolder information.
    499        this._overFolder.clear();
    500      }
    501 
    502      // Autoscroll the popup strip if we drag over the scroll buttons.
    503      let scrollDir = 0;
    504      if (event.originalTarget == this.scrollBox._scrollButtonUp) {
    505        scrollDir = -1;
    506      } else if (event.originalTarget == this.scrollBox._scrollButtonDown) {
    507        scrollDir = 1;
    508      }
    509      if (scrollDir != 0) {
    510        this.scrollBox.scrollByIndex(scrollDir, true);
    511      }
    512 
    513      // Check if we should hide the drop indicator for this target.
    514      if (dropPoint.folderElt || this._hideDropIndicator(event)) {
    515        this._indicatorBar.hidden = true;
    516        event.preventDefault();
    517        event.stopPropagation();
    518        return;
    519      }
    520 
    521      // We should display the drop indicator relative to the arrowscrollbox.
    522      let scrollRect = this.scrollBox.getBoundingClientRect();
    523      let newMarginTop = 0;
    524      if (scrollDir == 0) {
    525        let elt = this.firstElementChild;
    526        for (; elt; elt = elt.nextElementSibling) {
    527          let height = elt.getBoundingClientRect().height;
    528          if (height == 0) {
    529            continue;
    530          }
    531          if (event.screenY <= elt.screenY + height / 2) {
    532            break;
    533          }
    534        }
    535        newMarginTop = elt
    536          ? elt.screenY - this.scrollBox.screenY
    537          : scrollRect.height;
    538      } else if (scrollDir == 1) {
    539        newMarginTop = scrollRect.height;
    540      }
    541 
    542      // Set the new marginTop based on arrowscrollbox.
    543      newMarginTop +=
    544        scrollRect.y - this._indicatorBar.parentNode.getBoundingClientRect().y;
    545      this._indicatorBar.firstElementChild.style.marginTop =
    546        newMarginTop + "px";
    547      this._indicatorBar.hidden = false;
    548 
    549      event.preventDefault();
    550      event.stopPropagation();
    551    }
    552 
    553    on_dragleave(event) {
    554      PlacesControllerDragHelper.currentDropTarget = null;
    555      this.removeAttribute("dragover");
    556 
    557      // If we have not moved to a valid new target clear the drop indicator
    558      // this happens when moving out of the popup.
    559      let target = event.relatedTarget;
    560      if (!target || !this.contains(target)) {
    561        this._indicatorBar.hidden = true;
    562      }
    563 
    564      // Close any folder being hovered over
    565      if (this._overFolder.elt) {
    566        this._overFolder.closeTimer = this._overFolder.setTimer(
    567          this._overFolder.hoverTime
    568        );
    569      }
    570 
    571      // The autoopened attribute is set when this folder was automatically
    572      // opened after the user dragged over it.  If this attribute is set,
    573      // auto-close the folder on drag exit.
    574      // We should also try to close this popup if the drag has started
    575      // from here, the timer will check if we are dragging over a child.
    576      if (this.hasAttribute("autoopened") || this.hasAttribute("dragstart")) {
    577        this._overFolder.closeMenuTimer = this._overFolder.setTimer(
    578          this._overFolder.hoverTime
    579        );
    580      }
    581 
    582      event.stopPropagation();
    583    }
    584 
    585    on_dragend() {
    586      this._cleanupDragDetails();
    587    }
    588 
    589    uninit() {
    590      this.__rootView = null;
    591    }
    592  }
    593 
    594  customElements.define("places-popup", MozPlacesPopup, {
    595    extends: "menupopup",
    596  });
    597 
    598  /**
    599   * Custom element for the places popup arrow.
    600   */
    601  class MozPlacesPopupArrow extends MozPlacesPopup {
    602    constructor() {
    603      super();
    604 
    605      const event_names = [
    606        "popupshowing",
    607        "popuppositioned",
    608        "popupshown",
    609        "popuphiding",
    610        "popuphidden",
    611      ];
    612      for (let event_name of event_names) {
    613        this.addEventListener(event_name, this);
    614      }
    615    }
    616 
    617    connectedCallback() {
    618      if (this.delayConnectedCallback()) {
    619        return;
    620      }
    621 
    622      super.connectedCallback();
    623      this.initializeAttributeInheritance();
    624 
    625      this.setAttribute("flip", "both");
    626      this.setAttribute("side", "top");
    627      this.setAttribute("position", "bottomright topright");
    628    }
    629 
    630    _setSideAttribute(event) {
    631      if (!this.anchorNode) {
    632        return;
    633      }
    634 
    635      var position = event.alignmentPosition;
    636      if (position.indexOf("start_") == 0 || position.indexOf("end_") == 0) {
    637        // The assigned side stays the same regardless of direction.
    638        let isRTL = this.matches(":-moz-locale-dir(rtl)");
    639 
    640        if (position.indexOf("start_") == 0) {
    641          this.setAttribute("side", isRTL ? "left" : "right");
    642        } else {
    643          this.setAttribute("side", isRTL ? "right" : "left");
    644        }
    645      } else if (
    646        position.indexOf("before_") == 0 ||
    647        position.indexOf("after_") == 0
    648      ) {
    649        if (position.indexOf("before_") == 0) {
    650          this.setAttribute("side", "bottom");
    651        } else {
    652          this.setAttribute("side", "top");
    653        }
    654      }
    655    }
    656 
    657    on_popupshowing(event) {
    658      if (event.target == this) {
    659        this.setAttribute("animate", "open");
    660        this.style.pointerEvents = "none";
    661      }
    662    }
    663 
    664    on_popuppositioned(event) {
    665      if (event.target == this) {
    666        this._setSideAttribute(event);
    667      }
    668    }
    669 
    670    on_popupshown(event) {
    671      if (event.target != this) {
    672        return;
    673      }
    674 
    675      this.setAttribute("panelopen", "true");
    676      this.style.removeProperty("pointer-events");
    677    }
    678 
    679    on_popuphiding(event) {
    680      if (event.target == this) {
    681        this.setAttribute("animate", "cancel");
    682      }
    683    }
    684 
    685    on_popuphidden(event) {
    686      if (event.target == this) {
    687        this.removeAttribute("panelopen");
    688        this.removeAttribute("animate");
    689      }
    690    }
    691  }
    692 
    693  customElements.define("places-popup-arrow", MozPlacesPopupArrow, {
    694    extends: "menupopup",
    695  });
    696 }