places.js
Go to the documentation of this file.
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* ***** BEGIN LICENSE BLOCK *****
3  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
4  *
5  * The contents of this file are subject to the Mozilla Public License Version
6  * 1.1 (the "License"); you may not use this file except in compliance with
7  * the License. You may obtain a copy of the License at
8  * http://www.mozilla.org/MPL/
9  *
10  * Software distributed under the License is distributed on an "AS IS" basis,
11  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
12  * for the specific language governing rights and limitations under the
13  * License.
14  *
15  * The Original Code is Mozilla Places Organizer.
16  *
17  * The Initial Developer of the Original Code is Google Inc.
18  * Portions created by the Initial Developer are Copyright (C) 2005-2006
19  * the Initial Developer. All Rights Reserved.
20  *
21  * Contributor(s):
22  * Ben Goodger <beng@google.com>
23  * Annie Sullivan <annie.sullivan@gmail.com>
24  * Asaf Romano <mano@mozilla.com>
25  * Ehsan Akhgari <ehsan.akhgari@gmail.com>
26  * Drew Willcoxon <adw@mozilla.com>
27  *
28  * Alternatively, the contents of this file may be used under the terms of
29  * either the GNU General Public License Version 2 or later (the "GPL"), or
30  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
31  * in which case the provisions of the GPL or the LGPL are applicable instead
32  * of those above. If you wish to allow use of your version of this file only
33  * under the terms of either the GPL or the LGPL, and not to allow others to
34  * use your version of this file under the terms of the MPL, indicate your
35  * decision by deleting the provisions above and replace them with the notice
36  * and other provisions required by the GPL or the LGPL. If you do not delete
37  * the provisions above, a recipient may use your version of this file under
38  * the terms of any one of the MPL, the GPL or the LGPL.
39  *
40  * ***** END LICENSE BLOCK ***** */
41 
43  _places: null,
44  _content: null,
45 
46  // IDs of fields from editBookmarkOverlay that should be hidden when infoBox
47  // is minimal. IDs should be kept in sync with the IDs of the elements
48  // observing additionalInfoBroadcaster.
49  _additionalInfoFields: [
50  "editBMPanel_descriptionRow",
51  "editBMPanel_loadInSidebarCheckbox",
52  "editBMPanel_keywordRow",
53  ],
54 
55  _initFolderTree: function() {
56  var leftPaneRoot = PlacesUIUtils.leftPaneFolderId;
57  this._places.place = "place:excludeItems=1&expandQueries=0&folder=" + leftPaneRoot;
58  },
59 
60  selectLeftPaneQuery: function PO_selectLeftPaneQuery(aQueryName) {
61  var itemId = PlacesUIUtils.leftPaneQueries[aQueryName];
62  this._places.selectItems([itemId]);
63  // Forcefully expand all-bookmarks
64  if (aQueryName == "AllBookmarks")
65  asContainer(this._places.selectedNode).containerOpen = true;
66  },
67 
68  init: function PO_init() {
69  this._places = document.getElementById("placesList");
70  this._content = document.getElementById("placeContent");
71  this._initFolderTree();
72 
73  var leftPaneSelection = "AllBookmarks"; // default to all-bookmarks
74  if ("arguments" in window && window.arguments.length > 0)
75  leftPaneSelection = window.arguments[0];
76 
77  this.selectLeftPaneQuery(leftPaneSelection);
78  // clear the back-stack
79  this._backHistory.splice(0);
80  document.getElementById("OrganizerCommand:Back").setAttribute("disabled", true);
81 
82  var view = this._content.treeBoxObject.view;
83  if (view.rowCount > 0)
84  view.selection.select(0);
85 
86  this._content.focus();
87 
88  // Set up the search UI.
89  PlacesSearchBox.init();
90 
91 #ifdef PLACES_QUERY_BUILDER
92  // Set up the advanced query builder UI
93  PlacesQueryBuilder.init();
94 #endif
95 
96  window.addEventListener("AppCommand", this, true);
97 #ifdef XP_MACOSX
98  // 1. Map Edit->Find command to OrganizerCommand_find:all. Need to map
99  // both the menuitem and the Find key.
100  var findMenuItem = document.getElementById("menu_find");
101  findMenuItem.setAttribute("command", "OrganizerCommand_find:all");
102  var findKey = document.getElementById("key_find");
103  findKey.setAttribute("command", "OrganizerCommand_find:all");
104 
105  // 2. Disable some keybindings from browser.xul
106  var elements = ["cmd_handleBackspace", "cmd_handleShiftBackspace"];
107  for (var i=0; i < elements.length; i++) {
108  document.getElementById(elements[i]).setAttribute("disabled", "true");
109  }
110 
111  // 3. Disable the keyboard shortcut for the History menu back/forward
112  // in order to support those in the Library
113  var historyMenuBack = document.getElementById("historyMenuBack");
114  historyMenuBack.removeAttribute("key");
115  var historyMenuForward = document.getElementById("historyMenuForward");
116  historyMenuForward.removeAttribute("key");
117 #endif
118 
119  // remove the "Properties" context-menu item, we've our own details pane
120  document.getElementById("placesContext")
121  .removeChild(document.getElementById("placesContext_show:info"));
122  },
123 
124  QueryInterface: function PO_QueryInterface(aIID) {
125  if (aIID.equals(Components.interfaces.nsIDOMEventListener) ||
126  aIID.equals(Components.interfaces.nsISupports))
127  return this;
128 
129  throw Components.results.NS_NOINTERFACE;
130  },
131 
132  handleEvent: function PO_handleEvent(aEvent) {
133  if (aEvent.type != "AppCommand")
134  return;
135 
136  aEvent.stopPropagation();
137  switch (aEvent.command) {
138  case "Back":
139  if (this._backHistory.length > 0)
140  this.back();
141  break;
142  case "Forward":
143  if (this._forwardHistory.length > 0)
144  this.forward();
145  break;
146  case "Search":
147  PlacesSearchBox.findAll();
148  break;
149  }
150  },
151 
152  destroy: function PO_destroy() {
153  },
154 
155  _location: null,
156  get location() {
157  return this._location;
158  },
159 
160  set location(aLocation) {
161  if (!aLocation || this._location == aLocation)
162  return aLocation;
163 
164  if (this.location) {
165  this._backHistory.unshift(this.location);
166  this._forwardHistory.splice(0);
167  }
168 
169  this._location = aLocation;
170  this._places.selectPlaceURI(aLocation);
171 
172  if (!this._places.hasSelection) {
173  // If no node was found for the given place: uri, just load it directly
174  this._content.place = aLocation;
175  }
176  this.onContentTreeSelect();
177 
178  // update navigation commands
179  if (this._backHistory.length == 0)
180  document.getElementById("OrganizerCommand:Back").setAttribute("disabled", true);
181  else
182  document.getElementById("OrganizerCommand:Back").removeAttribute("disabled");
183  if (this._forwardHistory.length == 0)
184  document.getElementById("OrganizerCommand:Forward").setAttribute("disabled", true);
185  else
186  document.getElementById("OrganizerCommand:Forward").removeAttribute("disabled");
187 
188  return aLocation;
189  },
190 
191  _backHistory: [],
192  _forwardHistory: [],
193 
194  back: function PO_back() {
195  this._forwardHistory.unshift(this.location);
196  var historyEntry = this._backHistory.shift();
197  this._location = null;
198  this.location = historyEntry;
199  },
200  forward: function PO_forward() {
201  this._backHistory.unshift(this.location);
202  var historyEntry = this._forwardHistory.shift();
203  this._location = null;
204  this.location = historyEntry;
205  },
206 
217  _cachedLeftPaneSelectedURI: null,
218  onPlaceSelected: function PO_onPlaceSelected(resetSearchBox) {
219  // Don't change the right-hand pane contents when there's no selection.
220  if (!this._places.hasSelection)
221  return;
222 
223  var node = this._places.selectedNode;
224  var queries = asQuery(node).getQueries({});
225 
226  // Items are only excluded on the left pane.
227  var options = node.queryOptions.clone();
228  options.excludeItems = false;
229  var placeURI = PlacesUtils.history.queriesToQueryString(queries,
230  queries.length,
231  options);
232 
233  // If either the place of the content tree in the right pane has changed or
234  // the user cleared the search box, update the place, hide the search UI,
235  // and update the back/forward buttons by setting location.
236  if (this._content.place != placeURI || !resetSearchBox) {
237  this._content.place = placeURI;
238  PlacesSearchBox.hideSearchUI();
239  this.location = node.uri;
240  }
241 
242  // Update the selected folder title where it appears in the UI: the folder
243  // scope button, "Find in <current collection>" command, and the search box
244  // emptytext. They must be updated even if the selection hasn't changed --
245  // specifically when node's title changes. In that case a selection event
246  // is generated, this method is called, but the selection does not change.
247  var folderButton = document.getElementById("scopeBarFolder");
248  var folderTitle = node.title || folderButton.getAttribute("emptytitle");
249  folderButton.setAttribute("label", folderTitle);
250  var cmd = document.getElementById("OrganizerCommand_find:current");
251  var label = PlacesUIUtils.getFormattedString("findInPrefix", [folderTitle]);
252  cmd.setAttribute("label", label);
253  if (PlacesSearchBox.filterCollection == "collection")
254  PlacesSearchBox.updateCollectionTitle(folderTitle);
255 
256  // When we invalidate a container we use suppressSelectionEvent, when it is
257  // unset a select event is fired, in many cases the selection did not really
258  // change, so we should check for it, and return early in such a case. Note
259  // that we cannot return any earlier than this point, because when
260  // !resetSearchBox, we need to update location and hide the UI as above,
261  // even though the selection has not changed.
262  if (node.uri == this._cachedLeftPaneSelectedURI)
263  return;
264  this._cachedLeftPaneSelectedURI = node.uri;
265 
266  // At this point, resetSearchBox is true, because the left pane selection
267  // has changed; otherwise we would have returned earlier.
268 
269  PlacesSearchBox.searchFilter.reset();
270  this._setSearchScopeForNode(node);
271  if (this._places.treeBoxObject.focused)
272  this._fillDetailsPane([node]);
273  },
274 
280  _setSearchScopeForNode: function PO__setScopeForNode(aNode) {
281  var itemId = aNode.itemId;
282  if (PlacesUtils.nodeIsHistoryContainer(aNode) ||
283  itemId == PlacesUIUtils.leftPaneQueries["History"]) {
284  PlacesQueryBuilder.setScope("history");
285  }
286  // Default to All Bookmarks for all other nodes, per bug 469437.
287  else
288  PlacesQueryBuilder.setScope("bookmarks");
289 
290  // Enable or disable the folder scope button.
291  var folderButton = document.getElementById("scopeBarFolder");
292  folderButton.hidden = !PlacesUtils.nodeIsFolder(aNode) ||
293  itemId == PlacesUIUtils.allBookmarksFolderId;
294  },
295 
303  onTreeClick: function PO_onTreeClick(aEvent) {
304  // Only handle clicks on tree children.
305  if (aEvent.target.localName != "treechildren")
306  return;
307 
308  var currentView = aEvent.currentTarget;
309  var selectedNode = currentView.selectedNode;
310  if (selectedNode) {
311  var doubleClickOnFlatList = (aEvent.button == 0 && aEvent.detail == 2 &&
312  aEvent.target.parentNode.flatList);
313  var middleClick = (aEvent.button == 1 && aEvent.detail == 1);
314 
315  if (PlacesUtils.nodeIsURI(selectedNode) &&
316  (doubleClickOnFlatList || middleClick)) {
317  // Open associated uri in the browser.
318  PlacesOrganizer.openSelectedNode(aEvent);
319  }
320  else if (middleClick &&
321  PlacesUtils.nodeIsContainer(selectedNode)) {
322  // The command execution function will take care of seeing if the
323  // selection is a folder or a different container type, and will
324  // load its contents in tabs.
325  PlacesUIUtils.openContainerNodeInTabs(selectedNode, aEvent);
326  }
327  }
328  },
329 
336  onTreeFocus: function PO_onTreeFocus(aEvent) {
337  var currentView = aEvent.currentTarget;
338  var selectedNodes = currentView.selectedNode ? [currentView.selectedNode] :
339  this._content.getSelectionNodes();
340  this._fillDetailsPane(selectedNodes);
341  },
342 
343  openFlatContainer: function PO_openFlatContainerFlatContainer(aContainer) {
344  if (aContainer.itemId != -1)
345  this._places.selectItems([aContainer.itemId]);
346  else if (PlacesUtils.nodeIsQuery(aContainer))
347  this._places.selectPlaceURI(aContainer.uri);
348  },
349 
350  openSelectedNode: function PO_openSelectedNode(aEvent) {
351  PlacesUIUtils.openNodeWithEvent(this._content.selectedNode, aEvent);
352  },
353 
358  getCurrentOptions: function PO_getCurrentOptions() {
359  return asQuery(this._content.getResult().root).queryOptions;
360  },
361 
366  getCurrentQueries: function PO_getCurrentQueries() {
367  return asQuery(this._content.getResult().root).getQueries({});
368  },
369 
373  importBookmarks: function PO_import() {
374  // XXX: ifdef it to be non-modal (non-"sheet") on mac (see bug 259039)
375  var features = "modal,centerscreen,chrome,resizable=no";
376 
377  // The migrator window will set this to true when it closes, if the user
378  // chose to migrate from a specific file.
379  window.fromFile = false;
380  openDialog("chrome://browser/content/migration/migration.xul",
381  "migration", features, "bookmarks");
382  if (window.fromFile)
383  this.importFromFile();
384  },
385 
389  importFromFile: function PO_importFromFile() {
390  var fp = Cc["@mozilla.org/filepicker;1"].
391  createInstance(Ci.nsIFilePicker);
392  fp.init(window, PlacesUIUtils.getString("SelectImport"),
393  Ci.nsIFilePicker.modeOpen);
394  fp.appendFilters(Ci.nsIFilePicker.filterHTML);
395  if (fp.show() != Ci.nsIFilePicker.returnCancel) {
396  if (fp.file) {
397  var importer = Cc["@mozilla.org/browser/places/import-export-service;1"].
398  getService(Ci.nsIPlacesImportExportService);
399  var file = fp.file.QueryInterface(Ci.nsILocalFile);
400  importer.importHTMLFromFile(file, false);
401  }
402  }
403  },
404 
408  exportBookmarks: function PO_exportBookmarks() {
409  var fp = Cc["@mozilla.org/filepicker;1"].
410  createInstance(Ci.nsIFilePicker);
411  fp.init(window, PlacesUIUtils.getString("EnterExport"),
412  Ci.nsIFilePicker.modeSave);
413  fp.appendFilters(Ci.nsIFilePicker.filterHTML);
414  fp.defaultString = "bookmarks.html";
415  if (fp.show() != Ci.nsIFilePicker.returnCancel) {
416  var exporter = Cc["@mozilla.org/browser/places/import-export-service;1"].
417  getService(Ci.nsIPlacesImportExportService);
418  exporter.exportHTMLToFile(fp.file);
419  }
420  },
421 
425  populateRestoreMenu: function PO_populateRestoreMenu() {
426  var restorePopup = document.getElementById("fileRestorePopup");
427 
428  var dateSvc = Cc["@mozilla.org/intl/scriptabledateformat;1"].
429  getService(Ci.nsIScriptableDateFormat);
430 
431  // remove existing menu items
432  // last item is the restoreFromFile item
433  while (restorePopup.childNodes.length > 1)
434  restorePopup.removeChild(restorePopup.firstChild);
435 
436  // get list of files
437  var localizedFilename = PlacesUtils.getString("bookmarksArchiveFilename");
438  var localizedFilenamePrefix = localizedFilename.substr(0, localizedFilename.indexOf("-"));
439  var fileList = [];
440  var files = this.bookmarksBackupDir.directoryEntries;
441  while (files.hasMoreElements()) {
442  var f = files.getNext().QueryInterface(Ci.nsIFile);
443  var rx = new RegExp("^(bookmarks|" + localizedFilenamePrefix +
444  ")-([0-9]{4}-[0-9]{2}-[0-9]{2})\.json$");
445  if (!f.isHidden() && f.leafName.match(rx)) {
446  var date = f.leafName.match(rx)[2].replace(/-/g, "/");
447  var dateObj = new Date(date);
448  fileList.push({date: dateObj, filename: f.leafName});
449  }
450  }
451 
452  fileList.sort(function PO_fileList_compare(a, b) {
453  return b.date - a.date;
454  });
455 
456  if (fileList.length == 0)
457  return;
458 
459  // populate menu
460  for (var i = 0; i < fileList.length; i++) {
461  var m = restorePopup.insertBefore
462  (document.createElement("menuitem"),
463  document.getElementById("restoreFromFile"));
464  m.setAttribute("label",
465  dateSvc.FormatDate("",
466  Ci.nsIScriptableDateFormat.dateFormatLong,
467  fileList[i].date.getFullYear(),
468  fileList[i].date.getMonth() + 1,
469  fileList[i].date.getDate()));
470  m.setAttribute("value", fileList[i].filename);
471  m.setAttribute("oncommand",
472  "PlacesOrganizer.onRestoreMenuItemClick(this);");
473  }
474  restorePopup.insertBefore(document.createElement("menuseparator"),
475  document.getElementById("restoreFromFile"));
476  },
477 
481  onRestoreMenuItemClick: function PO_onRestoreMenuItemClick(aMenuItem) {
482  var dirSvc = Cc["@mozilla.org/file/directory_service;1"].
483  getService(Ci.nsIProperties);
484  var bookmarksFile = dirSvc.get("ProfD", Ci.nsIFile);
485  bookmarksFile.append("bookmarkbackups");
486  bookmarksFile.append(aMenuItem.getAttribute("value"));
487  if (!bookmarksFile.exists())
488  return;
489  this.restoreBookmarksFromFile(bookmarksFile);
490  },
491 
496  onRestoreBookmarksFromFile: function PO_onRestoreBookmarksFromFile() {
497  var fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
498  fp.init(window, PlacesUIUtils.getString("bookmarksRestoreTitle"),
499  Ci.nsIFilePicker.modeOpen);
500  fp.appendFilter(PlacesUIUtils.getString("bookmarksRestoreFilterName"),
501  PlacesUIUtils.getString("bookmarksRestoreFilterExtension"));
502  fp.appendFilters(Ci.nsIFilePicker.filterAll);
503 
504  var dirSvc = Cc["@mozilla.org/file/directory_service;1"].
505  getService(Ci.nsIProperties);
506  var backupsDir = dirSvc.get("Desk", Ci.nsILocalFile);
507  fp.displayDirectory = backupsDir;
508 
509  if (fp.show() != Ci.nsIFilePicker.returnCancel)
510  this.restoreBookmarksFromFile(fp.file);
511  },
512 
516  restoreBookmarksFromFile: function PO_restoreBookmarksFromFile(aFile) {
517  // check file extension
518  if (!aFile.leafName.match(/\.json$/)) {
519  this._showErrorAlert(PlacesUIUtils.getString("bookmarksRestoreFormatError"));
520  return;
521  }
522 
523  // confirm ok to delete existing bookmarks
524  var prompts = Cc["@mozilla.org/embedcomp/prompt-service;1"].
525  getService(Ci.nsIPromptService);
526  if (!prompts.confirm(null,
527  PlacesUIUtils.getString("bookmarksRestoreAlertTitle"),
528  PlacesUIUtils.getString("bookmarksRestoreAlert")))
529  return;
530 
531  try {
532  PlacesUtils.restoreBookmarksFromJSONFile(aFile);
533  }
534  catch(ex) {
535  this._showErrorAlert(PlacesUIUtils.getString("bookmarksRestoreParseError"));
536  }
537  },
538 
539  _showErrorAlert: function PO__showErrorAlert(aMsg) {
540  var brandShortName = document.getElementById("brandStrings").
541  getString("brandShortName");
542 
543  Cc["@mozilla.org/embedcomp/prompt-service;1"].
544  getService(Ci.nsIPromptService).
545  alert(window, brandShortName, aMsg);
546  },
547 
553  backupBookmarks: function PO_backupBookmarks() {
554  var fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
555  fp.init(window, PlacesUIUtils.getString("bookmarksBackupTitle"),
556  Ci.nsIFilePicker.modeSave);
557  fp.appendFilter(PlacesUIUtils.getString("bookmarksRestoreFilterName"),
558  PlacesUIUtils.getString("bookmarksRestoreFilterExtension"));
559 
560  var dirSvc = Cc["@mozilla.org/file/directory_service;1"].
561  getService(Ci.nsIProperties);
562  var backupsDir = dirSvc.get("Desk", Ci.nsILocalFile);
563  fp.displayDirectory = backupsDir;
564 
565  fp.defaultString = PlacesUtils.getBackupFilename();
566 
567  if (fp.show() != Ci.nsIFilePicker.returnCancel) {
568  PlacesUtils.backupBookmarksToFile(fp.file);
569 
570  // copy new backup to /backups dir (bug 424389)
571  var latestBackup = PlacesUtils.getMostRecentBackup();
572  if (!latestBackup || latestBackup != fp.file) {
573  latestBackup.remove(false);
574  var name = PlacesUtils.getBackupFilename();
575  fp.file.copyTo(this.bookmarksBackupDir, name);
576  }
577  }
578  },
579 
580  get bookmarksBackupDir() {
581  delete this.bookmarksBackupDir;
582  var dirSvc = Cc["@mozilla.org/file/directory_service;1"].
583  getService(Ci.nsIProperties);
584  var bookmarksBackupDir = dirSvc.get("ProfD", Ci.nsIFile);
585  bookmarksBackupDir.append("bookmarkbackups");
586  if (!bookmarksBackupDir.exists())
587  bookmarksBackupDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0700);
588  return this.bookmarksBackupDir = bookmarksBackupDir;
589  },
590 
591  _paneDisabled: false,
592  _setDetailsFieldsDisabledState:
593  function PO__setDetailsFieldsDisabledState(aDisabled) {
594  if (aDisabled) {
595  document.getElementById("paneElementsBroadcaster")
596  .setAttribute("disabled", "true");
597  }
598  else {
599  document.getElementById("paneElementsBroadcaster")
600  .removeAttribute("disabled");
601  }
602  },
603 
604  _detectAndSetDetailsPaneMinimalState:
605  function PO__detectAndSetDetailsPaneMinimalState(aNode) {
614  var infoBox = document.getElementById("infoBox");
615  var infoBoxExpander = document.getElementById("infoBoxExpander");
616  var infoBoxExpanderWrapper = document.getElementById("infoBoxExpanderWrapper");
617  var additionalInfoBroadcaster = document.getElementById("additionalInfoBroadcaster");
618 
619  if (!aNode) {
620  infoBoxExpanderWrapper.hidden = true;
621  return;
622  }
623  if (aNode.itemId != -1 &&
624  ((PlacesUtils.nodeIsFolder(aNode) &&
625  !PlacesUtils.nodeIsLivemarkContainer(aNode)) ||
626  PlacesUtils.nodeIsLivemarkItem(aNode))) {
627  if (infoBox.getAttribute("minimal") == "true")
628  infoBox.setAttribute("wasminimal", "true");
629  infoBox.removeAttribute("minimal");
630  infoBoxExpanderWrapper.hidden = true;
631  }
632  else {
633  if (infoBox.getAttribute("wasminimal") == "true")
634  infoBox.setAttribute("minimal", "true");
635  infoBox.removeAttribute("wasminimal");
636  infoBoxExpanderWrapper.hidden =
637  this._additionalInfoFields.every(function (id)
638  document.getElementById(id).collapsed);
639  }
640  additionalInfoBroadcaster.hidden = infoBox.getAttribute("minimal") == "true";
641  },
642 
643  // NOT YET USED
644  updateThumbnailProportions: function PO_updateThumbnailProportions() {
645  var previewBox = document.getElementById("previewBox");
646  var canvas = document.getElementById("itemThumbnail");
647  var height = previewBox.boxObject.height;
648  var width = height * (screen.width / screen.height);
649  canvas.width = width;
650  canvas.height = height;
651  },
652 
653  onContentTreeSelect: function PO_onContentTreeSelect() {
654  if (this._content.treeBoxObject.focused)
655  this._fillDetailsPane(this._content.getSelectionNodes());
656  },
657 
658  _fillDetailsPane: function PO__fillDetailsPane(aNodeList) {
659  var infoBox = document.getElementById("infoBox");
660  var detailsDeck = document.getElementById("detailsDeck");
661 
662  // Make sure the infoBox UI is visible if we need to use it, we hide it
663  // below when we don't.
664  infoBox.hidden = false;
665  var aSelectedNode = aNodeList.length == 1 ? aNodeList[0] : null;
666  // If a textbox within a panel is focused, force-blur it so its contents
667  // are saved
668  if (gEditItemOverlay.itemId != -1) {
669  var focusedElement = document.commandDispatcher.focusedElement;
670  if ((focusedElement instanceof HTMLInputElement ||
671  focusedElement instanceof HTMLTextAreaElement) &&
672  /^editBMPanel.*/.test(focusedElement.parentNode.parentNode.id))
673  focusedElement.blur();
674 
675  // don't update the panel if we are already editing this node unless we're
676  // in multi-edit mode
677  if (aSelectedNode) {
678  var concreteId = PlacesUtils.getConcreteItemId(aSelectedNode);
679  var nodeIsSame = gEditItemOverlay.itemId == aSelectedNode.itemId ||
680  gEditItemOverlay.itemId == concreteId ||
681  (aSelectedNode.itemId == -1 && gEditItemOverlay.uri &&
682  gEditItemOverlay.uri == aSelectedNode.uri);
683  if (nodeIsSame && detailsDeck.selectedIndex == 1 &&
684  !gEditItemOverlay.multiEdit)
685  return;
686  }
687  }
688 
689  // Clean up the panel before initing it again.
690  gEditItemOverlay.uninitPanel(false);
691 
692  if (aSelectedNode && !PlacesUtils.nodeIsSeparator(aSelectedNode)) {
693  detailsDeck.selectedIndex = 1;
694  // Using the concrete itemId is arguably wrong. The bookmarks API
695  // does allow setting properties for folder shortcuts as well, but since
696  // the UI does not distinct between the couple, we better just show
697  // the concrete item properties for shortcuts to root nodes.
698  var concreteId = PlacesUtils.getConcreteItemId(aSelectedNode);
699  var isRootItem = concreteId != -1 && PlacesUtils.isRootItem(concreteId);
700  var readOnly = isRootItem ||
701  aSelectedNode.parent.itemId == PlacesUIUtils.leftPaneFolderId;
702  var useConcreteId = isRootItem ||
703  PlacesUtils.nodeIsTagQuery(aSelectedNode);
704  var itemId = -1;
705  if (concreteId != -1 && useConcreteId)
706  itemId = concreteId;
707  else if (aSelectedNode.itemId != -1)
708  itemId = aSelectedNode.itemId;
709  else
710  itemId = PlacesUtils._uri(aSelectedNode.uri);
711 
712  gEditItemOverlay.initPanel(itemId, { hiddenRows: ["folderPicker"],
713  forceReadOnly: readOnly });
714 
715  // Dynamically generated queries, like history date containers, have
716  // itemId !=0 and do not exist in history. For them the panel is
717  // read-only, but empty, since it can't get a valid title for the object.
718  // In such a case we force the title using the selectedNode one, for UI
719  // polishness.
720  if (aSelectedNode.itemId == -1 &&
721  (PlacesUtils.nodeIsDay(aSelectedNode) ||
722  PlacesUtils.nodeIsHost(aSelectedNode)))
723  gEditItemOverlay._element("namePicker").value = aSelectedNode.title;
724 
725  this._detectAndSetDetailsPaneMinimalState(aSelectedNode);
726  }
727  else if (!aSelectedNode && aNodeList[0]) {
728  var itemIds = [];
729  for (var i = 0; i < aNodeList.length; i++) {
730  if (!PlacesUtils.nodeIsBookmark(aNodeList[i]) &&
731  !PlacesUtils.nodeIsURI(aNodeList[i])) {
732  detailsDeck.selectedIndex = 0;
733  var selectItemDesc = document.getElementById("selectItemDescription");
734  var itemsCountLabel = document.getElementById("itemsCountText");
735  selectItemDesc.hidden = false;
736  itemsCountLabel.value =
737  PlacesUIUtils.getFormattedString("detailsPane.multipleItems",
738  [aNodeList.length]);
739  infoBox.hidden = true;
740  return;
741  }
742  itemIds[i] = aNodeList[i].itemId != -1 ? aNodeList[i].itemId :
743  PlacesUtils._uri(aNodeList[i].uri);
744  }
745  detailsDeck.selectedIndex = 1;
746  gEditItemOverlay.initPanel(itemIds,
747  { hiddenRows: ["folderPicker",
748  "loadInSidebar",
749  "location",
750  "keyword",
751  "description",
752  "name"]});
753  this._detectAndSetDetailsPaneMinimalState(aSelectedNode);
754  }
755  else {
756  detailsDeck.selectedIndex = 0;
757  infoBox.hidden = true;
758  var selectItemDesc = document.getElementById("selectItemDescription");
759  var itemsCountLabel = document.getElementById("itemsCountText");
760  var rowCount = this._content.treeBoxObject.view.rowCount;
761  if (rowCount == 0) {
762  selectItemDesc.hidden = true;
763  itemsCountLabel.value = PlacesUIUtils.getString("detailsPane.noItems");
764  }
765  else {
766  selectItemDesc.hidden = false;
767  if (rowCount == 1)
768  itemsCountLabel.value = PlacesUIUtils.getString("detailsPane.oneItem");
769  else {
770  itemsCountLabel.value =
771  PlacesUIUtils.getFormattedString("detailsPane.multipleItems",
772  [rowCount]);
773  }
774  }
775  }
776  },
777 
778  // NOT YET USED
779  _updateThumbnail: function PO__updateThumbnail() {
780  var bo = document.getElementById("previewBox").boxObject;
781  var width = bo.width;
782  var height = bo.height;
783 
784  var canvas = document.getElementById("itemThumbnail");
785  var ctx = canvas.getContext('2d');
786  var notAvailableText = canvas.getAttribute("notavailabletext");
787  ctx.save();
788  ctx.fillStyle = "-moz-Dialog";
789  ctx.fillRect(0, 0, width, height);
790  ctx.translate(width/2, height/2);
791 
792  ctx.fillStyle = "GrayText";
793  ctx.mozTextStyle = "12pt sans serif";
794  var len = ctx.mozMeasureText(notAvailableText);
795  ctx.translate(-len/2,0);
796  ctx.mozDrawText(notAvailableText);
797  ctx.restore();
798  },
799 
800  toggleAdditionalInfoFields: function PO_toggleAdditionalInfoFields() {
801  var infoBox = document.getElementById("infoBox");
802  var infoBoxExpander = document.getElementById("infoBoxExpander");
803  var infoBoxExpanderLabel = document.getElementById("infoBoxExpanderLabel");
804  var additionalInfoBroadcaster = document.getElementById("additionalInfoBroadcaster");
805 
806  if (infoBox.getAttribute("minimal") == "true") {
807  infoBox.removeAttribute("minimal");
808  infoBoxExpanderLabel.value = infoBoxExpanderLabel.getAttribute("lesslabel");
809  infoBoxExpanderLabel.accessKey = infoBoxExpanderLabel.getAttribute("lessaccesskey");
810  infoBoxExpander.className = "expander-up";
811  additionalInfoBroadcaster.removeAttribute("hidden");
812  }
813  else {
814  infoBox.setAttribute("minimal", "true");
815  infoBoxExpanderLabel.value = infoBoxExpanderLabel.getAttribute("morelabel");
816  infoBoxExpanderLabel.accessKey = infoBoxExpanderLabel.getAttribute("moreaccesskey");
817  infoBoxExpander.className = "expander-down";
818  additionalInfoBroadcaster.setAttribute("hidden", "true");
819  }
820  },
821 
825  saveSearch: function PO_saveSearch() {
826  // Get the place: uri for the query.
827  // If the advanced query builder is showing, use that.
828  var options = this.getCurrentOptions();
829 
830 #ifdef PLACES_QUERY_BUILDER
831  var queries = PlacesQueryBuilder.queries;
832 #else
833  var queries = this.getCurrentQueries();
834 #endif
835 
836  var placeSpec = PlacesUtils.history.queriesToQueryString(queries,
837  queries.length,
838  options);
839  var placeURI = Cc["@mozilla.org/network/io-service;1"].
840  getService(Ci.nsIIOService).
841  newURI(placeSpec, null, null);
842 
843  // Prompt the user for a name for the query.
844  // XXX - using prompt service for now; will need to make
845  // a real dialog and localize when we're sure this is the UI we want.
846  var title = PlacesUIUtils.getString("saveSearch.title");
847  var inputLabel = PlacesUIUtils.getString("saveSearch.inputLabel");
848  var defaultText = PlacesUIUtils.getString("saveSearch.inputDefaultText");
849 
850  var prompts = Cc["@mozilla.org/embedcomp/prompt-service;1"].
851  getService(Ci.nsIPromptService);
852  var check = {value: false};
853  var input = {value: defaultText};
854  var save = prompts.prompt(null, title, inputLabel, input, null, check);
855 
856  // Don't add the query if the user cancels or clears the seach name.
857  if (!save || input.value == "")
858  return;
859 
860  // Add the place: uri as a bookmark under the bookmarks root.
861  var txn = PlacesUIUtils.ptm.createItem(placeURI,
862  PlacesUtils.bookmarksMenuFolderId,
863  PlacesUtils.bookmarks.DEFAULT_INDEX,
864  input.value);
865  PlacesUIUtils.ptm.doTransaction(txn);
866 
867  // select and load the new query
868  this._places.selectPlaceURI(placeSpec);
869  }
870 };
871 
876 
880  get searchFilter() {
881  return document.getElementById("searchFilter");
882  },
883 
887  _folders: [],
888  get folders() {
889  if (this._folders.length == 0)
890  this._folders.push(PlacesUtils.bookmarksMenuFolderId,
891  PlacesUtils.unfiledBookmarksFolderId,
892  PlacesUtils.toolbarFolderId);
893  return this._folders;
894  },
895  set folders(aFolders) {
896  this._folders = aFolders;
897  return aFolders;
898  },
899 
907  search: function PSB_search(filterString) {
908  var PO = PlacesOrganizer;
909  // If the user empties the search box manually, reset it and load all
910  // contents of the current scope.
911  // XXX this might be to jumpy, maybe should search for "", so results
912  // are ungrouped, and search box not reset
913  if (filterString == "") {
914  PO.onPlaceSelected(false);
915  return;
916  }
917 
918  var currentOptions = PO.getCurrentOptions();
919  var content = PO._content;
920 
921  // Search according to the current scope and folders, which were set by
922  // PQB_setScope()
923  switch (PlacesSearchBox.filterCollection) {
924  case "collection":
925  content.applyFilter(filterString, this.folders);
926  // XXX changing the button text is badness
927  //var scopeBtn = document.getElementById("scopeBarFolder");
928  //scopeBtn.label = PlacesOrganizer._places.selectedNode.title;
929  break;
930  case "bookmarks":
931  content.applyFilter(filterString, this.folders);
932  break;
933  case "history":
934  if (currentOptions.queryType != Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) {
935  var query = PlacesUtils.history.getNewQuery();
936  query.searchTerms = filterString;
937  var options = currentOptions.clone();
938  // Make sure we're getting uri results.
939  options.resultType = currentOptions.RESULT_TYPE_URI;
940  options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY;
941  content.load([query], options);
942  }
943  else
944  content.applyFilter(filterString);
945  break;
946  default:
947  throw "Invalid filterCollection on search";
948  break;
949  }
950 
951  PlacesSearchBox.showSearchUI();
952 
953  // Update the details panel
954  PlacesOrganizer.onContentTreeSelect();
955  },
956 
960  findAll: function PSB_findAll() {
961  PlacesQueryBuilder.setScope("bookmarks");
962  this.focus();
963  },
964 
968  findCurrent: function PSB_findCurrent() {
969  PlacesQueryBuilder.setScope("collection");
970  this.focus();
971  },
972 
978  updateCollectionTitle: function PSB_updateCollectionTitle(title) {
979  if (title)
980  this.searchFilter.emptyText =
981  PlacesUIUtils.getFormattedString("searchCurrentDefault", [title]);
982  else
983  this.searchFilter.emptyText = this.filterCollection == "history" ?
984  PlacesUIUtils.getString("searchHistory") :
985  PlacesUIUtils.getString("searchBookmarks");
986  },
987 
991  get filterCollection() {
992  return this.searchFilter.getAttribute("collection");
993  },
994  set filterCollection(collectionName) {
995  if (collectionName == this.filterCollection)
996  return collectionName;
997 
998  this.searchFilter.setAttribute("collection", collectionName);
999 
1000  var newGrayText = null;
1001  if (collectionName == "collection") {
1002  newGrayText = PlacesOrganizer._places.selectedNode.title ||
1003  document.getElementById("scopeBarFolder").
1004  getAttribute("emptytitle");
1005  }
1006  this.updateCollectionTitle(newGrayText);
1007  return collectionName;
1008  },
1009 
1013  focus: function PSB_focus() {
1014  this.searchFilter.focus();
1015  },
1016 
1020  init: function PSB_init() {
1021  this.updateCollectionTitle();
1022  },
1023 
1027  get value() {
1028  return this.searchFilter.value;
1029  },
1030  set value(value) {
1031  return this.searchFilter.value = value;
1032  },
1033 
1034  showSearchUI: function PSB_showSearchUI() {
1035  // Hide the advanced search controls when the user hasn't searched
1036  var searchModifiers = document.getElementById("searchModifiers");
1037  searchModifiers.hidden = false;
1038 
1039 #ifdef PLACES_QUERY_BUILDER
1040  // if new search, open builder with pre-populated text row
1041  if (PlacesQueryBuilder.numRows == 0)
1042  document.getElementById("OrganizerCommand_search:moreCriteria").doCommand();
1043 #endif
1044  },
1045 
1046  hideSearchUI: function PSB_hideSearchUI() {
1047  var searchModifiers = document.getElementById("searchModifiers");
1048  searchModifiers.hidden = true;
1049  }
1050 };
1051 
1056 
1057  queries: [],
1058  queryOptions: null,
1059 
1060 #ifdef PLACES_QUERY_BUILDER
1061  numRows: 0,
1062 
1066  _maxRows: null,
1067 
1068  _keywordSearch: {
1069  advancedSearch_N_Subject: "advancedSearch_N_SubjectKeyword",
1070  advancedSearch_N_LocationMenulist: false,
1071  advancedSearch_N_TimeMenulist: false,
1072  advancedSearch_N_Textbox: "",
1073  advancedSearch_N_TimePicker: false,
1074  advancedSearch_N_TimeMenulist2: false
1075  },
1076  _locationSearch: {
1077  advancedSearch_N_Subject: "advancedSearch_N_SubjectLocation",
1078  advancedSearch_N_LocationMenulist: "advancedSearch_N_LocationMenuSelected",
1079  advancedSearch_N_TimeMenulist: false,
1080  advancedSearch_N_Textbox: "",
1081  advancedSearch_N_TimePicker: false,
1082  advancedSearch_N_TimeMenulist2: false
1083  },
1084  _timeSearch: {
1085  advancedSearch_N_Subject: "advancedSearch_N_SubjectVisited",
1086  advancedSearch_N_LocationMenulist: false,
1087  advancedSearch_N_TimeMenulist: true,
1088  advancedSearch_N_Textbox: false,
1089  advancedSearch_N_TimePicker: "date",
1090  advancedSearch_N_TimeMenulist2: false
1091  },
1092  _timeInLastSearch: {
1093  advancedSearch_N_Subject: "advancedSearch_N_SubjectVisited",
1094  advancedSearch_N_LocationMenulist: false,
1095  advancedSearch_N_TimeMenulist: true,
1096  advancedSearch_N_Textbox: "7",
1097  advancedSearch_N_TimePicker: false,
1098  advancedSearch_N_TimeMenulist2: true
1099  },
1100  _nextSearch: null,
1101  _queryBuilders: null,
1102 
1103 
1104  init: function PQB_init() {
1105  // Initialize advanced search
1106  this._nextSearch = {
1107  "keyword": this._timeSearch,
1108  "visited": this._locationSearch,
1109  "location": null
1110  };
1111 
1112  this._queryBuilders = {
1113  "keyword": this.setKeywordQuery,
1114  "visited": this.setVisitedQuery,
1115  "location": this.setLocationQuery
1116  };
1117 
1118  this._maxRows = this._queryBuilders.length;
1119 
1120  this._dateService = Cc["@mozilla.org/intl/scriptabledateformat;1"].
1121  getService(Ci.nsIScriptableDateFormat);
1122  },
1123 
1127  hide: function PQB_hide() {
1128  var advancedSearch = document.getElementById("advancedSearch");
1129  // Need to collapse the advanced search box.
1130  advancedSearch.collapsed = true;
1131  },
1132 
1136  show: function PQB_show() {
1137  var advancedSearch = document.getElementById("advancedSearch");
1138  advancedSearch.collapsed = false;
1139  },
1140 
1141  toggleVisibility: function ABP_toggleVisibility() {
1142  var expander = document.getElementById("organizerScopeBarExpander");
1143  var advancedSearch = document.getElementById("advancedSearch");
1144  if (advancedSearch.collapsed) {
1145  advancedSearch.collapsed = false;
1146  expander.className = "expander-down";
1147  expander.setAttribute("tooltiptext",
1148  expander.getAttribute("tooltiptextdown"));
1149  }
1150  else {
1151  advancedSearch.collapsed = true;
1152  expander.className = "expander-up"
1153  expander.setAttribute("tooltiptext",
1154  expander.getAttribute("tooltiptextup"));
1155  }
1156  },
1157 
1166  _setRowId: function PQB__setRowId(element, rowId) {
1167  if (element.id)
1168  element.id = element.id.replace("advancedSearch0", "advancedSearch" + rowId);
1169  if (element.hasAttribute("rowid"))
1170  element.setAttribute("rowid", rowId);
1171  for (var i = 0; i < element.childNodes.length; ++i) {
1172  this._setRowId(element.childNodes[i], rowId);
1173  }
1174  },
1175 
1176  _updateUIForRowChange: function PQB__updateUIForRowChange() {
1177  // Update the "can add more criteria" command to make sure various +
1178  // buttons are disabled.
1179  var command = document.getElementById("OrganizerCommand_search:moreCriteria");
1180  if (this.numRows >= this._maxRows)
1181  command.setAttribute("disabled", "true");
1182  else
1183  command.removeAttribute("disabled");
1184  },
1185 
1190  addRow: function PQB_addRow() {
1191  // Limits the number of rows that can be added based on the maximum number
1192  // of search query subjects.
1193  if (this.numRows >= this._maxRows)
1194  return;
1195 
1196  // Clone the template row and unset the hidden attribute.
1197  var gridRows = document.getElementById("advancedSearchRows");
1198  var newRow = gridRows.firstChild.cloneNode(true);
1199  newRow.hidden = false;
1200 
1201  // Determine what the search type is based on the last visible row. If this
1202  // is the first row, the type is "keyword search". Otherwise, it's the next
1203  // in the sequence after the one defined by the previous visible row's
1204  // Subject selector, as defined in _nextSearch.
1205  var searchType = this._keywordSearch;
1206  var lastMenu = document.getElementById("advancedSearch" +
1207  this.numRows +
1208  "Subject");
1209  if (this.numRows > 0 && lastMenu && lastMenu.selectedItem)
1210  searchType = this._nextSearch[lastMenu.selectedItem.value];
1211 
1212  // There is no "next" search type. We are here in error.
1213  if (!searchType)
1214  return;
1215  // We don't insert into the document until _after_ the searchType is
1216  // determined, since this will interfere with the computation.
1217  gridRows.appendChild(newRow);
1218  this._setRowId(newRow, ++this.numRows);
1219 
1220  // Ensure the Advanced Search container is visible, if this is the first
1221  // row being added.
1222  if (this.numRows == 1) {
1223  this.show();
1224 
1225  // Pre-fill the search terms field with the value from the one on the
1226  // toolbar.
1227  // For some reason, setting.value here synchronously does not appear to
1228  // work.
1229  var searchTermsField = document.getElementById("advancedSearch1Textbox");
1230  if (searchTermsField)
1231  setTimeout(function() { searchTermsField.value = PlacesSearchBox.value; }, 10);
1232  this.queries = PlacesOrganizer.getCurrentQueries();
1233  return;
1234  }
1235 
1236  this.showSearch(this.numRows, searchType);
1237  this._updateUIForRowChange();
1238  },
1239 
1246  removeRow: function PQB_removeRow(row) {
1247  if (!row)
1248  row = document.getElementById("advancedSearch" + this.numRows + "Row");
1249  row.parentNode.removeChild(row);
1250  --this.numRows;
1251 
1252  if (this.numRows < 1) {
1253  this.hide();
1254 
1255  // Re-do the original toolbar-search-box search that the user used to
1256  // spawn the advanced UI... this effectively "reverts" the UI to the
1257  // point it was in before they began monkeying with advanced search.
1258  PlacesSearchBox.search(PlacesSearchBox.value);
1259  return;
1260  }
1261 
1262  this.doSearch();
1263  this._updateUIForRowChange();
1264  },
1265 
1266  onDateTyped: function PQB_onDateTyped(event, row) {
1267  var textbox = document.getElementById("advancedSearch" + row + "TimePicker");
1268  var dateString = textbox.value;
1269  var dateArr = dateString.split("-");
1270  // The date can be split into a range by the '-' character, i.e.
1271  // 9/5/05 - 10/2/05. Unfortunately, dates can also be written like
1272  // 9-5-05 - 10-2-05. Try to parse the date based on how many hyphens
1273  // there are.
1274  var d0 = null;
1275  var d1 = null;
1276  // If there are an even number of elements in the date array, try to
1277  // parse it as a range of two dates.
1278  if ((dateArr.length & 1) == 0) {
1279  var mid = dateArr.length / 2;
1280  var dateStr0 = dateArr[0];
1281  var dateStr1 = dateArr[mid];
1282  for (var i = 1; i < mid; ++i) {
1283  dateStr0 += "-" + dateArr[i];
1284  dateStr1 += "-" + dateArr[i + mid];
1285  }
1286  d0 = new Date(dateStr0);
1287  d1 = new Date(dateStr1);
1288  }
1289  // If that didn't work, try to parse it as a single date.
1290  if (d0 == null || d0 == "Invalid Date") {
1291  d0 = new Date(dateString);
1292  }
1293 
1294  if (d0 != null && d0 != "Invalid Date") {
1295  // Parsing succeeded -- update the calendar.
1296  var calendar = document.getElementById("advancedSearch" + row + "Calendar");
1297  if (d0.getFullYear() < 2000)
1298  d0.setFullYear(2000 + (d0.getFullYear() % 100));
1299  if (d1 != null && d1 != "Invalid Date") {
1300  if (d1.getFullYear() < 2000)
1301  d1.setFullYear(2000 + (d1.getFullYear() % 100));
1302  calendar.updateSelection(d0, d1);
1303  }
1304  else {
1305  calendar.updateSelection(d0, d0);
1306  }
1307 
1308  // And update the search.
1309  this.doSearch();
1310  }
1311  },
1312 
1313  onCalendarChanged: function PQB_onCalendarChanged(event, row) {
1314  var calendar = document.getElementById("advancedSearch" + row + "Calendar");
1315  var begin = calendar.beginrange;
1316  var end = calendar.endrange;
1317 
1318  // If the calendar doesn't have a begin/end, don't change the textbox.
1319  if (begin == null || end == null)
1320  return true;
1321 
1322  // If the begin and end are the same day, only fill that into the textbox.
1323  var textbox = document.getElementById("advancedSearch" + row + "TimePicker");
1324  var beginDate = begin.getDate();
1325  var beginMonth = begin.getMonth() + 1;
1326  var beginYear = begin.getFullYear();
1327  var endDate = end.getDate();
1328  var endMonth = end.getMonth() + 1;
1329  var endYear = end.getFullYear();
1330  if (beginDate == endDate && beginMonth == endMonth && beginYear == endYear) {
1331  // Just one date.
1332  textbox.value = this._dateService.FormatDate("",
1333  this._dateService.dateFormatShort,
1334  beginYear,
1335  beginMonth,
1336  beginDate);
1337  }
1338  else
1339  {
1340  // Two dates.
1341  var beginStr = this._dateService.FormatDate("",
1342  this._dateService.dateFormatShort,
1343  beginYear,
1344  beginMonth,
1345  beginDate);
1346  var endStr = this._dateService.FormatDate("",
1347  this._dateService.dateFormatShort,
1348  endYear,
1349  endMonth,
1350  endDate);
1351  textbox.value = beginStr + " - " + endStr;
1352  }
1353 
1354  // Update the search.
1355  this.doSearch();
1356 
1357  return true;
1358  },
1359 
1360  handleTimePickerClick: function PQB_handleTimePickerClick(event, row) {
1361  var popup = document.getElementById("advancedSearch" + row + "DatePopup");
1362  if (popup.showing)
1363  popup.hidePopup();
1364  else {
1365  var textbox = document.getElementById("advancedSearch" + row + "TimePicker");
1366  popup.showPopup(textbox, -1, -1, "popup", "bottomleft", "topleft");
1367  }
1368  },
1369 
1370  showSearch: function PQB_showSearch(row, values) {
1371  for (val in values) {
1372  var id = val.replace("_N_", row);
1373  var element = document.getElementById(id);
1374  if (values[val] || typeof(values[val]) == "string") {
1375  if (typeof(values[val]) == "string") {
1376  if (values[val] == "date") {
1377  // "date" means that the current date should be filled into the
1378  // textbox, and the calendar for the row updated.
1379  var d = new Date();
1380  element.value = this._dateService.FormatDate("",
1381  this._dateService.dateFormatShort,
1382  d.getFullYear(),
1383  d.getMonth() + 1,
1384  d.getDate());
1385  var calendar = document.getElementById("advancedSearch" + row + "Calendar");
1386  calendar.updateSelection(d, d);
1387  }
1388  else if (element.nodeName == "textbox") {
1389  // values[val] is the initial value of the textbox.
1390  element.value = values[val];
1391  } else {
1392  // values[val] is the menuitem which should be selected.
1393  var itemId = values[val].replace("_N_", row);
1394  var item = document.getElementById(itemId);
1395  element.selectedItem = item;
1396  }
1397  }
1398  element.hidden = false;
1399  }
1400  else {
1401  element.hidden = true;
1402  }
1403  }
1404 
1405  this.doSearch();
1406  },
1407 
1408  setKeywordQuery: function PQB_setKeywordQuery(query, prefix) {
1409  query.searchTerms += document.getElementById(prefix + "Textbox").value + " ";
1410  },
1411 
1412  setLocationQuery: function PQB_setLocationQuery(query, prefix) {
1413  var type = document.getElementById(prefix + "LocationMenulist").selectedItem.value;
1414  if (type == "onsite") {
1415  query.domain = document.getElementById(prefix + "Textbox").value;
1416  }
1417  else {
1418  query.uriIsPrefix = (type == "startswith");
1419  var spec = document.getElementById(prefix + "Textbox").value;
1420  var ios = Cc["@mozilla.org/network/io-service;1"].
1421  getService(Ci.nsIIOService);
1422  try {
1423  query.uri = ios.newURI(spec, null, null);
1424  }
1425  catch (e) {
1426  // Invalid input can cause newURI to barf, that's OK, tack "http://"
1427  // onto the front and try again to see if the user omitted it
1428  try {
1429  query.uri = ios.newURI("http://" + spec, null, null);
1430  }
1431  catch (e) {
1432  // OK, they have entered something which can never match. This should
1433  // not happen.
1434  }
1435  }
1436  }
1437  },
1438 
1439  setVisitedQuery: function PQB_setVisitedQuery(query, prefix) {
1440  var searchType = document.getElementById(prefix + "TimeMenulist").selectedItem.value;
1441  const DAY_MSEC = 86400000;
1442  switch (searchType) {
1443  case "on":
1444  var calendar = document.getElementById(prefix + "Calendar");
1445  var begin = calendar.beginrange.getTime();
1446  var end = calendar.endrange.getTime();
1447  if (begin == end) {
1448  end = begin + DAY_MSEC;
1449  }
1450  query.beginTime = begin * 1000;
1451  query.endTime = end * 1000;
1452  break;
1453  case "before":
1454  var calendar = document.getElementById(prefix + "Calendar");
1455  var time = calendar.beginrange.getTime();
1456  query.endTime = time * 1000;
1457  break;
1458  case "after":
1459  var calendar = document.getElementById(prefix + "Calendar");
1460  var time = calendar.endrange.getTime();
1461  query.beginTime = time * 1000;
1462  break;
1463  case "inLast":
1464  var textbox = document.getElementById(prefix + "Textbox");
1465  var amount = parseInt(textbox.value);
1466  amount = amount * DAY_MSEC;
1467  var menulist = document.getElementById(prefix + "TimeMenulist2");
1468  if (menulist.selectedItem.value == "weeks")
1469  amount = amount * 7;
1470  else if (menulist.selectedItem.value == "months")
1471  amount = amount * 30;
1472  var now = new Date();
1473  now = now - amount;
1474  query.beginTime = now * 1000;
1475  break;
1476  }
1477  },
1478 
1479  doSearch: function PQB_doSearch() {
1480  // Create the individual queries.
1481  var queryType = document.getElementById("advancedSearchType").selectedItem.value;
1482  this.queries = [];
1483  if (queryType == "and")
1484  this.queries.push(PlacesUtils.history.getNewQuery());
1485  var updated = 0;
1486  for (var i = 1; updated < this.numRows; ++i) {
1487  var prefix = "advancedSearch" + i;
1488 
1489  // The user can remove rows from the middle and start of the list, not
1490  // just from the end, so we need to make sure that this row actually
1491  // exists before attempting to construct a query for it.
1492  var querySubjectElement = document.getElementById(prefix + "Subject");
1493  if (querySubjectElement) {
1494  // If the queries are being AND-ed, put all the rows in one query.
1495  // If they're being OR-ed, add a separate query for each row.
1496  var query;
1497  if (queryType == "and")
1498  query = this.queries[0];
1499  else
1500  query = PlacesUtils.history.getNewQuery();
1501 
1502  var querySubject = querySubjectElement.value;
1503  this._queryBuilders[querySubject](query, prefix);
1504 
1505  if (queryType == "or")
1506  this.queries.push(query);
1507 
1508  ++updated;
1509  }
1510  }
1511 
1512  // Make sure we're getting uri results, not visits
1513  this.options = PlacesOrganizer.getCurrentOptions();
1514  this.options.resultType = this.options.RESULT_TYPE_URI;
1515 
1516  // XXXben - find some public way of doing this!
1517  PlacesOrganizer._content.load(this.queries, this.options);
1518 
1519  // Update the details panel
1520  PlacesOrganizer.onContentTreeSelect();
1521  },
1522 #endif
1523 
1529  onScopeSelected: function PQB_onScopeSelected(aButton) {
1530  switch (aButton.id) {
1531  case "scopeBarHistory":
1532  this.setScope("history");
1533  break;
1534  case "scopeBarFolder":
1535  this.setScope("collection");
1536  break;
1537  case "scopeBarAll":
1538  this.setScope("bookmarks");
1539  break;
1540  default:
1541  throw "Invalid search scope button ID";
1542  break;
1543  }
1544  },
1545 
1554  setScope: function PQB_setScope(aScope) {
1555  // Determine filterCollection, folders, and scopeButtonId based on aScope.
1556  var filterCollection;
1557  var folders = [];
1558  var scopeButtonId;
1559  switch (aScope) {
1560  case "history":
1561  filterCollection = "history";
1562  scopeButtonId = "scopeBarHistory";
1563  break;
1564  case "collection":
1565  // The folder scope button can only become hidden upon selecting a new
1566  // folder in the left pane, and the disabled state will remain unchanged
1567  // until a new folder is selected. See PO__setScopeForNode().
1568  if (!document.getElementById("scopeBarFolder").hidden) {
1569  filterCollection = "collection";
1570  scopeButtonId = "scopeBarFolder";
1571  folders.push(PlacesUtils.getConcreteItemId(
1572  PlacesOrganizer._places.selectedNode));
1573  break;
1574  }
1575  // Fall through. If collection scope doesn't make sense for the
1576  // selected node, choose bookmarks scope.
1577  case "bookmarks":
1578  filterCollection = "bookmarks";
1579  scopeButtonId = "scopeBarAll";
1580  folders.push(PlacesUtils.bookmarksMenuFolderId,
1581  PlacesUtils.toolbarFolderId,
1582  PlacesUtils.unfiledBookmarksFolderId);
1583  break;
1584  default:
1585  throw "Invalid search scope";
1586  break;
1587  }
1588 
1589  // Check the appropriate scope button in the scope bar.
1590  document.getElementById(scopeButtonId).checked = true;
1591 
1592  // Update the search box. Re-search if there's an active search.
1593  PlacesSearchBox.filterCollection = filterCollection;
1594  PlacesSearchBox.folders = folders;
1595  var searchStr = PlacesSearchBox.searchFilter.value;
1596  if (searchStr)
1597  PlacesSearchBox.search(searchStr);
1598  }
1599 };
1600 
1604 var ViewMenu = {
1625  _clean: function VM__clean(popup, startID, endID) {
1626  if (endID)
1627  NS_ASSERT(startID, "meaningless to have valid endID and null startID");
1628  if (startID) {
1629  var startElement = document.getElementById(startID);
1630  NS_ASSERT(startElement.parentNode ==
1631  popup, "startElement is not in popup");
1632  NS_ASSERT(startElement,
1633  "startID does not correspond to an existing element");
1634  var endElement = null;
1635  if (endID) {
1636  endElement = document.getElementById(endID);
1637  NS_ASSERT(endElement.parentNode == popup,
1638  "endElement is not in popup");
1639  NS_ASSERT(endElement,
1640  "endID does not correspond to an existing element");
1641  }
1642  while (startElement.nextSibling != endElement)
1643  popup.removeChild(startElement.nextSibling);
1644  return endElement;
1645  }
1646  else {
1647  while(popup.hasChildNodes())
1648  popup.removeChild(popup.firstChild);
1649  }
1650  return null;
1651  },
1652 
1674  fillWithColumns: function VM_fillWithColumns(event, startID, endID, type, propertyPrefix) {
1675  var popup = event.target;
1676  var pivot = this._clean(popup, startID, endID);
1677 
1678  // If no column is "sort-active", the "Unsorted" item needs to be checked,
1679  // so track whether or not we find a column that is sort-active.
1680  var isSorted = false;
1681  var content = document.getElementById("placeContent");
1682  var columns = content.columns;
1683  for (var i = 0; i < columns.count; ++i) {
1684  var column = columns.getColumnAt(i).element;
1685  var menuitem = document.createElement("menuitem");
1686  menuitem.id = "menucol_" + column.id;
1687  menuitem.column = column;
1688  var label = column.getAttribute("label");
1689  if (propertyPrefix) {
1690  var menuitemPrefix = propertyPrefix;
1691  // for string properties, use "name" as the id, instead of "title"
1692  // see bug #386287 for details
1693  var columnId = column.getAttribute("anonid");
1694  menuitemPrefix += columnId == "title" ? "name" : columnId;
1695  label = PlacesUIUtils.getString(menuitemPrefix + ".label");
1696  var accesskey = PlacesUIUtils.getString(menuitemPrefix + ".accesskey");
1697  menuitem.setAttribute("accesskey", accesskey);
1698  }
1699  menuitem.setAttribute("label", label);
1700  if (type == "radio") {
1701  menuitem.setAttribute("type", "radio");
1702  menuitem.setAttribute("name", "columns");
1703  // This column is the sort key. Its item is checked.
1704  if (column.getAttribute("sortDirection") != "") {
1705  menuitem.setAttribute("checked", "true");
1706  isSorted = true;
1707  }
1708  }
1709  else if (type == "checkbox") {
1710  menuitem.setAttribute("type", "checkbox");
1711  // Cannot uncheck the primary column.
1712  if (column.getAttribute("primary") == "true")
1713  menuitem.setAttribute("disabled", "true");
1714  // Items for visible columns are checked.
1715  if (!column.hidden)
1716  menuitem.setAttribute("checked", "true");
1717  }
1718  if (pivot)
1719  popup.insertBefore(menuitem, pivot);
1720  else
1721  popup.appendChild(menuitem);
1722  }
1723  event.stopPropagation();
1724  },
1725 
1729  populateSortMenu: function VM_populateSortMenu(event) {
1730  this.fillWithColumns(event, "viewUnsorted", "directionSeparator", "radio", "view.sortBy.");
1731 
1732  var sortColumn = this._getSortColumn();
1733  var viewSortAscending = document.getElementById("viewSortAscending");
1734  var viewSortDescending = document.getElementById("viewSortDescending");
1735  // We need to remove an existing checked attribute because the unsorted
1736  // menu item is not rebuilt every time we open the menu like the others.
1737  var viewUnsorted = document.getElementById("viewUnsorted");
1738  if (!sortColumn) {
1739  viewSortAscending.removeAttribute("checked");
1740  viewSortDescending.removeAttribute("checked");
1741  viewUnsorted.setAttribute("checked", "true");
1742  }
1743  else if (sortColumn.getAttribute("sortDirection") == "ascending") {
1744  viewSortAscending.setAttribute("checked", "true");
1745  viewSortDescending.removeAttribute("checked");
1746  viewUnsorted.removeAttribute("checked");
1747  }
1748  else if (sortColumn.getAttribute("sortDirection") == "descending") {
1749  viewSortDescending.setAttribute("checked", "true");
1750  viewSortAscending.removeAttribute("checked");
1751  viewUnsorted.removeAttribute("checked");
1752  }
1753  },
1754 
1760  showHideColumn: function VM_showHideColumn(element) {
1761  var column = element.column;
1762 
1763  var splitter = column.nextSibling;
1764  if (splitter && splitter.localName != "splitter")
1765  splitter = null;
1766 
1767  if (element.getAttribute("checked") == "true") {
1768  column.setAttribute("hidden", "false");
1769  if (splitter)
1770  splitter.removeAttribute("hidden");
1771  }
1772  else {
1773  column.setAttribute("hidden", "true");
1774  if (splitter)
1775  splitter.setAttribute("hidden", "true");
1776  }
1777  },
1778 
1783  _getSortColumn: function VM__getSortColumn() {
1784  var content = document.getElementById("placeContent");
1785  var cols = content.columns;
1786  for (var i = 0; i < cols.count; ++i) {
1787  var column = cols.getColumnAt(i).element;
1788  var sortDirection = column.getAttribute("sortDirection");
1789  if (sortDirection == "ascending" || sortDirection == "descending")
1790  return column;
1791  }
1792  return null;
1793  },
1794 
1806  setSortColumn: function VM_setSortColumn(aColumn, aDirection) {
1807  var result = document.getElementById("placeContent").getResult();
1808  if (!aColumn && !aDirection) {
1809  result.sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_NONE;
1810  return;
1811  }
1812 
1813  var columnId;
1814  if (aColumn) {
1815  columnId = aColumn.getAttribute("anonid");
1816  if (!aDirection) {
1817  var sortColumn = this._getSortColumn();
1818  if (sortColumn)
1819  aDirection = sortColumn.getAttribute("sortDirection");
1820  }
1821  }
1822  else {
1823  var sortColumn = this._getSortColumn();
1824  columnId = sortColumn ? sortColumn.getAttribute("anonid") : "title";
1825  }
1826 
1827  // This maps the possible values of columnId (i.e., anonid's of treecols in
1828  // placeContent) to the default sortingMode and sortingAnnotation values for
1829  // each column.
1830  // key: Sort key in the name of one of the
1831  // nsINavHistoryQueryOptions.SORT_BY_* constants
1832  // dir: Default sort direction to use if none has been specified
1833  // anno: The annotation to sort by, if key is "ANNOTATION"
1834  var colLookupTable = {
1835  title: { key: "TITLE", dir: "ascending" },
1836  tags: { key: "TAGS", dir: "ascending" },
1837  url: { key: "URI", dir: "ascending" },
1838  date: { key: "DATE", dir: "descending" },
1839  visitCount: { key: "VISITCOUNT", dir: "descending" },
1840  keyword: { key: "KEYWORD", dir: "ascending" },
1841  dateAdded: { key: "DATEADDED", dir: "descending" },
1842  lastModified: { key: "LASTMODIFIED", dir: "descending" },
1843  description: { key: "ANNOTATION",
1844  dir: "ascending",
1845  anno: DESCRIPTION_ANNO }
1846  };
1847 
1848  // Make sure we have a valid column.
1849  if (!colLookupTable.hasOwnProperty(columnId))
1850  throw("Invalid column");
1851 
1852  // Use a default sort direction if none has been specified. If aDirection
1853  // is invalid, result.sortingMode will be undefined, which has the effect
1854  // of unsorting the tree.
1855  aDirection = (aDirection || colLookupTable[columnId].dir).toUpperCase();
1856 
1857  var sortConst = "SORT_BY_" + colLookupTable[columnId].key + "_" + aDirection;
1858  result.sortingAnnotation = colLookupTable[columnId].anno || "";
1859  result.sortingMode = Ci.nsINavHistoryQueryOptions[sortConst];
1860  }
1861 };
var PlacesUIUtils
Definition: utils.js:85
const Cc
var dirSvc
var gEditItemOverlay
_setDateDatepicker date
#define DESCRIPTION_ANNO
var event
sidebarFactory createInstance
Definition: nsSidebar.js:351
sbOSDControlService prototype QueryInterface
function asQuery(aNode)
Definition: utils.js:83
getService(Ci.sbIFaceplateManager)
function width(ele) rect(ele).width
let window
function addRow(name, type, url)
Definition: feeds.js:84
function d(s)
_window init
Definition: FeedWriter.js:1144
this _contentSandbox label
Definition: FeedWriter.js:814
function search(aFolderId, aSearchStr, aExpectedScopeButtonId)
var d1
this _dialogInput val(dateText)
var PlacesOrganizer
Definition: places.js:42
aWindow setTimeout(function(){_this.restoreHistory(aWindow, aTabs, aTabData, aIdMap);}, 0)
var columns
var ViewMenu
Definition: places.js:1604
var PlacesQueryBuilder
Definition: places.js:1055
return null
Definition: FeedWriter.js:1143
function check(ch, cx)
_updateDatepicker height
function newURI(aURLString)
let node
function NS_ASSERT(cond, msg)
Definition: httpd.js:70
function url(spec)
var uri
Definition: FeedWriter.js:1135
countRef value
Definition: FeedWriter.js:1423
return aWindow document documentElement getAttribute(aAttribute)||dimension
_setDateDatepicker endDate
_setFaviconForWebReader aMenuItem
Definition: FeedWriter.js:1369
foldersync options
Definition: options.js:13
const Ci
function doSearch(searchTerm, cmdLine)
function now()
var ios
Definition: head_feeds.js:5
var PlacesSearchBox
Definition: places.js:875
let< body >< buttononblur="this.parentNode.removeChild(this);">< script > document body firstChild focus()
_getSelectedPageStyle s i
function asContainer(aNode)
Definition: utils.js:82
var file