browser/components/places/content/places.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
     2 /* This Source Code Form is subject to the terms of the Mozilla Public
     3  * License, v. 2.0. If a copy of the MPL was not distributed with this
     4  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     6 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
     7 XPCOMUtils.defineLazyModuleGetter(this, "MigrationUtils",
     8                                   "resource:///modules/MigrationUtils.jsm");
     9 XPCOMUtils.defineLazyModuleGetter(this, "Task",
    10                                   "resource://gre/modules/Task.jsm");
    11 XPCOMUtils.defineLazyModuleGetter(this, "BookmarkJSONUtils",
    12                                   "resource://gre/modules/BookmarkJSONUtils.jsm");
    13 XPCOMUtils.defineLazyModuleGetter(this, "PlacesBackups",
    14                                   "resource://gre/modules/PlacesBackups.jsm");
    15 XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils",
    16                                   "resource://gre/modules/DownloadUtils.jsm");
    18 var PlacesOrganizer = {
    19   _places: null,
    21   // IDs of fields from editBookmarkOverlay that should be hidden when infoBox
    22   // is minimal. IDs should be kept in sync with the IDs of the elements
    23   // observing additionalInfoBroadcaster.
    24   _additionalInfoFields: [
    25     "editBMPanel_descriptionRow",
    26     "editBMPanel_loadInSidebarCheckbox",
    27     "editBMPanel_keywordRow",
    28   ],
    30   _initFolderTree: function() {
    31     var leftPaneRoot = PlacesUIUtils.leftPaneFolderId;
    32     this._places.place = "place:excludeItems=1&expandQueries=0&folder=" + leftPaneRoot;
    33   },
    35   selectLeftPaneQuery: function PO_selectLeftPaneQuery(aQueryName) {
    36     var itemId = PlacesUIUtils.leftPaneQueries[aQueryName];
    37     this._places.selectItems([itemId]);
    38     // Forcefully expand all-bookmarks
    39     if (aQueryName == "AllBookmarks" || aQueryName == "History")
    40       PlacesUtils.asContainer(this._places.selectedNode).containerOpen = true;
    41   },
    43   /**
    44    * Opens a given hierarchy in the left pane, stopping at the last reachable
    45    * container.
    46    *
    47    * @param aHierarchy A single container or an array of containers, sorted from
    48    *                   the outmost to the innermost in the hierarchy. Each
    49    *                   container may be either an item id, a Places URI string,
    50    *                   or a named query.
    51    * @see PlacesUIUtils.leftPaneQueries for supported named queries.
    52    */
    53   selectLeftPaneContainerByHierarchy:
    54   function PO_selectLeftPaneContainerByHierarchy(aHierarchy) {
    55     if (!aHierarchy)
    56       throw new Error("Invalid containers hierarchy");
    57     let hierarchy = [].concat(aHierarchy);
    58     let selectWasSuppressed = this._places.view.selection.selectEventsSuppressed;
    59     if (!selectWasSuppressed)
    60       this._places.view.selection.selectEventsSuppressed = true;
    61     try {
    62       for (let container of hierarchy) {
    63         switch (typeof container) {
    64           case "number":
    65             this._places.selectItems([container], false);
    66             break;
    67           case "string":
    68             if (container.substr(0, 6) == "place:")
    69               this._places.selectPlaceURI(container);
    70             else if (container in PlacesUIUtils.leftPaneQueries)
    71               this.selectLeftPaneQuery(container);
    72             else
    73               throw new Error("Invalid container found: " + container);
    74             break;
    75           default:
    76             throw new Error("Invalid container type found: " + container);
    77             break;
    78         }
    79         PlacesUtils.asContainer(this._places.selectedNode).containerOpen = true;
    80       }
    81     } finally {
    82       if (!selectWasSuppressed)
    83         this._places.view.selection.selectEventsSuppressed = false;
    84     }
    85   },
    87   init: function PO_init() {
    88     ContentArea.init();
    90     this._places = document.getElementById("placesList");
    91     this._initFolderTree();
    93     var leftPaneSelection = "AllBookmarks"; // default to all-bookmarks
    94     if (window.arguments && window.arguments[0])
    95       leftPaneSelection = window.arguments[0];
    97     this.selectLeftPaneContainerByHierarchy(leftPaneSelection);
    98     if (leftPaneSelection === "History") {
    99       let historyNode = this._places.selectedNode;
   100       if (historyNode.childCount > 0)
   101         this._places.selectNode(historyNode.getChild(0));
   102     }
   104     // clear the back-stack
   105     this._backHistory.splice(0, this._backHistory.length);
   106     document.getElementById("OrganizerCommand:Back").setAttribute("disabled", true);
   108     // Set up the search UI.
   109     PlacesSearchBox.init();
   111     window.addEventListener("AppCommand", this, true);
   112 #ifdef XP_MACOSX
   113     // 1. Map Edit->Find command to OrganizerCommand_find:all.  Need to map
   114     // both the menuitem and the Find key.
   115     var findMenuItem = document.getElementById("menu_find");
   116     findMenuItem.setAttribute("command", "OrganizerCommand_find:all");
   117     var findKey = document.getElementById("key_find");
   118     findKey.setAttribute("command", "OrganizerCommand_find:all");
   120     // 2. Disable some keybindings from browser.xul
   121     var elements = ["cmd_handleBackspace", "cmd_handleShiftBackspace"];
   122     for (var i=0; i < elements.length; i++) {
   123       document.getElementById(elements[i]).setAttribute("disabled", "true");
   124     }
   125 #endif
   127     // remove the "Properties" context-menu item, we've our own details pane
   128     document.getElementById("placesContext")
   129             .removeChild(document.getElementById("placesContext_show:info"));
   131     ContentArea.focus();
   132   },
   134   QueryInterface: function PO_QueryInterface(aIID) {
   135     if (aIID.equals(Components.interfaces.nsIDOMEventListener) ||
   136         aIID.equals(Components.interfaces.nsISupports))
   137       return this;
   139     throw Components.results.NS_NOINTERFACE;
   140   },
   142   handleEvent: function PO_handleEvent(aEvent) {
   143     if (aEvent.type != "AppCommand")
   144       return;
   146     aEvent.stopPropagation();
   147     switch (aEvent.command) {
   148       case "Back":
   149         if (this._backHistory.length > 0)
   150           this.back();
   151         break;
   152       case "Forward":
   153         if (this._forwardHistory.length > 0)
   154           this.forward();
   155         break;
   156       case "Search":
   157         PlacesSearchBox.findAll();
   158         break;
   159     }
   160   },
   162   destroy: function PO_destroy() {
   163   },
   165   _location: null,
   166   get location() {
   167     return this._location;
   168   },
   170   set location(aLocation) {
   171     if (!aLocation || this._location == aLocation)
   172       return aLocation;
   174     if (this.location) {
   175       this._backHistory.unshift(this.location);
   176       this._forwardHistory.splice(0, this._forwardHistory.length);
   177     }
   179     this._location = aLocation;
   180     this._places.selectPlaceURI(aLocation);
   182     if (!this._places.hasSelection) {
   183       // If no node was found for the given place: uri, just load it directly
   184       ContentArea.currentPlace = aLocation;
   185     }
   186     this.updateDetailsPane();
   188     // update navigation commands
   189     if (this._backHistory.length == 0)
   190       document.getElementById("OrganizerCommand:Back").setAttribute("disabled", true);
   191     else
   192       document.getElementById("OrganizerCommand:Back").removeAttribute("disabled");
   193     if (this._forwardHistory.length == 0)
   194       document.getElementById("OrganizerCommand:Forward").setAttribute("disabled", true);
   195     else
   196       document.getElementById("OrganizerCommand:Forward").removeAttribute("disabled");
   198     return aLocation;
   199   },
   201   _backHistory: [],
   202   _forwardHistory: [],
   204   back: function PO_back() {
   205     this._forwardHistory.unshift(this.location);
   206     var historyEntry = this._backHistory.shift();
   207     this._location = null;
   208     this.location = historyEntry;
   209   },
   210   forward: function PO_forward() {
   211     this._backHistory.unshift(this.location);
   212     var historyEntry = this._forwardHistory.shift();
   213     this._location = null;
   214     this.location = historyEntry;
   215   },
   217   /**
   218    * Called when a place folder is selected in the left pane.
   219    * @param   resetSearchBox
   220    *          true if the search box should also be reset, false otherwise.
   221    *          The search box should be reset when a new folder in the left
   222    *          pane is selected; the search scope and text need to be cleared in
   223    *          preparation for the new folder.  Note that if the user manually
   224    *          resets the search box, either by clicking its reset button or by
   225    *          deleting its text, this will be false.
   226    */
   227   _cachedLeftPaneSelectedURI: null,
   228   onPlaceSelected: function PO_onPlaceSelected(resetSearchBox) {
   229     // Don't change the right-hand pane contents when there's no selection.
   230     if (!this._places.hasSelection)
   231       return;
   233     var node = this._places.selectedNode;
   234     var queries = PlacesUtils.asQuery(node).getQueries();
   236     // Items are only excluded on the left pane.
   237     var options = node.queryOptions.clone();
   238     options.excludeItems = false;
   239     var placeURI = PlacesUtils.history.queriesToQueryString(queries,
   240                                                             queries.length,
   241                                                             options);
   243     // If either the place of the content tree in the right pane has changed or
   244     // the user cleared the search box, update the place, hide the search UI,
   245     // and update the back/forward buttons by setting location.
   246     if (ContentArea.currentPlace != placeURI || !resetSearchBox) {
   247       ContentArea.currentPlace = placeURI;
   248       this.location = node.uri;
   249     }
   251     // When we invalidate a container we use suppressSelectionEvent, when it is
   252     // unset a select event is fired, in many cases the selection did not really
   253     // change, so we should check for it, and return early in such a case. Note
   254     // that we cannot return any earlier than this point, because when
   255     // !resetSearchBox, we need to update location and hide the UI as above,
   256     // even though the selection has not changed.
   257     if (node.uri == this._cachedLeftPaneSelectedURI)
   258       return;
   259     this._cachedLeftPaneSelectedURI = node.uri;
   261     // At this point, resetSearchBox is true, because the left pane selection
   262     // has changed; otherwise we would have returned earlier.
   264     PlacesSearchBox.searchFilter.reset();
   265     this._setSearchScopeForNode(node);
   266     this.updateDetailsPane();
   267   },
   269   /**
   270    * Sets the search scope based on aNode's properties.
   271    * @param   aNode
   272    *          the node to set up scope from
   273    */
   274   _setSearchScopeForNode: function PO__setScopeForNode(aNode) {
   275     let itemId = aNode.itemId;
   277     if (PlacesUtils.nodeIsHistoryContainer(aNode) ||
   278         itemId == PlacesUIUtils.leftPaneQueries["History"]) {
   279       PlacesQueryBuilder.setScope("history");
   280     }
   281     else if (itemId == PlacesUIUtils.leftPaneQueries["Downloads"]) {
   282       PlacesQueryBuilder.setScope("downloads");
   283     }
   284     else {
   285       // Default to All Bookmarks for all other nodes, per bug 469437.
   286       PlacesQueryBuilder.setScope("bookmarks");
   287     }
   288   },
   290   /**
   291    * Handle clicks on the places list.
   292    * Single Left click, right click or modified click do not result in any
   293    * special action, since they're related to selection.
   294    * @param   aEvent
   295    *          The mouse event.
   296    */
   297   onPlacesListClick: function PO_onPlacesListClick(aEvent) {
   298     // Only handle clicks on tree children.
   299     if (aEvent.target.localName != "treechildren")
   300       return;
   302     let node = this._places.selectedNode;
   303     if (node) {
   304       let middleClick = aEvent.button == 1 && aEvent.detail == 1;
   305       if (middleClick && PlacesUtils.nodeIsContainer(node)) {
   306         // The command execution function will take care of seeing if the
   307         // selection is a folder or a different container type, and will
   308         // load its contents in tabs.
   309         PlacesUIUtils.openContainerNodeInTabs(selectedNode, aEvent, this._places);
   310       }
   311     }
   312   },
   314   /**
   315    * Handle focus changes on the places list and the current content view.
   316    */
   317   updateDetailsPane: function PO_updateDetailsPane() {
   318     if (!ContentArea.currentViewOptions.showDetailsPane)
   319       return;
   320     let view = PlacesUIUtils.getViewForNode(document.activeElement);
   321     if (view) {
   322       let selectedNodes = view.selectedNode ?
   323                           [view.selectedNode] : view.selectedNodes;
   324       this._fillDetailsPane(selectedNodes);
   325     }
   326   },
   328   openFlatContainer: function PO_openFlatContainerFlatContainer(aContainer) {
   329     if (aContainer.itemId != -1) {
   330       PlacesUtils.asContainer(this._places.selectedNode).containerOpen = true;
   331       this._places.selectItems([aContainer.itemId], false);
   332     }
   333     else if (PlacesUtils.nodeIsQuery(aContainer)) {
   334       this._places.selectPlaceURI(aContainer.uri);
   335     }
   336   },
   338   /**
   339    * Returns the options associated with the query currently loaded in the
   340    * main places pane.
   341    */
   342   getCurrentOptions: function PO_getCurrentOptions() {
   343     return PlacesUtils.asQuery(ContentArea.currentView.result.root).queryOptions;
   344   },
   346   /**
   347    * Returns the queries associated with the query currently loaded in the
   348    * main places pane.
   349    */
   350   getCurrentQueries: function PO_getCurrentQueries() {
   351     return PlacesUtils.asQuery(ContentArea.currentView.result.root).getQueries();
   352   },
   354   /**
   355    * Show the migration wizard for importing passwords,
   356    * cookies, history, preferences, and bookmarks.
   357    */
   358   importFromBrowser: function PO_importFromBrowser() {
   359     MigrationUtils.showMigrationWizard(window);
   360   },
   362   /**
   363    * Open a file-picker and import the selected file into the bookmarks store
   364    */
   365   importFromFile: function PO_importFromFile() {
   366     let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
   367     let fpCallback = function fpCallback_done(aResult) {
   368       if (aResult != Ci.nsIFilePicker.returnCancel && fp.fileURL) {
   369         Components.utils.import("resource://gre/modules/BookmarkHTMLUtils.jsm");
   370         BookmarkHTMLUtils.importFromURL(fp.fileURL.spec, false)
   371                          .then(null, Components.utils.reportError);
   372       }
   373     };
   375     fp.init(window, PlacesUIUtils.getString("SelectImport"),
   376             Ci.nsIFilePicker.modeOpen);
   377     fp.appendFilters(Ci.nsIFilePicker.filterHTML);
   378     fp.open(fpCallback);
   379   },
   381   /**
   382    * Allows simple exporting of bookmarks.
   383    */
   384   exportBookmarks: function PO_exportBookmarks() {
   385     let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
   386     let fpCallback = function fpCallback_done(aResult) {
   387       if (aResult != Ci.nsIFilePicker.returnCancel) {
   388         Components.utils.import("resource://gre/modules/BookmarkHTMLUtils.jsm");
   389         BookmarkHTMLUtils.exportToFile(fp.file.path)
   390                          .then(null, Components.utils.reportError);
   391       }
   392     };
   394     fp.init(window, PlacesUIUtils.getString("EnterExport"),
   395             Ci.nsIFilePicker.modeSave);
   396     fp.appendFilters(Ci.nsIFilePicker.filterHTML);
   397     fp.defaultString = "bookmarks.html";
   398     fp.open(fpCallback);
   399   },
   401   /**
   402    * Populates the restore menu with the dates of the backups available.
   403    */
   404   populateRestoreMenu: function PO_populateRestoreMenu() {
   405     let restorePopup = document.getElementById("fileRestorePopup");
   407     let dateSvc = Cc["@mozilla.org/intl/scriptabledateformat;1"].
   408                   getService(Ci.nsIScriptableDateFormat);
   410     // Remove existing menu items.  Last item is the restoreFromFile item.
   411     while (restorePopup.childNodes.length > 1)
   412       restorePopup.removeChild(restorePopup.firstChild);
   414     Task.spawn(function() {
   415       let backupFiles = yield PlacesBackups.getBackupFiles();
   416       if (backupFiles.length == 0)
   417         return;
   419       // Populate menu with backups.
   420       for (let i = 0; i < backupFiles.length; i++) {
   421         let fileSize = (yield OS.File.stat(backupFiles[i])).size;
   422         let [size, unit] = DownloadUtils.convertByteUnits(fileSize);
   423         let sizeString = PlacesUtils.getFormattedString("backupFileSizeText",
   424                                                         [size, unit]);
   425         let sizeInfo;
   426         let bookmarkCount = PlacesBackups.getBookmarkCountForFile(backupFiles[i]);
   427         if (bookmarkCount != null) {
   428           sizeInfo = " (" + sizeString + " - " +
   429                      PlacesUIUtils.getPluralString("detailsPane.itemsCountLabel",
   430                                                    bookmarkCount,
   431                                                    [bookmarkCount]) +
   432                      ")";
   433         } else {
   434           sizeInfo = " (" + sizeString + ")";
   435         }
   437         let backupDate = PlacesBackups.getDateForFile(backupFiles[i]);
   438         let m = restorePopup.insertBefore(document.createElement("menuitem"),
   439                                           document.getElementById("restoreFromFile"));
   440         m.setAttribute("label",
   441                        dateSvc.FormatDate("",
   442                                           Ci.nsIScriptableDateFormat.dateFormatLong,
   443                                           backupDate.getFullYear(),
   444                                           backupDate.getMonth() + 1,
   445                                           backupDate.getDate()) +
   446                                           sizeInfo);
   447         m.setAttribute("value", OS.Path.basename(backupFiles[i]));
   448         m.setAttribute("oncommand",
   449                        "PlacesOrganizer.onRestoreMenuItemClick(this);");
   450       }
   452       // Add the restoreFromFile item.
   453       restorePopup.insertBefore(document.createElement("menuseparator"),
   454                                 document.getElementById("restoreFromFile"));
   455     });
   456   },
   458   /**
   459    * Called when a menuitem is selected from the restore menu.
   460    */
   461   onRestoreMenuItemClick: function PO_onRestoreMenuItemClick(aMenuItem) {
   462     Task.spawn(function() {
   463       let backupName = aMenuItem.getAttribute("value");
   464       let backupFilePaths = yield PlacesBackups.getBackupFiles();
   465       for (let backupFilePath of backupFilePaths) {
   466         if (OS.Path.basename(backupFilePath) == backupName) {
   467           PlacesOrganizer.restoreBookmarksFromFile(backupFilePath);
   468           break;
   469         }
   470       }
   471     });
   472   },
   474   /**
   475    * Called when 'Choose File...' is selected from the restore menu.
   476    * Prompts for a file and restores bookmarks to those in the file.
   477    */
   478   onRestoreBookmarksFromFile: function PO_onRestoreBookmarksFromFile() {
   479     let dirSvc = Cc["@mozilla.org/file/directory_service;1"].
   480                  getService(Ci.nsIProperties);
   481     let backupsDir = dirSvc.get("Desk", Ci.nsILocalFile);
   482     let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
   483     let fpCallback = function fpCallback_done(aResult) {
   484       if (aResult != Ci.nsIFilePicker.returnCancel) {
   485         this.restoreBookmarksFromFile(fp.file.path);
   486       }
   487     }.bind(this);
   489     fp.init(window, PlacesUIUtils.getString("bookmarksRestoreTitle"),
   490             Ci.nsIFilePicker.modeOpen);
   491     fp.appendFilter(PlacesUIUtils.getString("bookmarksRestoreFilterName"),
   492                     PlacesUIUtils.getString("bookmarksRestoreFilterExtension"));
   493     fp.appendFilters(Ci.nsIFilePicker.filterAll);
   494     fp.displayDirectory = backupsDir;
   495     fp.open(fpCallback);
   496   },
   498   /**
   499    * Restores bookmarks from a JSON file.
   500    */
   501   restoreBookmarksFromFile: function PO_restoreBookmarksFromFile(aFilePath) {
   502     // check file extension
   503     if (!aFilePath.endsWith("json")) {
   504       this._showErrorAlert(PlacesUIUtils.getString("bookmarksRestoreFormatError"));
   505       return;
   506     }
   508     // confirm ok to delete existing bookmarks
   509     var prompts = Cc["@mozilla.org/embedcomp/prompt-service;1"].
   510                   getService(Ci.nsIPromptService);
   511     if (!prompts.confirm(null,
   512                          PlacesUIUtils.getString("bookmarksRestoreAlertTitle"),
   513                          PlacesUIUtils.getString("bookmarksRestoreAlert")))
   514       return;
   516     Task.spawn(function() {
   517       try {
   518         yield BookmarkJSONUtils.importFromFile(aFilePath, true);
   519       } catch(ex) {
   520         PlacesOrganizer._showErrorAlert(PlacesUIUtils.getString("bookmarksRestoreParseError"));
   521       }
   522     });
   523   },
   525   _showErrorAlert: function PO__showErrorAlert(aMsg) {
   526     var brandShortName = document.getElementById("brandStrings").
   527                                   getString("brandShortName");
   529     Cc["@mozilla.org/embedcomp/prompt-service;1"].
   530       getService(Ci.nsIPromptService).
   531       alert(window, brandShortName, aMsg);
   532   },
   534   /**
   535    * Backup bookmarks to desktop, auto-generate a filename with a date.
   536    * The file is a JSON serialization of bookmarks, tags and any annotations
   537    * of those items.
   538    */
   539   backupBookmarks: function PO_backupBookmarks() {
   540     let dirSvc = Cc["@mozilla.org/file/directory_service;1"].
   541                  getService(Ci.nsIProperties);
   542     let backupsDir = dirSvc.get("Desk", Ci.nsILocalFile);
   543     let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
   544     let fpCallback = function fpCallback_done(aResult) {
   545       if (aResult != Ci.nsIFilePicker.returnCancel) {
   546         // There is no OS.File version of the filepicker yet (Bug 937812).
   547         PlacesBackups.saveBookmarksToJSONFile(fp.file.path);
   548       }
   549     };
   551     fp.init(window, PlacesUIUtils.getString("bookmarksBackupTitle"),
   552             Ci.nsIFilePicker.modeSave);
   553     fp.appendFilter(PlacesUIUtils.getString("bookmarksRestoreFilterName"),
   554                     PlacesUIUtils.getString("bookmarksRestoreFilterExtension"));
   555     fp.defaultString = PlacesBackups.getFilenameForDate();
   556     fp.displayDirectory = backupsDir;
   557     fp.open(fpCallback);
   558   },
   560   _paneDisabled: false,
   561   _setDetailsFieldsDisabledState:
   562   function PO__setDetailsFieldsDisabledState(aDisabled) {
   563     if (aDisabled) {
   564       document.getElementById("paneElementsBroadcaster")
   565               .setAttribute("disabled", "true");
   566     }
   567     else {
   568       document.getElementById("paneElementsBroadcaster")
   569               .removeAttribute("disabled");
   570     }
   571   },
   573   _detectAndSetDetailsPaneMinimalState:
   574   function PO__detectAndSetDetailsPaneMinimalState(aNode) {
   575     /**
   576      * The details of simple folder-items (as opposed to livemarks) or the
   577      * of livemark-children are not likely to fill the infoBox anyway,
   578      * thus we remove the "More/Less" button and show all details.
   579      *
   580      * the wasminimal attribute here is used to persist the "more/less"
   581      * state in a bookmark->folder->bookmark scenario.
   582      */
   583     var infoBox = document.getElementById("infoBox");
   584     var infoBoxExpander = document.getElementById("infoBoxExpander");
   585     var infoBoxExpanderWrapper = document.getElementById("infoBoxExpanderWrapper");
   586     var additionalInfoBroadcaster = document.getElementById("additionalInfoBroadcaster");
   588     if (!aNode) {
   589       infoBoxExpanderWrapper.hidden = true;
   590       return;
   591     }
   592     if (aNode.itemId != -1 &&
   593         PlacesUtils.nodeIsFolder(aNode) && !aNode._feedURI) {
   594       if (infoBox.getAttribute("minimal") == "true")
   595         infoBox.setAttribute("wasminimal", "true");
   596       infoBox.removeAttribute("minimal");
   597       infoBoxExpanderWrapper.hidden = true;
   598     }
   599     else {
   600       if (infoBox.getAttribute("wasminimal") == "true")
   601         infoBox.setAttribute("minimal", "true");
   602       infoBox.removeAttribute("wasminimal");
   603       infoBoxExpanderWrapper.hidden =
   604         this._additionalInfoFields.every(function (id)
   605           document.getElementById(id).collapsed);
   606     }
   607     additionalInfoBroadcaster.hidden = infoBox.getAttribute("minimal") == "true";
   608   },
   610   // NOT YET USED
   611   updateThumbnailProportions: function PO_updateThumbnailProportions() {
   612     var previewBox = document.getElementById("previewBox");
   613     var canvas = document.getElementById("itemThumbnail");
   614     var height = previewBox.boxObject.height;
   615     var width = height * (screen.width / screen.height);
   616     canvas.width = width;
   617     canvas.height = height;
   618   },
   620   _fillDetailsPane: function PO__fillDetailsPane(aNodeList) {
   621     var infoBox = document.getElementById("infoBox");
   622     var detailsDeck = document.getElementById("detailsDeck");
   624     // Make sure the infoBox UI is visible if we need to use it, we hide it
   625     // below when we don't.
   626     infoBox.hidden = false;
   627     var aSelectedNode = aNodeList.length == 1 ? aNodeList[0] : null;
   628     // If a textbox within a panel is focused, force-blur it so its contents
   629     // are saved
   630     if (gEditItemOverlay.itemId != -1) {
   631       var focusedElement = document.commandDispatcher.focusedElement;
   632       if ((focusedElement instanceof HTMLInputElement ||
   633            focusedElement instanceof HTMLTextAreaElement) &&
   634           /^editBMPanel.*/.test(focusedElement.parentNode.parentNode.id))
   635         focusedElement.blur();
   637       // don't update the panel if we are already editing this node unless we're
   638       // in multi-edit mode
   639       if (aSelectedNode) {
   640         var concreteId = PlacesUtils.getConcreteItemId(aSelectedNode);
   641         var nodeIsSame = gEditItemOverlay.itemId == aSelectedNode.itemId ||
   642                          gEditItemOverlay.itemId == concreteId ||
   643                          (aSelectedNode.itemId == -1 && gEditItemOverlay.uri &&
   644                           gEditItemOverlay.uri == aSelectedNode.uri);
   645         if (nodeIsSame && detailsDeck.selectedIndex == 1 &&
   646             !gEditItemOverlay.multiEdit)
   647           return;
   648       }
   649     }
   651     // Clean up the panel before initing it again.
   652     gEditItemOverlay.uninitPanel(false);
   654     if (aSelectedNode && !PlacesUtils.nodeIsSeparator(aSelectedNode)) {
   655       detailsDeck.selectedIndex = 1;
   656       // Using the concrete itemId is arguably wrong.  The bookmarks API
   657       // does allow setting properties for folder shortcuts as well, but since
   658       // the UI does not distinct between the couple, we better just show
   659       // the concrete item properties for shortcuts to root nodes.
   660       var concreteId = PlacesUtils.getConcreteItemId(aSelectedNode);
   661       var isRootItem = concreteId != -1 && PlacesUtils.isRootItem(concreteId);
   662       var readOnly = isRootItem ||
   663                      aSelectedNode.parent.itemId == PlacesUIUtils.leftPaneFolderId;
   664       var useConcreteId = isRootItem ||
   665                           PlacesUtils.nodeIsTagQuery(aSelectedNode);
   666       var itemId = -1;
   667       if (concreteId != -1 && useConcreteId)
   668         itemId = concreteId;
   669       else if (aSelectedNode.itemId != -1)
   670         itemId = aSelectedNode.itemId;
   671       else
   672         itemId = PlacesUtils._uri(aSelectedNode.uri);
   674       gEditItemOverlay.initPanel(itemId, { hiddenRows: ["folderPicker"]
   675                                          , forceReadOnly: readOnly
   676                                          , titleOverride: aSelectedNode.title
   677                                          });
   679       // Dynamically generated queries, like history date containers, have
   680       // itemId !=0 and do not exist in history.  For them the panel is
   681       // read-only, but empty, since it can't get a valid title for the object.
   682       // In such a case we force the title using the selectedNode one, for UI
   683       // polishness.
   684       if (aSelectedNode.itemId == -1 &&
   685           (PlacesUtils.nodeIsDay(aSelectedNode) ||
   686            PlacesUtils.nodeIsHost(aSelectedNode)))
   687         gEditItemOverlay._element("namePicker").value = aSelectedNode.title;
   689       this._detectAndSetDetailsPaneMinimalState(aSelectedNode);
   690     }
   691     else if (!aSelectedNode && aNodeList[0]) {
   692       var itemIds = [];
   693       for (var i = 0; i < aNodeList.length; i++) {
   694         if (!PlacesUtils.nodeIsBookmark(aNodeList[i]) &&
   695             !PlacesUtils.nodeIsURI(aNodeList[i])) {
   696           detailsDeck.selectedIndex = 0;
   697           var selectItemDesc = document.getElementById("selectItemDescription");
   698           var itemsCountLabel = document.getElementById("itemsCountText");
   699           selectItemDesc.hidden = false;
   700           itemsCountLabel.value =
   701             PlacesUIUtils.getPluralString("detailsPane.itemsCountLabel",
   702                                           aNodeList.length, [aNodeList.length]);
   703           infoBox.hidden = true;
   704           return;
   705         }
   706         itemIds[i] = aNodeList[i].itemId != -1 ? aNodeList[i].itemId :
   707                      PlacesUtils._uri(aNodeList[i].uri);
   708       }
   709       detailsDeck.selectedIndex = 1;
   710       gEditItemOverlay.initPanel(itemIds,
   711                                  { hiddenRows: ["folderPicker",
   712                                                 "loadInSidebar",
   713                                                 "location",
   714                                                 "keyword",
   715                                                 "description",
   716                                                 "name"]});
   717       this._detectAndSetDetailsPaneMinimalState(aSelectedNode);
   718     }
   719     else {
   720       detailsDeck.selectedIndex = 0;
   721       infoBox.hidden = true;
   722       let selectItemDesc = document.getElementById("selectItemDescription");
   723       let itemsCountLabel = document.getElementById("itemsCountText");
   724       let itemsCount = 0;
   725       if (ContentArea.currentView.result) {
   726         let rootNode = ContentArea.currentView.result.root;
   727         if (rootNode.containerOpen)
   728           itemsCount = rootNode.childCount;
   729       }
   730       if (itemsCount == 0) {
   731         selectItemDesc.hidden = true;
   732         itemsCountLabel.value = PlacesUIUtils.getString("detailsPane.noItems");
   733       }
   734       else {
   735         selectItemDesc.hidden = false;
   736         itemsCountLabel.value =
   737           PlacesUIUtils.getPluralString("detailsPane.itemsCountLabel",
   738                                         itemsCount, [itemsCount]);
   739       }
   740     }
   741   },
   743   // NOT YET USED
   744   _updateThumbnail: function PO__updateThumbnail() {
   745     var bo = document.getElementById("previewBox").boxObject;
   746     var width  = bo.width;
   747     var height = bo.height;
   749     var canvas = document.getElementById("itemThumbnail");
   750     var ctx = canvas.getContext('2d');
   751     var notAvailableText = canvas.getAttribute("notavailabletext");
   752     ctx.save();
   753     ctx.fillStyle = "-moz-Dialog";
   754     ctx.fillRect(0, 0, width, height);
   755     ctx.translate(width/2, height/2);
   757     ctx.fillStyle = "GrayText";
   758     ctx.mozTextStyle = "12pt sans serif";
   759     var len = ctx.mozMeasureText(notAvailableText);
   760     ctx.translate(-len/2,0);
   761     ctx.mozDrawText(notAvailableText);
   762     ctx.restore();
   763   },
   765   toggleAdditionalInfoFields: function PO_toggleAdditionalInfoFields() {
   766     var infoBox = document.getElementById("infoBox");
   767     var infoBoxExpander = document.getElementById("infoBoxExpander");
   768     var infoBoxExpanderLabel = document.getElementById("infoBoxExpanderLabel");
   769     var additionalInfoBroadcaster = document.getElementById("additionalInfoBroadcaster");
   771     if (infoBox.getAttribute("minimal") == "true") {
   772       infoBox.removeAttribute("minimal");
   773       infoBoxExpanderLabel.value = infoBoxExpanderLabel.getAttribute("lesslabel");
   774       infoBoxExpanderLabel.accessKey = infoBoxExpanderLabel.getAttribute("lessaccesskey");
   775       infoBoxExpander.className = "expander-up";
   776       additionalInfoBroadcaster.removeAttribute("hidden");
   777     }
   778     else {
   779       infoBox.setAttribute("minimal", "true");
   780       infoBoxExpanderLabel.value = infoBoxExpanderLabel.getAttribute("morelabel");
   781       infoBoxExpanderLabel.accessKey = infoBoxExpanderLabel.getAttribute("moreaccesskey");
   782       infoBoxExpander.className = "expander-down";
   783       additionalInfoBroadcaster.setAttribute("hidden", "true");
   784     }
   785   },
   786 };
   788 /**
   789  * A set of utilities relating to search within Bookmarks and History.
   790  */
   791 var PlacesSearchBox = {
   793   /**
   794    * The Search text field
   795    */
   796   get searchFilter() {
   797     return document.getElementById("searchFilter");
   798   },
   800   /**
   801    * Folders to include when searching.
   802    */
   803   _folders: [],
   804   get folders() {
   805     if (this._folders.length == 0) {
   806       this._folders.push(PlacesUtils.bookmarksMenuFolderId,
   807                          PlacesUtils.unfiledBookmarksFolderId,
   808                          PlacesUtils.toolbarFolderId);
   809     }
   810     return this._folders;
   811   },
   812   set folders(aFolders) {
   813     this._folders = aFolders;
   814     return aFolders;
   815   },
   817   /**
   818    * Run a search for the specified text, over the collection specified by
   819    * the dropdown arrow. The default is all bookmarks, but can be
   820    * localized to the active collection.
   821    * @param   filterString
   822    *          The text to search for.
   823    */
   824   search: function PSB_search(filterString) {
   825     var PO = PlacesOrganizer;
   826     // If the user empties the search box manually, reset it and load all
   827     // contents of the current scope.
   828     // XXX this might be to jumpy, maybe should search for "", so results
   829     // are ungrouped, and search box not reset
   830     if (filterString == "") {
   831       PO.onPlaceSelected(false);
   832       return;
   833     }
   835     let currentView = ContentArea.currentView;
   836     let currentOptions = PO.getCurrentOptions();
   838     // Search according to the current scope, which was set by
   839     // PQB_setScope()
   840     switch (PlacesSearchBox.filterCollection) {
   841       case "bookmarks":
   842         currentView.applyFilter(filterString, this.folders);
   843         break;
   844       case "history":
   845         if (currentOptions.queryType != Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) {
   846           var query = PlacesUtils.history.getNewQuery();
   847           query.searchTerms = filterString;
   848           var options = currentOptions.clone();
   849           // Make sure we're getting uri results.
   850           options.resultType = currentOptions.RESULTS_AS_URI;
   851           options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY;
   852           options.includeHidden = true;
   853           currentView.load([query], options);
   854         }
   855         else {
   856           currentView.applyFilter(filterString, null, true);
   857         }
   858         break;
   859       case "downloads":
   860         if (currentView == ContentTree.view) {
   861           let query = PlacesUtils.history.getNewQuery();
   862           query.searchTerms = filterString;
   863           query.setTransitions([Ci.nsINavHistoryService.TRANSITION_DOWNLOAD], 1);
   864           let options = currentOptions.clone();
   865           // Make sure we're getting uri results.
   866           options.resultType = currentOptions.RESULTS_AS_URI;
   867           options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY;
   868           options.includeHidden = true;
   869           currentView.load([query], options);
   870         }
   871         else {
   872           // The new downloads view doesn't use places for searching downloads.
   873           currentView.searchTerm = filterString;
   874         }
   875         break;
   876       default:
   877         throw "Invalid filterCollection on search";
   878     }
   880     // Update the details panel
   881     PlacesOrganizer.updateDetailsPane();
   882   },
   884   /**
   885    * Finds across all history, downloads or all bookmarks.
   886    */
   887   findAll: function PSB_findAll() {
   888     switch (this.filterCollection) {
   889       case "history":
   890         PlacesQueryBuilder.setScope("history");
   891         break;
   892       case "downloads":
   893         PlacesQueryBuilder.setScope("downloads");
   894         break;
   895       default:
   896         PlacesQueryBuilder.setScope("bookmarks");
   897         break;
   898     }
   899     this.focus();
   900   },
   902   /**
   903    * Updates the display with the title of the current collection.
   904    * @param   aTitle
   905    *          The title of the current collection.
   906    */
   907   updateCollectionTitle: function PSB_updateCollectionTitle(aTitle) {
   908     let title = "";
   909     switch (this.filterCollection) {
   910       case "history":
   911         title = PlacesUIUtils.getString("searchHistory");
   912         break;
   913       case "downloads":
   914         title = PlacesUIUtils.getString("searchDownloads");
   915         break;
   916       default:
   917         title = PlacesUIUtils.getString("searchBookmarks");                                    
   918     }
   919     this.searchFilter.placeholder = title;
   920   },
   922   /**
   923    * Gets/sets the active collection from the dropdown menu.
   924    */
   925   get filterCollection() {
   926     return this.searchFilter.getAttribute("collection");
   927   },
   928   set filterCollection(collectionName) {
   929     if (collectionName == this.filterCollection)
   930       return collectionName;
   932     this.searchFilter.setAttribute("collection", collectionName);
   933     this.updateCollectionTitle();
   935     return collectionName;
   936   },
   938   /**
   939    * Focus the search box
   940    */
   941   focus: function PSB_focus() {
   942     this.searchFilter.focus();
   943   },
   945   /**
   946    * Set up the gray text in the search bar as the Places View loads.
   947    */
   948   init: function PSB_init() {
   949     this.updateCollectionTitle();
   950   },
   952   /**
   953    * Gets or sets the text shown in the Places Search Box
   954    */
   955   get value() {
   956     return this.searchFilter.value;
   957   },
   958   set value(value) {
   959     return this.searchFilter.value = value;
   960   },
   961 };
   963 /**
   964  * Functions and data for advanced query builder
   965  */
   966 var PlacesQueryBuilder = {
   968   queries: [],
   969   queryOptions: null,
   971   /**
   972    * Sets the search scope.  This can be called when no search is active, and
   973    * in that case, when the user does begin a search aScope will be used (see
   974    * PSB_search()).  If there is an active search, it's performed again to
   975    * update the content tree.
   976    * @param   aScope
   977    *          The search scope: "bookmarks", "collection", "downloads" or
   978    *          "history".
   979    */
   980   setScope: function PQB_setScope(aScope) {
   981     // Determine filterCollection, folders, and scopeButtonId based on aScope.
   982     var filterCollection;
   983     var folders = [];
   984     switch (aScope) {
   985       case "history":
   986         filterCollection = "history";
   987         break;
   988       case "bookmarks":
   989         filterCollection = "bookmarks";
   990         folders.push(PlacesUtils.bookmarksMenuFolderId,
   991                      PlacesUtils.toolbarFolderId,
   992                      PlacesUtils.unfiledBookmarksFolderId);
   993         break;
   994       case "downloads":
   995         filterCollection = "downloads";
   996         break;
   997       default:
   998         throw "Invalid search scope";
   999         break;
  1002     // Update the search box.  Re-search if there's an active search.
  1003     PlacesSearchBox.filterCollection = filterCollection;
  1004     PlacesSearchBox.folders = folders;
  1005     var searchStr = PlacesSearchBox.searchFilter.value;
  1006     if (searchStr)
  1007       PlacesSearchBox.search(searchStr);
  1009 };
  1011 /**
  1012  * Population and commands for the View Menu.
  1013  */
  1014 var ViewMenu = {
  1015   /**
  1016    * Removes content generated previously from a menupopup.
  1017    * @param   popup
  1018    *          The popup that contains the previously generated content.
  1019    * @param   startID
  1020    *          The id attribute of an element that is the start of the
  1021    *          dynamically generated region - remove elements after this
  1022    *          item only.
  1023    *          Must be contained by popup. Can be null (in which case the
  1024    *          contents of popup are removed).
  1025    * @param   endID
  1026    *          The id attribute of an element that is the end of the
  1027    *          dynamically generated region - remove elements up to this
  1028    *          item only.
  1029    *          Must be contained by popup. Can be null (in which case all
  1030    *          items until the end of the popup will be removed). Ignored
  1031    *          if startID is null.
  1032    * @returns The element for the caller to insert new items before,
  1033    *          null if the caller should just append to the popup.
  1034    */
  1035   _clean: function VM__clean(popup, startID, endID) {
  1036     if (endID)
  1037       NS_ASSERT(startID, "meaningless to have valid endID and null startID");
  1038     if (startID) {
  1039       var startElement = document.getElementById(startID);
  1040       NS_ASSERT(startElement.parentNode ==
  1041                 popup, "startElement is not in popup");
  1042       NS_ASSERT(startElement,
  1043                 "startID does not correspond to an existing element");
  1044       var endElement = null;
  1045       if (endID) {
  1046         endElement = document.getElementById(endID);
  1047         NS_ASSERT(endElement.parentNode == popup,
  1048                   "endElement is not in popup");
  1049         NS_ASSERT(endElement,
  1050                   "endID does not correspond to an existing element");
  1052       while (startElement.nextSibling != endElement)
  1053         popup.removeChild(startElement.nextSibling);
  1054       return endElement;
  1056     else {
  1057       while(popup.hasChildNodes())
  1058         popup.removeChild(popup.firstChild);
  1060     return null;
  1061   },
  1063   /**
  1064    * Fills a menupopup with a list of columns
  1065    * @param   event
  1066    *          The popupshowing event that invoked this function.
  1067    * @param   startID
  1068    *          see _clean
  1069    * @param   endID
  1070    *          see _clean
  1071    * @param   type
  1072    *          the type of the menuitem, e.g. "radio" or "checkbox".
  1073    *          Can be null (no-type).
  1074    *          Checkboxes are checked if the column is visible.
  1075    * @param   propertyPrefix
  1076    *          If propertyPrefix is non-null:
  1077    *          propertyPrefix + column ID + ".label" will be used to get the
  1078    *          localized label string.
  1079    *          propertyPrefix + column ID + ".accesskey" will be used to get the
  1080    *          localized accesskey.
  1081    *          If propertyPrefix is null, the column label is used as label and
  1082    *          no accesskey is assigned.
  1083    */
  1084   fillWithColumns: function VM_fillWithColumns(event, startID, endID, type, propertyPrefix) {
  1085     var popup = event.target;
  1086     var pivot = this._clean(popup, startID, endID);
  1088     // If no column is "sort-active", the "Unsorted" item needs to be checked,
  1089     // so track whether or not we find a column that is sort-active.
  1090     var isSorted = false;
  1091     var content = document.getElementById("placeContent");
  1092     var columns = content.columns;
  1093     for (var i = 0; i < columns.count; ++i) {
  1094       var column = columns.getColumnAt(i).element;
  1095       var menuitem = document.createElement("menuitem");
  1096       menuitem.id = "menucol_" + column.id;
  1097       menuitem.column = column;
  1098       var label = column.getAttribute("label");
  1099       if (propertyPrefix) {
  1100         var menuitemPrefix = propertyPrefix;
  1101         // for string properties, use "name" as the id, instead of "title"
  1102         // see bug #386287 for details
  1103         var columnId = column.getAttribute("anonid");
  1104         menuitemPrefix += columnId == "title" ? "name" : columnId;
  1105         label = PlacesUIUtils.getString(menuitemPrefix + ".label");
  1106         var accesskey = PlacesUIUtils.getString(menuitemPrefix + ".accesskey");
  1107         menuitem.setAttribute("accesskey", accesskey);
  1109       menuitem.setAttribute("label", label);
  1110       if (type == "radio") {
  1111         menuitem.setAttribute("type", "radio");
  1112         menuitem.setAttribute("name", "columns");
  1113         // This column is the sort key. Its item is checked.
  1114         if (column.getAttribute("sortDirection") != "") {
  1115           menuitem.setAttribute("checked", "true");
  1116           isSorted = true;
  1119       else if (type == "checkbox") {
  1120         menuitem.setAttribute("type", "checkbox");
  1121         // Cannot uncheck the primary column.
  1122         if (column.getAttribute("primary") == "true")
  1123           menuitem.setAttribute("disabled", "true");
  1124         // Items for visible columns are checked.
  1125         if (!column.hidden)
  1126           menuitem.setAttribute("checked", "true");
  1128       if (pivot)
  1129         popup.insertBefore(menuitem, pivot);
  1130       else
  1131         popup.appendChild(menuitem);
  1133     event.stopPropagation();
  1134   },
  1136   /**
  1137    * Set up the content of the view menu.
  1138    */
  1139   populateSortMenu: function VM_populateSortMenu(event) {
  1140     this.fillWithColumns(event, "viewUnsorted", "directionSeparator", "radio", "view.sortBy.1.");
  1142     var sortColumn = this._getSortColumn();
  1143     var viewSortAscending = document.getElementById("viewSortAscending");
  1144     var viewSortDescending = document.getElementById("viewSortDescending");
  1145     // We need to remove an existing checked attribute because the unsorted
  1146     // menu item is not rebuilt every time we open the menu like the others.
  1147     var viewUnsorted = document.getElementById("viewUnsorted");
  1148     if (!sortColumn) {
  1149       viewSortAscending.removeAttribute("checked");
  1150       viewSortDescending.removeAttribute("checked");
  1151       viewUnsorted.setAttribute("checked", "true");
  1153     else if (sortColumn.getAttribute("sortDirection") == "ascending") {
  1154       viewSortAscending.setAttribute("checked", "true");
  1155       viewSortDescending.removeAttribute("checked");
  1156       viewUnsorted.removeAttribute("checked");
  1158     else if (sortColumn.getAttribute("sortDirection") == "descending") {
  1159       viewSortDescending.setAttribute("checked", "true");
  1160       viewSortAscending.removeAttribute("checked");
  1161       viewUnsorted.removeAttribute("checked");
  1163   },
  1165   /**
  1166    * Shows/Hides a tree column.
  1167    * @param   element
  1168    *          The menuitem element for the column
  1169    */
  1170   showHideColumn: function VM_showHideColumn(element) {
  1171     var column = element.column;
  1173     var splitter = column.nextSibling;
  1174     if (splitter && splitter.localName != "splitter")
  1175       splitter = null;
  1177     if (element.getAttribute("checked") == "true") {
  1178       column.setAttribute("hidden", "false");
  1179       if (splitter)
  1180         splitter.removeAttribute("hidden");
  1182     else {
  1183       column.setAttribute("hidden", "true");
  1184       if (splitter)
  1185         splitter.setAttribute("hidden", "true");
  1187   },
  1189   /**
  1190    * Gets the last column that was sorted.
  1191    * @returns  the currently sorted column, null if there is no sorted column.
  1192    */
  1193   _getSortColumn: function VM__getSortColumn() {
  1194     var content = document.getElementById("placeContent");
  1195     var cols = content.columns;
  1196     for (var i = 0; i < cols.count; ++i) {
  1197       var column = cols.getColumnAt(i).element;
  1198       var sortDirection = column.getAttribute("sortDirection");
  1199       if (sortDirection == "ascending" || sortDirection == "descending")
  1200         return column;
  1202     return null;
  1203   },
  1205   /**
  1206    * Sorts the view by the specified column.
  1207    * @param   aColumn
  1208    *          The colum that is the sort key. Can be null - the
  1209    *          current sort column or the title column will be used.
  1210    * @param   aDirection
  1211    *          The direction to sort - "ascending" or "descending".
  1212    *          Can be null - the last direction or descending will be used.
  1214    * If both aColumnID and aDirection are null, the view will be unsorted.
  1215    */
  1216   setSortColumn: function VM_setSortColumn(aColumn, aDirection) {
  1217     var result = document.getElementById("placeContent").result;
  1218     if (!aColumn && !aDirection) {
  1219       result.sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_NONE;
  1220       return;
  1223     var columnId;
  1224     if (aColumn) {
  1225       columnId = aColumn.getAttribute("anonid");
  1226       if (!aDirection) {
  1227         var sortColumn = this._getSortColumn();
  1228         if (sortColumn)
  1229           aDirection = sortColumn.getAttribute("sortDirection");
  1232     else {
  1233       var sortColumn = this._getSortColumn();
  1234       columnId = sortColumn ? sortColumn.getAttribute("anonid") : "title";
  1237     // This maps the possible values of columnId (i.e., anonid's of treecols in
  1238     // placeContent) to the default sortingMode and sortingAnnotation values for
  1239     // each column.
  1240     //   key:  Sort key in the name of one of the
  1241     //         nsINavHistoryQueryOptions.SORT_BY_* constants
  1242     //   dir:  Default sort direction to use if none has been specified
  1243     //   anno: The annotation to sort by, if key is "ANNOTATION"
  1244     var colLookupTable = {
  1245       title:        { key: "TITLE",        dir: "ascending"  },
  1246       tags:         { key: "TAGS",         dir: "ascending"  },
  1247       url:          { key: "URI",          dir: "ascending"  },
  1248       date:         { key: "DATE",         dir: "descending" },
  1249       visitCount:   { key: "VISITCOUNT",   dir: "descending" },
  1250       keyword:      { key: "KEYWORD",      dir: "ascending"  },
  1251       dateAdded:    { key: "DATEADDED",    dir: "descending" },
  1252       lastModified: { key: "LASTMODIFIED", dir: "descending" },
  1253       description:  { key: "ANNOTATION",
  1254                       dir: "ascending",
  1255                       anno: PlacesUIUtils.DESCRIPTION_ANNO }
  1256     };
  1258     // Make sure we have a valid column.
  1259     if (!colLookupTable.hasOwnProperty(columnId))
  1260       throw("Invalid column");
  1262     // Use a default sort direction if none has been specified.  If aDirection
  1263     // is invalid, result.sortingMode will be undefined, which has the effect
  1264     // of unsorting the tree.
  1265     aDirection = (aDirection || colLookupTable[columnId].dir).toUpperCase();
  1267     var sortConst = "SORT_BY_" + colLookupTable[columnId].key + "_" + aDirection;
  1268     result.sortingAnnotation = colLookupTable[columnId].anno || "";
  1269     result.sortingMode = Ci.nsINavHistoryQueryOptions[sortConst];
  1273 let ContentArea = {
  1274   _specialViews: new Map(),
  1276   init: function CA_init() {
  1277     this._deck = document.getElementById("placesViewsDeck");
  1278     this._toolbar = document.getElementById("placesToolbar");
  1279     ContentTree.init();
  1280     this._setupView();
  1281   },
  1283   /**
  1284    * Gets the content view to be used for loading the given query.
  1285    * If a custom view was set by setContentViewForQueryString, that
  1286    * view would be returned, else the default tree view is returned
  1288    * @param aQueryString
  1289    *        a query string
  1290    * @return the view to be used for loading aQueryString.
  1291    */
  1292   getContentViewForQueryString:
  1293   function CA_getContentViewForQueryString(aQueryString) {
  1294     try {
  1295       if (this._specialViews.has(aQueryString)) {
  1296         let { view, options } = this._specialViews.get(aQueryString);
  1297         if (typeof view == "function") {
  1298           view = view();
  1299           this._specialViews.set(aQueryString, { view: view, options: options });
  1301         return view;
  1304     catch(ex) {
  1305       Components.utils.reportError(ex);
  1307     return ContentTree.view;
  1308   },
  1310   /**
  1311    * Sets a custom view to be used rather than the default places tree
  1312    * whenever the given query is selected in the left pane.
  1313    * @param aQueryString
  1314    *        a query string
  1315    * @param aView
  1316    *        Either the custom view or a function that will return the view
  1317    *        the first (and only) time it's called.
  1318    * @param [optional] aOptions
  1319    *        Object defining special options for the view.
  1320    * @see ContentTree.viewOptions for supported options and default values.
  1321    */
  1322   setContentViewForQueryString:
  1323   function CA_setContentViewForQueryString(aQueryString, aView, aOptions) {
  1324     if (!aQueryString ||
  1325         typeof aView != "object" && typeof aView != "function")
  1326       throw new Error("Invalid arguments");
  1328     this._specialViews.set(aQueryString, { view: aView,
  1329                                            options: aOptions || new Object() });
  1330   },
  1332   get currentView() PlacesUIUtils.getViewForNode(this._deck.selectedPanel),
  1333   set currentView(aNewView) {
  1334     let oldView = this.currentView;
  1335     if (oldView != aNewView) {
  1336       this._deck.selectedPanel = aNewView.associatedElement;
  1338       // If the content area inactivated view was focused, move focus
  1339       // to the new view.
  1340       if (document.activeElement == oldView.associatedElement)
  1341         aNewView.associatedElement.focus();
  1343     return aNewView;
  1344   },
  1346   get currentPlace() this.currentView.place,
  1347   set currentPlace(aQueryString) {
  1348     let oldView = this.currentView;
  1349     let newView = this.getContentViewForQueryString(aQueryString);
  1350     newView.place = aQueryString;
  1351     if (oldView != newView) {
  1352       oldView.active = false;
  1353       this.currentView = newView;
  1354       this._setupView();
  1355       newView.active = true;
  1357     return aQueryString;
  1358   },
  1360   /**
  1361    * Applies view options.
  1362    */
  1363   _setupView: function CA__setupView() {
  1364     let options = this.currentViewOptions;
  1366     // showDetailsPane.
  1367     let detailsDeck = document.getElementById("detailsDeck");
  1368     detailsDeck.hidden = !options.showDetailsPane;
  1370     // toolbarSet.
  1371     for (let elt of this._toolbar.childNodes) {
  1372       // On Windows and Linux the menu buttons are menus wrapped in a menubar.
  1373       if (elt.id == "placesMenu") {
  1374         for (let menuElt of elt.childNodes) {
  1375           menuElt.hidden = options.toolbarSet.indexOf(menuElt.id) == -1;
  1378       else {
  1379         elt.hidden = options.toolbarSet.indexOf(elt.id) == -1;
  1382   },
  1384   /**
  1385    * Options for the current view.
  1387    * @see ContentTree.viewOptions for supported options and default values.
  1388    */
  1389   get currentViewOptions() {
  1390     // Use ContentTree options as default.
  1391     let viewOptions = ContentTree.viewOptions;
  1392     if (this._specialViews.has(this.currentPlace)) {
  1393       let { view, options } = this._specialViews.get(this.currentPlace);
  1394       for (let option in options) {
  1395         viewOptions[option] = options[option];
  1398     return viewOptions;
  1399   },
  1401   focus: function() {
  1402     this._deck.selectedPanel.focus();
  1404 };
  1406 let ContentTree = {
  1407   init: function CT_init() {
  1408     this._view = document.getElementById("placeContent");
  1409   },
  1411   get view() this._view,
  1413   get viewOptions() Object.seal({
  1414     showDetailsPane: true,
  1415     toolbarSet: "back-button, forward-button, organizeButton, viewMenu, maintenanceButton, libraryToolbarSpacer, searchFilter"
  1416   }),
  1418   openSelectedNode: function CT_openSelectedNode(aEvent) {
  1419     let view = this.view;
  1420     PlacesUIUtils.openNodeWithEvent(view.selectedNode, aEvent, view);
  1421   },
  1423   onClick: function CT_onClick(aEvent) {
  1424     let node = this.view.selectedNode;
  1425     if (node) {
  1426       let doubleClick = aEvent.button == 0 && aEvent.detail == 2;
  1427       let middleClick = aEvent.button == 1 && aEvent.detail == 1;
  1428       if (PlacesUtils.nodeIsURI(node) && (doubleClick || middleClick)) {
  1429         // Open associated uri in the browser.
  1430         this.openSelectedNode(aEvent);
  1432       else if (middleClick && PlacesUtils.nodeIsContainer(node)) {
  1433         // The command execution function will take care of seeing if the
  1434         // selection is a folder or a different container type, and will
  1435         // load its contents in tabs.
  1436         PlacesUIUtils.openContainerNodeInTabs(node, aEvent, this.view);
  1439   },
  1441   onKeyPress: function CT_onKeyPress(aEvent) {
  1442     if (aEvent.keyCode == KeyEvent.DOM_VK_RETURN)
  1443       this.openSelectedNode(aEvent);
  1445 };

mercurial