Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
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 file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
6 Components.utils.import("resource://gre/modules/Services.jsm");
8 /**
9 * The base view implements everything that's common to the toolbar and
10 * menu views.
11 */
12 function PlacesViewBase(aPlace, aOptions) {
13 this.place = aPlace;
14 this.options = aOptions;
15 this._controller = new PlacesController(this);
16 this._viewElt.controllers.appendController(this._controller);
17 }
19 PlacesViewBase.prototype = {
20 // The xul element that holds the entire view.
21 _viewElt: null,
22 get viewElt() this._viewElt,
24 get associatedElement() this._viewElt,
26 get controllers() this._viewElt.controllers,
28 // The xul element that represents the root container.
29 _rootElt: null,
31 // Set to true for views that are represented by native widgets (i.e.
32 // the native mac menu).
33 _nativeView: false,
35 QueryInterface: XPCOMUtils.generateQI(
36 [Components.interfaces.nsINavHistoryResultObserver,
37 Components.interfaces.nsISupportsWeakReference]),
39 _place: "",
40 get place() this._place,
41 set place(val) {
42 this._place = val;
44 let history = PlacesUtils.history;
45 let queries = { }, options = { };
46 history.queryStringToQueries(val, queries, { }, options);
47 if (!queries.value.length)
48 queries.value = [history.getNewQuery()];
50 let result = history.executeQueries(queries.value, queries.value.length,
51 options.value);
52 result.addObserver(this, false);
53 return val;
54 },
56 _result: null,
57 get result() this._result,
58 set result(val) {
59 if (this._result == val)
60 return val;
62 if (this._result) {
63 this._result.removeObserver(this);
64 this._resultNode.containerOpen = false;
65 }
67 if (this._rootElt.localName == "menupopup")
68 this._rootElt._built = false;
70 this._result = val;
71 if (val) {
72 this._resultNode = val.root;
73 this._rootElt._placesNode = this._resultNode;
74 this._domNodes = new Map();
75 this._domNodes.set(this._resultNode, this._rootElt);
77 // This calls _rebuild through invalidateContainer.
78 this._resultNode.containerOpen = true;
79 }
80 else {
81 this._resultNode = null;
82 delete this._domNodes;
83 }
85 return val;
86 },
88 _options: null,
89 get options() this._options,
90 set options(val) {
91 if (!val)
92 val = {};
94 if (!("extraClasses" in val))
95 val.extraClasses = {};
96 this._options = val;
98 return val;
99 },
101 /**
102 * Gets the DOM node used for the given places node.
103 *
104 * @param aPlacesNode
105 * a places result node.
106 * @throws if there is no DOM node set for aPlacesNode.
107 */
108 _getDOMNodeForPlacesNode:
109 function PVB__getDOMNodeForPlacesNode(aPlacesNode) {
110 let node = this._domNodes.get(aPlacesNode, null);
111 if (!node) {
112 throw new Error("No DOM node set for aPlacesNode.\nnode.type: " +
113 aPlacesNode.type + ". node.parent: " + aPlacesNode);
114 }
115 return node;
116 },
118 get controller() this._controller,
120 get selType() "single",
121 selectItems: function() { },
122 selectAll: function() { },
124 get selectedNode() {
125 if (this._contextMenuShown) {
126 let popup = document.popupNode;
127 return popup._placesNode || popup.parentNode._placesNode || null;
128 }
129 return null;
130 },
132 get hasSelection() this.selectedNode != null,
134 get selectedNodes() {
135 let selectedNode = this.selectedNode;
136 return selectedNode ? [selectedNode] : [];
137 },
139 get removableSelectionRanges() {
140 // On static content the current selectedNode would be the selection's
141 // parent node. We don't want to allow removing a node when the
142 // selection is not explicit.
143 if (document.popupNode &&
144 (document.popupNode == "menupopup" || !document.popupNode._placesNode))
145 return [];
147 return [this.selectedNodes];
148 },
150 get draggableSelection() [this._draggedElt],
152 get insertionPoint() {
153 // There is no insertion point for history queries, so bail out now and
154 // save a lot of work when updating commands.
155 let resultNode = this._resultNode;
156 if (PlacesUtils.nodeIsQuery(resultNode) &&
157 PlacesUtils.asQuery(resultNode).queryOptions.queryType ==
158 Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY)
159 return null;
161 // By default, the insertion point is at the top level, at the end.
162 let index = PlacesUtils.bookmarks.DEFAULT_INDEX;
163 let container = this._resultNode;
164 let orientation = Ci.nsITreeView.DROP_BEFORE;
165 let isTag = false;
167 let selectedNode = this.selectedNode;
168 if (selectedNode) {
169 let popup = document.popupNode;
170 if (!popup._placesNode || popup._placesNode == this._resultNode ||
171 popup._placesNode.itemId == -1) {
172 // If a static menuitem is selected, or if the root node is selected,
173 // the insertion point is inside the folder, at the end.
174 container = selectedNode;
175 orientation = Ci.nsITreeView.DROP_ON;
176 }
177 else {
178 // In all other cases the insertion point is before that node.
179 container = selectedNode.parent;
180 index = container.getChildIndex(selectedNode);
181 isTag = PlacesUtils.nodeIsTagQuery(container);
182 }
183 }
185 if (PlacesControllerDragHelper.disallowInsertion(container))
186 return null;
188 return new InsertionPoint(PlacesUtils.getConcreteItemId(container),
189 index, orientation, isTag);
190 },
192 buildContextMenu: function PVB_buildContextMenu(aPopup) {
193 this._contextMenuShown = true;
194 window.updateCommands("places");
195 return this.controller.buildContextMenu(aPopup);
196 },
198 destroyContextMenu: function PVB_destroyContextMenu(aPopup) {
199 this._contextMenuShown = false;
200 },
202 _cleanPopup: function PVB_cleanPopup(aPopup, aDelay) {
203 // Remove Places nodes from the popup.
204 let child = aPopup._startMarker;
205 while (child.nextSibling != aPopup._endMarker) {
206 let sibling = child.nextSibling;
207 if (sibling._placesNode && !aDelay) {
208 aPopup.removeChild(sibling);
209 }
210 else if (sibling._placesNode && aDelay) {
211 // HACK (bug 733419): the popups originating from the OS X native
212 // menubar don't live-update while open, thus we don't clean it
213 // until the next popupshowing, to avoid zombie menuitems.
214 if (!aPopup._delayedRemovals)
215 aPopup._delayedRemovals = [];
216 aPopup._delayedRemovals.push(sibling);
217 child = child.nextSibling;
218 }
219 else {
220 child = child.nextSibling;
221 }
222 }
223 },
225 _rebuildPopup: function PVB__rebuildPopup(aPopup) {
226 let resultNode = aPopup._placesNode;
227 if (!resultNode.containerOpen)
228 return;
230 if (this.controller.hasCachedLivemarkInfo(resultNode)) {
231 this._setEmptyPopupStatus(aPopup, false);
232 aPopup._built = true;
233 this._populateLivemarkPopup(aPopup);
234 return;
235 }
237 this._cleanPopup(aPopup);
239 let cc = resultNode.childCount;
240 if (cc > 0) {
241 this._setEmptyPopupStatus(aPopup, false);
243 for (let i = 0; i < cc; ++i) {
244 let child = resultNode.getChild(i);
245 this._insertNewItemToPopup(child, aPopup, null);
246 }
247 }
248 else {
249 this._setEmptyPopupStatus(aPopup, true);
250 }
251 aPopup._built = true;
252 },
254 _removeChild: function PVB__removeChild(aChild) {
255 // If document.popupNode pointed to this child, null it out,
256 // otherwise controller's command-updating may rely on the removed
257 // item still being "selected".
258 if (document.popupNode == aChild)
259 document.popupNode = null;
261 aChild.parentNode.removeChild(aChild);
262 },
264 _setEmptyPopupStatus:
265 function PVB__setEmptyPopupStatus(aPopup, aEmpty) {
266 if (!aPopup._emptyMenuitem) {
267 let label = PlacesUIUtils.getString("bookmarksMenuEmptyFolder");
268 aPopup._emptyMenuitem = document.createElement("menuitem");
269 aPopup._emptyMenuitem.setAttribute("label", label);
270 aPopup._emptyMenuitem.setAttribute("disabled", true);
271 aPopup._emptyMenuitem.className = "bookmark-item";
272 if (typeof this.options.extraClasses.entry == "string")
273 aPopup._emptyMenuitem.classList.add(this.options.extraClasses.entry);
274 }
276 if (aEmpty) {
277 aPopup.setAttribute("emptyplacesresult", "true");
278 // Don't add the menuitem if there is static content.
279 if (!aPopup._startMarker.previousSibling &&
280 !aPopup._endMarker.nextSibling)
281 aPopup.insertBefore(aPopup._emptyMenuitem, aPopup._endMarker);
282 }
283 else {
284 aPopup.removeAttribute("emptyplacesresult");
285 try {
286 aPopup.removeChild(aPopup._emptyMenuitem);
287 } catch (ex) {}
288 }
289 },
291 _createMenuItemForPlacesNode:
292 function PVB__createMenuItemForPlacesNode(aPlacesNode) {
293 this._domNodes.delete(aPlacesNode);
295 let element;
296 let type = aPlacesNode.type;
297 if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) {
298 element = document.createElement("menuseparator");
299 element.setAttribute("class", "small-separator");
300 }
301 else {
302 let itemId = aPlacesNode.itemId;
303 if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_URI) {
304 element = document.createElement("menuitem");
305 element.className = "menuitem-iconic bookmark-item menuitem-with-favicon";
306 element.setAttribute("scheme",
307 PlacesUIUtils.guessUrlSchemeForUI(aPlacesNode.uri));
308 }
309 else if (PlacesUtils.containerTypes.indexOf(type) != -1) {
310 element = document.createElement("menu");
311 element.setAttribute("container", "true");
313 if (aPlacesNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY) {
314 element.setAttribute("query", "true");
315 if (PlacesUtils.nodeIsTagQuery(aPlacesNode))
316 element.setAttribute("tagContainer", "true");
317 else if (PlacesUtils.nodeIsDay(aPlacesNode))
318 element.setAttribute("dayContainer", "true");
319 else if (PlacesUtils.nodeIsHost(aPlacesNode))
320 element.setAttribute("hostContainer", "true");
321 }
322 else if (itemId != -1) {
323 PlacesUtils.livemarks.getLivemark({ id: itemId })
324 .then(aLivemark => {
325 element.setAttribute("livemark", "true");
326 #ifdef XP_MACOSX
327 // OS X native menubar doesn't track list-style-images since
328 // it doesn't have a frame (bug 733415). Thus enforce updating.
329 element.setAttribute("image", "");
330 element.removeAttribute("image");
331 #endif
332 this.controller.cacheLivemarkInfo(aPlacesNode, aLivemark);
333 }, () => undefined);
334 }
336 let popup = document.createElement("menupopup");
337 popup._placesNode = PlacesUtils.asContainer(aPlacesNode);
339 if (!this._nativeView) {
340 popup.setAttribute("placespopup", "true");
341 }
343 element.appendChild(popup);
344 element.className = "menu-iconic bookmark-item";
345 if (typeof this.options.extraClasses.entry == "string") {
346 element.classList.add(this.options.extraClasses.entry);
347 }
349 this._domNodes.set(aPlacesNode, popup);
350 }
351 else
352 throw "Unexpected node";
354 element.setAttribute("label", PlacesUIUtils.getBestTitle(aPlacesNode));
356 let icon = aPlacesNode.icon;
357 if (icon)
358 element.setAttribute("image", icon);
359 }
361 element._placesNode = aPlacesNode;
362 if (!this._domNodes.has(aPlacesNode))
363 this._domNodes.set(aPlacesNode, element);
365 return element;
366 },
368 _insertNewItemToPopup:
369 function PVB__insertNewItemToPopup(aNewChild, aPopup, aBefore) {
370 let element = this._createMenuItemForPlacesNode(aNewChild);
371 let before = aBefore || aPopup._endMarker;
373 if (element.localName == "menuitem" || element.localName == "menu") {
374 if (typeof this.options.extraClasses.entry == "string")
375 element.classList.add(this.options.extraClasses.entry);
376 }
378 aPopup.insertBefore(element, before);
379 return element;
380 },
382 _setLivemarkSiteURIMenuItem:
383 function PVB__setLivemarkSiteURIMenuItem(aPopup) {
384 let livemarkInfo = this.controller.getCachedLivemarkInfo(aPopup._placesNode);
385 let siteUrl = livemarkInfo && livemarkInfo.siteURI ?
386 livemarkInfo.siteURI.spec : null;
387 if (!siteUrl && aPopup._siteURIMenuitem) {
388 aPopup.removeChild(aPopup._siteURIMenuitem);
389 aPopup._siteURIMenuitem = null;
390 aPopup.removeChild(aPopup._siteURIMenuseparator);
391 aPopup._siteURIMenuseparator = null;
392 }
393 else if (siteUrl && !aPopup._siteURIMenuitem) {
394 // Add "Open (Feed Name)" menuitem.
395 aPopup._siteURIMenuitem = document.createElement("menuitem");
396 aPopup._siteURIMenuitem.className = "openlivemarksite-menuitem";
397 if (typeof this.options.extraClasses.entry == "string") {
398 aPopup._siteURIMenuitem.classList.add(this.options.extraClasses.entry);
399 }
400 aPopup._siteURIMenuitem.setAttribute("targetURI", siteUrl);
401 aPopup._siteURIMenuitem.setAttribute("oncommand",
402 "openUILink(this.getAttribute('targetURI'), event);");
404 // If a user middle-clicks this item we serve the oncommand event.
405 // We are using checkForMiddleClick because of Bug 246720.
406 // Note: stopPropagation is needed to avoid serving middle-click
407 // with BT_onClick that would open all items in tabs.
408 aPopup._siteURIMenuitem.setAttribute("onclick",
409 "checkForMiddleClick(this, event); event.stopPropagation();");
410 let label =
411 PlacesUIUtils.getFormattedString("menuOpenLivemarkOrigin.label",
412 [aPopup.parentNode.getAttribute("label")])
413 aPopup._siteURIMenuitem.setAttribute("label", label);
414 aPopup.insertBefore(aPopup._siteURIMenuitem, aPopup._startMarker);
416 aPopup._siteURIMenuseparator = document.createElement("menuseparator");
417 aPopup.insertBefore(aPopup._siteURIMenuseparator, aPopup._startMarker);
418 }
419 },
421 /**
422 * Add, update or remove the livemark status menuitem.
423 * @param aPopup
424 * The livemark container popup
425 * @param aStatus
426 * The livemark status
427 */
428 _setLivemarkStatusMenuItem:
429 function PVB_setLivemarkStatusMenuItem(aPopup, aStatus) {
430 let statusMenuitem = aPopup._statusMenuitem;
431 if (!statusMenuitem) {
432 // Create the status menuitem and cache it in the popup object.
433 statusMenuitem = document.createElement("menuitem");
434 statusMenuitem.className = "livemarkstatus-menuitem";
435 if (typeof this.options.extraClasses.entry == "string") {
436 statusMenuitem.classList.add(this.options.extraClasses.entry);
437 }
438 statusMenuitem.setAttribute("disabled", true);
439 aPopup._statusMenuitem = statusMenuitem;
440 }
442 if (aStatus == Ci.mozILivemark.STATUS_LOADING ||
443 aStatus == Ci.mozILivemark.STATUS_FAILED) {
444 // Status has changed, update the cached status menuitem.
445 let stringId = aStatus == Ci.mozILivemark.STATUS_LOADING ?
446 "bookmarksLivemarkLoading" : "bookmarksLivemarkFailed";
447 statusMenuitem.setAttribute("label", PlacesUIUtils.getString(stringId));
448 if (aPopup._startMarker.nextSibling != statusMenuitem)
449 aPopup.insertBefore(statusMenuitem, aPopup._startMarker.nextSibling);
450 }
451 else {
452 // The livemark has finished loading.
453 if (aPopup._statusMenuitem.parentNode == aPopup)
454 aPopup.removeChild(aPopup._statusMenuitem);
455 }
456 },
458 toggleCutNode: function PVB_toggleCutNode(aPlacesNode, aValue) {
459 let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
461 // We may get the popup for menus, but we need the menu itself.
462 if (elt.localName == "menupopup")
463 elt = elt.parentNode;
464 if (aValue)
465 elt.setAttribute("cutting", "true");
466 else
467 elt.removeAttribute("cutting");
468 },
470 nodeURIChanged: function PVB_nodeURIChanged(aPlacesNode, aURIString) {
471 let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
473 // Here we need the <menu>.
474 if (elt.localName == "menupopup")
475 elt = elt.parentNode;
477 elt.setAttribute("scheme", PlacesUIUtils.guessUrlSchemeForUI(aURIString));
478 },
480 nodeIconChanged: function PVB_nodeIconChanged(aPlacesNode) {
481 let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
483 // There's no UI representation for the root node, thus there's nothing to
484 // be done when the icon changes.
485 if (elt == this._rootElt)
486 return;
488 // Here we need the <menu>.
489 if (elt.localName == "menupopup")
490 elt = elt.parentNode;
492 let icon = aPlacesNode.icon;
493 if (!icon)
494 elt.removeAttribute("image");
495 else if (icon != elt.getAttribute("image"))
496 elt.setAttribute("image", icon);
497 },
499 nodeAnnotationChanged:
500 function PVB_nodeAnnotationChanged(aPlacesNode, aAnno) {
501 let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
503 // All livemarks have a feedURI, so use it as our indicator of a livemark
504 // being modified.
505 if (aAnno == PlacesUtils.LMANNO_FEEDURI) {
506 let menu = elt.parentNode;
507 if (!menu.hasAttribute("livemark")) {
508 menu.setAttribute("livemark", "true");
509 #ifdef XP_MACOSX
510 // OS X native menubar doesn't track list-style-images since
511 // it doesn't have a frame (bug 733415). Thus enforce updating.
512 menu.setAttribute("image", "");
513 menu.removeAttribute("image");
514 #endif
515 }
517 PlacesUtils.livemarks.getLivemark({ id: aPlacesNode.itemId })
518 .then(aLivemark => {
519 // Controller will use this to build the meta data for the node.
520 this.controller.cacheLivemarkInfo(aPlacesNode, aLivemark);
521 this.invalidateContainer(aPlacesNode);
522 }, () => undefined);
523 }
524 },
526 nodeTitleChanged:
527 function PVB_nodeTitleChanged(aPlacesNode, aNewTitle) {
528 let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
530 // There's no UI representation for the root node, thus there's
531 // nothing to be done when the title changes.
532 if (elt == this._rootElt)
533 return;
535 // Here we need the <menu>.
536 if (elt.localName == "menupopup")
537 elt = elt.parentNode;
539 if (!aNewTitle && elt.localName != "toolbarbutton") {
540 // Many users consider toolbars as shortcuts containers, so explicitly
541 // allow empty labels on toolbarbuttons. For any other element try to be
542 // smarter, guessing a title from the uri.
543 elt.setAttribute("label", PlacesUIUtils.getBestTitle(aPlacesNode));
544 }
545 else {
546 elt.setAttribute("label", aNewTitle);
547 }
548 },
550 nodeRemoved:
551 function PVB_nodeRemoved(aParentPlacesNode, aPlacesNode, aIndex) {
552 let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode);
553 let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
555 // Here we need the <menu>.
556 if (elt.localName == "menupopup")
557 elt = elt.parentNode;
559 if (parentElt._built) {
560 parentElt.removeChild(elt);
562 // Figure out if we need to show the "<Empty>" menu-item.
563 // TODO Bug 517701: This doesn't seem to handle the case of an empty
564 // root.
565 if (parentElt._startMarker.nextSibling == parentElt._endMarker)
566 this._setEmptyPopupStatus(parentElt, true);
567 }
568 },
570 nodeHistoryDetailsChanged:
571 function PVB_nodeHistoryDetailsChanged(aPlacesNode, aTime, aCount) {
572 if (aPlacesNode.parent &&
573 this.controller.hasCachedLivemarkInfo(aPlacesNode.parent)) {
574 // Find the node in the parent.
575 let popup = this._getDOMNodeForPlacesNode(aPlacesNode.parent);
576 for (let child = popup._startMarker.nextSibling;
577 child != popup._endMarker;
578 child = child.nextSibling) {
579 if (child._placesNode && child._placesNode.uri == aPlacesNode.uri) {
580 if (aCount)
581 child.setAttribute("visited", "true");
582 else
583 child.removeAttribute("visited");
584 break;
585 }
586 }
587 }
588 },
590 nodeTagsChanged: function() { },
591 nodeDateAddedChanged: function() { },
592 nodeLastModifiedChanged: function() { },
593 nodeKeywordChanged: function() { },
594 sortingChanged: function() { },
595 batching: function() { },
597 nodeInserted:
598 function PVB_nodeInserted(aParentPlacesNode, aPlacesNode, aIndex) {
599 let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode);
600 if (!parentElt._built)
601 return;
603 let index = Array.indexOf(parentElt.childNodes, parentElt._startMarker) +
604 aIndex + 1;
605 this._insertNewItemToPopup(aPlacesNode, parentElt,
606 parentElt.childNodes[index]);
607 this._setEmptyPopupStatus(parentElt, false);
608 },
610 nodeMoved:
611 function PBV_nodeMoved(aPlacesNode,
612 aOldParentPlacesNode, aOldIndex,
613 aNewParentPlacesNode, aNewIndex) {
614 // Note: the current implementation of moveItem does not actually
615 // use this notification when the item in question is moved from one
616 // folder to another. Instead, it calls nodeRemoved and nodeInserted
617 // for the two folders. Thus, we can assume old-parent == new-parent.
618 let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
620 // Here we need the <menu>.
621 if (elt.localName == "menupopup")
622 elt = elt.parentNode;
624 // If our root node is a folder, it might be moved. There's nothing
625 // we need to do in that case.
626 if (elt == this._rootElt)
627 return;
629 let parentElt = this._getDOMNodeForPlacesNode(aNewParentPlacesNode);
630 if (parentElt._built) {
631 // Move the node.
632 parentElt.removeChild(elt);
633 let index = Array.indexOf(parentElt.childNodes, parentElt._startMarker) +
634 aNewIndex + 1;
635 parentElt.insertBefore(elt, parentElt.childNodes[index]);
636 }
637 },
639 containerStateChanged:
640 function PVB_containerStateChanged(aPlacesNode, aOldState, aNewState) {
641 if (aNewState == Ci.nsINavHistoryContainerResultNode.STATE_OPENED ||
642 aNewState == Ci.nsINavHistoryContainerResultNode.STATE_CLOSED) {
643 this.invalidateContainer(aPlacesNode);
645 if (PlacesUtils.nodeIsFolder(aPlacesNode)) {
646 let queryOptions = PlacesUtils.asQuery(this._result.root).queryOptions;
647 if (queryOptions.excludeItems) {
648 return;
649 }
651 PlacesUtils.livemarks.getLivemark({ id: aPlacesNode.itemId })
652 .then(aLivemark => {
653 let shouldInvalidate =
654 !this.controller.hasCachedLivemarkInfo(aPlacesNode);
655 this.controller.cacheLivemarkInfo(aPlacesNode, aLivemark);
656 if (aNewState == Ci.nsINavHistoryContainerResultNode.STATE_OPENED) {
657 aLivemark.registerForUpdates(aPlacesNode, this);
658 // Prioritize the current livemark.
659 aLivemark.reload();
660 PlacesUtils.livemarks.reloadLivemarks();
661 if (shouldInvalidate)
662 this.invalidateContainer(aPlacesNode);
663 }
664 else {
665 aLivemark.unregisterForUpdates(aPlacesNode);
666 }
667 }, () => undefined);
668 }
669 }
670 },
672 _populateLivemarkPopup: function PVB__populateLivemarkPopup(aPopup)
673 {
674 this._setLivemarkSiteURIMenuItem(aPopup);
675 // Show the loading status only if there are no entries yet.
676 if (aPopup._startMarker.nextSibling == aPopup._endMarker)
677 this._setLivemarkStatusMenuItem(aPopup, Ci.mozILivemark.STATUS_LOADING);
679 PlacesUtils.livemarks.getLivemark({ id: aPopup._placesNode.itemId })
680 .then(aLivemark => {
681 let placesNode = aPopup._placesNode;
682 if (!placesNode.containerOpen)
683 return;
685 if (aLivemark.status != Ci.mozILivemark.STATUS_LOADING)
686 this._setLivemarkStatusMenuItem(aPopup, aLivemark.status);
687 this._cleanPopup(aPopup,
688 this._nativeView && aPopup.parentNode.hasAttribute("open"));
690 let children = aLivemark.getNodesForContainer(placesNode);
691 for (let i = 0; i < children.length; i++) {
692 let child = children[i];
693 this.nodeInserted(placesNode, child, i);
694 if (child.accessCount)
695 this._getDOMNodeForPlacesNode(child).setAttribute("visited", true);
696 else
697 this._getDOMNodeForPlacesNode(child).removeAttribute("visited");
698 }
699 }, Components.utils.reportError);
700 },
702 invalidateContainer: function PVB_invalidateContainer(aPlacesNode) {
703 let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
704 elt._built = false;
706 // If the menupopup is open we should live-update it.
707 if (elt.parentNode.open)
708 this._rebuildPopup(elt);
709 },
711 uninit: function PVB_uninit() {
712 if (this._result) {
713 this._result.removeObserver(this);
714 this._resultNode.containerOpen = false;
715 this._resultNode = null;
716 this._result = null;
717 }
719 if (this._controller) {
720 this._controller.terminate();
721 // Removing the controller will fail if it is already no longer there.
722 // This can happen if the view element was removed/reinserted without
723 // our knowledge. There is no way to check for that having happened
724 // without the possibility of an exception. :-(
725 try {
726 this._viewElt.controllers.removeController(this._controller);
727 } catch (ex) {
728 } finally {
729 this._controller = null;
730 }
731 }
733 delete this._viewElt._placesView;
734 },
736 get isRTL() {
737 if ("_isRTL" in this)
738 return this._isRTL;
740 return this._isRTL = document.defaultView
741 .getComputedStyle(this.viewElt, "")
742 .direction == "rtl";
743 },
745 get ownerWindow() window,
747 /**
748 * Adds an "Open All in Tabs" menuitem to the bottom of the popup.
749 * @param aPopup
750 * a Places popup.
751 */
752 _mayAddCommandsItems: function PVB__mayAddCommandsItems(aPopup) {
753 // The command items are never added to the root popup.
754 if (aPopup == this._rootElt)
755 return;
757 let hasMultipleURIs = false;
759 // Check if the popup contains at least 2 menuitems with places nodes.
760 // We don't currently support opening multiple uri nodes when they are not
761 // populated by the result.
762 if (aPopup._placesNode.childCount > 0) {
763 let currentChild = aPopup.firstChild;
764 let numURINodes = 0;
765 while (currentChild) {
766 if (currentChild.localName == "menuitem" && currentChild._placesNode) {
767 if (++numURINodes == 2)
768 break;
769 }
770 currentChild = currentChild.nextSibling;
771 }
772 hasMultipleURIs = numURINodes > 1;
773 }
775 if (!hasMultipleURIs) {
776 aPopup.setAttribute("singleitempopup", "true");
777 } else {
778 aPopup.removeAttribute("singleitempopup");
779 }
781 if (!hasMultipleURIs) {
782 // We don't have to show any option.
783 if (aPopup._endOptOpenAllInTabs) {
784 aPopup.removeChild(aPopup._endOptOpenAllInTabs);
785 aPopup._endOptOpenAllInTabs = null;
787 aPopup.removeChild(aPopup._endOptSeparator);
788 aPopup._endOptSeparator = null;
789 }
790 }
791 else if (!aPopup._endOptOpenAllInTabs) {
792 // Create a separator before options.
793 aPopup._endOptSeparator = document.createElement("menuseparator");
794 aPopup._endOptSeparator.className = "bookmarks-actions-menuseparator";
795 aPopup.appendChild(aPopup._endOptSeparator);
797 // Add the "Open All in Tabs" menuitem.
798 aPopup._endOptOpenAllInTabs = document.createElement("menuitem");
799 aPopup._endOptOpenAllInTabs.className = "openintabs-menuitem";
801 if (typeof this.options.extraClasses.entry == "string")
802 aPopup._endOptOpenAllInTabs.classList.add(this.options.extraClasses.entry);
803 if (typeof this.options.extraClasses.footer == "string")
804 aPopup._endOptOpenAllInTabs.classList.add(this.options.extraClasses.footer);
806 aPopup._endOptOpenAllInTabs.setAttribute("oncommand",
807 "PlacesUIUtils.openContainerNodeInTabs(this.parentNode._placesNode, event, " +
808 "PlacesUIUtils.getViewForNode(this));");
809 aPopup._endOptOpenAllInTabs.setAttribute("onclick",
810 "checkForMiddleClick(this, event); event.stopPropagation();");
811 aPopup._endOptOpenAllInTabs.setAttribute("label",
812 gNavigatorBundle.getString("menuOpenAllInTabs.label"));
813 aPopup.appendChild(aPopup._endOptOpenAllInTabs);
814 }
815 },
817 _ensureMarkers: function PVB__ensureMarkers(aPopup) {
818 if (aPopup._startMarker)
819 return;
821 // _startMarker is an hidden menuseparator that lives before places nodes.
822 aPopup._startMarker = document.createElement("menuseparator");
823 aPopup._startMarker.hidden = true;
824 aPopup.insertBefore(aPopup._startMarker, aPopup.firstChild);
826 // _endMarker is a DOM node that lives after places nodes, specified with
827 // the 'insertionPoint' option or will be a hidden menuseparator.
828 let node = ("insertionPoint" in this.options) ?
829 aPopup.querySelector(this.options.insertionPoint) : null;
830 if (node) {
831 aPopup._endMarker = node;
832 } else {
833 aPopup._endMarker = document.createElement("menuseparator");
834 aPopup._endMarker.hidden = true;
835 }
836 aPopup.appendChild(aPopup._endMarker);
838 // Move the markers to the right position.
839 let firstNonStaticNodeFound = false;
840 for (let i = 0; i < aPopup.childNodes.length; i++) {
841 let child = aPopup.childNodes[i];
842 // Menus that have static content at the end, but are initially empty,
843 // use a special "builder" attribute to figure out where to start
844 // inserting places nodes.
845 if (child.getAttribute("builder") == "end") {
846 aPopup.insertBefore(aPopup._endMarker, child);
847 break;
848 }
850 if (child._placesNode && !firstNonStaticNodeFound) {
851 firstNonStaticNodeFound = true;
852 aPopup.insertBefore(aPopup._startMarker, child);
853 }
854 }
855 if (!firstNonStaticNodeFound) {
856 aPopup.insertBefore(aPopup._startMarker, aPopup._endMarker);
857 }
858 },
860 _onPopupShowing: function PVB__onPopupShowing(aEvent) {
861 // Avoid handling popupshowing of inner views.
862 let popup = aEvent.originalTarget;
864 this._ensureMarkers(popup);
866 // Remove any delayed element, see _cleanPopup for details.
867 if ("_delayedRemovals" in popup) {
868 while (popup._delayedRemovals.length > 0) {
869 popup.removeChild(popup._delayedRemovals.shift());
870 }
871 }
873 if (popup._placesNode && PlacesUIUtils.getViewForNode(popup) == this) {
874 if (!popup._placesNode.containerOpen)
875 popup._placesNode.containerOpen = true;
876 if (!popup._built)
877 this._rebuildPopup(popup);
879 this._mayAddCommandsItems(popup);
880 }
881 },
883 _addEventListeners:
884 function PVB__addEventListeners(aObject, aEventNames, aCapturing) {
885 for (let i = 0; i < aEventNames.length; i++) {
886 aObject.addEventListener(aEventNames[i], this, aCapturing);
887 }
888 },
890 _removeEventListeners:
891 function PVB__removeEventListeners(aObject, aEventNames, aCapturing) {
892 for (let i = 0; i < aEventNames.length; i++) {
893 aObject.removeEventListener(aEventNames[i], this, aCapturing);
894 }
895 },
896 };
898 function PlacesToolbar(aPlace) {
899 let startTime = Date.now();
900 // Add some smart getters for our elements.
901 let thisView = this;
902 [
903 ["_viewElt", "PlacesToolbar"],
904 ["_rootElt", "PlacesToolbarItems"],
905 ["_dropIndicator", "PlacesToolbarDropIndicator"],
906 ["_chevron", "PlacesChevron"],
907 ["_chevronPopup", "PlacesChevronPopup"]
908 ].forEach(function (elementGlobal) {
909 let [name, id] = elementGlobal;
910 thisView.__defineGetter__(name, function () {
911 let element = document.getElementById(id);
912 if (!element)
913 return null;
915 delete thisView[name];
916 return thisView[name] = element;
917 });
918 });
920 this._viewElt._placesView = this;
922 this._addEventListeners(this._viewElt, this._cbEvents, false);
923 this._addEventListeners(this._rootElt, ["popupshowing", "popuphidden"], true);
924 this._addEventListeners(this._rootElt, ["overflow", "underflow"], true);
925 this._addEventListeners(window, ["resize", "unload"], false);
927 // If personal-bookmarks has been dragged to the tabs toolbar,
928 // we have to track addition and removals of tabs, to properly
929 // recalculate the available space for bookmarks.
930 // TODO (bug 734730): Use a performant mutation listener when available.
931 if (this._viewElt.parentNode.parentNode == document.getElementById("TabsToolbar")) {
932 this._addEventListeners(gBrowser.tabContainer, ["TabOpen", "TabClose"], false);
933 }
935 PlacesViewBase.call(this, aPlace);
937 Services.telemetry.getHistogramById("FX_BOOKMARKS_TOOLBAR_INIT_MS")
938 .add(Date.now() - startTime);
939 }
941 PlacesToolbar.prototype = {
942 __proto__: PlacesViewBase.prototype,
944 _cbEvents: ["dragstart", "dragover", "dragexit", "dragend", "drop",
945 "mousemove", "mouseover", "mouseout"],
947 QueryInterface: function PT_QueryInterface(aIID) {
948 if (aIID.equals(Ci.nsIDOMEventListener) ||
949 aIID.equals(Ci.nsITimerCallback))
950 return this;
952 return PlacesViewBase.prototype.QueryInterface.apply(this, arguments);
953 },
955 uninit: function PT_uninit() {
956 this._removeEventListeners(this._viewElt, this._cbEvents, false);
957 this._removeEventListeners(this._rootElt, ["popupshowing", "popuphidden"],
958 true);
959 this._removeEventListeners(this._rootElt, ["overflow", "underflow"], true);
960 this._removeEventListeners(window, ["resize", "unload"], false);
961 this._removeEventListeners(gBrowser.tabContainer, ["TabOpen", "TabClose"], false);
963 if (this._chevron._placesView) {
964 this._chevron._placesView.uninit();
965 }
967 PlacesViewBase.prototype.uninit.apply(this, arguments);
968 },
970 _openedMenuButton: null,
971 _allowPopupShowing: true,
973 _rebuild: function PT__rebuild() {
974 // Clear out references to existing nodes, since they will be removed
975 // and re-added.
976 if (this._overFolder.elt)
977 this._clearOverFolder();
979 this._openedMenuButton = null;
980 while (this._rootElt.hasChildNodes()) {
981 this._rootElt.removeChild(this._rootElt.firstChild);
982 }
984 let cc = this._resultNode.childCount;
985 for (let i = 0; i < cc; ++i) {
986 this._insertNewItem(this._resultNode.getChild(i), null);
987 }
989 if (this._chevronPopup.hasAttribute("type")) {
990 // Chevron has already been initialized, but since we are forcing
991 // a rebuild of the toolbar, it has to be rebuilt.
992 // Otherwise, it will be initialized when the toolbar overflows.
993 this._chevronPopup.place = this.place;
994 }
995 },
997 _insertNewItem:
998 function PT__insertNewItem(aChild, aBefore) {
999 this._domNodes.delete(aChild);
1001 let type = aChild.type;
1002 let button;
1003 if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) {
1004 button = document.createElement("toolbarseparator");
1005 }
1006 else {
1007 button = document.createElement("toolbarbutton");
1008 button.className = "bookmark-item";
1009 button.setAttribute("label", aChild.title);
1010 let icon = aChild.icon;
1011 if (icon)
1012 button.setAttribute("image", icon);
1014 if (PlacesUtils.containerTypes.indexOf(type) != -1) {
1015 button.setAttribute("type", "menu");
1016 button.setAttribute("container", "true");
1018 if (PlacesUtils.nodeIsQuery(aChild)) {
1019 button.setAttribute("query", "true");
1020 if (PlacesUtils.nodeIsTagQuery(aChild))
1021 button.setAttribute("tagContainer", "true");
1022 }
1023 else if (PlacesUtils.nodeIsFolder(aChild)) {
1024 PlacesUtils.livemarks.getLivemark({ id: aChild.itemId })
1025 .then(aLivemark => {
1026 button.setAttribute("livemark", "true");
1027 this.controller.cacheLivemarkInfo(aChild, aLivemark);
1028 }, () => undefined);
1029 }
1031 let popup = document.createElement("menupopup");
1032 popup.setAttribute("placespopup", "true");
1033 button.appendChild(popup);
1034 popup._placesNode = PlacesUtils.asContainer(aChild);
1035 popup.setAttribute("context", "placesContext");
1037 this._domNodes.set(aChild, popup);
1038 }
1039 else if (PlacesUtils.nodeIsURI(aChild)) {
1040 button.setAttribute("scheme",
1041 PlacesUIUtils.guessUrlSchemeForUI(aChild.uri));
1042 }
1043 }
1045 button._placesNode = aChild;
1046 if (!this._domNodes.has(aChild))
1047 this._domNodes.set(aChild, button);
1049 if (aBefore)
1050 this._rootElt.insertBefore(button, aBefore);
1051 else
1052 this._rootElt.appendChild(button);
1053 },
1055 _updateChevronPopupNodesVisibility:
1056 function PT__updateChevronPopupNodesVisibility() {
1057 for (let i = 0, node = this._chevronPopup._startMarker.nextSibling;
1058 node != this._chevronPopup._endMarker;
1059 i++, node = node.nextSibling) {
1060 node.hidden = this._rootElt.childNodes[i].style.visibility != "hidden";
1061 }
1062 },
1064 _onChevronPopupShowing:
1065 function PT__onChevronPopupShowing(aEvent) {
1066 // Handle popupshowing only for the chevron popup, not for nested ones.
1067 if (aEvent.target != this._chevronPopup)
1068 return;
1070 if (!this._chevron._placesView)
1071 this._chevron._placesView = new PlacesMenu(aEvent, this.place);
1073 this._updateChevronPopupNodesVisibility();
1074 },
1076 handleEvent: function PT_handleEvent(aEvent) {
1077 switch (aEvent.type) {
1078 case "unload":
1079 this.uninit();
1080 break;
1081 case "resize":
1082 // This handler updates nodes visibility in both the toolbar
1083 // and the chevron popup when a window resize does not change
1084 // the overflow status of the toolbar.
1085 this.updateChevron();
1086 break;
1087 case "overflow":
1088 if (!this._isOverflowStateEventRelevant(aEvent))
1089 return;
1090 this._onOverflow();
1091 break;
1092 case "underflow":
1093 if (!this._isOverflowStateEventRelevant(aEvent))
1094 return;
1095 this._onUnderflow();
1096 break;
1097 case "TabOpen":
1098 case "TabClose":
1099 this.updateChevron();
1100 break;
1101 case "dragstart":
1102 this._onDragStart(aEvent);
1103 break;
1104 case "dragover":
1105 this._onDragOver(aEvent);
1106 break;
1107 case "dragexit":
1108 this._onDragExit(aEvent);
1109 break;
1110 case "dragend":
1111 this._onDragEnd(aEvent);
1112 break;
1113 case "drop":
1114 this._onDrop(aEvent);
1115 break;
1116 case "mouseover":
1117 this._onMouseOver(aEvent);
1118 break;
1119 case "mousemove":
1120 this._onMouseMove(aEvent);
1121 break;
1122 case "mouseout":
1123 this._onMouseOut(aEvent);
1124 break;
1125 case "popupshowing":
1126 this._onPopupShowing(aEvent);
1127 break;
1128 case "popuphidden":
1129 this._onPopupHidden(aEvent);
1130 break;
1131 default:
1132 throw "Trying to handle unexpected event.";
1133 }
1134 },
1136 updateOverflowStatus: function() {
1137 if (this._rootElt.scrollLeftMax > 0) {
1138 this._onOverflow();
1139 } else {
1140 this._onUnderflow();
1141 }
1142 },
1144 _isOverflowStateEventRelevant: function PT_isOverflowStateEventRelevant(aEvent) {
1145 // Ignore events not aimed at ourselves, as well as purely vertical ones:
1146 return aEvent.target == aEvent.currentTarget && aEvent.detail > 0;
1147 },
1149 _onOverflow: function PT_onOverflow() {
1150 // Attach the popup binding to the chevron popup if it has not yet
1151 // been initialized.
1152 if (!this._chevronPopup.hasAttribute("type")) {
1153 this._chevronPopup.setAttribute("place", this.place);
1154 this._chevronPopup.setAttribute("type", "places");
1155 }
1156 this._chevron.collapsed = false;
1157 this.updateChevron();
1158 },
1160 _onUnderflow: function PT_onUnderflow() {
1161 this.updateChevron();
1162 this._chevron.collapsed = true;
1163 },
1165 updateChevron: function PT_updateChevron() {
1166 // If the chevron is collapsed there's nothing to update.
1167 if (this._chevron.collapsed)
1168 return;
1170 // Update the chevron on a timer. This will avoid repeated work when
1171 // lot of changes happen in a small timeframe.
1172 if (this._updateChevronTimer)
1173 this._updateChevronTimer.cancel();
1175 this._updateChevronTimer = this._setTimer(100);
1176 },
1178 _updateChevronTimerCallback: function PT__updateChevronTimerCallback() {
1179 let scrollRect = this._rootElt.getBoundingClientRect();
1180 let childOverflowed = false;
1181 for (let i = 0; i < this._rootElt.childNodes.length; i++) {
1182 let child = this._rootElt.childNodes[i];
1183 // Once a child overflows, all the next ones will.
1184 if (!childOverflowed) {
1185 let childRect = child.getBoundingClientRect();
1186 childOverflowed = this.isRTL ? (childRect.left < scrollRect.left)
1187 : (childRect.right > scrollRect.right);
1189 }
1190 child.style.visibility = childOverflowed ? "hidden" : "visible";
1191 }
1193 // We rebuild the chevron on popupShowing, so if it is open
1194 // we must update it.
1195 if (this._chevron.open)
1196 this._updateChevronPopupNodesVisibility();
1197 },
1199 nodeInserted:
1200 function PT_nodeInserted(aParentPlacesNode, aPlacesNode, aIndex) {
1201 let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode);
1202 if (parentElt == this._rootElt) {
1203 let children = this._rootElt.childNodes;
1204 this._insertNewItem(aPlacesNode,
1205 aIndex < children.length ? children[aIndex] : null);
1206 this.updateChevron();
1207 return;
1208 }
1210 PlacesViewBase.prototype.nodeInserted.apply(this, arguments);
1211 },
1213 nodeRemoved:
1214 function PT_nodeRemoved(aParentPlacesNode, aPlacesNode, aIndex) {
1215 let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode);
1216 let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
1218 // Here we need the <menu>.
1219 if (elt.localName == "menupopup")
1220 elt = elt.parentNode;
1222 if (parentElt == this._rootElt) {
1223 this._removeChild(elt);
1224 this.updateChevron();
1225 return;
1226 }
1228 PlacesViewBase.prototype.nodeRemoved.apply(this, arguments);
1229 },
1231 nodeMoved:
1232 function PT_nodeMoved(aPlacesNode,
1233 aOldParentPlacesNode, aOldIndex,
1234 aNewParentPlacesNode, aNewIndex) {
1235 let parentElt = this._getDOMNodeForPlacesNode(aNewParentPlacesNode);
1236 if (parentElt == this._rootElt) {
1237 // Container is on the toolbar.
1239 // Move the element.
1240 let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
1242 // Here we need the <menu>.
1243 if (elt.localName == "menupopup")
1244 elt = elt.parentNode;
1246 this._removeChild(elt);
1247 this._rootElt.insertBefore(elt, this._rootElt.childNodes[aNewIndex]);
1249 // The chevron view may get nodeMoved after the toolbar. In such a case,
1250 // we should ensure (by manually swapping menuitems) that the actual nodes
1251 // are in the final position before updateChevron tries to updates their
1252 // visibility, or the chevron may go out of sync.
1253 // Luckily updateChevron runs on a timer, so, by the time it updates
1254 // nodes, the menu has already handled the notification.
1256 this.updateChevron();
1257 return;
1258 }
1260 PlacesViewBase.prototype.nodeMoved.apply(this, arguments);
1261 },
1263 nodeAnnotationChanged:
1264 function PT_nodeAnnotationChanged(aPlacesNode, aAnno) {
1265 let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
1266 if (elt == this._rootElt)
1267 return;
1269 // We're notified for the menupopup, not the containing toolbarbutton.
1270 if (elt.localName == "menupopup")
1271 elt = elt.parentNode;
1273 if (elt.parentNode == this._rootElt) {
1274 // Node is on the toolbar.
1276 // All livemarks have a feedURI, so use it as our indicator.
1277 if (aAnno == PlacesUtils.LMANNO_FEEDURI) {
1278 elt.setAttribute("livemark", true);
1280 PlacesUtils.livemarks.getLivemark({ id: aPlacesNode.itemId })
1281 .then(aLivemark => {
1282 this.controller.cacheLivemarkInfo(aPlacesNode, aLivemark);
1283 this.invalidateContainer(aPlacesNode);
1284 }, Components.utils.reportError);
1285 }
1286 }
1287 else {
1288 // Node is in a submenu.
1289 PlacesViewBase.prototype.nodeAnnotationChanged.apply(this, arguments);
1290 }
1291 },
1293 nodeTitleChanged: function PT_nodeTitleChanged(aPlacesNode, aNewTitle) {
1294 let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
1296 // There's no UI representation for the root node, thus there's
1297 // nothing to be done when the title changes.
1298 if (elt == this._rootElt)
1299 return;
1301 PlacesViewBase.prototype.nodeTitleChanged.apply(this, arguments);
1303 // Here we need the <menu>.
1304 if (elt.localName == "menupopup")
1305 elt = elt.parentNode;
1307 if (elt.parentNode == this._rootElt) {
1308 // Node is on the toolbar
1309 this.updateChevron();
1310 }
1311 },
1313 invalidateContainer: function PT_invalidateContainer(aPlacesNode) {
1314 let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
1315 if (elt == this._rootElt) {
1316 // Container is the toolbar itself.
1317 this._rebuild();
1318 return;
1319 }
1321 PlacesViewBase.prototype.invalidateContainer.apply(this, arguments);
1322 },
1324 _overFolder: { elt: null,
1325 openTimer: null,
1326 hoverTime: 350,
1327 closeTimer: null },
1329 _clearOverFolder: function PT__clearOverFolder() {
1330 // The mouse is no longer dragging over the stored menubutton.
1331 // Close the menubutton, clear out drag styles, and clear all
1332 // timers for opening/closing it.
1333 if (this._overFolder.elt && this._overFolder.elt.lastChild) {
1334 if (!this._overFolder.elt.lastChild.hasAttribute("dragover")) {
1335 this._overFolder.elt.lastChild.hidePopup();
1336 }
1337 this._overFolder.elt.removeAttribute("dragover");
1338 this._overFolder.elt = null;
1339 }
1340 if (this._overFolder.openTimer) {
1341 this._overFolder.openTimer.cancel();
1342 this._overFolder.openTimer = null;
1343 }
1344 if (this._overFolder.closeTimer) {
1345 this._overFolder.closeTimer.cancel();
1346 this._overFolder.closeTimer = null;
1347 }
1348 },
1350 /**
1351 * This function returns information about where to drop when dragging over
1352 * the toolbar. The returned object has the following properties:
1353 * - ip: the insertion point for the bookmarks service.
1354 * - beforeIndex: child index to drop before, for the drop indicator.
1355 * - folderElt: the folder to drop into, if applicable.
1356 */
1357 _getDropPoint: function PT__getDropPoint(aEvent) {
1358 let result = this.result;
1359 if (!PlacesUtils.nodeIsFolder(this._resultNode))
1360 return null;
1362 let dropPoint = { ip: null, beforeIndex: null, folderElt: null };
1363 let elt = aEvent.target;
1364 if (elt._placesNode && elt != this._rootElt &&
1365 elt.localName != "menupopup") {
1366 let eltRect = elt.getBoundingClientRect();
1367 let eltIndex = Array.indexOf(this._rootElt.childNodes, elt);
1368 if (PlacesUtils.nodeIsFolder(elt._placesNode) &&
1369 !PlacesUtils.nodeIsReadOnly(elt._placesNode)) {
1370 // This is a folder.
1371 // If we are in the middle of it, drop inside it.
1372 // Otherwise, drop before it, with regards to RTL mode.
1373 let threshold = eltRect.width * 0.25;
1374 if (this.isRTL ? (aEvent.clientX > eltRect.right - threshold)
1375 : (aEvent.clientX < eltRect.left + threshold)) {
1376 // Drop before this folder.
1377 dropPoint.ip =
1378 new InsertionPoint(PlacesUtils.getConcreteItemId(this._resultNode),
1379 eltIndex, Ci.nsITreeView.DROP_BEFORE);
1380 dropPoint.beforeIndex = eltIndex;
1381 }
1382 else if (this.isRTL ? (aEvent.clientX > eltRect.left + threshold)
1383 : (aEvent.clientX < eltRect.right - threshold)) {
1384 // Drop inside this folder.
1385 dropPoint.ip =
1386 new InsertionPoint(PlacesUtils.getConcreteItemId(elt._placesNode),
1387 -1, Ci.nsITreeView.DROP_ON,
1388 PlacesUtils.nodeIsTagQuery(elt._placesNode));
1389 dropPoint.beforeIndex = eltIndex;
1390 dropPoint.folderElt = elt;
1391 }
1392 else {
1393 // Drop after this folder.
1394 let beforeIndex =
1395 (eltIndex == this._rootElt.childNodes.length - 1) ?
1396 -1 : eltIndex + 1;
1398 dropPoint.ip =
1399 new InsertionPoint(PlacesUtils.getConcreteItemId(this._resultNode),
1400 beforeIndex, Ci.nsITreeView.DROP_BEFORE);
1401 dropPoint.beforeIndex = beforeIndex;
1402 }
1403 }
1404 else {
1405 // This is a non-folder node or a read-only folder.
1406 // Drop before it with regards to RTL mode.
1407 let threshold = eltRect.width * 0.5;
1408 if (this.isRTL ? (aEvent.clientX > eltRect.left + threshold)
1409 : (aEvent.clientX < eltRect.left + threshold)) {
1410 // Drop before this bookmark.
1411 dropPoint.ip =
1412 new InsertionPoint(PlacesUtils.getConcreteItemId(this._resultNode),
1413 eltIndex, Ci.nsITreeView.DROP_BEFORE);
1414 dropPoint.beforeIndex = eltIndex;
1415 }
1416 else {
1417 // Drop after this bookmark.
1418 let beforeIndex =
1419 eltIndex == this._rootElt.childNodes.length - 1 ?
1420 -1 : eltIndex + 1;
1421 dropPoint.ip =
1422 new InsertionPoint(PlacesUtils.getConcreteItemId(this._resultNode),
1423 beforeIndex, Ci.nsITreeView.DROP_BEFORE);
1424 dropPoint.beforeIndex = beforeIndex;
1425 }
1426 }
1427 }
1428 else {
1429 // We are most likely dragging on the empty area of the
1430 // toolbar, we should drop after the last node.
1431 dropPoint.ip =
1432 new InsertionPoint(PlacesUtils.getConcreteItemId(this._resultNode),
1433 -1, Ci.nsITreeView.DROP_BEFORE);
1434 dropPoint.beforeIndex = -1;
1435 }
1437 return dropPoint;
1438 },
1440 _setTimer: function PT_setTimer(aTime) {
1441 let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
1442 timer.initWithCallback(this, aTime, timer.TYPE_ONE_SHOT);
1443 return timer;
1444 },
1446 notify: function PT_notify(aTimer) {
1447 if (aTimer == this._updateChevronTimer) {
1448 this._updateChevronTimer = null;
1449 this._updateChevronTimerCallback();
1450 }
1452 // * Timer to turn off indicator bar.
1453 else if (aTimer == this._ibTimer) {
1454 this._dropIndicator.collapsed = true;
1455 this._ibTimer = null;
1456 }
1458 // * Timer to open a menubutton that's being dragged over.
1459 else if (aTimer == this._overFolder.openTimer) {
1460 // Set the autoopen attribute on the folder's menupopup so that
1461 // the menu will automatically close when the mouse drags off of it.
1462 this._overFolder.elt.lastChild.setAttribute("autoopened", "true");
1463 this._overFolder.elt.open = true;
1464 this._overFolder.openTimer = null;
1465 }
1467 // * Timer to close a menubutton that's been dragged off of.
1468 else if (aTimer == this._overFolder.closeTimer) {
1469 // Close the menubutton if we are not dragging over it or one of
1470 // its children. The autoopened attribute will let the menu know to
1471 // close later if the menu is still being dragged over.
1472 let currentPlacesNode = PlacesControllerDragHelper.currentDropTarget;
1473 let inHierarchy = false;
1474 while (currentPlacesNode) {
1475 if (currentPlacesNode == this._rootElt) {
1476 inHierarchy = true;
1477 break;
1478 }
1479 currentPlacesNode = currentPlacesNode.parentNode;
1480 }
1481 // The _clearOverFolder() function will close the menu for
1482 // _overFolder.elt. So null it out if we don't want to close it.
1483 if (inHierarchy)
1484 this._overFolder.elt = null;
1486 // Clear out the folder and all associated timers.
1487 this._clearOverFolder();
1488 }
1489 },
1491 _onMouseOver: function PT__onMouseOver(aEvent) {
1492 let button = aEvent.target;
1493 if (button.parentNode == this._rootElt && button._placesNode &&
1494 PlacesUtils.nodeIsURI(button._placesNode))
1495 window.XULBrowserWindow.setOverLink(aEvent.target._placesNode.uri, null);
1496 },
1498 _onMouseOut: function PT__onMouseOut(aEvent) {
1499 window.XULBrowserWindow.setOverLink("", null);
1500 },
1502 _cleanupDragDetails: function PT__cleanupDragDetails() {
1503 // Called on dragend and drop.
1504 PlacesControllerDragHelper.currentDropTarget = null;
1505 this._draggedElt = null;
1506 if (this._ibTimer)
1507 this._ibTimer.cancel();
1509 this._dropIndicator.collapsed = true;
1510 },
1512 _onDragStart: function PT__onDragStart(aEvent) {
1513 // Sub menus have their own d&d handlers.
1514 let draggedElt = aEvent.target;
1515 if (draggedElt.parentNode != this._rootElt || !draggedElt._placesNode)
1516 return;
1518 if (draggedElt.localName == "toolbarbutton" &&
1519 draggedElt.getAttribute("type") == "menu") {
1520 // If the drag gesture on a container is toward down we open instead
1521 // of dragging.
1522 let translateY = this._cachedMouseMoveEvent.clientY - aEvent.clientY;
1523 let translateX = this._cachedMouseMoveEvent.clientX - aEvent.clientX;
1524 if ((translateY) >= Math.abs(translateX/2)) {
1525 // Don't start the drag.
1526 aEvent.preventDefault();
1527 // Open the menu.
1528 draggedElt.open = true;
1529 return;
1530 }
1532 // If the menu is open, close it.
1533 if (draggedElt.open) {
1534 draggedElt.lastChild.hidePopup();
1535 draggedElt.open = false;
1536 }
1537 }
1539 // Activate the view and cache the dragged element.
1540 this._draggedElt = draggedElt._placesNode;
1541 this._rootElt.focus();
1543 this._controller.setDataTransfer(aEvent);
1544 aEvent.stopPropagation();
1545 },
1547 _onDragOver: function PT__onDragOver(aEvent) {
1548 // Cache the dataTransfer
1549 PlacesControllerDragHelper.currentDropTarget = aEvent.target;
1550 let dt = aEvent.dataTransfer;
1552 let dropPoint = this._getDropPoint(aEvent);
1553 if (!dropPoint || !dropPoint.ip ||
1554 !PlacesControllerDragHelper.canDrop(dropPoint.ip, dt)) {
1555 this._dropIndicator.collapsed = true;
1556 aEvent.stopPropagation();
1557 return;
1558 }
1560 if (this._ibTimer) {
1561 this._ibTimer.cancel();
1562 this._ibTimer = null;
1563 }
1565 if (dropPoint.folderElt || aEvent.originalTarget == this._chevron) {
1566 // Dropping over a menubutton or chevron button.
1567 // Set styles and timer to open relative menupopup.
1568 let overElt = dropPoint.folderElt || this._chevron;
1569 if (this._overFolder.elt != overElt) {
1570 this._clearOverFolder();
1571 this._overFolder.elt = overElt;
1572 this._overFolder.openTimer = this._setTimer(this._overFolder.hoverTime);
1573 }
1574 if (!this._overFolder.elt.hasAttribute("dragover"))
1575 this._overFolder.elt.setAttribute("dragover", "true");
1577 this._dropIndicator.collapsed = true;
1578 }
1579 else {
1580 // Dragging over a normal toolbarbutton,
1581 // show indicator bar and move it to the appropriate drop point.
1582 let ind = this._dropIndicator;
1583 ind.parentNode.collapsed = false;
1584 let halfInd = ind.clientWidth / 2;
1585 let translateX;
1586 if (this.isRTL) {
1587 halfInd = Math.ceil(halfInd);
1588 translateX = 0 - this._rootElt.getBoundingClientRect().right - halfInd;
1589 if (this._rootElt.firstChild) {
1590 if (dropPoint.beforeIndex == -1)
1591 translateX += this._rootElt.lastChild.getBoundingClientRect().left;
1592 else {
1593 translateX += this._rootElt.childNodes[dropPoint.beforeIndex]
1594 .getBoundingClientRect().right;
1595 }
1596 }
1597 }
1598 else {
1599 halfInd = Math.floor(halfInd);
1600 translateX = 0 - this._rootElt.getBoundingClientRect().left +
1601 halfInd;
1602 if (this._rootElt.firstChild) {
1603 if (dropPoint.beforeIndex == -1)
1604 translateX += this._rootElt.lastChild.getBoundingClientRect().right;
1605 else {
1606 translateX += this._rootElt.childNodes[dropPoint.beforeIndex]
1607 .getBoundingClientRect().left;
1608 }
1609 }
1610 }
1612 ind.style.transform = "translate(" + Math.round(translateX) + "px)";
1613 ind.style.MozMarginStart = (-ind.clientWidth) + "px";
1614 ind.collapsed = false;
1616 // Clear out old folder information.
1617 this._clearOverFolder();
1618 }
1620 aEvent.preventDefault();
1621 aEvent.stopPropagation();
1622 },
1624 _onDrop: function PT__onDrop(aEvent) {
1625 PlacesControllerDragHelper.currentDropTarget = aEvent.target;
1627 let dropPoint = this._getDropPoint(aEvent);
1628 if (dropPoint && dropPoint.ip) {
1629 PlacesControllerDragHelper.onDrop(dropPoint.ip, aEvent.dataTransfer)
1630 aEvent.preventDefault();
1631 }
1633 this._cleanupDragDetails();
1634 aEvent.stopPropagation();
1635 },
1637 _onDragExit: function PT__onDragExit(aEvent) {
1638 PlacesControllerDragHelper.currentDropTarget = null;
1640 // Set timer to turn off indicator bar (if we turn it off
1641 // here, dragenter might be called immediately after, creating
1642 // flicker).
1643 if (this._ibTimer)
1644 this._ibTimer.cancel();
1645 this._ibTimer = this._setTimer(10);
1647 // If we hovered over a folder, close it now.
1648 if (this._overFolder.elt)
1649 this._overFolder.closeTimer = this._setTimer(this._overFolder.hoverTime);
1650 },
1652 _onDragEnd: function PT_onDragEnd(aEvent) {
1653 this._cleanupDragDetails();
1654 },
1656 _onPopupShowing: function PT__onPopupShowing(aEvent) {
1657 if (!this._allowPopupShowing) {
1658 this._allowPopupShowing = true;
1659 aEvent.preventDefault();
1660 return;
1661 }
1663 let parent = aEvent.target.parentNode;
1664 if (parent.localName == "toolbarbutton")
1665 this._openedMenuButton = parent;
1667 PlacesViewBase.prototype._onPopupShowing.apply(this, arguments);
1668 },
1670 _onPopupHidden: function PT__onPopupHidden(aEvent) {
1671 let popup = aEvent.target;
1672 let placesNode = popup._placesNode;
1673 // Avoid handling popuphidden of inner views
1674 if (placesNode && PlacesUIUtils.getViewForNode(popup) == this) {
1675 // UI performance: folder queries are cheap, keep the resultnode open
1676 // so we don't rebuild its contents whenever the popup is reopened.
1677 // Though, we want to always close feed containers so their expiration
1678 // status will be checked at next opening.
1679 if (!PlacesUtils.nodeIsFolder(placesNode) ||
1680 this.controller.hasCachedLivemarkInfo(placesNode)) {
1681 placesNode.containerOpen = false;
1682 }
1683 }
1685 let parent = popup.parentNode;
1686 if (parent.localName == "toolbarbutton") {
1687 this._openedMenuButton = null;
1688 // Clear the dragover attribute if present, if we are dragging into a
1689 // folder in the hierachy of current opened popup we don't clear
1690 // this attribute on clearOverFolder. See Notify for closeTimer.
1691 if (parent.hasAttribute("dragover"))
1692 parent.removeAttribute("dragover");
1693 }
1694 },
1696 _onMouseMove: function PT__onMouseMove(aEvent) {
1697 // Used in dragStart to prevent dragging folders when dragging down.
1698 this._cachedMouseMoveEvent = aEvent;
1700 if (this._openedMenuButton == null ||
1701 PlacesControllerDragHelper.getSession())
1702 return;
1704 let target = aEvent.originalTarget;
1705 if (this._openedMenuButton != target &&
1706 target.localName == "toolbarbutton" &&
1707 target.type == "menu") {
1708 this._openedMenuButton.open = false;
1709 target.open = true;
1710 }
1711 }
1712 };
1714 /**
1715 * View for Places menus. This object should be created during the first
1716 * popupshowing that's dispatched on the menu.
1717 */
1718 function PlacesMenu(aPopupShowingEvent, aPlace, aOptions) {
1719 this._rootElt = aPopupShowingEvent.target; // <menupopup>
1720 this._viewElt = this._rootElt.parentNode; // <menu>
1721 this._viewElt._placesView = this;
1722 this._addEventListeners(this._rootElt, ["popupshowing", "popuphidden"], true);
1723 this._addEventListeners(window, ["unload"], false);
1725 #ifdef XP_MACOSX
1726 // Must walk up to support views in sub-menus, like Bookmarks Toolbar menu.
1727 for (let elt = this._viewElt.parentNode; elt; elt = elt.parentNode) {
1728 if (elt.localName == "menubar") {
1729 this._nativeView = true;
1730 break;
1731 }
1732 }
1733 #endif
1735 PlacesViewBase.call(this, aPlace, aOptions);
1736 this._onPopupShowing(aPopupShowingEvent);
1737 }
1739 PlacesMenu.prototype = {
1740 __proto__: PlacesViewBase.prototype,
1742 QueryInterface: function PM_QueryInterface(aIID) {
1743 if (aIID.equals(Ci.nsIDOMEventListener))
1744 return this;
1746 return PlacesViewBase.prototype.QueryInterface.apply(this, arguments);
1747 },
1749 _removeChild: function PM_removeChild(aChild) {
1750 PlacesViewBase.prototype._removeChild.apply(this, arguments);
1751 },
1753 uninit: function PM_uninit() {
1754 this._removeEventListeners(this._rootElt, ["popupshowing", "popuphidden"],
1755 true);
1756 this._removeEventListeners(window, ["unload"], false);
1758 PlacesViewBase.prototype.uninit.apply(this, arguments);
1759 },
1761 handleEvent: function PM_handleEvent(aEvent) {
1762 switch (aEvent.type) {
1763 case "unload":
1764 this.uninit();
1765 break;
1766 case "popupshowing":
1767 this._onPopupShowing(aEvent);
1768 break;
1769 case "popuphidden":
1770 this._onPopupHidden(aEvent);
1771 break;
1772 }
1773 },
1775 _onPopupHidden: function PM__onPopupHidden(aEvent) {
1776 // Avoid handling popuphidden of inner views.
1777 let popup = aEvent.originalTarget;
1778 let placesNode = popup._placesNode;
1779 if (!placesNode || PlacesUIUtils.getViewForNode(popup) != this)
1780 return;
1782 // UI performance: folder queries are cheap, keep the resultnode open
1783 // so we don't rebuild its contents whenever the popup is reopened.
1784 // Though, we want to always close feed containers so their expiration
1785 // status will be checked at next opening.
1786 if (!PlacesUtils.nodeIsFolder(placesNode) ||
1787 this.controller.hasCachedLivemarkInfo(placesNode))
1788 placesNode.containerOpen = false;
1790 // The autoopened attribute is set for folders which have been
1791 // automatically opened when dragged over. Turn off this attribute
1792 // when the folder closes because it is no longer applicable.
1793 popup.removeAttribute("autoopened");
1794 popup.removeAttribute("dragstart");
1795 }
1796 };
1798 function PlacesPanelMenuView(aPlace, aViewId, aRootId, aOptions) {
1799 this._viewElt = document.getElementById(aViewId);
1800 this._rootElt = document.getElementById(aRootId);
1801 this._viewElt._placesView = this;
1802 this.options = aOptions;
1804 PlacesViewBase.call(this, aPlace, aOptions);
1805 }
1807 PlacesPanelMenuView.prototype = {
1808 __proto__: PlacesViewBase.prototype,
1810 QueryInterface: function PAMV_QueryInterface(aIID) {
1811 return PlacesViewBase.prototype.QueryInterface.apply(this, arguments);
1812 },
1814 uninit: function PAMV_uninit() {
1815 PlacesViewBase.prototype.uninit.apply(this, arguments);
1816 },
1818 _insertNewItem:
1819 function PAMV__insertNewItem(aChild, aBefore) {
1820 this._domNodes.delete(aChild);
1822 let type = aChild.type;
1823 let button;
1824 if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) {
1825 button = document.createElement("toolbarseparator");
1826 button.setAttribute("class", "small-separator");
1827 }
1828 else {
1829 button = document.createElement("toolbarbutton");
1830 button.className = "bookmark-item";
1831 if (typeof this.options.extraClasses.entry == "string")
1832 button.classList.add(this.options.extraClasses.entry);
1833 button.setAttribute("label", aChild.title);
1834 let icon = aChild.icon;
1835 if (icon)
1836 button.setAttribute("image", icon);
1838 if (PlacesUtils.containerTypes.indexOf(type) != -1) {
1839 button.setAttribute("container", "true");
1841 if (PlacesUtils.nodeIsQuery(aChild)) {
1842 button.setAttribute("query", "true");
1843 if (PlacesUtils.nodeIsTagQuery(aChild))
1844 button.setAttribute("tagContainer", "true");
1845 }
1846 else if (PlacesUtils.nodeIsFolder(aChild)) {
1847 PlacesUtils.livemarks.getLivemark({ id: aChild.itemId })
1848 .then(aLivemark => {
1849 button.setAttribute("livemark", "true");
1850 this.controller.cacheLivemarkInfo(aChild, aLivemark);
1851 }, () => undefined);
1852 }
1853 }
1854 else if (PlacesUtils.nodeIsURI(aChild)) {
1855 button.setAttribute("scheme",
1856 PlacesUIUtils.guessUrlSchemeForUI(aChild.uri));
1857 }
1858 }
1860 button._placesNode = aChild;
1861 if (!this._domNodes.has(aChild))
1862 this._domNodes.set(aChild, button);
1864 this._rootElt.insertBefore(button, aBefore);
1865 },
1867 nodeInserted:
1868 function PAMV_nodeInserted(aParentPlacesNode, aPlacesNode, aIndex) {
1869 let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode);
1870 if (parentElt != this._rootElt)
1871 return;
1873 let children = this._rootElt.childNodes;
1874 this._insertNewItem(aPlacesNode,
1875 aIndex < children.length ? children[aIndex] : null);
1876 },
1878 nodeRemoved:
1879 function PAMV_nodeRemoved(aParentPlacesNode, aPlacesNode, aIndex) {
1880 let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode);
1881 if (parentElt != this._rootElt)
1882 return;
1884 let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
1885 this._removeChild(elt);
1886 },
1888 nodeMoved:
1889 function PAMV_nodeMoved(aPlacesNode,
1890 aOldParentPlacesNode, aOldIndex,
1891 aNewParentPlacesNode, aNewIndex) {
1892 let parentElt = this._getDOMNodeForPlacesNode(aNewParentPlacesNode);
1893 if (parentElt != this._rootElt)
1894 return;
1896 let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
1897 this._removeChild(elt);
1898 this._rootElt.insertBefore(elt, this._rootElt.childNodes[aNewIndex]);
1899 },
1901 nodeAnnotationChanged:
1902 function PAMV_nodeAnnotationChanged(aPlacesNode, aAnno) {
1903 let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
1904 // There's no UI representation for the root node.
1905 if (elt == this._rootElt)
1906 return;
1908 if (elt.parentNode != this._rootElt)
1909 return;
1911 // All livemarks have a feedURI, so use it as our indicator.
1912 if (aAnno == PlacesUtils.LMANNO_FEEDURI) {
1913 elt.setAttribute("livemark", true);
1915 PlacesUtils.livemarks.getLivemark({ id: aPlacesNode.itemId })
1916 .then(aLivemark => {
1917 this.controller.cacheLivemarkInfo(aPlacesNode, aLivemark);
1918 this.invalidateContainer(aPlacesNode);
1919 }, Components.utils.reportError);
1920 }
1921 },
1923 nodeTitleChanged: function PAMV_nodeTitleChanged(aPlacesNode, aNewTitle) {
1924 let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
1926 // There's no UI representation for the root node.
1927 if (elt == this._rootElt)
1928 return;
1930 PlacesViewBase.prototype.nodeTitleChanged.apply(this, arguments);
1931 },
1933 invalidateContainer: function PAMV_invalidateContainer(aPlacesNode) {
1934 let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
1935 if (elt != this._rootElt)
1936 return;
1938 // Container is the toolbar itself.
1939 while (this._rootElt.hasChildNodes()) {
1940 this._rootElt.removeChild(this._rootElt.firstChild);
1941 }
1943 for (let i = 0; i < this._resultNode.childCount; ++i) {
1944 this._insertNewItem(this._resultNode.getChild(i), null);
1945 }
1946 }
1947 };