1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/components/places/content/places.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,1445 @@ 1.4 +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 1.5 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.7 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.8 + 1.9 +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); 1.10 +XPCOMUtils.defineLazyModuleGetter(this, "MigrationUtils", 1.11 + "resource:///modules/MigrationUtils.jsm"); 1.12 +XPCOMUtils.defineLazyModuleGetter(this, "Task", 1.13 + "resource://gre/modules/Task.jsm"); 1.14 +XPCOMUtils.defineLazyModuleGetter(this, "BookmarkJSONUtils", 1.15 + "resource://gre/modules/BookmarkJSONUtils.jsm"); 1.16 +XPCOMUtils.defineLazyModuleGetter(this, "PlacesBackups", 1.17 + "resource://gre/modules/PlacesBackups.jsm"); 1.18 +XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils", 1.19 + "resource://gre/modules/DownloadUtils.jsm"); 1.20 + 1.21 +var PlacesOrganizer = { 1.22 + _places: null, 1.23 + 1.24 + // IDs of fields from editBookmarkOverlay that should be hidden when infoBox 1.25 + // is minimal. IDs should be kept in sync with the IDs of the elements 1.26 + // observing additionalInfoBroadcaster. 1.27 + _additionalInfoFields: [ 1.28 + "editBMPanel_descriptionRow", 1.29 + "editBMPanel_loadInSidebarCheckbox", 1.30 + "editBMPanel_keywordRow", 1.31 + ], 1.32 + 1.33 + _initFolderTree: function() { 1.34 + var leftPaneRoot = PlacesUIUtils.leftPaneFolderId; 1.35 + this._places.place = "place:excludeItems=1&expandQueries=0&folder=" + leftPaneRoot; 1.36 + }, 1.37 + 1.38 + selectLeftPaneQuery: function PO_selectLeftPaneQuery(aQueryName) { 1.39 + var itemId = PlacesUIUtils.leftPaneQueries[aQueryName]; 1.40 + this._places.selectItems([itemId]); 1.41 + // Forcefully expand all-bookmarks 1.42 + if (aQueryName == "AllBookmarks" || aQueryName == "History") 1.43 + PlacesUtils.asContainer(this._places.selectedNode).containerOpen = true; 1.44 + }, 1.45 + 1.46 + /** 1.47 + * Opens a given hierarchy in the left pane, stopping at the last reachable 1.48 + * container. 1.49 + * 1.50 + * @param aHierarchy A single container or an array of containers, sorted from 1.51 + * the outmost to the innermost in the hierarchy. Each 1.52 + * container may be either an item id, a Places URI string, 1.53 + * or a named query. 1.54 + * @see PlacesUIUtils.leftPaneQueries for supported named queries. 1.55 + */ 1.56 + selectLeftPaneContainerByHierarchy: 1.57 + function PO_selectLeftPaneContainerByHierarchy(aHierarchy) { 1.58 + if (!aHierarchy) 1.59 + throw new Error("Invalid containers hierarchy"); 1.60 + let hierarchy = [].concat(aHierarchy); 1.61 + let selectWasSuppressed = this._places.view.selection.selectEventsSuppressed; 1.62 + if (!selectWasSuppressed) 1.63 + this._places.view.selection.selectEventsSuppressed = true; 1.64 + try { 1.65 + for (let container of hierarchy) { 1.66 + switch (typeof container) { 1.67 + case "number": 1.68 + this._places.selectItems([container], false); 1.69 + break; 1.70 + case "string": 1.71 + if (container.substr(0, 6) == "place:") 1.72 + this._places.selectPlaceURI(container); 1.73 + else if (container in PlacesUIUtils.leftPaneQueries) 1.74 + this.selectLeftPaneQuery(container); 1.75 + else 1.76 + throw new Error("Invalid container found: " + container); 1.77 + break; 1.78 + default: 1.79 + throw new Error("Invalid container type found: " + container); 1.80 + break; 1.81 + } 1.82 + PlacesUtils.asContainer(this._places.selectedNode).containerOpen = true; 1.83 + } 1.84 + } finally { 1.85 + if (!selectWasSuppressed) 1.86 + this._places.view.selection.selectEventsSuppressed = false; 1.87 + } 1.88 + }, 1.89 + 1.90 + init: function PO_init() { 1.91 + ContentArea.init(); 1.92 + 1.93 + this._places = document.getElementById("placesList"); 1.94 + this._initFolderTree(); 1.95 + 1.96 + var leftPaneSelection = "AllBookmarks"; // default to all-bookmarks 1.97 + if (window.arguments && window.arguments[0]) 1.98 + leftPaneSelection = window.arguments[0]; 1.99 + 1.100 + this.selectLeftPaneContainerByHierarchy(leftPaneSelection); 1.101 + if (leftPaneSelection === "History") { 1.102 + let historyNode = this._places.selectedNode; 1.103 + if (historyNode.childCount > 0) 1.104 + this._places.selectNode(historyNode.getChild(0)); 1.105 + } 1.106 + 1.107 + // clear the back-stack 1.108 + this._backHistory.splice(0, this._backHistory.length); 1.109 + document.getElementById("OrganizerCommand:Back").setAttribute("disabled", true); 1.110 + 1.111 + // Set up the search UI. 1.112 + PlacesSearchBox.init(); 1.113 + 1.114 + window.addEventListener("AppCommand", this, true); 1.115 +#ifdef XP_MACOSX 1.116 + // 1. Map Edit->Find command to OrganizerCommand_find:all. Need to map 1.117 + // both the menuitem and the Find key. 1.118 + var findMenuItem = document.getElementById("menu_find"); 1.119 + findMenuItem.setAttribute("command", "OrganizerCommand_find:all"); 1.120 + var findKey = document.getElementById("key_find"); 1.121 + findKey.setAttribute("command", "OrganizerCommand_find:all"); 1.122 + 1.123 + // 2. Disable some keybindings from browser.xul 1.124 + var elements = ["cmd_handleBackspace", "cmd_handleShiftBackspace"]; 1.125 + for (var i=0; i < elements.length; i++) { 1.126 + document.getElementById(elements[i]).setAttribute("disabled", "true"); 1.127 + } 1.128 +#endif 1.129 + 1.130 + // remove the "Properties" context-menu item, we've our own details pane 1.131 + document.getElementById("placesContext") 1.132 + .removeChild(document.getElementById("placesContext_show:info")); 1.133 + 1.134 + ContentArea.focus(); 1.135 + }, 1.136 + 1.137 + QueryInterface: function PO_QueryInterface(aIID) { 1.138 + if (aIID.equals(Components.interfaces.nsIDOMEventListener) || 1.139 + aIID.equals(Components.interfaces.nsISupports)) 1.140 + return this; 1.141 + 1.142 + throw Components.results.NS_NOINTERFACE; 1.143 + }, 1.144 + 1.145 + handleEvent: function PO_handleEvent(aEvent) { 1.146 + if (aEvent.type != "AppCommand") 1.147 + return; 1.148 + 1.149 + aEvent.stopPropagation(); 1.150 + switch (aEvent.command) { 1.151 + case "Back": 1.152 + if (this._backHistory.length > 0) 1.153 + this.back(); 1.154 + break; 1.155 + case "Forward": 1.156 + if (this._forwardHistory.length > 0) 1.157 + this.forward(); 1.158 + break; 1.159 + case "Search": 1.160 + PlacesSearchBox.findAll(); 1.161 + break; 1.162 + } 1.163 + }, 1.164 + 1.165 + destroy: function PO_destroy() { 1.166 + }, 1.167 + 1.168 + _location: null, 1.169 + get location() { 1.170 + return this._location; 1.171 + }, 1.172 + 1.173 + set location(aLocation) { 1.174 + if (!aLocation || this._location == aLocation) 1.175 + return aLocation; 1.176 + 1.177 + if (this.location) { 1.178 + this._backHistory.unshift(this.location); 1.179 + this._forwardHistory.splice(0, this._forwardHistory.length); 1.180 + } 1.181 + 1.182 + this._location = aLocation; 1.183 + this._places.selectPlaceURI(aLocation); 1.184 + 1.185 + if (!this._places.hasSelection) { 1.186 + // If no node was found for the given place: uri, just load it directly 1.187 + ContentArea.currentPlace = aLocation; 1.188 + } 1.189 + this.updateDetailsPane(); 1.190 + 1.191 + // update navigation commands 1.192 + if (this._backHistory.length == 0) 1.193 + document.getElementById("OrganizerCommand:Back").setAttribute("disabled", true); 1.194 + else 1.195 + document.getElementById("OrganizerCommand:Back").removeAttribute("disabled"); 1.196 + if (this._forwardHistory.length == 0) 1.197 + document.getElementById("OrganizerCommand:Forward").setAttribute("disabled", true); 1.198 + else 1.199 + document.getElementById("OrganizerCommand:Forward").removeAttribute("disabled"); 1.200 + 1.201 + return aLocation; 1.202 + }, 1.203 + 1.204 + _backHistory: [], 1.205 + _forwardHistory: [], 1.206 + 1.207 + back: function PO_back() { 1.208 + this._forwardHistory.unshift(this.location); 1.209 + var historyEntry = this._backHistory.shift(); 1.210 + this._location = null; 1.211 + this.location = historyEntry; 1.212 + }, 1.213 + forward: function PO_forward() { 1.214 + this._backHistory.unshift(this.location); 1.215 + var historyEntry = this._forwardHistory.shift(); 1.216 + this._location = null; 1.217 + this.location = historyEntry; 1.218 + }, 1.219 + 1.220 + /** 1.221 + * Called when a place folder is selected in the left pane. 1.222 + * @param resetSearchBox 1.223 + * true if the search box should also be reset, false otherwise. 1.224 + * The search box should be reset when a new folder in the left 1.225 + * pane is selected; the search scope and text need to be cleared in 1.226 + * preparation for the new folder. Note that if the user manually 1.227 + * resets the search box, either by clicking its reset button or by 1.228 + * deleting its text, this will be false. 1.229 + */ 1.230 + _cachedLeftPaneSelectedURI: null, 1.231 + onPlaceSelected: function PO_onPlaceSelected(resetSearchBox) { 1.232 + // Don't change the right-hand pane contents when there's no selection. 1.233 + if (!this._places.hasSelection) 1.234 + return; 1.235 + 1.236 + var node = this._places.selectedNode; 1.237 + var queries = PlacesUtils.asQuery(node).getQueries(); 1.238 + 1.239 + // Items are only excluded on the left pane. 1.240 + var options = node.queryOptions.clone(); 1.241 + options.excludeItems = false; 1.242 + var placeURI = PlacesUtils.history.queriesToQueryString(queries, 1.243 + queries.length, 1.244 + options); 1.245 + 1.246 + // If either the place of the content tree in the right pane has changed or 1.247 + // the user cleared the search box, update the place, hide the search UI, 1.248 + // and update the back/forward buttons by setting location. 1.249 + if (ContentArea.currentPlace != placeURI || !resetSearchBox) { 1.250 + ContentArea.currentPlace = placeURI; 1.251 + this.location = node.uri; 1.252 + } 1.253 + 1.254 + // When we invalidate a container we use suppressSelectionEvent, when it is 1.255 + // unset a select event is fired, in many cases the selection did not really 1.256 + // change, so we should check for it, and return early in such a case. Note 1.257 + // that we cannot return any earlier than this point, because when 1.258 + // !resetSearchBox, we need to update location and hide the UI as above, 1.259 + // even though the selection has not changed. 1.260 + if (node.uri == this._cachedLeftPaneSelectedURI) 1.261 + return; 1.262 + this._cachedLeftPaneSelectedURI = node.uri; 1.263 + 1.264 + // At this point, resetSearchBox is true, because the left pane selection 1.265 + // has changed; otherwise we would have returned earlier. 1.266 + 1.267 + PlacesSearchBox.searchFilter.reset(); 1.268 + this._setSearchScopeForNode(node); 1.269 + this.updateDetailsPane(); 1.270 + }, 1.271 + 1.272 + /** 1.273 + * Sets the search scope based on aNode's properties. 1.274 + * @param aNode 1.275 + * the node to set up scope from 1.276 + */ 1.277 + _setSearchScopeForNode: function PO__setScopeForNode(aNode) { 1.278 + let itemId = aNode.itemId; 1.279 + 1.280 + if (PlacesUtils.nodeIsHistoryContainer(aNode) || 1.281 + itemId == PlacesUIUtils.leftPaneQueries["History"]) { 1.282 + PlacesQueryBuilder.setScope("history"); 1.283 + } 1.284 + else if (itemId == PlacesUIUtils.leftPaneQueries["Downloads"]) { 1.285 + PlacesQueryBuilder.setScope("downloads"); 1.286 + } 1.287 + else { 1.288 + // Default to All Bookmarks for all other nodes, per bug 469437. 1.289 + PlacesQueryBuilder.setScope("bookmarks"); 1.290 + } 1.291 + }, 1.292 + 1.293 + /** 1.294 + * Handle clicks on the places list. 1.295 + * Single Left click, right click or modified click do not result in any 1.296 + * special action, since they're related to selection. 1.297 + * @param aEvent 1.298 + * The mouse event. 1.299 + */ 1.300 + onPlacesListClick: function PO_onPlacesListClick(aEvent) { 1.301 + // Only handle clicks on tree children. 1.302 + if (aEvent.target.localName != "treechildren") 1.303 + return; 1.304 + 1.305 + let node = this._places.selectedNode; 1.306 + if (node) { 1.307 + let middleClick = aEvent.button == 1 && aEvent.detail == 1; 1.308 + if (middleClick && PlacesUtils.nodeIsContainer(node)) { 1.309 + // The command execution function will take care of seeing if the 1.310 + // selection is a folder or a different container type, and will 1.311 + // load its contents in tabs. 1.312 + PlacesUIUtils.openContainerNodeInTabs(selectedNode, aEvent, this._places); 1.313 + } 1.314 + } 1.315 + }, 1.316 + 1.317 + /** 1.318 + * Handle focus changes on the places list and the current content view. 1.319 + */ 1.320 + updateDetailsPane: function PO_updateDetailsPane() { 1.321 + if (!ContentArea.currentViewOptions.showDetailsPane) 1.322 + return; 1.323 + let view = PlacesUIUtils.getViewForNode(document.activeElement); 1.324 + if (view) { 1.325 + let selectedNodes = view.selectedNode ? 1.326 + [view.selectedNode] : view.selectedNodes; 1.327 + this._fillDetailsPane(selectedNodes); 1.328 + } 1.329 + }, 1.330 + 1.331 + openFlatContainer: function PO_openFlatContainerFlatContainer(aContainer) { 1.332 + if (aContainer.itemId != -1) { 1.333 + PlacesUtils.asContainer(this._places.selectedNode).containerOpen = true; 1.334 + this._places.selectItems([aContainer.itemId], false); 1.335 + } 1.336 + else if (PlacesUtils.nodeIsQuery(aContainer)) { 1.337 + this._places.selectPlaceURI(aContainer.uri); 1.338 + } 1.339 + }, 1.340 + 1.341 + /** 1.342 + * Returns the options associated with the query currently loaded in the 1.343 + * main places pane. 1.344 + */ 1.345 + getCurrentOptions: function PO_getCurrentOptions() { 1.346 + return PlacesUtils.asQuery(ContentArea.currentView.result.root).queryOptions; 1.347 + }, 1.348 + 1.349 + /** 1.350 + * Returns the queries associated with the query currently loaded in the 1.351 + * main places pane. 1.352 + */ 1.353 + getCurrentQueries: function PO_getCurrentQueries() { 1.354 + return PlacesUtils.asQuery(ContentArea.currentView.result.root).getQueries(); 1.355 + }, 1.356 + 1.357 + /** 1.358 + * Show the migration wizard for importing passwords, 1.359 + * cookies, history, preferences, and bookmarks. 1.360 + */ 1.361 + importFromBrowser: function PO_importFromBrowser() { 1.362 + MigrationUtils.showMigrationWizard(window); 1.363 + }, 1.364 + 1.365 + /** 1.366 + * Open a file-picker and import the selected file into the bookmarks store 1.367 + */ 1.368 + importFromFile: function PO_importFromFile() { 1.369 + let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); 1.370 + let fpCallback = function fpCallback_done(aResult) { 1.371 + if (aResult != Ci.nsIFilePicker.returnCancel && fp.fileURL) { 1.372 + Components.utils.import("resource://gre/modules/BookmarkHTMLUtils.jsm"); 1.373 + BookmarkHTMLUtils.importFromURL(fp.fileURL.spec, false) 1.374 + .then(null, Components.utils.reportError); 1.375 + } 1.376 + }; 1.377 + 1.378 + fp.init(window, PlacesUIUtils.getString("SelectImport"), 1.379 + Ci.nsIFilePicker.modeOpen); 1.380 + fp.appendFilters(Ci.nsIFilePicker.filterHTML); 1.381 + fp.open(fpCallback); 1.382 + }, 1.383 + 1.384 + /** 1.385 + * Allows simple exporting of bookmarks. 1.386 + */ 1.387 + exportBookmarks: function PO_exportBookmarks() { 1.388 + let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); 1.389 + let fpCallback = function fpCallback_done(aResult) { 1.390 + if (aResult != Ci.nsIFilePicker.returnCancel) { 1.391 + Components.utils.import("resource://gre/modules/BookmarkHTMLUtils.jsm"); 1.392 + BookmarkHTMLUtils.exportToFile(fp.file.path) 1.393 + .then(null, Components.utils.reportError); 1.394 + } 1.395 + }; 1.396 + 1.397 + fp.init(window, PlacesUIUtils.getString("EnterExport"), 1.398 + Ci.nsIFilePicker.modeSave); 1.399 + fp.appendFilters(Ci.nsIFilePicker.filterHTML); 1.400 + fp.defaultString = "bookmarks.html"; 1.401 + fp.open(fpCallback); 1.402 + }, 1.403 + 1.404 + /** 1.405 + * Populates the restore menu with the dates of the backups available. 1.406 + */ 1.407 + populateRestoreMenu: function PO_populateRestoreMenu() { 1.408 + let restorePopup = document.getElementById("fileRestorePopup"); 1.409 + 1.410 + let dateSvc = Cc["@mozilla.org/intl/scriptabledateformat;1"]. 1.411 + getService(Ci.nsIScriptableDateFormat); 1.412 + 1.413 + // Remove existing menu items. Last item is the restoreFromFile item. 1.414 + while (restorePopup.childNodes.length > 1) 1.415 + restorePopup.removeChild(restorePopup.firstChild); 1.416 + 1.417 + Task.spawn(function() { 1.418 + let backupFiles = yield PlacesBackups.getBackupFiles(); 1.419 + if (backupFiles.length == 0) 1.420 + return; 1.421 + 1.422 + // Populate menu with backups. 1.423 + for (let i = 0; i < backupFiles.length; i++) { 1.424 + let fileSize = (yield OS.File.stat(backupFiles[i])).size; 1.425 + let [size, unit] = DownloadUtils.convertByteUnits(fileSize); 1.426 + let sizeString = PlacesUtils.getFormattedString("backupFileSizeText", 1.427 + [size, unit]); 1.428 + let sizeInfo; 1.429 + let bookmarkCount = PlacesBackups.getBookmarkCountForFile(backupFiles[i]); 1.430 + if (bookmarkCount != null) { 1.431 + sizeInfo = " (" + sizeString + " - " + 1.432 + PlacesUIUtils.getPluralString("detailsPane.itemsCountLabel", 1.433 + bookmarkCount, 1.434 + [bookmarkCount]) + 1.435 + ")"; 1.436 + } else { 1.437 + sizeInfo = " (" + sizeString + ")"; 1.438 + } 1.439 + 1.440 + let backupDate = PlacesBackups.getDateForFile(backupFiles[i]); 1.441 + let m = restorePopup.insertBefore(document.createElement("menuitem"), 1.442 + document.getElementById("restoreFromFile")); 1.443 + m.setAttribute("label", 1.444 + dateSvc.FormatDate("", 1.445 + Ci.nsIScriptableDateFormat.dateFormatLong, 1.446 + backupDate.getFullYear(), 1.447 + backupDate.getMonth() + 1, 1.448 + backupDate.getDate()) + 1.449 + sizeInfo); 1.450 + m.setAttribute("value", OS.Path.basename(backupFiles[i])); 1.451 + m.setAttribute("oncommand", 1.452 + "PlacesOrganizer.onRestoreMenuItemClick(this);"); 1.453 + } 1.454 + 1.455 + // Add the restoreFromFile item. 1.456 + restorePopup.insertBefore(document.createElement("menuseparator"), 1.457 + document.getElementById("restoreFromFile")); 1.458 + }); 1.459 + }, 1.460 + 1.461 + /** 1.462 + * Called when a menuitem is selected from the restore menu. 1.463 + */ 1.464 + onRestoreMenuItemClick: function PO_onRestoreMenuItemClick(aMenuItem) { 1.465 + Task.spawn(function() { 1.466 + let backupName = aMenuItem.getAttribute("value"); 1.467 + let backupFilePaths = yield PlacesBackups.getBackupFiles(); 1.468 + for (let backupFilePath of backupFilePaths) { 1.469 + if (OS.Path.basename(backupFilePath) == backupName) { 1.470 + PlacesOrganizer.restoreBookmarksFromFile(backupFilePath); 1.471 + break; 1.472 + } 1.473 + } 1.474 + }); 1.475 + }, 1.476 + 1.477 + /** 1.478 + * Called when 'Choose File...' is selected from the restore menu. 1.479 + * Prompts for a file and restores bookmarks to those in the file. 1.480 + */ 1.481 + onRestoreBookmarksFromFile: function PO_onRestoreBookmarksFromFile() { 1.482 + let dirSvc = Cc["@mozilla.org/file/directory_service;1"]. 1.483 + getService(Ci.nsIProperties); 1.484 + let backupsDir = dirSvc.get("Desk", Ci.nsILocalFile); 1.485 + let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); 1.486 + let fpCallback = function fpCallback_done(aResult) { 1.487 + if (aResult != Ci.nsIFilePicker.returnCancel) { 1.488 + this.restoreBookmarksFromFile(fp.file.path); 1.489 + } 1.490 + }.bind(this); 1.491 + 1.492 + fp.init(window, PlacesUIUtils.getString("bookmarksRestoreTitle"), 1.493 + Ci.nsIFilePicker.modeOpen); 1.494 + fp.appendFilter(PlacesUIUtils.getString("bookmarksRestoreFilterName"), 1.495 + PlacesUIUtils.getString("bookmarksRestoreFilterExtension")); 1.496 + fp.appendFilters(Ci.nsIFilePicker.filterAll); 1.497 + fp.displayDirectory = backupsDir; 1.498 + fp.open(fpCallback); 1.499 + }, 1.500 + 1.501 + /** 1.502 + * Restores bookmarks from a JSON file. 1.503 + */ 1.504 + restoreBookmarksFromFile: function PO_restoreBookmarksFromFile(aFilePath) { 1.505 + // check file extension 1.506 + if (!aFilePath.endsWith("json")) { 1.507 + this._showErrorAlert(PlacesUIUtils.getString("bookmarksRestoreFormatError")); 1.508 + return; 1.509 + } 1.510 + 1.511 + // confirm ok to delete existing bookmarks 1.512 + var prompts = Cc["@mozilla.org/embedcomp/prompt-service;1"]. 1.513 + getService(Ci.nsIPromptService); 1.514 + if (!prompts.confirm(null, 1.515 + PlacesUIUtils.getString("bookmarksRestoreAlertTitle"), 1.516 + PlacesUIUtils.getString("bookmarksRestoreAlert"))) 1.517 + return; 1.518 + 1.519 + Task.spawn(function() { 1.520 + try { 1.521 + yield BookmarkJSONUtils.importFromFile(aFilePath, true); 1.522 + } catch(ex) { 1.523 + PlacesOrganizer._showErrorAlert(PlacesUIUtils.getString("bookmarksRestoreParseError")); 1.524 + } 1.525 + }); 1.526 + }, 1.527 + 1.528 + _showErrorAlert: function PO__showErrorAlert(aMsg) { 1.529 + var brandShortName = document.getElementById("brandStrings"). 1.530 + getString("brandShortName"); 1.531 + 1.532 + Cc["@mozilla.org/embedcomp/prompt-service;1"]. 1.533 + getService(Ci.nsIPromptService). 1.534 + alert(window, brandShortName, aMsg); 1.535 + }, 1.536 + 1.537 + /** 1.538 + * Backup bookmarks to desktop, auto-generate a filename with a date. 1.539 + * The file is a JSON serialization of bookmarks, tags and any annotations 1.540 + * of those items. 1.541 + */ 1.542 + backupBookmarks: function PO_backupBookmarks() { 1.543 + let dirSvc = Cc["@mozilla.org/file/directory_service;1"]. 1.544 + getService(Ci.nsIProperties); 1.545 + let backupsDir = dirSvc.get("Desk", Ci.nsILocalFile); 1.546 + let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); 1.547 + let fpCallback = function fpCallback_done(aResult) { 1.548 + if (aResult != Ci.nsIFilePicker.returnCancel) { 1.549 + // There is no OS.File version of the filepicker yet (Bug 937812). 1.550 + PlacesBackups.saveBookmarksToJSONFile(fp.file.path); 1.551 + } 1.552 + }; 1.553 + 1.554 + fp.init(window, PlacesUIUtils.getString("bookmarksBackupTitle"), 1.555 + Ci.nsIFilePicker.modeSave); 1.556 + fp.appendFilter(PlacesUIUtils.getString("bookmarksRestoreFilterName"), 1.557 + PlacesUIUtils.getString("bookmarksRestoreFilterExtension")); 1.558 + fp.defaultString = PlacesBackups.getFilenameForDate(); 1.559 + fp.displayDirectory = backupsDir; 1.560 + fp.open(fpCallback); 1.561 + }, 1.562 + 1.563 + _paneDisabled: false, 1.564 + _setDetailsFieldsDisabledState: 1.565 + function PO__setDetailsFieldsDisabledState(aDisabled) { 1.566 + if (aDisabled) { 1.567 + document.getElementById("paneElementsBroadcaster") 1.568 + .setAttribute("disabled", "true"); 1.569 + } 1.570 + else { 1.571 + document.getElementById("paneElementsBroadcaster") 1.572 + .removeAttribute("disabled"); 1.573 + } 1.574 + }, 1.575 + 1.576 + _detectAndSetDetailsPaneMinimalState: 1.577 + function PO__detectAndSetDetailsPaneMinimalState(aNode) { 1.578 + /** 1.579 + * The details of simple folder-items (as opposed to livemarks) or the 1.580 + * of livemark-children are not likely to fill the infoBox anyway, 1.581 + * thus we remove the "More/Less" button and show all details. 1.582 + * 1.583 + * the wasminimal attribute here is used to persist the "more/less" 1.584 + * state in a bookmark->folder->bookmark scenario. 1.585 + */ 1.586 + var infoBox = document.getElementById("infoBox"); 1.587 + var infoBoxExpander = document.getElementById("infoBoxExpander"); 1.588 + var infoBoxExpanderWrapper = document.getElementById("infoBoxExpanderWrapper"); 1.589 + var additionalInfoBroadcaster = document.getElementById("additionalInfoBroadcaster"); 1.590 + 1.591 + if (!aNode) { 1.592 + infoBoxExpanderWrapper.hidden = true; 1.593 + return; 1.594 + } 1.595 + if (aNode.itemId != -1 && 1.596 + PlacesUtils.nodeIsFolder(aNode) && !aNode._feedURI) { 1.597 + if (infoBox.getAttribute("minimal") == "true") 1.598 + infoBox.setAttribute("wasminimal", "true"); 1.599 + infoBox.removeAttribute("minimal"); 1.600 + infoBoxExpanderWrapper.hidden = true; 1.601 + } 1.602 + else { 1.603 + if (infoBox.getAttribute("wasminimal") == "true") 1.604 + infoBox.setAttribute("minimal", "true"); 1.605 + infoBox.removeAttribute("wasminimal"); 1.606 + infoBoxExpanderWrapper.hidden = 1.607 + this._additionalInfoFields.every(function (id) 1.608 + document.getElementById(id).collapsed); 1.609 + } 1.610 + additionalInfoBroadcaster.hidden = infoBox.getAttribute("minimal") == "true"; 1.611 + }, 1.612 + 1.613 + // NOT YET USED 1.614 + updateThumbnailProportions: function PO_updateThumbnailProportions() { 1.615 + var previewBox = document.getElementById("previewBox"); 1.616 + var canvas = document.getElementById("itemThumbnail"); 1.617 + var height = previewBox.boxObject.height; 1.618 + var width = height * (screen.width / screen.height); 1.619 + canvas.width = width; 1.620 + canvas.height = height; 1.621 + }, 1.622 + 1.623 + _fillDetailsPane: function PO__fillDetailsPane(aNodeList) { 1.624 + var infoBox = document.getElementById("infoBox"); 1.625 + var detailsDeck = document.getElementById("detailsDeck"); 1.626 + 1.627 + // Make sure the infoBox UI is visible if we need to use it, we hide it 1.628 + // below when we don't. 1.629 + infoBox.hidden = false; 1.630 + var aSelectedNode = aNodeList.length == 1 ? aNodeList[0] : null; 1.631 + // If a textbox within a panel is focused, force-blur it so its contents 1.632 + // are saved 1.633 + if (gEditItemOverlay.itemId != -1) { 1.634 + var focusedElement = document.commandDispatcher.focusedElement; 1.635 + if ((focusedElement instanceof HTMLInputElement || 1.636 + focusedElement instanceof HTMLTextAreaElement) && 1.637 + /^editBMPanel.*/.test(focusedElement.parentNode.parentNode.id)) 1.638 + focusedElement.blur(); 1.639 + 1.640 + // don't update the panel if we are already editing this node unless we're 1.641 + // in multi-edit mode 1.642 + if (aSelectedNode) { 1.643 + var concreteId = PlacesUtils.getConcreteItemId(aSelectedNode); 1.644 + var nodeIsSame = gEditItemOverlay.itemId == aSelectedNode.itemId || 1.645 + gEditItemOverlay.itemId == concreteId || 1.646 + (aSelectedNode.itemId == -1 && gEditItemOverlay.uri && 1.647 + gEditItemOverlay.uri == aSelectedNode.uri); 1.648 + if (nodeIsSame && detailsDeck.selectedIndex == 1 && 1.649 + !gEditItemOverlay.multiEdit) 1.650 + return; 1.651 + } 1.652 + } 1.653 + 1.654 + // Clean up the panel before initing it again. 1.655 + gEditItemOverlay.uninitPanel(false); 1.656 + 1.657 + if (aSelectedNode && !PlacesUtils.nodeIsSeparator(aSelectedNode)) { 1.658 + detailsDeck.selectedIndex = 1; 1.659 + // Using the concrete itemId is arguably wrong. The bookmarks API 1.660 + // does allow setting properties for folder shortcuts as well, but since 1.661 + // the UI does not distinct between the couple, we better just show 1.662 + // the concrete item properties for shortcuts to root nodes. 1.663 + var concreteId = PlacesUtils.getConcreteItemId(aSelectedNode); 1.664 + var isRootItem = concreteId != -1 && PlacesUtils.isRootItem(concreteId); 1.665 + var readOnly = isRootItem || 1.666 + aSelectedNode.parent.itemId == PlacesUIUtils.leftPaneFolderId; 1.667 + var useConcreteId = isRootItem || 1.668 + PlacesUtils.nodeIsTagQuery(aSelectedNode); 1.669 + var itemId = -1; 1.670 + if (concreteId != -1 && useConcreteId) 1.671 + itemId = concreteId; 1.672 + else if (aSelectedNode.itemId != -1) 1.673 + itemId = aSelectedNode.itemId; 1.674 + else 1.675 + itemId = PlacesUtils._uri(aSelectedNode.uri); 1.676 + 1.677 + gEditItemOverlay.initPanel(itemId, { hiddenRows: ["folderPicker"] 1.678 + , forceReadOnly: readOnly 1.679 + , titleOverride: aSelectedNode.title 1.680 + }); 1.681 + 1.682 + // Dynamically generated queries, like history date containers, have 1.683 + // itemId !=0 and do not exist in history. For them the panel is 1.684 + // read-only, but empty, since it can't get a valid title for the object. 1.685 + // In such a case we force the title using the selectedNode one, for UI 1.686 + // polishness. 1.687 + if (aSelectedNode.itemId == -1 && 1.688 + (PlacesUtils.nodeIsDay(aSelectedNode) || 1.689 + PlacesUtils.nodeIsHost(aSelectedNode))) 1.690 + gEditItemOverlay._element("namePicker").value = aSelectedNode.title; 1.691 + 1.692 + this._detectAndSetDetailsPaneMinimalState(aSelectedNode); 1.693 + } 1.694 + else if (!aSelectedNode && aNodeList[0]) { 1.695 + var itemIds = []; 1.696 + for (var i = 0; i < aNodeList.length; i++) { 1.697 + if (!PlacesUtils.nodeIsBookmark(aNodeList[i]) && 1.698 + !PlacesUtils.nodeIsURI(aNodeList[i])) { 1.699 + detailsDeck.selectedIndex = 0; 1.700 + var selectItemDesc = document.getElementById("selectItemDescription"); 1.701 + var itemsCountLabel = document.getElementById("itemsCountText"); 1.702 + selectItemDesc.hidden = false; 1.703 + itemsCountLabel.value = 1.704 + PlacesUIUtils.getPluralString("detailsPane.itemsCountLabel", 1.705 + aNodeList.length, [aNodeList.length]); 1.706 + infoBox.hidden = true; 1.707 + return; 1.708 + } 1.709 + itemIds[i] = aNodeList[i].itemId != -1 ? aNodeList[i].itemId : 1.710 + PlacesUtils._uri(aNodeList[i].uri); 1.711 + } 1.712 + detailsDeck.selectedIndex = 1; 1.713 + gEditItemOverlay.initPanel(itemIds, 1.714 + { hiddenRows: ["folderPicker", 1.715 + "loadInSidebar", 1.716 + "location", 1.717 + "keyword", 1.718 + "description", 1.719 + "name"]}); 1.720 + this._detectAndSetDetailsPaneMinimalState(aSelectedNode); 1.721 + } 1.722 + else { 1.723 + detailsDeck.selectedIndex = 0; 1.724 + infoBox.hidden = true; 1.725 + let selectItemDesc = document.getElementById("selectItemDescription"); 1.726 + let itemsCountLabel = document.getElementById("itemsCountText"); 1.727 + let itemsCount = 0; 1.728 + if (ContentArea.currentView.result) { 1.729 + let rootNode = ContentArea.currentView.result.root; 1.730 + if (rootNode.containerOpen) 1.731 + itemsCount = rootNode.childCount; 1.732 + } 1.733 + if (itemsCount == 0) { 1.734 + selectItemDesc.hidden = true; 1.735 + itemsCountLabel.value = PlacesUIUtils.getString("detailsPane.noItems"); 1.736 + } 1.737 + else { 1.738 + selectItemDesc.hidden = false; 1.739 + itemsCountLabel.value = 1.740 + PlacesUIUtils.getPluralString("detailsPane.itemsCountLabel", 1.741 + itemsCount, [itemsCount]); 1.742 + } 1.743 + } 1.744 + }, 1.745 + 1.746 + // NOT YET USED 1.747 + _updateThumbnail: function PO__updateThumbnail() { 1.748 + var bo = document.getElementById("previewBox").boxObject; 1.749 + var width = bo.width; 1.750 + var height = bo.height; 1.751 + 1.752 + var canvas = document.getElementById("itemThumbnail"); 1.753 + var ctx = canvas.getContext('2d'); 1.754 + var notAvailableText = canvas.getAttribute("notavailabletext"); 1.755 + ctx.save(); 1.756 + ctx.fillStyle = "-moz-Dialog"; 1.757 + ctx.fillRect(0, 0, width, height); 1.758 + ctx.translate(width/2, height/2); 1.759 + 1.760 + ctx.fillStyle = "GrayText"; 1.761 + ctx.mozTextStyle = "12pt sans serif"; 1.762 + var len = ctx.mozMeasureText(notAvailableText); 1.763 + ctx.translate(-len/2,0); 1.764 + ctx.mozDrawText(notAvailableText); 1.765 + ctx.restore(); 1.766 + }, 1.767 + 1.768 + toggleAdditionalInfoFields: function PO_toggleAdditionalInfoFields() { 1.769 + var infoBox = document.getElementById("infoBox"); 1.770 + var infoBoxExpander = document.getElementById("infoBoxExpander"); 1.771 + var infoBoxExpanderLabel = document.getElementById("infoBoxExpanderLabel"); 1.772 + var additionalInfoBroadcaster = document.getElementById("additionalInfoBroadcaster"); 1.773 + 1.774 + if (infoBox.getAttribute("minimal") == "true") { 1.775 + infoBox.removeAttribute("minimal"); 1.776 + infoBoxExpanderLabel.value = infoBoxExpanderLabel.getAttribute("lesslabel"); 1.777 + infoBoxExpanderLabel.accessKey = infoBoxExpanderLabel.getAttribute("lessaccesskey"); 1.778 + infoBoxExpander.className = "expander-up"; 1.779 + additionalInfoBroadcaster.removeAttribute("hidden"); 1.780 + } 1.781 + else { 1.782 + infoBox.setAttribute("minimal", "true"); 1.783 + infoBoxExpanderLabel.value = infoBoxExpanderLabel.getAttribute("morelabel"); 1.784 + infoBoxExpanderLabel.accessKey = infoBoxExpanderLabel.getAttribute("moreaccesskey"); 1.785 + infoBoxExpander.className = "expander-down"; 1.786 + additionalInfoBroadcaster.setAttribute("hidden", "true"); 1.787 + } 1.788 + }, 1.789 +}; 1.790 + 1.791 +/** 1.792 + * A set of utilities relating to search within Bookmarks and History. 1.793 + */ 1.794 +var PlacesSearchBox = { 1.795 + 1.796 + /** 1.797 + * The Search text field 1.798 + */ 1.799 + get searchFilter() { 1.800 + return document.getElementById("searchFilter"); 1.801 + }, 1.802 + 1.803 + /** 1.804 + * Folders to include when searching. 1.805 + */ 1.806 + _folders: [], 1.807 + get folders() { 1.808 + if (this._folders.length == 0) { 1.809 + this._folders.push(PlacesUtils.bookmarksMenuFolderId, 1.810 + PlacesUtils.unfiledBookmarksFolderId, 1.811 + PlacesUtils.toolbarFolderId); 1.812 + } 1.813 + return this._folders; 1.814 + }, 1.815 + set folders(aFolders) { 1.816 + this._folders = aFolders; 1.817 + return aFolders; 1.818 + }, 1.819 + 1.820 + /** 1.821 + * Run a search for the specified text, over the collection specified by 1.822 + * the dropdown arrow. The default is all bookmarks, but can be 1.823 + * localized to the active collection. 1.824 + * @param filterString 1.825 + * The text to search for. 1.826 + */ 1.827 + search: function PSB_search(filterString) { 1.828 + var PO = PlacesOrganizer; 1.829 + // If the user empties the search box manually, reset it and load all 1.830 + // contents of the current scope. 1.831 + // XXX this might be to jumpy, maybe should search for "", so results 1.832 + // are ungrouped, and search box not reset 1.833 + if (filterString == "") { 1.834 + PO.onPlaceSelected(false); 1.835 + return; 1.836 + } 1.837 + 1.838 + let currentView = ContentArea.currentView; 1.839 + let currentOptions = PO.getCurrentOptions(); 1.840 + 1.841 + // Search according to the current scope, which was set by 1.842 + // PQB_setScope() 1.843 + switch (PlacesSearchBox.filterCollection) { 1.844 + case "bookmarks": 1.845 + currentView.applyFilter(filterString, this.folders); 1.846 + break; 1.847 + case "history": 1.848 + if (currentOptions.queryType != Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) { 1.849 + var query = PlacesUtils.history.getNewQuery(); 1.850 + query.searchTerms = filterString; 1.851 + var options = currentOptions.clone(); 1.852 + // Make sure we're getting uri results. 1.853 + options.resultType = currentOptions.RESULTS_AS_URI; 1.854 + options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY; 1.855 + options.includeHidden = true; 1.856 + currentView.load([query], options); 1.857 + } 1.858 + else { 1.859 + currentView.applyFilter(filterString, null, true); 1.860 + } 1.861 + break; 1.862 + case "downloads": 1.863 + if (currentView == ContentTree.view) { 1.864 + let query = PlacesUtils.history.getNewQuery(); 1.865 + query.searchTerms = filterString; 1.866 + query.setTransitions([Ci.nsINavHistoryService.TRANSITION_DOWNLOAD], 1); 1.867 + let options = currentOptions.clone(); 1.868 + // Make sure we're getting uri results. 1.869 + options.resultType = currentOptions.RESULTS_AS_URI; 1.870 + options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY; 1.871 + options.includeHidden = true; 1.872 + currentView.load([query], options); 1.873 + } 1.874 + else { 1.875 + // The new downloads view doesn't use places for searching downloads. 1.876 + currentView.searchTerm = filterString; 1.877 + } 1.878 + break; 1.879 + default: 1.880 + throw "Invalid filterCollection on search"; 1.881 + } 1.882 + 1.883 + // Update the details panel 1.884 + PlacesOrganizer.updateDetailsPane(); 1.885 + }, 1.886 + 1.887 + /** 1.888 + * Finds across all history, downloads or all bookmarks. 1.889 + */ 1.890 + findAll: function PSB_findAll() { 1.891 + switch (this.filterCollection) { 1.892 + case "history": 1.893 + PlacesQueryBuilder.setScope("history"); 1.894 + break; 1.895 + case "downloads": 1.896 + PlacesQueryBuilder.setScope("downloads"); 1.897 + break; 1.898 + default: 1.899 + PlacesQueryBuilder.setScope("bookmarks"); 1.900 + break; 1.901 + } 1.902 + this.focus(); 1.903 + }, 1.904 + 1.905 + /** 1.906 + * Updates the display with the title of the current collection. 1.907 + * @param aTitle 1.908 + * The title of the current collection. 1.909 + */ 1.910 + updateCollectionTitle: function PSB_updateCollectionTitle(aTitle) { 1.911 + let title = ""; 1.912 + switch (this.filterCollection) { 1.913 + case "history": 1.914 + title = PlacesUIUtils.getString("searchHistory"); 1.915 + break; 1.916 + case "downloads": 1.917 + title = PlacesUIUtils.getString("searchDownloads"); 1.918 + break; 1.919 + default: 1.920 + title = PlacesUIUtils.getString("searchBookmarks"); 1.921 + } 1.922 + this.searchFilter.placeholder = title; 1.923 + }, 1.924 + 1.925 + /** 1.926 + * Gets/sets the active collection from the dropdown menu. 1.927 + */ 1.928 + get filterCollection() { 1.929 + return this.searchFilter.getAttribute("collection"); 1.930 + }, 1.931 + set filterCollection(collectionName) { 1.932 + if (collectionName == this.filterCollection) 1.933 + return collectionName; 1.934 + 1.935 + this.searchFilter.setAttribute("collection", collectionName); 1.936 + this.updateCollectionTitle(); 1.937 + 1.938 + return collectionName; 1.939 + }, 1.940 + 1.941 + /** 1.942 + * Focus the search box 1.943 + */ 1.944 + focus: function PSB_focus() { 1.945 + this.searchFilter.focus(); 1.946 + }, 1.947 + 1.948 + /** 1.949 + * Set up the gray text in the search bar as the Places View loads. 1.950 + */ 1.951 + init: function PSB_init() { 1.952 + this.updateCollectionTitle(); 1.953 + }, 1.954 + 1.955 + /** 1.956 + * Gets or sets the text shown in the Places Search Box 1.957 + */ 1.958 + get value() { 1.959 + return this.searchFilter.value; 1.960 + }, 1.961 + set value(value) { 1.962 + return this.searchFilter.value = value; 1.963 + }, 1.964 +}; 1.965 + 1.966 +/** 1.967 + * Functions and data for advanced query builder 1.968 + */ 1.969 +var PlacesQueryBuilder = { 1.970 + 1.971 + queries: [], 1.972 + queryOptions: null, 1.973 + 1.974 + /** 1.975 + * Sets the search scope. This can be called when no search is active, and 1.976 + * in that case, when the user does begin a search aScope will be used (see 1.977 + * PSB_search()). If there is an active search, it's performed again to 1.978 + * update the content tree. 1.979 + * @param aScope 1.980 + * The search scope: "bookmarks", "collection", "downloads" or 1.981 + * "history". 1.982 + */ 1.983 + setScope: function PQB_setScope(aScope) { 1.984 + // Determine filterCollection, folders, and scopeButtonId based on aScope. 1.985 + var filterCollection; 1.986 + var folders = []; 1.987 + switch (aScope) { 1.988 + case "history": 1.989 + filterCollection = "history"; 1.990 + break; 1.991 + case "bookmarks": 1.992 + filterCollection = "bookmarks"; 1.993 + folders.push(PlacesUtils.bookmarksMenuFolderId, 1.994 + PlacesUtils.toolbarFolderId, 1.995 + PlacesUtils.unfiledBookmarksFolderId); 1.996 + break; 1.997 + case "downloads": 1.998 + filterCollection = "downloads"; 1.999 + break; 1.1000 + default: 1.1001 + throw "Invalid search scope"; 1.1002 + break; 1.1003 + } 1.1004 + 1.1005 + // Update the search box. Re-search if there's an active search. 1.1006 + PlacesSearchBox.filterCollection = filterCollection; 1.1007 + PlacesSearchBox.folders = folders; 1.1008 + var searchStr = PlacesSearchBox.searchFilter.value; 1.1009 + if (searchStr) 1.1010 + PlacesSearchBox.search(searchStr); 1.1011 + } 1.1012 +}; 1.1013 + 1.1014 +/** 1.1015 + * Population and commands for the View Menu. 1.1016 + */ 1.1017 +var ViewMenu = { 1.1018 + /** 1.1019 + * Removes content generated previously from a menupopup. 1.1020 + * @param popup 1.1021 + * The popup that contains the previously generated content. 1.1022 + * @param startID 1.1023 + * The id attribute of an element that is the start of the 1.1024 + * dynamically generated region - remove elements after this 1.1025 + * item only. 1.1026 + * Must be contained by popup. Can be null (in which case the 1.1027 + * contents of popup are removed). 1.1028 + * @param endID 1.1029 + * The id attribute of an element that is the end of the 1.1030 + * dynamically generated region - remove elements up to this 1.1031 + * item only. 1.1032 + * Must be contained by popup. Can be null (in which case all 1.1033 + * items until the end of the popup will be removed). Ignored 1.1034 + * if startID is null. 1.1035 + * @returns The element for the caller to insert new items before, 1.1036 + * null if the caller should just append to the popup. 1.1037 + */ 1.1038 + _clean: function VM__clean(popup, startID, endID) { 1.1039 + if (endID) 1.1040 + NS_ASSERT(startID, "meaningless to have valid endID and null startID"); 1.1041 + if (startID) { 1.1042 + var startElement = document.getElementById(startID); 1.1043 + NS_ASSERT(startElement.parentNode == 1.1044 + popup, "startElement is not in popup"); 1.1045 + NS_ASSERT(startElement, 1.1046 + "startID does not correspond to an existing element"); 1.1047 + var endElement = null; 1.1048 + if (endID) { 1.1049 + endElement = document.getElementById(endID); 1.1050 + NS_ASSERT(endElement.parentNode == popup, 1.1051 + "endElement is not in popup"); 1.1052 + NS_ASSERT(endElement, 1.1053 + "endID does not correspond to an existing element"); 1.1054 + } 1.1055 + while (startElement.nextSibling != endElement) 1.1056 + popup.removeChild(startElement.nextSibling); 1.1057 + return endElement; 1.1058 + } 1.1059 + else { 1.1060 + while(popup.hasChildNodes()) 1.1061 + popup.removeChild(popup.firstChild); 1.1062 + } 1.1063 + return null; 1.1064 + }, 1.1065 + 1.1066 + /** 1.1067 + * Fills a menupopup with a list of columns 1.1068 + * @param event 1.1069 + * The popupshowing event that invoked this function. 1.1070 + * @param startID 1.1071 + * see _clean 1.1072 + * @param endID 1.1073 + * see _clean 1.1074 + * @param type 1.1075 + * the type of the menuitem, e.g. "radio" or "checkbox". 1.1076 + * Can be null (no-type). 1.1077 + * Checkboxes are checked if the column is visible. 1.1078 + * @param propertyPrefix 1.1079 + * If propertyPrefix is non-null: 1.1080 + * propertyPrefix + column ID + ".label" will be used to get the 1.1081 + * localized label string. 1.1082 + * propertyPrefix + column ID + ".accesskey" will be used to get the 1.1083 + * localized accesskey. 1.1084 + * If propertyPrefix is null, the column label is used as label and 1.1085 + * no accesskey is assigned. 1.1086 + */ 1.1087 + fillWithColumns: function VM_fillWithColumns(event, startID, endID, type, propertyPrefix) { 1.1088 + var popup = event.target; 1.1089 + var pivot = this._clean(popup, startID, endID); 1.1090 + 1.1091 + // If no column is "sort-active", the "Unsorted" item needs to be checked, 1.1092 + // so track whether or not we find a column that is sort-active. 1.1093 + var isSorted = false; 1.1094 + var content = document.getElementById("placeContent"); 1.1095 + var columns = content.columns; 1.1096 + for (var i = 0; i < columns.count; ++i) { 1.1097 + var column = columns.getColumnAt(i).element; 1.1098 + var menuitem = document.createElement("menuitem"); 1.1099 + menuitem.id = "menucol_" + column.id; 1.1100 + menuitem.column = column; 1.1101 + var label = column.getAttribute("label"); 1.1102 + if (propertyPrefix) { 1.1103 + var menuitemPrefix = propertyPrefix; 1.1104 + // for string properties, use "name" as the id, instead of "title" 1.1105 + // see bug #386287 for details 1.1106 + var columnId = column.getAttribute("anonid"); 1.1107 + menuitemPrefix += columnId == "title" ? "name" : columnId; 1.1108 + label = PlacesUIUtils.getString(menuitemPrefix + ".label"); 1.1109 + var accesskey = PlacesUIUtils.getString(menuitemPrefix + ".accesskey"); 1.1110 + menuitem.setAttribute("accesskey", accesskey); 1.1111 + } 1.1112 + menuitem.setAttribute("label", label); 1.1113 + if (type == "radio") { 1.1114 + menuitem.setAttribute("type", "radio"); 1.1115 + menuitem.setAttribute("name", "columns"); 1.1116 + // This column is the sort key. Its item is checked. 1.1117 + if (column.getAttribute("sortDirection") != "") { 1.1118 + menuitem.setAttribute("checked", "true"); 1.1119 + isSorted = true; 1.1120 + } 1.1121 + } 1.1122 + else if (type == "checkbox") { 1.1123 + menuitem.setAttribute("type", "checkbox"); 1.1124 + // Cannot uncheck the primary column. 1.1125 + if (column.getAttribute("primary") == "true") 1.1126 + menuitem.setAttribute("disabled", "true"); 1.1127 + // Items for visible columns are checked. 1.1128 + if (!column.hidden) 1.1129 + menuitem.setAttribute("checked", "true"); 1.1130 + } 1.1131 + if (pivot) 1.1132 + popup.insertBefore(menuitem, pivot); 1.1133 + else 1.1134 + popup.appendChild(menuitem); 1.1135 + } 1.1136 + event.stopPropagation(); 1.1137 + }, 1.1138 + 1.1139 + /** 1.1140 + * Set up the content of the view menu. 1.1141 + */ 1.1142 + populateSortMenu: function VM_populateSortMenu(event) { 1.1143 + this.fillWithColumns(event, "viewUnsorted", "directionSeparator", "radio", "view.sortBy.1."); 1.1144 + 1.1145 + var sortColumn = this._getSortColumn(); 1.1146 + var viewSortAscending = document.getElementById("viewSortAscending"); 1.1147 + var viewSortDescending = document.getElementById("viewSortDescending"); 1.1148 + // We need to remove an existing checked attribute because the unsorted 1.1149 + // menu item is not rebuilt every time we open the menu like the others. 1.1150 + var viewUnsorted = document.getElementById("viewUnsorted"); 1.1151 + if (!sortColumn) { 1.1152 + viewSortAscending.removeAttribute("checked"); 1.1153 + viewSortDescending.removeAttribute("checked"); 1.1154 + viewUnsorted.setAttribute("checked", "true"); 1.1155 + } 1.1156 + else if (sortColumn.getAttribute("sortDirection") == "ascending") { 1.1157 + viewSortAscending.setAttribute("checked", "true"); 1.1158 + viewSortDescending.removeAttribute("checked"); 1.1159 + viewUnsorted.removeAttribute("checked"); 1.1160 + } 1.1161 + else if (sortColumn.getAttribute("sortDirection") == "descending") { 1.1162 + viewSortDescending.setAttribute("checked", "true"); 1.1163 + viewSortAscending.removeAttribute("checked"); 1.1164 + viewUnsorted.removeAttribute("checked"); 1.1165 + } 1.1166 + }, 1.1167 + 1.1168 + /** 1.1169 + * Shows/Hides a tree column. 1.1170 + * @param element 1.1171 + * The menuitem element for the column 1.1172 + */ 1.1173 + showHideColumn: function VM_showHideColumn(element) { 1.1174 + var column = element.column; 1.1175 + 1.1176 + var splitter = column.nextSibling; 1.1177 + if (splitter && splitter.localName != "splitter") 1.1178 + splitter = null; 1.1179 + 1.1180 + if (element.getAttribute("checked") == "true") { 1.1181 + column.setAttribute("hidden", "false"); 1.1182 + if (splitter) 1.1183 + splitter.removeAttribute("hidden"); 1.1184 + } 1.1185 + else { 1.1186 + column.setAttribute("hidden", "true"); 1.1187 + if (splitter) 1.1188 + splitter.setAttribute("hidden", "true"); 1.1189 + } 1.1190 + }, 1.1191 + 1.1192 + /** 1.1193 + * Gets the last column that was sorted. 1.1194 + * @returns the currently sorted column, null if there is no sorted column. 1.1195 + */ 1.1196 + _getSortColumn: function VM__getSortColumn() { 1.1197 + var content = document.getElementById("placeContent"); 1.1198 + var cols = content.columns; 1.1199 + for (var i = 0; i < cols.count; ++i) { 1.1200 + var column = cols.getColumnAt(i).element; 1.1201 + var sortDirection = column.getAttribute("sortDirection"); 1.1202 + if (sortDirection == "ascending" || sortDirection == "descending") 1.1203 + return column; 1.1204 + } 1.1205 + return null; 1.1206 + }, 1.1207 + 1.1208 + /** 1.1209 + * Sorts the view by the specified column. 1.1210 + * @param aColumn 1.1211 + * The colum that is the sort key. Can be null - the 1.1212 + * current sort column or the title column will be used. 1.1213 + * @param aDirection 1.1214 + * The direction to sort - "ascending" or "descending". 1.1215 + * Can be null - the last direction or descending will be used. 1.1216 + * 1.1217 + * If both aColumnID and aDirection are null, the view will be unsorted. 1.1218 + */ 1.1219 + setSortColumn: function VM_setSortColumn(aColumn, aDirection) { 1.1220 + var result = document.getElementById("placeContent").result; 1.1221 + if (!aColumn && !aDirection) { 1.1222 + result.sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_NONE; 1.1223 + return; 1.1224 + } 1.1225 + 1.1226 + var columnId; 1.1227 + if (aColumn) { 1.1228 + columnId = aColumn.getAttribute("anonid"); 1.1229 + if (!aDirection) { 1.1230 + var sortColumn = this._getSortColumn(); 1.1231 + if (sortColumn) 1.1232 + aDirection = sortColumn.getAttribute("sortDirection"); 1.1233 + } 1.1234 + } 1.1235 + else { 1.1236 + var sortColumn = this._getSortColumn(); 1.1237 + columnId = sortColumn ? sortColumn.getAttribute("anonid") : "title"; 1.1238 + } 1.1239 + 1.1240 + // This maps the possible values of columnId (i.e., anonid's of treecols in 1.1241 + // placeContent) to the default sortingMode and sortingAnnotation values for 1.1242 + // each column. 1.1243 + // key: Sort key in the name of one of the 1.1244 + // nsINavHistoryQueryOptions.SORT_BY_* constants 1.1245 + // dir: Default sort direction to use if none has been specified 1.1246 + // anno: The annotation to sort by, if key is "ANNOTATION" 1.1247 + var colLookupTable = { 1.1248 + title: { key: "TITLE", dir: "ascending" }, 1.1249 + tags: { key: "TAGS", dir: "ascending" }, 1.1250 + url: { key: "URI", dir: "ascending" }, 1.1251 + date: { key: "DATE", dir: "descending" }, 1.1252 + visitCount: { key: "VISITCOUNT", dir: "descending" }, 1.1253 + keyword: { key: "KEYWORD", dir: "ascending" }, 1.1254 + dateAdded: { key: "DATEADDED", dir: "descending" }, 1.1255 + lastModified: { key: "LASTMODIFIED", dir: "descending" }, 1.1256 + description: { key: "ANNOTATION", 1.1257 + dir: "ascending", 1.1258 + anno: PlacesUIUtils.DESCRIPTION_ANNO } 1.1259 + }; 1.1260 + 1.1261 + // Make sure we have a valid column. 1.1262 + if (!colLookupTable.hasOwnProperty(columnId)) 1.1263 + throw("Invalid column"); 1.1264 + 1.1265 + // Use a default sort direction if none has been specified. If aDirection 1.1266 + // is invalid, result.sortingMode will be undefined, which has the effect 1.1267 + // of unsorting the tree. 1.1268 + aDirection = (aDirection || colLookupTable[columnId].dir).toUpperCase(); 1.1269 + 1.1270 + var sortConst = "SORT_BY_" + colLookupTable[columnId].key + "_" + aDirection; 1.1271 + result.sortingAnnotation = colLookupTable[columnId].anno || ""; 1.1272 + result.sortingMode = Ci.nsINavHistoryQueryOptions[sortConst]; 1.1273 + } 1.1274 +} 1.1275 + 1.1276 +let ContentArea = { 1.1277 + _specialViews: new Map(), 1.1278 + 1.1279 + init: function CA_init() { 1.1280 + this._deck = document.getElementById("placesViewsDeck"); 1.1281 + this._toolbar = document.getElementById("placesToolbar"); 1.1282 + ContentTree.init(); 1.1283 + this._setupView(); 1.1284 + }, 1.1285 + 1.1286 + /** 1.1287 + * Gets the content view to be used for loading the given query. 1.1288 + * If a custom view was set by setContentViewForQueryString, that 1.1289 + * view would be returned, else the default tree view is returned 1.1290 + * 1.1291 + * @param aQueryString 1.1292 + * a query string 1.1293 + * @return the view to be used for loading aQueryString. 1.1294 + */ 1.1295 + getContentViewForQueryString: 1.1296 + function CA_getContentViewForQueryString(aQueryString) { 1.1297 + try { 1.1298 + if (this._specialViews.has(aQueryString)) { 1.1299 + let { view, options } = this._specialViews.get(aQueryString); 1.1300 + if (typeof view == "function") { 1.1301 + view = view(); 1.1302 + this._specialViews.set(aQueryString, { view: view, options: options }); 1.1303 + } 1.1304 + return view; 1.1305 + } 1.1306 + } 1.1307 + catch(ex) { 1.1308 + Components.utils.reportError(ex); 1.1309 + } 1.1310 + return ContentTree.view; 1.1311 + }, 1.1312 + 1.1313 + /** 1.1314 + * Sets a custom view to be used rather than the default places tree 1.1315 + * whenever the given query is selected in the left pane. 1.1316 + * @param aQueryString 1.1317 + * a query string 1.1318 + * @param aView 1.1319 + * Either the custom view or a function that will return the view 1.1320 + * the first (and only) time it's called. 1.1321 + * @param [optional] aOptions 1.1322 + * Object defining special options for the view. 1.1323 + * @see ContentTree.viewOptions for supported options and default values. 1.1324 + */ 1.1325 + setContentViewForQueryString: 1.1326 + function CA_setContentViewForQueryString(aQueryString, aView, aOptions) { 1.1327 + if (!aQueryString || 1.1328 + typeof aView != "object" && typeof aView != "function") 1.1329 + throw new Error("Invalid arguments"); 1.1330 + 1.1331 + this._specialViews.set(aQueryString, { view: aView, 1.1332 + options: aOptions || new Object() }); 1.1333 + }, 1.1334 + 1.1335 + get currentView() PlacesUIUtils.getViewForNode(this._deck.selectedPanel), 1.1336 + set currentView(aNewView) { 1.1337 + let oldView = this.currentView; 1.1338 + if (oldView != aNewView) { 1.1339 + this._deck.selectedPanel = aNewView.associatedElement; 1.1340 + 1.1341 + // If the content area inactivated view was focused, move focus 1.1342 + // to the new view. 1.1343 + if (document.activeElement == oldView.associatedElement) 1.1344 + aNewView.associatedElement.focus(); 1.1345 + } 1.1346 + return aNewView; 1.1347 + }, 1.1348 + 1.1349 + get currentPlace() this.currentView.place, 1.1350 + set currentPlace(aQueryString) { 1.1351 + let oldView = this.currentView; 1.1352 + let newView = this.getContentViewForQueryString(aQueryString); 1.1353 + newView.place = aQueryString; 1.1354 + if (oldView != newView) { 1.1355 + oldView.active = false; 1.1356 + this.currentView = newView; 1.1357 + this._setupView(); 1.1358 + newView.active = true; 1.1359 + } 1.1360 + return aQueryString; 1.1361 + }, 1.1362 + 1.1363 + /** 1.1364 + * Applies view options. 1.1365 + */ 1.1366 + _setupView: function CA__setupView() { 1.1367 + let options = this.currentViewOptions; 1.1368 + 1.1369 + // showDetailsPane. 1.1370 + let detailsDeck = document.getElementById("detailsDeck"); 1.1371 + detailsDeck.hidden = !options.showDetailsPane; 1.1372 + 1.1373 + // toolbarSet. 1.1374 + for (let elt of this._toolbar.childNodes) { 1.1375 + // On Windows and Linux the menu buttons are menus wrapped in a menubar. 1.1376 + if (elt.id == "placesMenu") { 1.1377 + for (let menuElt of elt.childNodes) { 1.1378 + menuElt.hidden = options.toolbarSet.indexOf(menuElt.id) == -1; 1.1379 + } 1.1380 + } 1.1381 + else { 1.1382 + elt.hidden = options.toolbarSet.indexOf(elt.id) == -1; 1.1383 + } 1.1384 + } 1.1385 + }, 1.1386 + 1.1387 + /** 1.1388 + * Options for the current view. 1.1389 + * 1.1390 + * @see ContentTree.viewOptions for supported options and default values. 1.1391 + */ 1.1392 + get currentViewOptions() { 1.1393 + // Use ContentTree options as default. 1.1394 + let viewOptions = ContentTree.viewOptions; 1.1395 + if (this._specialViews.has(this.currentPlace)) { 1.1396 + let { view, options } = this._specialViews.get(this.currentPlace); 1.1397 + for (let option in options) { 1.1398 + viewOptions[option] = options[option]; 1.1399 + } 1.1400 + } 1.1401 + return viewOptions; 1.1402 + }, 1.1403 + 1.1404 + focus: function() { 1.1405 + this._deck.selectedPanel.focus(); 1.1406 + } 1.1407 +}; 1.1408 + 1.1409 +let ContentTree = { 1.1410 + init: function CT_init() { 1.1411 + this._view = document.getElementById("placeContent"); 1.1412 + }, 1.1413 + 1.1414 + get view() this._view, 1.1415 + 1.1416 + get viewOptions() Object.seal({ 1.1417 + showDetailsPane: true, 1.1418 + toolbarSet: "back-button, forward-button, organizeButton, viewMenu, maintenanceButton, libraryToolbarSpacer, searchFilter" 1.1419 + }), 1.1420 + 1.1421 + openSelectedNode: function CT_openSelectedNode(aEvent) { 1.1422 + let view = this.view; 1.1423 + PlacesUIUtils.openNodeWithEvent(view.selectedNode, aEvent, view); 1.1424 + }, 1.1425 + 1.1426 + onClick: function CT_onClick(aEvent) { 1.1427 + let node = this.view.selectedNode; 1.1428 + if (node) { 1.1429 + let doubleClick = aEvent.button == 0 && aEvent.detail == 2; 1.1430 + let middleClick = aEvent.button == 1 && aEvent.detail == 1; 1.1431 + if (PlacesUtils.nodeIsURI(node) && (doubleClick || middleClick)) { 1.1432 + // Open associated uri in the browser. 1.1433 + this.openSelectedNode(aEvent); 1.1434 + } 1.1435 + else if (middleClick && PlacesUtils.nodeIsContainer(node)) { 1.1436 + // The command execution function will take care of seeing if the 1.1437 + // selection is a folder or a different container type, and will 1.1438 + // load its contents in tabs. 1.1439 + PlacesUIUtils.openContainerNodeInTabs(node, aEvent, this.view); 1.1440 + } 1.1441 + } 1.1442 + }, 1.1443 + 1.1444 + onKeyPress: function CT_onKeyPress(aEvent) { 1.1445 + if (aEvent.keyCode == KeyEvent.DOM_VK_RETURN) 1.1446 + this.openSelectedNode(aEvent); 1.1447 + } 1.1448 +};