//* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is Mozilla Places Organizer. * * The Initial Developer of the Original Code is Google Inc. * Portions created by the Initial Developer are Copyright (C) 2005-2006 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Ben Goodger * Annie Sullivan * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; /** * Selects a place URI in the places list. * This function is global so it can be easily accessed by openers. * @param placeURI * A place: URI string to select */ function selectPlaceURI(placeURI) { PlacesOrganizer._places.selectPlaceURI(placeURI); } var PlacesOrganizer = { _places: null, _content: null, init: function PO_init() { this._places = document.getElementById("placesList"); this._content = document.getElementById("placeContent"); OptionsFilter.init(Groupers); Groupers.init(); // Select the specified place in the places list. var placeURI = "place:"; if ("arguments" in window) placeURI = window.arguments[0]; selectPlaceURI(placeURI); // Initialize the active view so that all commands work properly without // the user needing to explicitly click in a view (since the search box is // focused by default). PlacesController.activeView = this._places; // Set up the search UI. PlacesSearchBox.init(); // Set up the advanced query builder UI PlacesQueryBuilder.init(); }, HEADER_TYPE_SHOWING: 1, HEADER_TYPE_SEARCH: 2, HEADER_TYPE_ADVANCED_SEARCH: 3, /** * Updates the text shown in the heading banner above the content view. * @param type * The type of information being shown - normal (built-in history or * other query, bookmark folder), search results from the toolbar * search box, or advanced search. * @param text * The text (if any) to display */ setHeaderText: function PO_setHeaderText(type, text) { NS_ASSERT(type == 1 || type == 2 || type == 3, "Invalid Header Type"); var bundle = document.getElementById("placeBundle"); var prefix = document.getElementById("showingPrefix"); prefix.setAttribute("value", bundle.getString("headerTextPrefix" + type)); var contentTitle = document.getElementById("contentTitle"); contentTitle.setAttribute("value", text); // Hide the advanced search controls when the user hasn't searched var searchModifiers = document.getElementById("searchModifiers"); searchModifiers.hidden = type == this.HEADER_TYPE_SHOWING; }, onPlaceURIKeypress: function PO_onPlaceURIKeypress(event) { if (event.keyCode == 13) this.loadPlaceURI(); else if (event.keyCode == 27) { event.target.value = ""; this.onPlaceSelected(true); } }, /** * Shows or hides the debug panel. */ toggleDebugPanel: function PO_toggleDebugPanel() { var dp = document.getElementById("debugPanel"); dp.hidden = !dp.hidden; if (!dp.hidden) document.getElementById("placeURI").focus(); }, /** * Loads the place URI entered in the debug */ loadPlaceURI: function PO_loadPlaceURI() { var placeURI = document.getElementById("placeURI"); var queriesRef = { }, optionsRef = { }; PlacesController.history.queryStringToQueries(placeURI.value, queriesRef, { }, optionsRef); var autoFilterResults = document.getElementById("autoFilterResults"); if (autoFilterResults.checked) { var options = OptionsFilter.filter(queriesRef.value, optionsRef.value, null); } else options = optionsRef.value; this._content.load(queriesRef.value, options); this.setHeaderText(this.HEADER_TYPE_SHOWING, "Debug results for: " + placeURI.value); this.updateLoadedURI(); placeURI.focus(); }, /** * Updates the URI displayed in the debug panel. */ updateLoadedURI: function PO_updateLoadedURI() { var queryNode = asQuery(this._content.getResult().root); var queries = queryNode.getQueries({}); var options = queryNode.queryOptions; var loadedURI = document.getElementById("loadedURI"); loadedURI.value = PlacesController.history.queriesToQueryString(queries, queries.length, options); }, /** * Called when a place folder is selected in the left pane. * @param resetSearchBox * true if the search box should also be reset, false if it should * be left alone. */ onPlaceSelected: function PO_onPlaceSelected(resetSearchBox) { if (!this._places.hasSelection) return; var node = asQuery(this._places.selectedNode); LOG("Node URI: " + node.uri); var queries = node.getQueries({}); this._content.load(queries, OptionsFilter.filter(queries, node.queryOptions, null)); // Make sure the query builder is hidden. PlacesQueryBuilder.hide(); if (resetSearchBox) { var searchFilter = document.getElementById("searchFilter"); searchFilter.reset(); } this.setHeaderText(this.HEADER_TYPE_SHOWING, node.title); this.updateLoadedURI(); // Update the "Find in " command and the gray text in // the search box in the toolbar if the active collection is the current // collection. var strings = document.getElementById("placeBundle"); var findCommand = document.getElementById("placesCmd_find:current"); var findLabel = strings.getFormattedString("findInPrefix", [node.title]); findCommand.setAttribute("label", findLabel); if (PlacesSearchBox.filterCollection == "collection") { PlacesSearchBox.updateCollectionTitle(node.title); PlacesSearchBox.syncGrayText(); } }, /** * Handle clicks on the tree. If the user middle clicks on a URL, load that * URL according to rules. Single clicks or modified clicks do not result in * any special action, since they're related to selection. * @param event * The mouse event. */ onTreeClick: function PO_onTreeClicked(event) { // If the user clicked on a tree column header, update the sorting // preferences to reflect their choices. if (event.target.localName == "treecol") { OptionsFilter.update(this._content.getResult()); return; } var v = PlacesController.activeView; if (v.hasSingleSelection && event.button == 1) { if (PlacesController.nodeIsURI(v.selectedNode)) PlacesController.mouseLoadURI(event); else if (PlacesController.nodeIsContainer(v.selectedNode)) { // The command execution function will take care of seeing the // selection is a folder/container and loading its contents in // tabs for us. PlacesController.openLinksInTabs(); } } }, /** * Returns the options associated with the query currently loaded in the * main places pane. */ getCurrentOptions: function PO_getCurrentOptions() { return asQuery(this._content.getResult().root).queryOptions; }, /** * Show the migration wizard for importing from a file. */ importBookmarks: function PO_import() { var features = "modal,centerscreen,chrome,resizable=no"; openDialog("chrome://browser/content/migration/migration.xul", "", features, "bookmarks"); }, /** * Allows simple exporting of bookmarks. */ exportBookmarks: function PO_export() { var fp = Cc["@mozilla.org/filepicker;1"]. createInstance(Ci.nsIFilePicker); fp.init(window, "", Ci.nsIFilePicker.modeSave); fp.appendFilters(Ci.nsIFilePicker.filterHTML); fp.defaultString = "bookmarks.html"; if (fp.show() != Ci.nsIFilePicker.returnCancel) { var bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. getService(Ci.nsINavBookmarksService); bms.exportBookmarksHTML(fp.file); } } }; /** * A set of utilities relating to search within Bookmarks and History. */ var PlacesSearchBox = { /** * The Search text field */ get searchFilter() { return document.getElementById("searchFilter"); }, /** * Run a search for the specified text, over the collection specified by * the dropdown arrow. The default is all bookmarks and history, but can be * localized to the active collection. * @param filterString * The text to search for. */ search: function PSB_search(filterString) { // Do not search for "" since it will match all history. Assume if the user // deleted everything that they want to type something else and don't // update the view. if (filterString == "" || this.searchFilter.hasAttribute("empty")) return; var content = PlacesOrganizer._content; var PO = PlacesOrganizer; switch (PlacesSearchBox.filterCollection) { case "collection": var folderId = asFolder(content.getResult().root).folderId; content.applyFilter(filterString, true, folderId, OptionsFilter); PO.setHeaderText(PO.HEADER_TYPE_SEARCH, filterString); break; case "all": content.applyFilter(filterString, false, 0, OptionsFilter); PO.setHeaderText(PO.HEADER_TYPE_SEARCH, filterString); break; } this.searchFilter.setAttribute("filtered", "true"); }, /** * Finds across all bookmarks and history. */ findAll: function PSB_findAll() { this.filterCollection = "all"; this.focus(); }, /** * Finds in the currently selected Place. */ findCurrent: function PSB_findCurrent() { this.filterCollection = "collection"; this.focus(); }, /** * Updates the display with the title of the current collection. * @param title * The title of the current collection. */ updateCollectionTitle: function PSB_updateCollectionTitle(title) { var strings = document.getElementById("placeBundle"); if (title) { this.searchFilter.grayText = strings.getFormattedString("searchCurrentDefault", [title]); } else this.searchFilter.grayText = strings.getString("searchDefault"); }, /** * Updates the display with the current gray text. */ syncGrayText: function PSB_syncGrayText() { this.searchFilter.value = this.searchFilter.grayText; }, /** * Gets/sets the active collection from the dropdown menu. */ get filterCollection() { return this.searchFilter.getAttribute("collection"); }, set filterCollection(collectionName) { this.searchFilter.setAttribute("collection", collectionName); var newGrayText = null; if (collectionName == "collection") newGrayText = PlacesOrganizer._places.selectedNode.title; this.updateCollectionTitle(newGrayText); return collectionName; }, /** * Focus the search box */ focus: function PSB_focus() { this.searchFilter.focus(); }, /** * Set up the gray text in the search bar as the Places View loads. */ init: function PSB_init() { var placeBundle = document.getElementById("placeBundle"); var searchFilter = this.searchFilter; searchFilter.grayText = placeBundle.getString("searchDefault"); searchFilter.reset(); searchFilter.focus(); }, /** * Gets or sets the text shown in the Places Search Box */ get value() { return this.searchFilter.value; }, set value(value) { return this.searchFilter.value = value; } }; /** * Functions and data for advanced query builder */ var PlacesQueryBuilder = { _numRows: 0, /** * The maximum number of terms that can be added. * XXXben - this should be generated dynamically based on the contents of a * list of terms searchable through this widget, rather than being * a hard coded number. */ _maxRows: 3, _keywordSearch: { advancedSearch_N_Subject: "advancedSearch_N_SubjectKeyword", advancedSearch_N_LocationMenulist: false, advancedSearch_N_TimeMenulist: false, advancedSearch_N_Textbox: "", advancedSearch_N_TimePicker: false, advancedSearch_N_TimeMenulist2: false }, _locationSearch: { advancedSearch_N_Subject: "advancedSearch_N_SubjectLocation", advancedSearch_N_LocationMenulist: "advancedSearch_N_LocationMenuSelected", advancedSearch_N_TimeMenulist: false, advancedSearch_N_Textbox: "", advancedSearch_N_TimePicker: false, advancedSearch_N_TimeMenulist2: false }, _timeSearch: { advancedSearch_N_Subject: "advancedSearch_N_SubjectVisited", advancedSearch_N_LocationMenulist: false, advancedSearch_N_TimeMenulist: true, advancedSearch_N_Textbox: false, advancedSearch_N_TimePicker: "date", advancedSearch_N_TimeMenulist2: false }, _timeInLastSearch: { advancedSearch_N_Subject: "advancedSearch_N_SubjectVisited", advancedSearch_N_LocationMenulist: false, advancedSearch_N_TimeMenulist: true, advancedSearch_N_Textbox: "7", advancedSearch_N_TimePicker: false, advancedSearch_N_TimeMenulist2: true }, _nextSearch: null, _queryBuilders: null, init: function PQB_init() { // Initialize advanced search this._nextSearch = { "keyword": this._timeSearch, "visited": this._locationSearch, "location": null }; this._queryBuilders = { "keyword": this.setKeywordQuery, "visited": this.setVisitedQuery, "location": this.setLocationQuery }; this._dateService = Cc["@mozilla.org/intl/scriptabledateformat;1"]. getService(Ci.nsIScriptableDateFormat); }, /** * Hides the query builder, and the match rule UI if visible. */ hide: function PQB_hide() { var advancedSearch = document.getElementById("advancedSearch"); // Need to collapse the advanced search box. advancedSearch.collapsed = true; var titleDeck = document.getElementById("titleDeck"); titleDeck.setAttribute("selectedIndex", 0); }, /** * Shows the query builder */ show: function PQB_show() { var advancedSearch = document.getElementById("advancedSearch"); advancedSearch.collapsed = false; }, /** * Includes the rowId in the id attribute of an element in a row newly * created from the template row. * @param element * The element whose id attribute needs to be updated. * @param rowId * The index of the new row. */ _setRowId: function PQB__setRowId(element, rowId) { if (element.id) element.id = element.id.replace("advancedSearch0", "advancedSearch" + rowId); if (element.hasAttribute("rowid")) element.setAttribute("rowid", rowId); for (var i = 0; i < element.childNodes.length; ++i) { this._setRowId(element.childNodes[i], rowId); } }, _updateUIForRowChange: function PQB__updateUIForRowChange() { // Titlebar should show "match any/all" iff there are > 1 queries visible. var titleDeck = document.getElementById("titleDeck"); titleDeck.setAttribute("selectedIndex", (this._numRows <= 1) ? 0 : 1); const asType = PlacesOrganizer.HEADER_TYPE_ADVANCED_SEARCH; PlacesOrganizer.setHeaderText(asType, ""); // Update the "can add more criteria" command to make sure various + // buttons are disabled. var command = document.getElementById("placesCmd_search:moreCriteria"); if (this._numRows >= this._maxRows) command.setAttribute("disabled", "true"); else command.removeAttribute("disabled"); }, /** * Adds a row to the view, prefilled with the next query subject. If the * query builder is not visible, it will be shown. */ addRow: function PQB_addRow() { // Limits the number of rows that can be added based on the maximum number // of search query subjects. if (this._numRows >= this._maxRows) return; // Clone the template row and unset the hidden attribute. var gridRows = document.getElementById("advancedSearchRows"); var newRow = gridRows.firstChild.cloneNode(true); newRow.hidden = false; // Determine what the search type is based on the last visible row. If this // is the first row, the type is "keyword search". Otherwise, it's the next // in the sequence after the one defined by the previous visible row's // Subject selector, as defined in _nextSearch. var searchType = this._keywordSearch; var lastMenu = document.getElementById("advancedSearch" + this._numRows + "Subject"); if (this._numRows > 0 && lastMenu && lastMenu.selectedItem) { searchType = this._nextSearch[lastMenu.selectedItem.value]; } // There is no "next" search type. We are here in error. if (!searchType) return; // We don't insert into the document until _after_ the searchType is // determined, since this will interfere with the computation. gridRows.appendChild(newRow); this._setRowId(newRow, ++this._numRows); // Ensure the Advanced Search container is visible, if this is the first // row being added. var advancedSearch = document.getElementById("advancedSearch"); if (advancedSearch.collapsed) { this.show(); // Update the header. const asType = PlacesOrganizer.HEADER_TYPE_ADVANCED_SEARCH; PlacesOrganizer.setHeaderText(asType, ""); // Pre-fill the search terms field with the value from the one on the // toolbar. // For some reason, setting.value here synchronously does not appear to // work. var searchTermsField = document.getElementById("advancedSearch1Textbox"); if (searchTermsField) setTimeout(function() { searchTermsField.value = PlacesSearchBox.value; }, 10); // Call ourselves again to add a second row so that the user is presented // with more than just "Keyword Search" which they already performed from // the toolbar. this.addRow(); return; } this.showSearch(this._numRows, searchType); this._updateUIForRowChange(); }, /** * Remove a row from the set of terms * @param row * The row to remove. If this is null, the last row will be removed. * If there are no more rows, the query builder will be hidden. */ removeRow: function PQB_removeRow(row) { if (!row) row = document.getElementById("advancedSearch" + this._numRows + "Row"); row.parentNode.removeChild(row); --this._numRows; if (this._numRows < 1) { this.hide(); // Re-do the original toolbar-search-box search that the user used to // spawn the advanced UI... this effectively "reverts" the UI to the // point it was in before they began monkeying with advanced search. PlacesSearchBox.search(PlacesSearchBox.value); return; } this.doSearch(); this._updateUIForRowChange(); }, onDateTyped: function PQB_onDateTyped(event, row) { var textbox = document.getElementById("advancedSearch" + row + "TimePicker"); var dateString = textbox.value; var dateArr = dateString.split("-"); // The date can be split into a range by the '-' character, i.e. // 9/5/05 - 10/2/05. Unfortunately, dates can also be written like // 9-5-05 - 10-2-05. Try to parse the date based on how many hyphens // there are. var d0 = null; var d1 = null; // If there are an even number of elements in the date array, try to // parse it as a range of two dates. if ((dateArr.length & 1) == 0) { var mid = dateArr.length / 2; var dateStr0 = dateArr[0]; var dateStr1 = dateArr[mid]; for (var i = 1; i < mid; ++i) { dateStr0 += "-" + dateArr[i]; dateStr1 += "-" + dateArr[i + mid]; } d0 = new Date(dateStr0); d1 = new Date(dateStr1); } // If that didn't work, try to parse it as a single date. if (d0 == null || d0 == "Invalid Date") { d0 = new Date(dateString); } if (d0 != null && d0 != "Invalid Date") { // Parsing succeeded -- update the calendar. var calendar = document.getElementById("advancedSearch" + row + "Calendar"); if (d0.getFullYear() < 2000) d0.setFullYear(2000 + (d0.getFullYear() % 100)); if (d1 != null && d1 != "Invalid Date") { if (d1.getFullYear() < 2000) d1.setFullYear(2000 + (d1.getFullYear() % 100)); calendar.updateSelection(d0, d1); } else { calendar.updateSelection(d0, d0); } // And update the search. this.doSearch(); } }, onCalendarChanged: function PQB_onCalendarChanged(event, row) { var calendar = document.getElementById("advancedSearch" + row + "Calendar"); var begin = calendar.beginrange; var end = calendar.endrange; // If the calendar doesn't have a begin/end, don't change the textbox. if (begin == null || end == null) return true; // If the begin and end are the same day, only fill that into the textbox. var textbox = document.getElementById("advancedSearch" + row + "TimePicker"); var beginDate = begin.getDate(); var beginMonth = begin.getMonth() + 1; var beginYear = begin.getFullYear(); var endDate = end.getDate(); var endMonth = end.getMonth() + 1; var endYear = end.getFullYear(); if (beginDate == endDate && beginMonth == endMonth && beginYear == endYear) { // Just one date. textbox.value = this._dateService.FormatDate("", this._dateService.dateFormatShort, beginYear, beginMonth, beginDate); } else { // Two dates. var beginStr = this._dateService.FormatDate("", this._dateService.dateFormatShort, beginYear, beginMonth, beginDate); var endStr = this._dateService.FormatDate("", this._dateService.dateFormatShort, endYear, endMonth, endDate); textbox.value = beginStr + " - " + endStr; } // Update the search. this.doSearch(); return true; }, handleTimePickerClick: function PQB_handleTimePickerClick(event, row) { var popup = document.getElementById("advancedSearch" + row + "DatePopup"); if (popup.showing) popup.hidePopup(); else { var textbox = document.getElementById("advancedSearch" + row + "TimePicker"); popup.showPopup(textbox, -1, -1, "popup", "bottomleft", "topleft"); } }, showSearch: function PQB_showSearch(row, values) { for (val in values) { var id = val.replace("_N_", row); var element = document.getElementById(id); if (values[val] || typeof(values[val]) == "string") { if (typeof(values[val]) == "string") { if (values[val] == "date") { // "date" means that the current date should be filled into the // textbox, and the calendar for the row updated. var d = new Date(); element.value = this._dateService.FormatDate("", this._dateService.dateFormatShort, d.getFullYear(), d.getMonth() + 1, d.getDate()); var calendar = document.getElementById("advancedSearch" + row + "Calendar"); calendar.updateSelection(d, d); } else if (element.nodeName == "textbox") { // values[val] is the initial value of the textbox. element.value = values[val]; } else { // values[val] is the menuitem which should be selected. var itemId = values[val].replace("_N_", row); var item = document.getElementById(itemId); element.selectedItem = item; } } element.hidden = false; } else { element.hidden = true; } } this.doSearch(); }, setKeywordQuery: function PQB_setKeywordQuery(query, prefix) { query.searchTerms += document.getElementById(prefix + "Textbox").value + " "; }, setLocationQuery: function PQB_setLocationQuery(query, prefix) { var type = document.getElementById(prefix + "LocationMenulist").selectedItem.value; if (type == "onsite") { query.domain = document.getElementById(prefix + "Textbox").value; } else { query.uriIsPrefix = (type == "startswith"); var ios = Cc["@mozilla.org/network/io-service;1"]. getService(Ci.nsIIOService); var spec = document.getElementById(prefix + "Textbox").value; try { query.uri = ios.newURI(spec, null, null); } catch (e) { // Invalid input can cause newURI to barf, that's OK, tack "http://" // onto the front and try again to see if the user omitted it try { query.uri = ios.newURI("http://" + spec, null, null); } catch (e) { // OK, they have entered something which can never match. This should // not happen. } } } }, setVisitedQuery: function PQB_setVisitedQuery(query, prefix) { var searchType = document.getElementById(prefix + "TimeMenulist").selectedItem.value; const DAY_MSEC = 86400000; switch (searchType) { case "on": var calendar = document.getElementById(prefix + "Calendar"); var begin = calendar.beginrange.getTime(); var end = calendar.endrange.getTime(); if (begin == end) { end = begin + DAY_MSEC; } query.beginTime = begin * 1000; query.endTime = end * 1000; break; case "before": var calendar = document.getElementById(prefix + "Calendar"); var time = calendar.beginrange.getTime(); query.endTime = time * 1000; break; case "after": var calendar = document.getElementById(prefix + "Calendar"); var time = calendar.endrange.getTime(); query.beginTime = time * 1000; break; case "inLast": var textbox = document.getElementById(prefix + "Textbox"); var amount = parseInt(textbox.value); amount = amount * DAY_MSEC; var menulist = document.getElementById(prefix + "TimeMenulist2"); if (menulist.selectedItem.value == "weeks") amount = amount * 7; else if (menulist.selectedItem.value == "months") amount = amount * 30; var now = new Date(); now = now - amount; query.beginTime = now * 1000; break; } }, doSearch: function PQB_doSearch() { // Create the individual queries. var queryType = document.getElementById("advancedSearchType").selectedItem.value; var queries = []; if (queryType == "and") queries.push(PlacesController.history.getNewQuery()); var updated = 0; for (var i = 1; updated < this._numRows; ++i) { var prefix = "advancedSearch" + i; // The user can remove rows from the middle and start of the list, not // just from the end, so we need to make sure that this row actually // exists before attempting to construct a query for it. var querySubjectElement = document.getElementById(prefix + "Subject"); if (querySubjectElement) { // If the queries are being AND-ed, put all the rows in one query. // If they're being OR-ed, add a separate query for each row. var query; if (queryType == "and") query = queries[0]; else query = PlacesController.history.getNewQuery(); var querySubject = querySubjectElement.value; this._queryBuilders[querySubject](query, prefix); if (queryType == "or") queries.push(query); ++updated; } } // Make sure we're getting uri results, not visits var options = PlacesOrganizer.getCurrentOptions(); options.resultType = options.RESULT_TYPE_URI; // XXXben - find some public way of doing this! PlacesOrganizer._content.load(queries, OptionsFilter.filter(queries, options, null)); PlacesOrganizer.updateLoadedURI(); } }; /** * Population and commands for the View Menu. */ var ViewMenu = { /** * Removes content generated previously from a menupopup. * @param popup * The popup that contains the previously generated content. * @param startID * The id attribute of an element that is the start of the * dynamically generated region - remove elements after this * item only. * Must be contained by popup. Can be null (in which case the * contents of popup are removed). * @param endID * The id attribute of an element that is the end of the * dynamically generated region - remove elements up to this * item only. * Must be contained by popup. Can be null (in which case all * items until the end of the popup will be removed). Ignored * if startID is null. * @returns The element for the caller to insert new items before, * null if the caller should just append to the popup. */ _clean: function VM__clean(popup, startID, endID) { if (endID) NS_ASSERT(startID, "meaningless to have valid endID and null startID"); if (startID) { var startElement = document.getElementById(startID); NS_ASSERT(startElement.parentNode == popup, "startElement is not in popup"); NS_ASSERT(startElement, "startID does not correspond to an existing element"); var endElement = null; if (endID) { endElement = document.getElementById(endID); NS_ASSERT(endElement.parentNode == popup, "endElement is not in popup"); NS_ASSERT(endElement, "endID does not correspond to an existing element"); } while (startElement.nextSibling != endElement) popup.removeChild(startElement.nextSibling); return endElement; } else { while(popup.hasChildNodes()) popup.removeChild(popup.firstChild); } return null; }, /** * Fills a menupopup with a list of columns * @param event * The popupshowing event that invoked this function. * @param startID * see _clean * @param endID * see _clean * @param type * the type of the menuitem, e.g. "radio" or "checkbox". * Can be null (no-type). * Checkboxes are checked if the column is visible. * @param labelFormat * A format string to be applied to item labels. If null, no format * is used. */ fillWithColumns: function VM_fillWithColumns(event, startID, endID, type, labelFormat) { var popup = event.target; var pivot = this._clean(popup, startID, endID); // If no column is "sort-active", the "Unsorted" item needs to be checked, // so track whether or not we find a column that is sort-active. var isSorted = false; var content = document.getElementById("placeContent"); var strings = document.getElementById("placeBundle"); var columns = content.columns; for (var i = 0; i < columns.count; ++i) { var column = columns.getColumnAt(i).element; var menuitem = document.createElementNS(XUL_NS, "menuitem"); menuitem.id = "menucol_" + column.id; var label = column.getAttribute("label"); if (labelFormat) label = strings.getFormattedString(labelFormat, [label]); menuitem.setAttribute("label", label); if (type == "radio") { menuitem.setAttribute("type", "radio"); menuitem.setAttribute("name", "columns"); // This column is the sort key. Its item is checked. if (column.hasAttribute("sortDirection")) { menuitem.setAttribute("checked", "true"); isSorted = true; } } else if (type == "checkbox") { menuitem.setAttribute("type", "checkbox"); // Cannot uncheck the primary column. if (column.getAttribute("primary") == "true") menuitem.setAttribute("disabled", "true"); // Items for visible columns are checked. if (column.getAttribute("hidden") != "true") menuitem.setAttribute("checked", "true"); } if (pivot) popup.insertBefore(menuitem, pivot); else popup.appendChild(menuitem); } event.stopPropagation(); }, /** * Set up the content of the view menu. */ populate: function VM_populate(event) { this.fillWithColumns(event, "viewUnsorted", "directionSeparator", "radio", "sortByPrefix"); var sortColumn = this._getSortColumn(); var viewSortAscending = document.getElementById("viewSortAscending"); var viewSortDescending = document.getElementById("viewSortDescending"); // We need to remove an existing checked attribute because the unsorted // menu item is not rebuilt every time we open the menu like the others. var viewUnsorted = document.getElementById("viewUnsorted"); if (!sortColumn) { viewSortAscending.removeAttribute("checked"); viewSortDescending.removeAttribute("checked"); viewUnsorted.setAttribute("checked", "true"); } else if (sortColumn.getAttribute("sortDirection") == "ascending") { viewSortAscending.setAttribute("checked", "true"); viewSortDescending.removeAttribute("checked"); viewUnsorted.removeAttribute("checked"); } else if (sortColumn.getAttribute("sortDirection") == "descending") { viewSortDescending.setAttribute("checked", "true"); viewSortAscending.removeAttribute("checked"); viewUnsorted.removeAttribute("checked"); } }, /** * Shows/Hides a tree column. * @param element * The menuitem element for the column */ showHideColumn: function VM_showHideColumn(element) { const PREFIX = "menucol_"; var columnID = element.id.substr(PREFIX.length, element.id.length); var column = document.getElementById(columnID); NS_ASSERT(column, "menu item for column that doesn't exist?! id = " + element.id); var splitter = column.nextSibling; if (splitter && splitter.localName != "splitter") splitter = null; if (element.getAttribute("checked") == "true") { column.removeAttribute("hidden"); splitter.removeAttribute("hidden"); } else { column.setAttribute("hidden", "true"); splitter.setAttribute("hidden", "true"); } }, /** * Gets the last column that was sorted. * @returns the currently sorted column, null if there is no sorted column. */ _getSortColumn: function VM__getSortColumn() { var content = document.getElementById("placeContent"); var cols = content.columns; for (var i = 0; i < cols.count; ++i) { var column = cols.getColumnAt(i).element; var sortDirection = column.getAttribute("sortDirection"); if (sortDirection == "ascending" || sortDirection == "descending") return column; } return null; }, /** * Sorts the view by the specified key. * @param element * The menuitem element for the column that is the sort key. * Can be null - the primary column will be sorted. * @param direction * The direction to sort - "ascending" or "descending". * Can be null - the last direction will be used. * If both element and direction are null, the view will be * unsorted. */ setSortColumn: function VM_setSortColumn(element, direction) { const PREFIX = "menucol_"; // Validate the click - check to see if it was on a valid sort item. if (element && (element.id.substring(0, PREFIX.length) != PREFIX && element.id != "viewSortDescending" && element.id != "viewSortAscending")) return; // If both element and direction are null, all will be unsorted. var unsorted = !element && !direction; // If only the element is null, the currently sorted column will be sorted // with the specified direction, if there is no currently sorted column, // the primary column will be sorted with the specified direction. if (!element && direction) { element = this._getSortColumn(); if (!element) element = document.getElementById("title"); } else if (element && !direction) { var elementID = element.id.substr(PREFIX.length, element.id.length); element = document.getElementById(elementID); // If direction is null, use the default (ascending) direction = "ascending"; } var content = document.getElementById("placeContent"); var columns = content.columns; for (var i = 0; i < columns.count; ++i) { var column = columns.getColumnAt(i).element; if (unsorted) { column.removeAttribute("sortActive"); column.removeAttribute("sortDirection"); content.getResult().sortAll(Ci.nsINavHistoryQueryOptions.SORT_BY_NONE); } else if (column.id == element.id) { // We need to do this to ensure the UI updates properly next time it is built. if (column.getAttribute("sortDirection") != direction) { column.setAttribute("sortActive", "true"); column.setAttribute("sortDirection", direction); } var columnObj = content.columns.getColumnFor(column); content.view.cycleHeader(columnObj); break; } } } }; /** * A "Configuration" set for a class of history query results. Some results * will require that the grouping UI be labeled differently from the standard * so this object is provided to allow those results to configure the UI when * they are displayed. * @param substr * A prefix substring of an annotation that the result's query * matches. * @param onLabel * The label for the "Grouping On" command * @param onAccesskey * The accesskey for the "Grouping On" command * @param offLabel * The label for the "Grouping Off" command * @param offAccesskey * The accesskey for the "Grouping Off" command * @param onOncommand * The "oncommand" attribute of the "Grouping On" command * @param offOncommand * The "oncommand" attribute of the "Grouping Off" command * @param disabled * Whether or not grouping is disabled for results that match this * config. */ function GroupingConfig(substr, onLabel, onAccesskey, offLabel, offAccesskey, onOncommand, offOncommand, disabled) { this.substr = substr; this.onLabel = onLabel; this.onAccesskey = onAccesskey; this.offLabel = offLabel; this.offAccesskey = offAccesskey; this.onOncommand = onOncommand; this.offOncommand = offOncommand; this.disabled = disabled; } /** * Handles Grouping within the Content View, and the commands that support it. */ var Groupers = { defaultGrouper: null, annotationGroupers: [], /** * Initializes groupings for various vie types. */ init: function G_init() { var placeBundle = document.getElementById("placeBundle"); this.defaultGrouper = new GroupingConfig(null, placeBundle.getString("defaultGroupOnLabel"), placeBundle.getString("defaultGroupOnAccesskey"), placeBundle.getString("defaultGroupOffLabel"), placeBundle.getString("defaultGroupOffAccesskey"), "Groupers.groupBySite()", "Groupers.groupByPage()", false); var subscriptionConfig = new GroupingConfig("livemark/", placeBundle.getString("livemarkGroupOnLabel"), placeBundle.getString("livemarkGroupOnAccesskey"), placeBundle.getString("livemarkGroupOffLabel"), placeBundle.getString("livemarkGroupOffAccesskey"), "Groupers.groupByFeed()", "Groupers.groupByPost()", false); this.annotationGroupers.push(subscriptionConfig); }, /** * Get the most appropriate GroupingConfig for the set of queries that are * about to be executed. * @param queries * An array of queries that are about to be executed. * @param handler * Optionally specify which handler to use to filter the options. * If null, the default handler for the queries will be used. */ _getConfig: function G__getConfig(queries, handler) { if (!handler) handler = OptionsFilter.getHandler(queries); // If the queries will generate a bookmarks folder, there is no "grouper // config" since all of the grouping UI should be hidden (there is only one // grouping mode - group by folder). if (handler == OptionsFilter.bookmarksHandler) return null; var query = queries[0]; for (var i = 0; i < this.annotationGroupers.length; ++i) { var config = this.annotationGroupers[i]; if (query.annotation.substr(0, config.substr.length) == config.substr && !config.disabled) return config; } return this.defaultGrouper; }, /** * Updates the grouping broadcasters for the given result. * @param queries * An array of queries that are going to be executed. * @param options * The options that are being used to generate the forthcoming * result. * @param handler * Optionally specify which handler to use to filter the options. * If null, the default handler for the queries will be used. * @param */ updateGroupingUI: function G_updateGroupingUI(queries, options, handler) { var separator = document.getElementById("placesBC_grouping:separator"); var groupOff = document.getElementById("placesBC_grouping:off"); var groupOn = document.getElementById("placesBC_grouping:on"); separator.removeAttribute("hidden"); groupOff.removeAttribute("hidden"); groupOn.removeAttribute("hidden"); // Walk the list of registered annotationGroupers, determining what are // available and notifying the broadcaster var config = this._getConfig(queries, handler); if (!config) { // Generic Bookmarks Folder or custom container that disables grouping. separator.setAttribute("hidden", "true"); groupOff.setAttribute("hidden", "true"); groupOn.setAttribute("hidden", "true"); } else { groupOn.setAttribute("label", config.onLabel); groupOn.setAttribute("accesskey", config.onAccesskey); groupOn.setAttribute("oncommand", config.onOncommand); groupOff.setAttribute("label", config.offLabel); groupOff.setAttribute("accesskey", config.offAccesskey); groupOff.setAttribute("oncommand", config.offOncommand); // Update the checked state of the UI: // Grouping UI is "enabled" if there is at least one set of grouping // options. var groupingsCountRef = { }; options.getGroupingMode(groupingsCountRef); this._updateBroadcasters(groupingsCountRef.value > 0); } }, /** * Update the visual state of UI that controls grouping. */ _updateBroadcasters: function G__updateGroupingBroadcasters(on) { var groupingOn = document.getElementById("placesBC_grouping:on"); var groupingOff = document.getElementById("placesBC_grouping:off"); if (on) { groupingOn.setAttribute("checked", "true"); groupingOff.removeAttribute("checked"); } else { groupingOff.setAttribute("checked", "true"); groupingOn.removeAttribute("checked"); } }, /** * Shows visited pages grouped by site. */ groupBySite: function G_groupBySite() { var query = asQuery(PlacesOrganizer._content.getResult().root); var queries = query.getQueries({ }); var options = query.queryOptions; var newOptions = options.clone(); const NHQO = Ci.nsINavHistoryQueryOptions; newOptions.setGroupingMode([NHQO.GROUP_BY_DOMAIN], 1); var content = PlacesOrganizer._content; content.load(queries, newOptions); PlacesOrganizer.updateLoadedURI(); this._updateBroadcasters(true); OptionsFilter.update(content.getResult()); }, /** * Shows visited pages without grouping. */ groupByPage: function G_groupByPage() { var query = asQuery(PlacesOrganizer._content.getResult().root); var queries = query.getQueries({ }); var options = query.queryOptions; var newOptions = options.clone(); newOptions.setGroupingMode([], 0); var content = PlacesOrganizer._content; content.load(queries, newOptions); PlacesOrganizer.updateLoadedURI(); this._updateBroadcasters(false); OptionsFilter.update(content.getResult()); }, /** * Shows all subscribed feeds (Live Bookmarks) grouped under their parent * feed. */ groupByFeed: function G_groupByFeed() { var content = PlacesOrganizer._content; var query = asQuery(content.getResult().root); var queries = query.getQueries({ }); var newOptions = query.queryOptions.clone(); var newQuery = queries[0].clone(); newQuery.annotation = "livemark/feedURI"; content.load([newQuery], newOptions); PlacesOrganizer.updateLoadedURI(); this._updateBroadcasters(false); OptionsFilter.update(content.getResult()); }, /** * Shows all subscribed feed (Live Bookmarks) content in a flat list */ groupByPost: function G_groupByPost() { var content = PlacesOrganizer._content; var query = asQuery(content.getResult().root); var queries = query.getQueries({ }); var newOptions = query.queryOptions.clone(); var newQuery = queries[0].clone(); newQuery.annotation = "livemark/bookmarkFeedURI"; content.load([newQuery], newOptions); PlacesOrganizer.updateLoadedURI(); this._updateBroadcasters(false); OptionsFilter.update(content.getResult()); } }; #include ../../../../toolkit/content/debug.js