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 }