|
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/. */ |
|
4 |
|
5 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
6 Components.utils.import("resource://gre/modules/Services.jsm"); |
|
7 |
|
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 } |
|
18 |
|
19 PlacesViewBase.prototype = { |
|
20 // The xul element that holds the entire view. |
|
21 _viewElt: null, |
|
22 get viewElt() this._viewElt, |
|
23 |
|
24 get associatedElement() this._viewElt, |
|
25 |
|
26 get controllers() this._viewElt.controllers, |
|
27 |
|
28 // The xul element that represents the root container. |
|
29 _rootElt: null, |
|
30 |
|
31 // Set to true for views that are represented by native widgets (i.e. |
|
32 // the native mac menu). |
|
33 _nativeView: false, |
|
34 |
|
35 QueryInterface: XPCOMUtils.generateQI( |
|
36 [Components.interfaces.nsINavHistoryResultObserver, |
|
37 Components.interfaces.nsISupportsWeakReference]), |
|
38 |
|
39 _place: "", |
|
40 get place() this._place, |
|
41 set place(val) { |
|
42 this._place = val; |
|
43 |
|
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()]; |
|
49 |
|
50 let result = history.executeQueries(queries.value, queries.value.length, |
|
51 options.value); |
|
52 result.addObserver(this, false); |
|
53 return val; |
|
54 }, |
|
55 |
|
56 _result: null, |
|
57 get result() this._result, |
|
58 set result(val) { |
|
59 if (this._result == val) |
|
60 return val; |
|
61 |
|
62 if (this._result) { |
|
63 this._result.removeObserver(this); |
|
64 this._resultNode.containerOpen = false; |
|
65 } |
|
66 |
|
67 if (this._rootElt.localName == "menupopup") |
|
68 this._rootElt._built = false; |
|
69 |
|
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); |
|
76 |
|
77 // This calls _rebuild through invalidateContainer. |
|
78 this._resultNode.containerOpen = true; |
|
79 } |
|
80 else { |
|
81 this._resultNode = null; |
|
82 delete this._domNodes; |
|
83 } |
|
84 |
|
85 return val; |
|
86 }, |
|
87 |
|
88 _options: null, |
|
89 get options() this._options, |
|
90 set options(val) { |
|
91 if (!val) |
|
92 val = {}; |
|
93 |
|
94 if (!("extraClasses" in val)) |
|
95 val.extraClasses = {}; |
|
96 this._options = val; |
|
97 |
|
98 return val; |
|
99 }, |
|
100 |
|
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 }, |
|
117 |
|
118 get controller() this._controller, |
|
119 |
|
120 get selType() "single", |
|
121 selectItems: function() { }, |
|
122 selectAll: function() { }, |
|
123 |
|
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 }, |
|
131 |
|
132 get hasSelection() this.selectedNode != null, |
|
133 |
|
134 get selectedNodes() { |
|
135 let selectedNode = this.selectedNode; |
|
136 return selectedNode ? [selectedNode] : []; |
|
137 }, |
|
138 |
|
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 []; |
|
146 |
|
147 return [this.selectedNodes]; |
|
148 }, |
|
149 |
|
150 get draggableSelection() [this._draggedElt], |
|
151 |
|
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; |
|
160 |
|
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; |
|
166 |
|
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 } |
|
184 |
|
185 if (PlacesControllerDragHelper.disallowInsertion(container)) |
|
186 return null; |
|
187 |
|
188 return new InsertionPoint(PlacesUtils.getConcreteItemId(container), |
|
189 index, orientation, isTag); |
|
190 }, |
|
191 |
|
192 buildContextMenu: function PVB_buildContextMenu(aPopup) { |
|
193 this._contextMenuShown = true; |
|
194 window.updateCommands("places"); |
|
195 return this.controller.buildContextMenu(aPopup); |
|
196 }, |
|
197 |
|
198 destroyContextMenu: function PVB_destroyContextMenu(aPopup) { |
|
199 this._contextMenuShown = false; |
|
200 }, |
|
201 |
|
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 }, |
|
224 |
|
225 _rebuildPopup: function PVB__rebuildPopup(aPopup) { |
|
226 let resultNode = aPopup._placesNode; |
|
227 if (!resultNode.containerOpen) |
|
228 return; |
|
229 |
|
230 if (this.controller.hasCachedLivemarkInfo(resultNode)) { |
|
231 this._setEmptyPopupStatus(aPopup, false); |
|
232 aPopup._built = true; |
|
233 this._populateLivemarkPopup(aPopup); |
|
234 return; |
|
235 } |
|
236 |
|
237 this._cleanPopup(aPopup); |
|
238 |
|
239 let cc = resultNode.childCount; |
|
240 if (cc > 0) { |
|
241 this._setEmptyPopupStatus(aPopup, false); |
|
242 |
|
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 }, |
|
253 |
|
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; |
|
260 |
|
261 aChild.parentNode.removeChild(aChild); |
|
262 }, |
|
263 |
|
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 } |
|
275 |
|
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 }, |
|
290 |
|
291 _createMenuItemForPlacesNode: |
|
292 function PVB__createMenuItemForPlacesNode(aPlacesNode) { |
|
293 this._domNodes.delete(aPlacesNode); |
|
294 |
|
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"); |
|
312 |
|
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 } |
|
335 |
|
336 let popup = document.createElement("menupopup"); |
|
337 popup._placesNode = PlacesUtils.asContainer(aPlacesNode); |
|
338 |
|
339 if (!this._nativeView) { |
|
340 popup.setAttribute("placespopup", "true"); |
|
341 } |
|
342 |
|
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 } |
|
348 |
|
349 this._domNodes.set(aPlacesNode, popup); |
|
350 } |
|
351 else |
|
352 throw "Unexpected node"; |
|
353 |
|
354 element.setAttribute("label", PlacesUIUtils.getBestTitle(aPlacesNode)); |
|
355 |
|
356 let icon = aPlacesNode.icon; |
|
357 if (icon) |
|
358 element.setAttribute("image", icon); |
|
359 } |
|
360 |
|
361 element._placesNode = aPlacesNode; |
|
362 if (!this._domNodes.has(aPlacesNode)) |
|
363 this._domNodes.set(aPlacesNode, element); |
|
364 |
|
365 return element; |
|
366 }, |
|
367 |
|
368 _insertNewItemToPopup: |
|
369 function PVB__insertNewItemToPopup(aNewChild, aPopup, aBefore) { |
|
370 let element = this._createMenuItemForPlacesNode(aNewChild); |
|
371 let before = aBefore || aPopup._endMarker; |
|
372 |
|
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 } |
|
377 |
|
378 aPopup.insertBefore(element, before); |
|
379 return element; |
|
380 }, |
|
381 |
|
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);"); |
|
403 |
|
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); |
|
415 |
|
416 aPopup._siteURIMenuseparator = document.createElement("menuseparator"); |
|
417 aPopup.insertBefore(aPopup._siteURIMenuseparator, aPopup._startMarker); |
|
418 } |
|
419 }, |
|
420 |
|
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 } |
|
441 |
|
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 }, |
|
457 |
|
458 toggleCutNode: function PVB_toggleCutNode(aPlacesNode, aValue) { |
|
459 let elt = this._getDOMNodeForPlacesNode(aPlacesNode); |
|
460 |
|
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 }, |
|
469 |
|
470 nodeURIChanged: function PVB_nodeURIChanged(aPlacesNode, aURIString) { |
|
471 let elt = this._getDOMNodeForPlacesNode(aPlacesNode); |
|
472 |
|
473 // Here we need the <menu>. |
|
474 if (elt.localName == "menupopup") |
|
475 elt = elt.parentNode; |
|
476 |
|
477 elt.setAttribute("scheme", PlacesUIUtils.guessUrlSchemeForUI(aURIString)); |
|
478 }, |
|
479 |
|
480 nodeIconChanged: function PVB_nodeIconChanged(aPlacesNode) { |
|
481 let elt = this._getDOMNodeForPlacesNode(aPlacesNode); |
|
482 |
|
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; |
|
487 |
|
488 // Here we need the <menu>. |
|
489 if (elt.localName == "menupopup") |
|
490 elt = elt.parentNode; |
|
491 |
|
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 }, |
|
498 |
|
499 nodeAnnotationChanged: |
|
500 function PVB_nodeAnnotationChanged(aPlacesNode, aAnno) { |
|
501 let elt = this._getDOMNodeForPlacesNode(aPlacesNode); |
|
502 |
|
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 } |
|
516 |
|
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 }, |
|
525 |
|
526 nodeTitleChanged: |
|
527 function PVB_nodeTitleChanged(aPlacesNode, aNewTitle) { |
|
528 let elt = this._getDOMNodeForPlacesNode(aPlacesNode); |
|
529 |
|
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; |
|
534 |
|
535 // Here we need the <menu>. |
|
536 if (elt.localName == "menupopup") |
|
537 elt = elt.parentNode; |
|
538 |
|
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 }, |
|
549 |
|
550 nodeRemoved: |
|
551 function PVB_nodeRemoved(aParentPlacesNode, aPlacesNode, aIndex) { |
|
552 let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode); |
|
553 let elt = this._getDOMNodeForPlacesNode(aPlacesNode); |
|
554 |
|
555 // Here we need the <menu>. |
|
556 if (elt.localName == "menupopup") |
|
557 elt = elt.parentNode; |
|
558 |
|
559 if (parentElt._built) { |
|
560 parentElt.removeChild(elt); |
|
561 |
|
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 }, |
|
569 |
|
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 }, |
|
589 |
|
590 nodeTagsChanged: function() { }, |
|
591 nodeDateAddedChanged: function() { }, |
|
592 nodeLastModifiedChanged: function() { }, |
|
593 nodeKeywordChanged: function() { }, |
|
594 sortingChanged: function() { }, |
|
595 batching: function() { }, |
|
596 |
|
597 nodeInserted: |
|
598 function PVB_nodeInserted(aParentPlacesNode, aPlacesNode, aIndex) { |
|
599 let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode); |
|
600 if (!parentElt._built) |
|
601 return; |
|
602 |
|
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 }, |
|
609 |
|
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); |
|
619 |
|
620 // Here we need the <menu>. |
|
621 if (elt.localName == "menupopup") |
|
622 elt = elt.parentNode; |
|
623 |
|
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; |
|
628 |
|
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 }, |
|
638 |
|
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); |
|
644 |
|
645 if (PlacesUtils.nodeIsFolder(aPlacesNode)) { |
|
646 let queryOptions = PlacesUtils.asQuery(this._result.root).queryOptions; |
|
647 if (queryOptions.excludeItems) { |
|
648 return; |
|
649 } |
|
650 |
|
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 }, |
|
671 |
|
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); |
|
678 |
|
679 PlacesUtils.livemarks.getLivemark({ id: aPopup._placesNode.itemId }) |
|
680 .then(aLivemark => { |
|
681 let placesNode = aPopup._placesNode; |
|
682 if (!placesNode.containerOpen) |
|
683 return; |
|
684 |
|
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")); |
|
689 |
|
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 }, |
|
701 |
|
702 invalidateContainer: function PVB_invalidateContainer(aPlacesNode) { |
|
703 let elt = this._getDOMNodeForPlacesNode(aPlacesNode); |
|
704 elt._built = false; |
|
705 |
|
706 // If the menupopup is open we should live-update it. |
|
707 if (elt.parentNode.open) |
|
708 this._rebuildPopup(elt); |
|
709 }, |
|
710 |
|
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 } |
|
718 |
|
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 } |
|
732 |
|
733 delete this._viewElt._placesView; |
|
734 }, |
|
735 |
|
736 get isRTL() { |
|
737 if ("_isRTL" in this) |
|
738 return this._isRTL; |
|
739 |
|
740 return this._isRTL = document.defaultView |
|
741 .getComputedStyle(this.viewElt, "") |
|
742 .direction == "rtl"; |
|
743 }, |
|
744 |
|
745 get ownerWindow() window, |
|
746 |
|
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; |
|
756 |
|
757 let hasMultipleURIs = false; |
|
758 |
|
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 } |
|
774 |
|
775 if (!hasMultipleURIs) { |
|
776 aPopup.setAttribute("singleitempopup", "true"); |
|
777 } else { |
|
778 aPopup.removeAttribute("singleitempopup"); |
|
779 } |
|
780 |
|
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; |
|
786 |
|
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); |
|
796 |
|
797 // Add the "Open All in Tabs" menuitem. |
|
798 aPopup._endOptOpenAllInTabs = document.createElement("menuitem"); |
|
799 aPopup._endOptOpenAllInTabs.className = "openintabs-menuitem"; |
|
800 |
|
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); |
|
805 |
|
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 }, |
|
816 |
|
817 _ensureMarkers: function PVB__ensureMarkers(aPopup) { |
|
818 if (aPopup._startMarker) |
|
819 return; |
|
820 |
|
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); |
|
825 |
|
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); |
|
837 |
|
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 } |
|
849 |
|
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 }, |
|
859 |
|
860 _onPopupShowing: function PVB__onPopupShowing(aEvent) { |
|
861 // Avoid handling popupshowing of inner views. |
|
862 let popup = aEvent.originalTarget; |
|
863 |
|
864 this._ensureMarkers(popup); |
|
865 |
|
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 } |
|
872 |
|
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); |
|
878 |
|
879 this._mayAddCommandsItems(popup); |
|
880 } |
|
881 }, |
|
882 |
|
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 }, |
|
889 |
|
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 }; |
|
897 |
|
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; |
|
914 |
|
915 delete thisView[name]; |
|
916 return thisView[name] = element; |
|
917 }); |
|
918 }); |
|
919 |
|
920 this._viewElt._placesView = this; |
|
921 |
|
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); |
|
926 |
|
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 } |
|
934 |
|
935 PlacesViewBase.call(this, aPlace); |
|
936 |
|
937 Services.telemetry.getHistogramById("FX_BOOKMARKS_TOOLBAR_INIT_MS") |
|
938 .add(Date.now() - startTime); |
|
939 } |
|
940 |
|
941 PlacesToolbar.prototype = { |
|
942 __proto__: PlacesViewBase.prototype, |
|
943 |
|
944 _cbEvents: ["dragstart", "dragover", "dragexit", "dragend", "drop", |
|
945 "mousemove", "mouseover", "mouseout"], |
|
946 |
|
947 QueryInterface: function PT_QueryInterface(aIID) { |
|
948 if (aIID.equals(Ci.nsIDOMEventListener) || |
|
949 aIID.equals(Ci.nsITimerCallback)) |
|
950 return this; |
|
951 |
|
952 return PlacesViewBase.prototype.QueryInterface.apply(this, arguments); |
|
953 }, |
|
954 |
|
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); |
|
962 |
|
963 if (this._chevron._placesView) { |
|
964 this._chevron._placesView.uninit(); |
|
965 } |
|
966 |
|
967 PlacesViewBase.prototype.uninit.apply(this, arguments); |
|
968 }, |
|
969 |
|
970 _openedMenuButton: null, |
|
971 _allowPopupShowing: true, |
|
972 |
|
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(); |
|
978 |
|
979 this._openedMenuButton = null; |
|
980 while (this._rootElt.hasChildNodes()) { |
|
981 this._rootElt.removeChild(this._rootElt.firstChild); |
|
982 } |
|
983 |
|
984 let cc = this._resultNode.childCount; |
|
985 for (let i = 0; i < cc; ++i) { |
|
986 this._insertNewItem(this._resultNode.getChild(i), null); |
|
987 } |
|
988 |
|
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 }, |
|
996 |
|
997 _insertNewItem: |
|
998 function PT__insertNewItem(aChild, aBefore) { |
|
999 this._domNodes.delete(aChild); |
|
1000 |
|
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); |
|
1013 |
|
1014 if (PlacesUtils.containerTypes.indexOf(type) != -1) { |
|
1015 button.setAttribute("type", "menu"); |
|
1016 button.setAttribute("container", "true"); |
|
1017 |
|
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 } |
|
1030 |
|
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"); |
|
1036 |
|
1037 this._domNodes.set(aChild, popup); |
|
1038 } |
|
1039 else if (PlacesUtils.nodeIsURI(aChild)) { |
|
1040 button.setAttribute("scheme", |
|
1041 PlacesUIUtils.guessUrlSchemeForUI(aChild.uri)); |
|
1042 } |
|
1043 } |
|
1044 |
|
1045 button._placesNode = aChild; |
|
1046 if (!this._domNodes.has(aChild)) |
|
1047 this._domNodes.set(aChild, button); |
|
1048 |
|
1049 if (aBefore) |
|
1050 this._rootElt.insertBefore(button, aBefore); |
|
1051 else |
|
1052 this._rootElt.appendChild(button); |
|
1053 }, |
|
1054 |
|
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 }, |
|
1063 |
|
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; |
|
1069 |
|
1070 if (!this._chevron._placesView) |
|
1071 this._chevron._placesView = new PlacesMenu(aEvent, this.place); |
|
1072 |
|
1073 this._updateChevronPopupNodesVisibility(); |
|
1074 }, |
|
1075 |
|
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 }, |
|
1135 |
|
1136 updateOverflowStatus: function() { |
|
1137 if (this._rootElt.scrollLeftMax > 0) { |
|
1138 this._onOverflow(); |
|
1139 } else { |
|
1140 this._onUnderflow(); |
|
1141 } |
|
1142 }, |
|
1143 |
|
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 }, |
|
1148 |
|
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 }, |
|
1159 |
|
1160 _onUnderflow: function PT_onUnderflow() { |
|
1161 this.updateChevron(); |
|
1162 this._chevron.collapsed = true; |
|
1163 }, |
|
1164 |
|
1165 updateChevron: function PT_updateChevron() { |
|
1166 // If the chevron is collapsed there's nothing to update. |
|
1167 if (this._chevron.collapsed) |
|
1168 return; |
|
1169 |
|
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(); |
|
1174 |
|
1175 this._updateChevronTimer = this._setTimer(100); |
|
1176 }, |
|
1177 |
|
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); |
|
1188 |
|
1189 } |
|
1190 child.style.visibility = childOverflowed ? "hidden" : "visible"; |
|
1191 } |
|
1192 |
|
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 }, |
|
1198 |
|
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 } |
|
1209 |
|
1210 PlacesViewBase.prototype.nodeInserted.apply(this, arguments); |
|
1211 }, |
|
1212 |
|
1213 nodeRemoved: |
|
1214 function PT_nodeRemoved(aParentPlacesNode, aPlacesNode, aIndex) { |
|
1215 let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode); |
|
1216 let elt = this._getDOMNodeForPlacesNode(aPlacesNode); |
|
1217 |
|
1218 // Here we need the <menu>. |
|
1219 if (elt.localName == "menupopup") |
|
1220 elt = elt.parentNode; |
|
1221 |
|
1222 if (parentElt == this._rootElt) { |
|
1223 this._removeChild(elt); |
|
1224 this.updateChevron(); |
|
1225 return; |
|
1226 } |
|
1227 |
|
1228 PlacesViewBase.prototype.nodeRemoved.apply(this, arguments); |
|
1229 }, |
|
1230 |
|
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. |
|
1238 |
|
1239 // Move the element. |
|
1240 let elt = this._getDOMNodeForPlacesNode(aPlacesNode); |
|
1241 |
|
1242 // Here we need the <menu>. |
|
1243 if (elt.localName == "menupopup") |
|
1244 elt = elt.parentNode; |
|
1245 |
|
1246 this._removeChild(elt); |
|
1247 this._rootElt.insertBefore(elt, this._rootElt.childNodes[aNewIndex]); |
|
1248 |
|
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. |
|
1255 |
|
1256 this.updateChevron(); |
|
1257 return; |
|
1258 } |
|
1259 |
|
1260 PlacesViewBase.prototype.nodeMoved.apply(this, arguments); |
|
1261 }, |
|
1262 |
|
1263 nodeAnnotationChanged: |
|
1264 function PT_nodeAnnotationChanged(aPlacesNode, aAnno) { |
|
1265 let elt = this._getDOMNodeForPlacesNode(aPlacesNode); |
|
1266 if (elt == this._rootElt) |
|
1267 return; |
|
1268 |
|
1269 // We're notified for the menupopup, not the containing toolbarbutton. |
|
1270 if (elt.localName == "menupopup") |
|
1271 elt = elt.parentNode; |
|
1272 |
|
1273 if (elt.parentNode == this._rootElt) { |
|
1274 // Node is on the toolbar. |
|
1275 |
|
1276 // All livemarks have a feedURI, so use it as our indicator. |
|
1277 if (aAnno == PlacesUtils.LMANNO_FEEDURI) { |
|
1278 elt.setAttribute("livemark", true); |
|
1279 |
|
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 }, |
|
1292 |
|
1293 nodeTitleChanged: function PT_nodeTitleChanged(aPlacesNode, aNewTitle) { |
|
1294 let elt = this._getDOMNodeForPlacesNode(aPlacesNode); |
|
1295 |
|
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; |
|
1300 |
|
1301 PlacesViewBase.prototype.nodeTitleChanged.apply(this, arguments); |
|
1302 |
|
1303 // Here we need the <menu>. |
|
1304 if (elt.localName == "menupopup") |
|
1305 elt = elt.parentNode; |
|
1306 |
|
1307 if (elt.parentNode == this._rootElt) { |
|
1308 // Node is on the toolbar |
|
1309 this.updateChevron(); |
|
1310 } |
|
1311 }, |
|
1312 |
|
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 } |
|
1320 |
|
1321 PlacesViewBase.prototype.invalidateContainer.apply(this, arguments); |
|
1322 }, |
|
1323 |
|
1324 _overFolder: { elt: null, |
|
1325 openTimer: null, |
|
1326 hoverTime: 350, |
|
1327 closeTimer: null }, |
|
1328 |
|
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 }, |
|
1349 |
|
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; |
|
1361 |
|
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; |
|
1397 |
|
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 } |
|
1436 |
|
1437 return dropPoint; |
|
1438 }, |
|
1439 |
|
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 }, |
|
1445 |
|
1446 notify: function PT_notify(aTimer) { |
|
1447 if (aTimer == this._updateChevronTimer) { |
|
1448 this._updateChevronTimer = null; |
|
1449 this._updateChevronTimerCallback(); |
|
1450 } |
|
1451 |
|
1452 // * Timer to turn off indicator bar. |
|
1453 else if (aTimer == this._ibTimer) { |
|
1454 this._dropIndicator.collapsed = true; |
|
1455 this._ibTimer = null; |
|
1456 } |
|
1457 |
|
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 } |
|
1466 |
|
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; |
|
1485 |
|
1486 // Clear out the folder and all associated timers. |
|
1487 this._clearOverFolder(); |
|
1488 } |
|
1489 }, |
|
1490 |
|
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 }, |
|
1497 |
|
1498 _onMouseOut: function PT__onMouseOut(aEvent) { |
|
1499 window.XULBrowserWindow.setOverLink("", null); |
|
1500 }, |
|
1501 |
|
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(); |
|
1508 |
|
1509 this._dropIndicator.collapsed = true; |
|
1510 }, |
|
1511 |
|
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; |
|
1517 |
|
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 } |
|
1531 |
|
1532 // If the menu is open, close it. |
|
1533 if (draggedElt.open) { |
|
1534 draggedElt.lastChild.hidePopup(); |
|
1535 draggedElt.open = false; |
|
1536 } |
|
1537 } |
|
1538 |
|
1539 // Activate the view and cache the dragged element. |
|
1540 this._draggedElt = draggedElt._placesNode; |
|
1541 this._rootElt.focus(); |
|
1542 |
|
1543 this._controller.setDataTransfer(aEvent); |
|
1544 aEvent.stopPropagation(); |
|
1545 }, |
|
1546 |
|
1547 _onDragOver: function PT__onDragOver(aEvent) { |
|
1548 // Cache the dataTransfer |
|
1549 PlacesControllerDragHelper.currentDropTarget = aEvent.target; |
|
1550 let dt = aEvent.dataTransfer; |
|
1551 |
|
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 } |
|
1559 |
|
1560 if (this._ibTimer) { |
|
1561 this._ibTimer.cancel(); |
|
1562 this._ibTimer = null; |
|
1563 } |
|
1564 |
|
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"); |
|
1576 |
|
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 } |
|
1611 |
|
1612 ind.style.transform = "translate(" + Math.round(translateX) + "px)"; |
|
1613 ind.style.MozMarginStart = (-ind.clientWidth) + "px"; |
|
1614 ind.collapsed = false; |
|
1615 |
|
1616 // Clear out old folder information. |
|
1617 this._clearOverFolder(); |
|
1618 } |
|
1619 |
|
1620 aEvent.preventDefault(); |
|
1621 aEvent.stopPropagation(); |
|
1622 }, |
|
1623 |
|
1624 _onDrop: function PT__onDrop(aEvent) { |
|
1625 PlacesControllerDragHelper.currentDropTarget = aEvent.target; |
|
1626 |
|
1627 let dropPoint = this._getDropPoint(aEvent); |
|
1628 if (dropPoint && dropPoint.ip) { |
|
1629 PlacesControllerDragHelper.onDrop(dropPoint.ip, aEvent.dataTransfer) |
|
1630 aEvent.preventDefault(); |
|
1631 } |
|
1632 |
|
1633 this._cleanupDragDetails(); |
|
1634 aEvent.stopPropagation(); |
|
1635 }, |
|
1636 |
|
1637 _onDragExit: function PT__onDragExit(aEvent) { |
|
1638 PlacesControllerDragHelper.currentDropTarget = null; |
|
1639 |
|
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); |
|
1646 |
|
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 }, |
|
1651 |
|
1652 _onDragEnd: function PT_onDragEnd(aEvent) { |
|
1653 this._cleanupDragDetails(); |
|
1654 }, |
|
1655 |
|
1656 _onPopupShowing: function PT__onPopupShowing(aEvent) { |
|
1657 if (!this._allowPopupShowing) { |
|
1658 this._allowPopupShowing = true; |
|
1659 aEvent.preventDefault(); |
|
1660 return; |
|
1661 } |
|
1662 |
|
1663 let parent = aEvent.target.parentNode; |
|
1664 if (parent.localName == "toolbarbutton") |
|
1665 this._openedMenuButton = parent; |
|
1666 |
|
1667 PlacesViewBase.prototype._onPopupShowing.apply(this, arguments); |
|
1668 }, |
|
1669 |
|
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 } |
|
1684 |
|
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 }, |
|
1695 |
|
1696 _onMouseMove: function PT__onMouseMove(aEvent) { |
|
1697 // Used in dragStart to prevent dragging folders when dragging down. |
|
1698 this._cachedMouseMoveEvent = aEvent; |
|
1699 |
|
1700 if (this._openedMenuButton == null || |
|
1701 PlacesControllerDragHelper.getSession()) |
|
1702 return; |
|
1703 |
|
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 }; |
|
1713 |
|
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); |
|
1724 |
|
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 |
|
1734 |
|
1735 PlacesViewBase.call(this, aPlace, aOptions); |
|
1736 this._onPopupShowing(aPopupShowingEvent); |
|
1737 } |
|
1738 |
|
1739 PlacesMenu.prototype = { |
|
1740 __proto__: PlacesViewBase.prototype, |
|
1741 |
|
1742 QueryInterface: function PM_QueryInterface(aIID) { |
|
1743 if (aIID.equals(Ci.nsIDOMEventListener)) |
|
1744 return this; |
|
1745 |
|
1746 return PlacesViewBase.prototype.QueryInterface.apply(this, arguments); |
|
1747 }, |
|
1748 |
|
1749 _removeChild: function PM_removeChild(aChild) { |
|
1750 PlacesViewBase.prototype._removeChild.apply(this, arguments); |
|
1751 }, |
|
1752 |
|
1753 uninit: function PM_uninit() { |
|
1754 this._removeEventListeners(this._rootElt, ["popupshowing", "popuphidden"], |
|
1755 true); |
|
1756 this._removeEventListeners(window, ["unload"], false); |
|
1757 |
|
1758 PlacesViewBase.prototype.uninit.apply(this, arguments); |
|
1759 }, |
|
1760 |
|
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 }, |
|
1774 |
|
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; |
|
1781 |
|
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; |
|
1789 |
|
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 }; |
|
1797 |
|
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; |
|
1803 |
|
1804 PlacesViewBase.call(this, aPlace, aOptions); |
|
1805 } |
|
1806 |
|
1807 PlacesPanelMenuView.prototype = { |
|
1808 __proto__: PlacesViewBase.prototype, |
|
1809 |
|
1810 QueryInterface: function PAMV_QueryInterface(aIID) { |
|
1811 return PlacesViewBase.prototype.QueryInterface.apply(this, arguments); |
|
1812 }, |
|
1813 |
|
1814 uninit: function PAMV_uninit() { |
|
1815 PlacesViewBase.prototype.uninit.apply(this, arguments); |
|
1816 }, |
|
1817 |
|
1818 _insertNewItem: |
|
1819 function PAMV__insertNewItem(aChild, aBefore) { |
|
1820 this._domNodes.delete(aChild); |
|
1821 |
|
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); |
|
1837 |
|
1838 if (PlacesUtils.containerTypes.indexOf(type) != -1) { |
|
1839 button.setAttribute("container", "true"); |
|
1840 |
|
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 } |
|
1859 |
|
1860 button._placesNode = aChild; |
|
1861 if (!this._domNodes.has(aChild)) |
|
1862 this._domNodes.set(aChild, button); |
|
1863 |
|
1864 this._rootElt.insertBefore(button, aBefore); |
|
1865 }, |
|
1866 |
|
1867 nodeInserted: |
|
1868 function PAMV_nodeInserted(aParentPlacesNode, aPlacesNode, aIndex) { |
|
1869 let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode); |
|
1870 if (parentElt != this._rootElt) |
|
1871 return; |
|
1872 |
|
1873 let children = this._rootElt.childNodes; |
|
1874 this._insertNewItem(aPlacesNode, |
|
1875 aIndex < children.length ? children[aIndex] : null); |
|
1876 }, |
|
1877 |
|
1878 nodeRemoved: |
|
1879 function PAMV_nodeRemoved(aParentPlacesNode, aPlacesNode, aIndex) { |
|
1880 let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode); |
|
1881 if (parentElt != this._rootElt) |
|
1882 return; |
|
1883 |
|
1884 let elt = this._getDOMNodeForPlacesNode(aPlacesNode); |
|
1885 this._removeChild(elt); |
|
1886 }, |
|
1887 |
|
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; |
|
1895 |
|
1896 let elt = this._getDOMNodeForPlacesNode(aPlacesNode); |
|
1897 this._removeChild(elt); |
|
1898 this._rootElt.insertBefore(elt, this._rootElt.childNodes[aNewIndex]); |
|
1899 }, |
|
1900 |
|
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; |
|
1907 |
|
1908 if (elt.parentNode != this._rootElt) |
|
1909 return; |
|
1910 |
|
1911 // All livemarks have a feedURI, so use it as our indicator. |
|
1912 if (aAnno == PlacesUtils.LMANNO_FEEDURI) { |
|
1913 elt.setAttribute("livemark", true); |
|
1914 |
|
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 }, |
|
1922 |
|
1923 nodeTitleChanged: function PAMV_nodeTitleChanged(aPlacesNode, aNewTitle) { |
|
1924 let elt = this._getDOMNodeForPlacesNode(aPlacesNode); |
|
1925 |
|
1926 // There's no UI representation for the root node. |
|
1927 if (elt == this._rootElt) |
|
1928 return; |
|
1929 |
|
1930 PlacesViewBase.prototype.nodeTitleChanged.apply(this, arguments); |
|
1931 }, |
|
1932 |
|
1933 invalidateContainer: function PAMV_invalidateContainer(aPlacesNode) { |
|
1934 let elt = this._getDOMNodeForPlacesNode(aPlacesNode); |
|
1935 if (elt != this._rootElt) |
|
1936 return; |
|
1937 |
|
1938 // Container is the toolbar itself. |
|
1939 while (this._rootElt.hasChildNodes()) { |
|
1940 this._rootElt.removeChild(this._rootElt.firstChild); |
|
1941 } |
|
1942 |
|
1943 for (let i = 0; i < this._resultNode.childCount; ++i) { |
|
1944 this._insertNewItem(this._resultNode.getChild(i), null); |
|
1945 } |
|
1946 } |
|
1947 }; |