From 4492b5f8e774bf3b4f21e4e468fc052cbcbb468a Mon Sep 17 00:00:00 2001 From: Thomas Groman Date: Mon, 16 Dec 2019 19:48:42 -0800 Subject: initial commit --- components/places/content/bookmarkProperties.js | 675 +++++++ components/places/content/bookmarkProperties.xul | 43 + components/places/content/bookmarksPanel.js | 25 + components/places/content/bookmarksPanel.xul | 55 + components/places/content/browserPlacesViews.js | 1754 ++++++++++++++++++ components/places/content/controller.js | 1895 ++++++++++++++++++++ components/places/content/downloadsViewOverlay.xul | 47 + components/places/content/editBookmarkOverlay.js | 1063 +++++++++++ components/places/content/editBookmarkOverlay.xul | 228 +++ components/places/content/history-panel.js | 91 + components/places/content/history-panel.xul | 95 + components/places/content/menu.xml | 488 +++++ components/places/content/moveBookmarks.js | 54 + components/places/content/moveBookmarks.xul | 53 + components/places/content/organizer.css | 7 + components/places/content/places.css | 16 + components/places/content/places.js | 1553 ++++++++++++++++ components/places/content/places.xul | 471 +++++ components/places/content/placesOverlay.xul | 247 +++ components/places/content/sidebarUtils.js | 108 ++ components/places/content/tree.xml | 789 ++++++++ components/places/content/treeView.js | 1770 ++++++++++++++++++ 22 files changed, 11527 insertions(+) create mode 100644 components/places/content/bookmarkProperties.js create mode 100644 components/places/content/bookmarkProperties.xul create mode 100644 components/places/content/bookmarksPanel.js create mode 100644 components/places/content/bookmarksPanel.xul create mode 100644 components/places/content/browserPlacesViews.js create mode 100644 components/places/content/controller.js create mode 100644 components/places/content/downloadsViewOverlay.xul create mode 100644 components/places/content/editBookmarkOverlay.js create mode 100644 components/places/content/editBookmarkOverlay.xul create mode 100644 components/places/content/history-panel.js create mode 100644 components/places/content/history-panel.xul create mode 100644 components/places/content/menu.xml create mode 100644 components/places/content/moveBookmarks.js create mode 100644 components/places/content/moveBookmarks.xul create mode 100644 components/places/content/organizer.css create mode 100644 components/places/content/places.css create mode 100644 components/places/content/places.js create mode 100644 components/places/content/places.xul create mode 100644 components/places/content/placesOverlay.xul create mode 100644 components/places/content/sidebarUtils.js create mode 100644 components/places/content/tree.xml create mode 100644 components/places/content/treeView.js (limited to 'components/places/content') diff --git a/components/places/content/bookmarkProperties.js b/components/places/content/bookmarkProperties.js new file mode 100644 index 0000000..e1d1077 --- /dev/null +++ b/components/places/content/bookmarkProperties.js @@ -0,0 +1,675 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * The panel is initialized based on data given in the js object passed + * as window.arguments[0]. The object must have the following fields set: + * @ action (String). Possible values: + * - "add" - for adding a new item. + * @ type (String). Possible values: + * - "bookmark" + * @ loadBookmarkInSidebar - optional, the default state for the + * "Load this bookmark in the sidebar" field. + * - "folder" + * @ URIList (Array of nsIURI objects) - optional, list of uris to + * be bookmarked under the new folder. + * - "livemark" + * @ uri (nsIURI object) - optional, the default uri for the new item. + * The property is not used for the "folder with items" type. + * @ title (String) - optional, the default title for the new item. + * @ description (String) - optional, the default description for the new + * item. + * @ defaultInsertionPoint (InsertionPoint JS object) - optional, the + * default insertion point for the new item. + * @ keyword (String) - optional, the default keyword for the new item. + * @ postData (String) - optional, POST data to accompany the keyword. + * @ charSet (String) - optional, character-set to accompany the keyword. + * Notes: + * 1) If |uri| is set for a bookmark/livemark item and |title| isn't, + * the dialog will query the history tables for the title associated + * with the given uri. If the dialog is set to adding a folder with + * bookmark items under it (see URIList), a default static title is + * used ("[Folder Name]"). + * 2) The index field of the default insertion point is ignored if + * the folder picker is shown. + * - "edit" - for editing a bookmark item or a folder. + * @ type (String). Possible values: + * - "bookmark" + * @ itemId (Integer) - the id of the bookmark item. + * - "folder" (also applies to livemarks) + * @ itemId (Integer) - the id of the folder. + * @ hiddenRows (Strings array) - optional, list of rows to be hidden + * regardless of the item edited or added by the dialog. + * Possible values: + * - "title" + * - "location" + * - "description" + * - "keyword" + * - "tags" + * - "loadInSidebar" + * - "feedLocation" + * - "siteLocation" + * - "folderPicker" - hides both the tree and the menu. + * @ readOnly (Boolean) - optional, states if the panel should be read-only + * + * window.arguments[0].performed is set to true if any transaction has + * been performed by the dialog. + */ + +Components.utils.import('resource://gre/modules/XPCOMUtils.jsm'); +XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", + "resource://gre/modules/PrivateBrowsingUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Task", + "resource://gre/modules/Task.jsm"); + +const BOOKMARK_ITEM = 0; +const BOOKMARK_FOLDER = 1; +const LIVEMARK_CONTAINER = 2; + +const ACTION_EDIT = 0; +const ACTION_ADD = 1; + +var elementsHeight = new Map(); + +var BookmarkPropertiesPanel = { + + /** UI Text Strings */ + __strings: null, + get _strings() { + if (!this.__strings) { + this.__strings = document.getElementById("stringBundle"); + } + return this.__strings; + }, + + _action: null, + _itemType: null, + _itemId: -1, + _uri: null, + _loadInSidebar: false, + _title: "", + _description: "", + _URIs: [], + _keyword: "", + _postData: null, + _charSet: "", + _feedURI: null, + _siteURI: null, + + _defaultInsertionPoint: null, + _hiddenRows: [], + _batching: false, + _readOnly: false, + + /** + * This method returns the correct label for the dialog's "accept" + * button based on the variant of the dialog. + */ + _getAcceptLabel: function BPP__getAcceptLabel() { + if (this._action == ACTION_ADD) { + if (this._URIs.length) + return this._strings.getString("dialogAcceptLabelAddMulti"); + + if (this._itemType == LIVEMARK_CONTAINER) + return this._strings.getString("dialogAcceptLabelAddLivemark"); + + if (this._dummyItem || this._loadInSidebar) + return this._strings.getString("dialogAcceptLabelAddItem"); + + return this._strings.getString("dialogAcceptLabelSaveItem"); + } + return this._strings.getString("dialogAcceptLabelEdit"); + }, + + /** + * This method returns the correct title for the current variant + * of this dialog. + */ + _getDialogTitle: function BPP__getDialogTitle() { + if (this._action == ACTION_ADD) { + if (this._itemType == BOOKMARK_ITEM) + return this._strings.getString("dialogTitleAddBookmark"); + if (this._itemType == LIVEMARK_CONTAINER) + return this._strings.getString("dialogTitleAddLivemark"); + + // add folder + NS_ASSERT(this._itemType == BOOKMARK_FOLDER, "Unknown item type"); + if (this._URIs.length) + return this._strings.getString("dialogTitleAddMulti"); + + return this._strings.getString("dialogTitleAddFolder"); + } + if (this._action == ACTION_EDIT) { + return this._strings.getFormattedString("dialogTitleEdit", [this._title]); + } + return ""; + }, + + /** + * Determines the initial data for the item edited or added by this dialog + */ + _determineItemInfo: function BPP__determineItemInfo() { + var dialogInfo = window.arguments[0]; + this._action = dialogInfo.action == "add" ? ACTION_ADD : ACTION_EDIT; + this._hiddenRows = dialogInfo.hiddenRows ? dialogInfo.hiddenRows : []; + if (this._action == ACTION_ADD) { + NS_ASSERT("type" in dialogInfo, "missing type property for add action"); + + if ("title" in dialogInfo) + this._title = dialogInfo.title; + + if ("defaultInsertionPoint" in dialogInfo) { + this._defaultInsertionPoint = dialogInfo.defaultInsertionPoint; + } + else + this._defaultInsertionPoint = + new InsertionPoint(PlacesUtils.bookmarksMenuFolderId, + PlacesUtils.bookmarks.DEFAULT_INDEX, + Ci.nsITreeView.DROP_ON); + + switch (dialogInfo.type) { + case "bookmark": + this._itemType = BOOKMARK_ITEM; + if ("uri" in dialogInfo) { + NS_ASSERT(dialogInfo.uri instanceof Ci.nsIURI, + "uri property should be a uri object"); + this._uri = dialogInfo.uri; + if (typeof(this._title) != "string") { + this._title = this._getURITitleFromHistory(this._uri) || + this._uri.spec; + } + } + else { + this._uri = PlacesUtils._uri("about:blank"); + this._title = this._strings.getString("newBookmarkDefault"); + this._dummyItem = true; + } + + if ("loadBookmarkInSidebar" in dialogInfo) + this._loadInSidebar = dialogInfo.loadBookmarkInSidebar; + + if ("keyword" in dialogInfo) { + this._keyword = dialogInfo.keyword; + this._isAddKeywordDialog = true; + if ("postData" in dialogInfo) + this._postData = dialogInfo.postData; + if ("charSet" in dialogInfo) + this._charSet = dialogInfo.charSet; + } + break; + + case "folder": + this._itemType = BOOKMARK_FOLDER; + if (!this._title) { + if ("URIList" in dialogInfo) { + this._title = this._strings.getString("bookmarkAllTabsDefault"); + this._URIs = dialogInfo.URIList; + } + else + this._title = this._strings.getString("newFolderDefault"); + this._dummyItem = true; + } + break; + + case "livemark": + this._itemType = LIVEMARK_CONTAINER; + if ("feedURI" in dialogInfo) + this._feedURI = dialogInfo.feedURI; + if ("siteURI" in dialogInfo) + this._siteURI = dialogInfo.siteURI; + + if (!this._title) { + if (this._feedURI) { + this._title = this._getURITitleFromHistory(this._feedURI) || + this._feedURI.spec; + } + else + this._title = this._strings.getString("newLivemarkDefault"); + } + } + + if ("description" in dialogInfo) + this._description = dialogInfo.description; + } + else { // edit + NS_ASSERT("itemId" in dialogInfo); + this._itemId = dialogInfo.itemId; + this._title = PlacesUtils.bookmarks.getItemTitle(this._itemId); + this._readOnly = !!dialogInfo.readOnly; + + switch (dialogInfo.type) { + case "bookmark": + this._itemType = BOOKMARK_ITEM; + + this._uri = PlacesUtils.bookmarks.getBookmarkURI(this._itemId); + // keyword + this._keyword = PlacesUtils.bookmarks + .getKeywordForBookmark(this._itemId); + // Load In Sidebar + this._loadInSidebar = PlacesUtils.annotations + .itemHasAnnotation(this._itemId, + PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO); + break; + + case "folder": + this._itemType = BOOKMARK_FOLDER; + PlacesUtils.livemarks.getLivemark({ id: this._itemId }) + .then(aLivemark => { + this._itemType = LIVEMARK_CONTAINER; + this._feedURI = aLivemark.feedURI; + this._siteURI = aLivemark.siteURI; + this._fillEditProperties(); + + let acceptButton = document.documentElement.getButton("accept"); + acceptButton.disabled = !this._inputIsValid(); + + let newHeight = window.outerHeight + + this._element("descriptionField").boxObject.height; + window.resizeTo(window.outerWidth, newHeight); + }, () => undefined); + + break; + } + + // Description + if (PlacesUtils.annotations + .itemHasAnnotation(this._itemId, PlacesUIUtils.DESCRIPTION_ANNO)) { + this._description = PlacesUtils.annotations + .getItemAnnotation(this._itemId, + PlacesUIUtils.DESCRIPTION_ANNO); + } + } + }, + + /** + * This method returns the title string corresponding to a given URI. + * If none is available from the bookmark service (probably because + * the given URI doesn't appear in bookmarks or history), we synthesize + * a title from the first 100 characters of the URI. + * + * @param aURI + * nsIURI object for which we want the title + * + * @returns a title string + */ + _getURITitleFromHistory: function BPP__getURITitleFromHistory(aURI) { + NS_ASSERT(aURI instanceof Ci.nsIURI); + + // get the title from History + return PlacesUtils.history.getPageTitle(aURI); + }, + + /** + * This method should be called by the onload of the Bookmark Properties + * dialog to initialize the state of the panel. + */ + onDialogLoad: Task.async(function* BPP_onDialogLoad() { + this._determineItemInfo(); + + document.title = this._getDialogTitle(); + var acceptButton = document.documentElement.getButton("accept"); + acceptButton.label = this._getAcceptLabel(); + + // Do not use sizeToContent, otherwise, due to bug 90276, the dialog will + // grow at every opening. + // Since elements can be uncollapsed asynchronously, we must observe their + // mutations and resize the dialog using a cached element size. + this._height = window.outerHeight; + this._mutationObserver = new MutationObserver(mutations => { + for (let mutation of mutations) { + let target = mutation.target; + let id = target.id; + if (!/^editBMPanel_.*(Row|Checkbox)$/.test(id)) + continue; + + let collapsed = target.getAttribute("collapsed") === "true"; + let wasCollapsed = mutation.oldValue === "true"; + if (collapsed == wasCollapsed) + continue; + + if (collapsed) { + this._height -= elementsHeight.get(id); + elementsHeight.delete(id); + } else { + elementsHeight.set(id, target.boxObject.height); + this._height += elementsHeight.get(id); + } + window.resizeTo(window.outerWidth, this._height); + } + }); + + this._mutationObserver.observe(document, + { subtree: true, + attributeOldValue: true, + attributeFilter: ["collapsed"] }); + + // Some controls are flexible and we want to update their cached size when + // the dialog is resized. + window.addEventListener("resize", this); + + this._beginBatch(); + + switch (this._action) { + case ACTION_EDIT: + this._fillEditProperties(); + acceptButton.disabled = this._readOnly; + break; + case ACTION_ADD: + yield this._fillAddProperties(); + // if this is an uri related dialog disable accept button until + // the user fills an uri value. + if (this._itemType == BOOKMARK_ITEM) + acceptButton.disabled = !this._inputIsValid(); + break; + } + + if (!this._readOnly) { + // Listen on uri fields to enable accept button if input is valid + if (this._itemType == BOOKMARK_ITEM) { + this._element("locationField") + .addEventListener("input", this, false); + if (this._isAddKeywordDialog) { + this._element("keywordField") + .addEventListener("input", this, false); + } + } + else if (this._itemType == LIVEMARK_CONTAINER) { + this._element("feedLocationField") + .addEventListener("input", this, false); + this._element("siteLocationField") + .addEventListener("input", this, false); + } + } + + // Ensure the Name Picker textbox is focused on load + var namePickerElem = document.getElementById('editBMPanel_namePicker'); + namePickerElem.focus(); + namePickerElem.select(); + }), + + // nsIDOMEventListener + handleEvent: function BPP_handleEvent(aEvent) { + var target = aEvent.target; + switch (aEvent.type) { + case "input": + if (target.id == "editBMPanel_locationField" || + target.id == "editBMPanel_feedLocationField" || + target.id == "editBMPanel_siteLocationField" || + target.id == "editBMPanel_keywordField") { + // Check uri fields to enable accept button if input is valid + document.documentElement + .getButton("accept").disabled = !this._inputIsValid(); + } + break; + case "resize": + for (let [id, oldHeight] of elementsHeight) { + let newHeight = document.getElementById(id).boxObject.height; + this._height += - oldHeight + newHeight; + elementsHeight.set(id, newHeight); + } + break; + } + }, + + _beginBatch: function BPP__beginBatch() { + if (this._batching) + return; + + PlacesUtils.transactionManager.beginBatch(null); + this._batching = true; + }, + + _endBatch: function BPP__endBatch() { + if (!this._batching) + return; + + PlacesUtils.transactionManager.endBatch(false); + this._batching = false; + }, + + _fillEditProperties: function BPP__fillEditProperties() { + gEditItemOverlay.initPanel(this._itemId, + { hiddenRows: this._hiddenRows, + forceReadOnly: this._readOnly }); + }, + + _fillAddProperties: Task.async(function* BPP__fillAddProperties() { + yield this._createNewItem(); + // Edit the new item + gEditItemOverlay.initPanel(this._itemId, + { hiddenRows: this._hiddenRows }); + // Empty location field if the uri is about:blank, this way inserting a new + // url will be easier for the user, Accept button will be automatically + // disabled by the input listener until the user fills the field. + var locationField = this._element("locationField"); + if (locationField.value == "about:blank") + locationField.value = ""; + }), + + // nsISupports + QueryInterface: function BPP_QueryInterface(aIID) { + if (aIID.equals(Ci.nsIDOMEventListener) || + aIID.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_NOINTERFACE; + }, + + _element: function BPP__element(aID) { + return document.getElementById("editBMPanel_" + aID); + }, + + onDialogUnload: function BPP_onDialogUnload() { + // gEditItemOverlay does not exist anymore here, so don't rely on it. + this._mutationObserver.disconnect(); + delete this._mutationObserver; + + window.removeEventListener("resize", this); + + // Calling removeEventListener with arguments which do not identify any + // currently registered EventListener on the EventTarget has no effect. + this._element("locationField") + .removeEventListener("input", this, false); + this._element("feedLocationField") + .removeEventListener("input", this, false); + this._element("siteLocationField") + .removeEventListener("input", this, false); + }, + + onDialogAccept: function BPP_onDialogAccept() { + // We must blur current focused element to save its changes correctly + document.commandDispatcher.focusedElement.blur(); + // The order here is important! We have to uninit the panel first, otherwise + // late changes could force it to commit more transactions. + gEditItemOverlay.uninitPanel(true); + this._endBatch(); + window.arguments[0].performed = true; + }, + + onDialogCancel: function BPP_onDialogCancel() { + // The order here is important! We have to uninit the panel first, otherwise + // changes done as part of Undo may change the panel contents and by + // that force it to commit more transactions. + gEditItemOverlay.uninitPanel(true); + this._endBatch(); + PlacesUtils.transactionManager.undoTransaction(); + window.arguments[0].performed = false; + }, + + /** + * This method checks to see if the input fields are in a valid state. + * + * @returns true if the input is valid, false otherwise + */ + _inputIsValid: function BPP__inputIsValid() { + if (this._itemType == BOOKMARK_ITEM && + !this._containsValidURI("locationField")) + return false; + if (this._isAddKeywordDialog && !this._element("keywordField").value.length) + return false; + + return true; + }, + + /** + * Determines whether the XUL textbox with the given ID contains a + * string that can be converted into an nsIURI. + * + * @param aTextboxID + * the ID of the textbox element whose contents we'll test + * + * @returns true if the textbox contains a valid URI string, false otherwise + */ + _containsValidURI: function BPP__containsValidURI(aTextboxID) { + try { + var value = this._element(aTextboxID).value; + if (value) { + PlacesUIUtils.createFixedURI(value); + return true; + } + } catch (e) { } + return false; + }, + + /** + * [New Item Mode] Get the insertion point details for the new item, given + * dialog state and opening arguments. + * + * The container-identifier and insertion-index are returned separately in + * the form of [containerIdentifier, insertionIndex] + */ + _getInsertionPointDetails: function BPP__getInsertionPointDetails() { + var containerId = this._defaultInsertionPoint.itemId; + var indexInContainer = this._defaultInsertionPoint.index; + + return [containerId, indexInContainer]; + }, + + /** + * Returns a transaction for creating a new bookmark item representing the + * various fields and opening arguments of the dialog. + */ + _getCreateNewBookmarkTransaction: + function BPP__getCreateNewBookmarkTransaction(aContainer, aIndex) { + var annotations = []; + var childTransactions = []; + + if (this._description) { + let annoObj = { name : PlacesUIUtils.DESCRIPTION_ANNO, + type : Ci.nsIAnnotationService.TYPE_STRING, + flags : 0, + value : this._description, + expires: Ci.nsIAnnotationService.EXPIRE_NEVER }; + let editItemTxn = new PlacesSetItemAnnotationTransaction(-1, annoObj); + childTransactions.push(editItemTxn); + } + + if (this._loadInSidebar) { + let annoObj = { name : PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO, + value : true }; + let setLoadTxn = new PlacesSetItemAnnotationTransaction(-1, annoObj); + childTransactions.push(setLoadTxn); + } + + if (this._postData) { + let postDataTxn = new PlacesEditBookmarkPostDataTransaction(-1, this._postData); + childTransactions.push(postDataTxn); + } + + //XXX TODO: this should be in a transaction! + if (this._charSet && !PrivateBrowsingUtils.isWindowPrivate(window)) + PlacesUtils.setCharsetForURI(this._uri, this._charSet); + + let createTxn = new PlacesCreateBookmarkTransaction(this._uri, + aContainer, + aIndex, + this._title, + this._keyword, + annotations, + childTransactions); + + return new PlacesAggregatedTransaction(this._getDialogTitle(), + [createTxn]); + }, + + /** + * Returns a childItems-transactions array representing the URIList with + * which the dialog has been opened. + */ + _getTransactionsForURIList: function BPP__getTransactionsForURIList() { + var transactions = []; + for (var i = 0; i < this._URIs.length; ++i) { + var uri = this._URIs[i]; + var title = this._getURITitleFromHistory(uri); + var createTxn = new PlacesCreateBookmarkTransaction(uri, -1, + PlacesUtils.bookmarks.DEFAULT_INDEX, + title); + transactions.push(createTxn); + } + return transactions; + }, + + /** + * Returns a transaction for creating a new folder item representing the + * various fields and opening arguments of the dialog. + */ + _getCreateNewFolderTransaction: + function BPP__getCreateNewFolderTransaction(aContainer, aIndex) { + var annotations = []; + var childItemsTransactions; + if (this._URIs.length) + childItemsTransactions = this._getTransactionsForURIList(); + + if (this._description) + annotations.push(this._getDescriptionAnnotation(this._description)); + + return new PlacesCreateFolderTransaction(this._title, aContainer, + aIndex, annotations, + childItemsTransactions); + }, + + /** + * Returns a transaction for creating a new live-bookmark item representing + * the various fields and opening arguments of the dialog. + */ + _getCreateNewLivemarkTransaction: + function BPP__getCreateNewLivemarkTransaction(aContainer, aIndex) { + return new PlacesCreateLivemarkTransaction(this._feedURI, this._siteURI, + this._title, + aContainer, aIndex); + }, + + /** + * Dialog-accept code-path for creating a new item (any type) + */ + _createNewItem: Task.async(function* BPP__getCreateItemTransaction() { + var [container, index] = this._getInsertionPointDetails(); + var txn; + + switch (this._itemType) { + case BOOKMARK_FOLDER: + txn = this._getCreateNewFolderTransaction(container, index); + break; + case LIVEMARK_CONTAINER: + txn = this._getCreateNewLivemarkTransaction(container, index); + break; + default: // BOOKMARK_ITEM + txn = this._getCreateNewBookmarkTransaction(container, index); + } + + PlacesUtils.transactionManager.doTransaction(txn); + // This is a temporary hack until we use PlacesTransactions.jsm + if (txn._promise) { + yield txn._promise; + } + + let folderGuid = yield PlacesUtils.promiseItemGuid(container); + let bm = yield PlacesUtils.bookmarks.fetch({ + parentGuid: folderGuid, + index: index + }); + this._itemId = yield PlacesUtils.promiseItemId(bm.guid); + }) +}; diff --git a/components/places/content/bookmarkProperties.xul b/components/places/content/bookmarkProperties.xul new file mode 100644 index 0000000..2c04f8b --- /dev/null +++ b/components/places/content/bookmarkProperties.xul @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + %editBookmarkOverlayDTD; +]> + + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/places/content/editBookmarkOverlay.js b/components/places/content/editBookmarkOverlay.js new file mode 100644 index 0000000..69d7d32 --- /dev/null +++ b/components/places/content/editBookmarkOverlay.js @@ -0,0 +1,1063 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const LAST_USED_ANNO = "bookmarkPropertiesDialog/folderLastUsed"; +const MAX_FOLDER_ITEM_IN_MENU_LIST = 5; + +var gEditItemOverlay = { + _uri: null, + _itemId: -1, + _itemIds: [], + _uris: [], + _tags: [], + _allTags: [], + _keyword: null, + _multiEdit: false, + _itemType: -1, + _readOnly: false, + _hiddenRows: [], + _onPanelReady: false, + _observersAdded: false, + _staticFoldersListBuilt: false, + _initialized: false, + _titleOverride: "", + + // the first field which was edited after this panel was initialized for + // a certain item + _firstEditedField: "", + + get itemId() { + return this._itemId; + }, + + get uri() { + return this._uri; + }, + + get multiEdit() { + return this._multiEdit; + }, + + /** + * Determines the initial data for the item edited or added by this dialog + */ + _determineInfo: function EIO__determineInfo(aInfo) { + // hidden rows + if (aInfo && aInfo.hiddenRows) + this._hiddenRows = aInfo.hiddenRows; + else + this._hiddenRows.splice(0, this._hiddenRows.length); + // force-read-only + this._readOnly = aInfo && aInfo.forceReadOnly; + this._titleOverride = aInfo && aInfo.titleOverride ? aInfo.titleOverride + : ""; + this._onPanelReady = aInfo && aInfo.onPanelReady; + }, + + _showHideRows: function EIO__showHideRows() { + var isBookmark = this._itemId != -1 && + this._itemType == Ci.nsINavBookmarksService.TYPE_BOOKMARK; + var isQuery = false; + if (this._uri) + isQuery = this._uri.schemeIs("place"); + + this._element("nameRow").collapsed = this._hiddenRows.indexOf("name") != -1; + this._element("folderRow").collapsed = + this._hiddenRows.indexOf("folderPicker") != -1 || this._readOnly; + this._element("tagsRow").collapsed = !this._uri || + this._hiddenRows.indexOf("tags") != -1 || isQuery; + // Collapse the tag selector if the item does not accept tags. + if (!this._element("tagsSelectorRow").collapsed && + this._element("tagsRow").collapsed) + this.toggleTagsSelector(); + this._element("descriptionRow").collapsed = + this._hiddenRows.indexOf("description") != -1 || this._readOnly; + this._element("keywordRow").collapsed = !isBookmark || this._readOnly || + this._hiddenRows.indexOf("keyword") != -1 || isQuery; + this._element("locationRow").collapsed = !(this._uri && !isQuery) || + this._hiddenRows.indexOf("location") != -1; + this._element("loadInSidebarCheckbox").collapsed = !isBookmark || isQuery || + this._readOnly || this._hiddenRows.indexOf("loadInSidebar") != -1; + this._element("feedLocationRow").collapsed = !this._isLivemark || + this._hiddenRows.indexOf("feedLocation") != -1; + this._element("siteLocationRow").collapsed = !this._isLivemark || + this._hiddenRows.indexOf("siteLocation") != -1; + this._element("selectionCount").hidden = !this._multiEdit; + }, + + /** + * Initialize the panel + * @param aFor + * Either a places-itemId (of a bookmark, folder or a live bookmark), + * an array of itemIds (used for bulk tagging), or a URI object (in + * which case, the panel would be initialized in read-only mode). + * @param [optional] aInfo + * JS object which stores additional info for the panel + * initialization. The following properties may bet set: + * * hiddenRows (Strings array): list of rows to be hidden regardless + * of the item edited. Possible values: "title", "location", + * "description", "keyword", "loadInSidebar", "feedLocation", + * "siteLocation", folderPicker" + * * forceReadOnly - set this flag to initialize the panel to its + * read-only (view) mode even if the given item is editable. + */ + initPanel: function EIO_initPanel(aFor, aInfo) { + // For sanity ensure that the implementer has uninited the panel before + // trying to init it again, or we could end up leaking due to observers. + if (this._initialized) + this.uninitPanel(false); + + var aItemIdList; + if (Array.isArray(aFor)) { + aItemIdList = aFor; + aFor = aItemIdList[0]; + } + else if (this._multiEdit) { + this._multiEdit = false; + this._tags = []; + this._uris = []; + this._allTags = []; + this._itemIds = []; + this._element("selectionCount").hidden = true; + } + + this._folderMenuList = this._element("folderMenuList"); + this._folderTree = this._element("folderTree"); + + this._determineInfo(aInfo); + if (aFor instanceof Ci.nsIURI) { + this._itemId = -1; + this._uri = aFor; + this._readOnly = true; + } + else { + this._itemId = aFor; + // We can't store information on invalid itemIds. + this._readOnly = this._readOnly || this._itemId == -1; + + var containerId = PlacesUtils.bookmarks.getFolderIdForItem(this._itemId); + this._itemType = PlacesUtils.bookmarks.getItemType(this._itemId); + if (this._itemType == Ci.nsINavBookmarksService.TYPE_BOOKMARK) { + this._uri = PlacesUtils.bookmarks.getBookmarkURI(this._itemId); + this._keyword = PlacesUtils.bookmarks.getKeywordForBookmark(this._itemId); + this._initTextField("keywordField", this._keyword); + this._element("loadInSidebarCheckbox").checked = + PlacesUtils.annotations.itemHasAnnotation(this._itemId, + PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO); + } + else { + this._uri = null; + this._isLivemark = false; + PlacesUtils.livemarks.getLivemark({id: this._itemId }) + .then(aLivemark => { + this._isLivemark = true; + this._initTextField("feedLocationField", aLivemark.feedURI.spec, true); + this._initTextField("siteLocationField", aLivemark.siteURI ? aLivemark.siteURI.spec : "", true); + this._showHideRows(); + }, () => undefined); + } + + // folder picker + this._initFolderMenuList(containerId); + + // description field + this._initTextField("descriptionField", + PlacesUIUtils.getItemDescription(this._itemId)); + } + + if (this._itemId == -1 || + this._itemType == Ci.nsINavBookmarksService.TYPE_BOOKMARK) { + this._isLivemark = false; + + this._initTextField("locationField", this._uri.spec); + if (!aItemIdList) { + var tags = PlacesUtils.tagging.getTagsForURI(this._uri).join(", "); + this._initTextField("tagsField", tags, false); + } + else { + this._multiEdit = true; + this._allTags = []; + this._itemIds = aItemIdList; + for (var i = 0; i < aItemIdList.length; i++) { + if (aItemIdList[i] instanceof Ci.nsIURI) { + this._uris[i] = aItemIdList[i]; + this._itemIds[i] = -1; + } + else + this._uris[i] = PlacesUtils.bookmarks.getBookmarkURI(this._itemIds[i]); + this._tags[i] = PlacesUtils.tagging.getTagsForURI(this._uris[i]); + } + this._allTags = this._getCommonTags(); + this._initTextField("tagsField", this._allTags.join(", "), false); + this._element("itemsCountText").value = + PlacesUIUtils.getPluralString("detailsPane.itemsCountLabel", + this._itemIds.length, + [this._itemIds.length]); + } + + // tags selector + this._rebuildTagsSelectorList(); + } + + // name picker + this._initNamePicker(); + + this._showHideRows(); + + // observe changes + if (!this._observersAdded) { + // Single bookmarks observe any change. History entries and multiEdit + // observe only tags changes, through bookmarks. + if (this._itemId != -1 || this._uri || this._multiEdit) + PlacesUtils.bookmarks.addObserver(this, false); + + this._element("namePicker").addEventListener("blur", this); + this._element("locationField").addEventListener("blur", this); + this._element("tagsField").addEventListener("blur", this); + this._element("keywordField").addEventListener("blur", this); + this._element("descriptionField").addEventListener("blur", this); + window.addEventListener("unload", this, false); + this._observersAdded = true; + } + + let focusElement = () => { + this._initialized = true; + }; + + if (this._onPanelReady) { + this._onPanelReady(focusElement); + } else { + focusElement(); + } + }, + + /** + * Finds tags that are in common among this._tags entries that track tags + * for each selected uri. + * The tags arrays should be kept up-to-date for this to work properly. + * + * @return array of common tags for the selected uris. + */ + _getCommonTags: function() { + return this._tags[0].filter( + function (aTag) this._tags.every( + function (aTags) aTags.indexOf(aTag) != -1 + ), this + ); + }, + + _initTextField: function(aTextFieldId, aValue, aReadOnly) { + var field = this._element(aTextFieldId); + field.readOnly = aReadOnly !== undefined ? aReadOnly : this._readOnly; + + if (field.value != aValue) { + field.value = aValue; + this._editorTransactionManagerClear(field); + } + }, + + /** + * Appends a menu-item representing a bookmarks folder to a menu-popup. + * @param aMenupopup + * The popup to which the menu-item should be added. + * @param aFolderId + * The identifier of the bookmarks folder. + * @return the new menu item. + */ + _appendFolderItemToMenupopup: + function EIO__appendFolderItemToMenuList(aMenupopup, aFolderId) { + // First make sure the folders-separator is visible + this._element("foldersSeparator").hidden = false; + + var folderMenuItem = document.createElement("menuitem"); + var folderTitle = PlacesUtils.bookmarks.getItemTitle(aFolderId) + folderMenuItem.folderId = aFolderId; + folderMenuItem.setAttribute("label", folderTitle); + folderMenuItem.className = "menuitem-iconic folder-icon"; + aMenupopup.appendChild(folderMenuItem); + return folderMenuItem; + }, + + _initFolderMenuList: function EIO__initFolderMenuList(aSelectedFolder) { + // clean up first + var menupopup = this._folderMenuList.menupopup; + while (menupopup.childNodes.length > 6) + menupopup.removeChild(menupopup.lastChild); + + const bms = PlacesUtils.bookmarks; + const annos = PlacesUtils.annotations; + + // Build the static list + var unfiledItem = this._element("unfiledRootItem"); + if (!this._staticFoldersListBuilt) { + unfiledItem.label = bms.getItemTitle(PlacesUtils.unfiledBookmarksFolderId); + unfiledItem.folderId = PlacesUtils.unfiledBookmarksFolderId; + var bmMenuItem = this._element("bmRootItem"); + bmMenuItem.label = bms.getItemTitle(PlacesUtils.bookmarksMenuFolderId); + bmMenuItem.folderId = PlacesUtils.bookmarksMenuFolderId; + var toolbarItem = this._element("toolbarFolderItem"); + toolbarItem.label = bms.getItemTitle(PlacesUtils.toolbarFolderId); + toolbarItem.folderId = PlacesUtils.toolbarFolderId; + this._staticFoldersListBuilt = true; + } + + // List of recently used folders: + var folderIds = annos.getItemsWithAnnotation(LAST_USED_ANNO); + + /** + * The value of the LAST_USED_ANNO annotation is the time (in the form of + * Date.getTime) at which the folder has been last used. + * + * First we build the annotated folders array, each item has both the + * folder identifier and the time at which it was last-used by this dialog + * set. Then we sort it descendingly based on the time field. + */ + this._recentFolders = []; + for (var i = 0; i < folderIds.length; i++) { + var lastUsed = annos.getItemAnnotation(folderIds[i], LAST_USED_ANNO); + this._recentFolders.push({ folderId: folderIds[i], lastUsed: lastUsed }); + } + this._recentFolders.sort(function(a, b) { + if (b.lastUsed < a.lastUsed) + return -1; + if (b.lastUsed > a.lastUsed) + return 1; + return 0; + }); + + var numberOfItems = Math.min(MAX_FOLDER_ITEM_IN_MENU_LIST, + this._recentFolders.length); + for (var i = 0; i < numberOfItems; i++) { + this._appendFolderItemToMenupopup(menupopup, + this._recentFolders[i].folderId); + } + + var defaultItem = this._getFolderMenuItem(aSelectedFolder); + this._folderMenuList.selectedItem = defaultItem; + + // Set a selectedIndex attribute to show special icons + this._folderMenuList.setAttribute("selectedIndex", + this._folderMenuList.selectedIndex); + + // Hide the folders-separator if no folder is annotated as recently-used + this._element("foldersSeparator").hidden = (menupopup.childNodes.length <= 6); + this._folderMenuList.disabled = this._readOnly; + }, + + QueryInterface: function EIO_QueryInterface(aIID) { + if (aIID.equals(Ci.nsIDOMEventListener) || + aIID.equals(Ci.nsINavBookmarkObserver) || + aIID.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + _element: function EIO__element(aID) { + return document.getElementById("editBMPanel_" + aID); + }, + + _editorTransactionManagerClear: function EIO__editorTransactionManagerClear(aItem) { + // Clear the editor's undo stack + let transactionManager; + try { + transactionManager = aItem.editor.transactionManager; + } catch (e) { + // When retrieving the transaction manager, editor may be null resulting + // in a TypeError. Additionally, the transaction manager may not + // exist yet, which causes access to it to throw NS_ERROR_FAILURE. + // In either event, the transaction manager doesn't exist it, so we + // don't need to worry about clearing it. + if (!(e instanceof TypeError) && e.result != Cr.NS_ERROR_FAILURE) { + throw e; + } + } + if (transactionManager) { + transactionManager.clear(); + } + }, + + _getItemStaticTitle: function EIO__getItemStaticTitle() { + if (this._titleOverride) + return this._titleOverride; + + let title = ""; + if (this._itemId == -1) { + title = PlacesUtils.history.getPageTitle(this._uri); + } + else { + title = PlacesUtils.bookmarks.getItemTitle(this._itemId); + } + return title; + }, + + _initNamePicker: function EIO_initNamePicker() { + var namePicker = this._element("namePicker"); + namePicker.value = this._getItemStaticTitle(); + namePicker.readOnly = this._readOnly; + this._editorTransactionManagerClear(namePicker); + }, + + uninitPanel: function EIO_uninitPanel(aHideCollapsibleElements) { + if (aHideCollapsibleElements) { + // hide the folder tree if it was previously visible + var folderTreeRow = this._element("folderTreeRow"); + if (!folderTreeRow.collapsed) + this.toggleFolderTreeVisibility(); + + // hide the tag selector if it was previously visible + var tagsSelectorRow = this._element("tagsSelectorRow"); + if (!tagsSelectorRow.collapsed) + this.toggleTagsSelector(); + } + + if (this._observersAdded) { + if (this._itemId != -1 || this._uri || this._multiEdit) + PlacesUtils.bookmarks.removeObserver(this); + + this._element("namePicker").removeEventListener("blur", this); + this._element("locationField").removeEventListener("blur", this); + this._element("tagsField").removeEventListener("blur", this); + this._element("keywordField").removeEventListener("blur", this); + this._element("descriptionField").removeEventListener("blur", this); + + this._observersAdded = false; + } + + this._itemId = -1; + this._uri = null; + this._uris = []; + this._tags = []; + this._allTags = []; + this._itemIds = []; + this._multiEdit = false; + this._firstEditedField = ""; + this._initialized = false; + this._titleOverride = ""; + this._readOnly = false; + }, + + onTagsFieldBlur: function EIO_onTagsFieldBlur() { + if (this._updateTags()) // if anything has changed + this._mayUpdateFirstEditField("tagsField"); + }, + + _updateTags: function EIO__updateTags() { + if (this._multiEdit) + return this._updateMultipleTagsForItems(); + return this._updateSingleTagForItem(); + }, + + _updateSingleTagForItem: function EIO__updateSingleTagForItem() { + var currentTags = PlacesUtils.tagging.getTagsForURI(this._uri); + var tags = this._getTagsArrayFromTagField(); + if (tags.length > 0 || currentTags.length > 0) { + var tagsToRemove = []; + var tagsToAdd = []; + var txns = []; + for (var i = 0; i < currentTags.length; i++) { + if (tags.indexOf(currentTags[i]) == -1) + tagsToRemove.push(currentTags[i]); + } + for (var i = 0; i < tags.length; i++) { + if (currentTags.indexOf(tags[i]) == -1) + tagsToAdd.push(tags[i]); + } + + if (tagsToRemove.length > 0) { + let untagTxn = new PlacesUntagURITransaction(this._uri, tagsToRemove); + txns.push(untagTxn); + } + if (tagsToAdd.length > 0) { + let tagTxn = new PlacesTagURITransaction(this._uri, tagsToAdd); + txns.push(tagTxn); + } + + if (txns.length > 0) { + let aggregate = new PlacesAggregatedTransaction("Update tags", txns); + PlacesUtils.transactionManager.doTransaction(aggregate); + + // Ensure the tagsField is in sync, clean it up from empty tags + var tags = PlacesUtils.tagging.getTagsForURI(this._uri).join(", "); + this._initTextField("tagsField", tags, false); + return true; + } + } + return false; + }, + + /** + * Stores the first-edit field for this dialog, if the passed-in field + * is indeed the first edited field + * @param aNewField + * the id of the field that may be set (without the "editBMPanel_" + * prefix) + */ + _mayUpdateFirstEditField: function EIO__mayUpdateFirstEditField(aNewField) { + // * The first-edit-field behavior is not applied in the multi-edit case + // * if this._firstEditedField is already set, this is not the first field, + // so there's nothing to do + if (this._multiEdit || this._firstEditedField) + return; + + this._firstEditedField = aNewField; + + // set the pref + var prefs = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch); + prefs.setCharPref("browser.bookmarks.editDialog.firstEditField", aNewField); + }, + + _updateMultipleTagsForItems: function EIO__updateMultipleTagsForItems() { + var tags = this._getTagsArrayFromTagField(); + if (tags.length > 0 || this._allTags.length > 0) { + var tagsToRemove = []; + var tagsToAdd = []; + var txns = []; + for (var i = 0; i < this._allTags.length; i++) { + if (tags.indexOf(this._allTags[i]) == -1) + tagsToRemove.push(this._allTags[i]); + } + for (var i = 0; i < this._tags.length; i++) { + tagsToAdd[i] = []; + for (var j = 0; j < tags.length; j++) { + if (this._tags[i].indexOf(tags[j]) == -1) + tagsToAdd[i].push(tags[j]); + } + } + + if (tagsToAdd.length > 0) { + for (let i = 0; i < this._uris.length; i++) { + if (tagsToAdd[i].length > 0) { + let tagTxn = new PlacesTagURITransaction(this._uris[i], + tagsToAdd[i]); + txns.push(tagTxn); + } + } + } + if (tagsToRemove.length > 0) { + for (let i = 0; i < this._uris.length; i++) { + let untagTxn = new PlacesUntagURITransaction(this._uris[i], + tagsToRemove); + txns.push(untagTxn); + } + } + + if (txns.length > 0) { + let aggregate = new PlacesAggregatedTransaction("Update tags", txns); + PlacesUtils.transactionManager.doTransaction(aggregate); + + this._allTags = tags; + this._tags = []; + for (let i = 0; i < this._uris.length; i++) { + this._tags[i] = PlacesUtils.tagging.getTagsForURI(this._uris[i]); + } + + // Ensure the tagsField is in sync, clean it up from empty tags + this._initTextField("tagsField", tags, false); + return true; + } + } + return false; + }, + + onNamePickerBlur: function EIO_onNamePickerBlur() { + if (this._itemId == -1) + return; + + var namePicker = this._element("namePicker") + + // Here we update either the item title or its cached static title + var newTitle = namePicker.value; + if (!newTitle && + PlacesUtils.bookmarks.getFolderIdForItem(this._itemId) == PlacesUtils.tagsFolderId) { + // We don't allow setting an empty title for a tag, restore the old one. + this._initNamePicker(); + } + else if (this._getItemStaticTitle() != newTitle) { + this._mayUpdateFirstEditField("namePicker"); + let txn = new PlacesEditItemTitleTransaction(this._itemId, newTitle); + PlacesUtils.transactionManager.doTransaction(txn); + } + }, + + onDescriptionFieldBlur: function EIO_onDescriptionFieldBlur() { + var description = this._element("descriptionField").value; + if (description != PlacesUIUtils.getItemDescription(this._itemId)) { + var annoObj = { name : PlacesUIUtils.DESCRIPTION_ANNO, + type : Ci.nsIAnnotationService.TYPE_STRING, + flags : 0, + value : description, + expires: Ci.nsIAnnotationService.EXPIRE_NEVER }; + var txn = new PlacesSetItemAnnotationTransaction(this._itemId, annoObj); + PlacesUtils.transactionManager.doTransaction(txn); + } + }, + + onLocationFieldBlur: function EIO_onLocationFieldBlur() { + var uri; + try { + uri = PlacesUIUtils.createFixedURI(this._element("locationField").value); + } + catch(ex) { return; } + + if (!this._uri.equals(uri)) { + var txn = new PlacesEditBookmarkURITransaction(this._itemId, uri); + PlacesUtils.transactionManager.doTransaction(txn); + this._uri = uri; + } + }, + + onKeywordFieldBlur: function EIO_onKeywordFieldBlur() { + let oldKeyword = this._keyword; + let keyword = this._keyword = this._element("keywordField").value; + if (keyword != oldKeyword) { + let txn = new PlacesEditBookmarkKeywordTransaction(this._itemId, + keyword, + null, + oldKeyword); + PlacesUtils.transactionManager.doTransaction(txn); + } + }, + + onLoadInSidebarCheckboxCommand: + function EIO_onLoadInSidebarCheckboxCommand() { + let annoObj = { name : PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO }; + if (this._element("loadInSidebarCheckbox").checked) + annoObj.value = true; + let txn = new PlacesSetItemAnnotationTransaction(this._itemId, annoObj); + PlacesUtils.transactionManager.doTransaction(txn); + }, + + toggleFolderTreeVisibility: function EIO_toggleFolderTreeVisibility() { + var expander = this._element("foldersExpander"); + var folderTreeRow = this._element("folderTreeRow"); + if (!folderTreeRow.collapsed) { + expander.className = "expander-down"; + expander.setAttribute("tooltiptext", + expander.getAttribute("tooltiptextdown")); + folderTreeRow.collapsed = true; + this._element("chooseFolderSeparator").hidden = + this._element("chooseFolderMenuItem").hidden = false; + } + else { + expander.className = "expander-up" + expander.setAttribute("tooltiptext", + expander.getAttribute("tooltiptextup")); + folderTreeRow.collapsed = false; + + // XXXmano: Ideally we would only do this once, but for some odd reason, + // the editable mode set on this tree, together with its collapsed state + // breaks the view. + const FOLDER_TREE_PLACE_URI = + "place:excludeItems=1&excludeQueries=1&excludeReadOnlyFolders=1&folder=" + + PlacesUIUtils.allBookmarksFolderId; + this._folderTree.place = FOLDER_TREE_PLACE_URI; + + this._element("chooseFolderSeparator").hidden = + this._element("chooseFolderMenuItem").hidden = true; + var currentFolder = this._getFolderIdFromMenuList(); + this._folderTree.selectItems([currentFolder]); + this._folderTree.focus(); + } + }, + + _getFolderIdFromMenuList: + function EIO__getFolderIdFromMenuList() { + var selectedItem = this._folderMenuList.selectedItem; + NS_ASSERT("folderId" in selectedItem, + "Invalid menuitem in the folders-menulist"); + return selectedItem.folderId; + }, + + /** + * Get the corresponding menu-item in the folder-menu-list for a bookmarks + * folder if such an item exists. Otherwise, this creates a menu-item for the + * folder. If the items-count limit (see MAX_FOLDERS_IN_MENU_LIST) is reached, + * the new item replaces the last menu-item. + * @param aFolderId + * The identifier of the bookmarks folder. + */ + _getFolderMenuItem: + function EIO__getFolderMenuItem(aFolderId) { + var menupopup = this._folderMenuList.menupopup; + + for (let i = 0; i < menupopup.childNodes.length; i++) { + if ("folderId" in menupopup.childNodes[i] && + menupopup.childNodes[i].folderId == aFolderId) + return menupopup.childNodes[i]; + } + + // 3 special folders + separator + folder-items-count limit + if (menupopup.childNodes.length == 4 + MAX_FOLDER_ITEM_IN_MENU_LIST) + menupopup.removeChild(menupopup.lastChild); + + return this._appendFolderItemToMenupopup(menupopup, aFolderId); + }, + + onFolderMenuListCommand: function EIO_onFolderMenuListCommand(aEvent) { + // Set a selectedIndex attribute to show special icons + this._folderMenuList.setAttribute("selectedIndex", + this._folderMenuList.selectedIndex); + + if (aEvent.target.id == "editBMPanel_chooseFolderMenuItem") { + // reset the selection back to where it was and expand the tree + // (this menu-item is hidden when the tree is already visible + var container = PlacesUtils.bookmarks.getFolderIdForItem(this._itemId); + var item = this._getFolderMenuItem(container); + this._folderMenuList.selectedItem = item; + // XXXmano HACK: setTimeout 100, otherwise focus goes back to the + // menulist right away + setTimeout(function(self) self.toggleFolderTreeVisibility(), 100, this); + return; + } + + // Move the item + var container = this._getFolderIdFromMenuList(); + if (PlacesUtils.bookmarks.getFolderIdForItem(this._itemId) != container) { + var txn = new PlacesMoveItemTransaction(this._itemId, + container, + PlacesUtils.bookmarks.DEFAULT_INDEX); + PlacesUtils.transactionManager.doTransaction(txn); + + // Mark the containing folder as recently-used if it isn't in the + // static list + if (container != PlacesUtils.unfiledBookmarksFolderId && + container != PlacesUtils.toolbarFolderId && + container != PlacesUtils.bookmarksMenuFolderId) + this._markFolderAsRecentlyUsed(container); + } + + // Update folder-tree selection + var folderTreeRow = this._element("folderTreeRow"); + if (!folderTreeRow.collapsed) { + var selectedNode = this._folderTree.selectedNode; + if (!selectedNode || + PlacesUtils.getConcreteItemId(selectedNode) != container) + this._folderTree.selectItems([container]); + } + }, + + onFolderTreeSelect: function EIO_onFolderTreeSelect() { + var selectedNode = this._folderTree.selectedNode; + + // Disable the "New Folder" button if we cannot create a new folder + this._element("newFolderButton") + .disabled = !this._folderTree.insertionPoint || !selectedNode; + + if (!selectedNode) + return; + + var folderId = PlacesUtils.getConcreteItemId(selectedNode); + if (this._getFolderIdFromMenuList() == folderId) + return; + + var folderItem = this._getFolderMenuItem(folderId); + this._folderMenuList.selectedItem = folderItem; + folderItem.doCommand(); + }, + + _markFolderAsRecentlyUsed: + function EIO__markFolderAsRecentlyUsed(aFolderId) { + var txns = []; + + // Expire old unused recent folders + var anno = this._getLastUsedAnnotationObject(false); + while (this._recentFolders.length > MAX_FOLDER_ITEM_IN_MENU_LIST) { + var folderId = this._recentFolders.pop().folderId; + let annoTxn = new PlacesSetItemAnnotationTransaction(folderId, anno); + txns.push(annoTxn); + } + + // Mark folder as recently used + anno = this._getLastUsedAnnotationObject(true); + let annoTxn = new PlacesSetItemAnnotationTransaction(aFolderId, anno); + txns.push(annoTxn); + + let aggregate = new PlacesAggregatedTransaction("Update last used folders", txns); + PlacesUtils.transactionManager.doTransaction(aggregate); + }, + + /** + * Returns an object which could then be used to set/unset the + * LAST_USED_ANNO annotation for a folder. + * + * @param aLastUsed + * Whether to set or unset the LAST_USED_ANNO annotation. + * @returns an object representing the annotation which could then be used + * with the transaction manager. + */ + _getLastUsedAnnotationObject: + function EIO__getLastUsedAnnotationObject(aLastUsed) { + var anno = { name: LAST_USED_ANNO, + type: Ci.nsIAnnotationService.TYPE_INT32, + flags: 0, + value: aLastUsed ? new Date().getTime() : null, + expires: Ci.nsIAnnotationService.EXPIRE_NEVER }; + + return anno; + }, + + _rebuildTagsSelectorList: function EIO__rebuildTagsSelectorList() { + var tagsSelector = this._element("tagsSelector"); + var tagsSelectorRow = this._element("tagsSelectorRow"); + if (tagsSelectorRow.collapsed) + return; + + // Save the current scroll position and restore it after the rebuild. + let firstIndex = tagsSelector.getIndexOfFirstVisibleRow(); + let selectedIndex = tagsSelector.selectedIndex; + let selectedTag = selectedIndex >= 0 ? tagsSelector.selectedItem.label + : null; + + while (tagsSelector.hasChildNodes()) + tagsSelector.removeChild(tagsSelector.lastChild); + + var tagsInField = this._getTagsArrayFromTagField(); + var allTags = PlacesUtils.tagging.allTags; + for (var i = 0; i < allTags.length; i++) { + var tag = allTags[i]; + var elt = document.createElement("listitem"); + elt.setAttribute("type", "checkbox"); + elt.setAttribute("label", tag); + if (tagsInField.indexOf(tag) != -1) + elt.setAttribute("checked", "true"); + tagsSelector.appendChild(elt); + if (selectedTag === tag) + selectedIndex = tagsSelector.getIndexOfItem(elt); + } + + // Restore position. + // The listbox allows to scroll only if the required offset doesn't + // overflow its capacity, thus need to adjust the index for removals. + firstIndex = + Math.min(firstIndex, + tagsSelector.itemCount - tagsSelector.getNumberOfVisibleRows()); + tagsSelector.scrollToIndex(firstIndex); + if (selectedIndex >= 0 && tagsSelector.itemCount > 0) { + selectedIndex = Math.min(selectedIndex, tagsSelector.itemCount - 1); + tagsSelector.selectedIndex = selectedIndex; + tagsSelector.ensureIndexIsVisible(selectedIndex); + } + }, + + toggleTagsSelector: function EIO_toggleTagsSelector() { + var tagsSelector = this._element("tagsSelector"); + var tagsSelectorRow = this._element("tagsSelectorRow"); + var expander = this._element("tagsSelectorExpander"); + if (tagsSelectorRow.collapsed) { + expander.className = "expander-up"; + expander.setAttribute("tooltiptext", + expander.getAttribute("tooltiptextup")); + tagsSelectorRow.collapsed = false; + this._rebuildTagsSelectorList(); + + // This is a no-op if we've added the listener. + tagsSelector.addEventListener("CheckboxStateChange", this, false); + } + else { + expander.className = "expander-down"; + expander.setAttribute("tooltiptext", + expander.getAttribute("tooltiptextdown")); + tagsSelectorRow.collapsed = true; + } + }, + + /** + * Splits "tagsField" element value, returning an array of valid tag strings. + * + * @return Array of tag strings found in the field value. + */ + _getTagsArrayFromTagField: function EIO__getTagsArrayFromTagField() { + let tags = this._element("tagsField").value; + return tags.trim() + .split(/\s*,\s*/) // Split on commas and remove spaces. + .filter(function (tag) tag.length > 0); // Kill empty tags. + }, + + newFolder: function EIO_newFolder() { + var ip = this._folderTree.insertionPoint; + + // default to the bookmarks menu folder + if (!ip || ip.itemId == PlacesUIUtils.allBookmarksFolderId) { + ip = new InsertionPoint(PlacesUtils.bookmarksMenuFolderId, + PlacesUtils.bookmarks.DEFAULT_INDEX, + Ci.nsITreeView.DROP_ON); + } + + // XXXmano: add a separate "New Folder" string at some point... + var defaultLabel = this._element("newFolderButton").label; + var txn = new PlacesCreateFolderTransaction(defaultLabel, ip.itemId, ip.index); + PlacesUtils.transactionManager.doTransaction(txn); + this._folderTree.focus(); + this._folderTree.selectItems([this._lastNewItem]); + this._folderTree.startEditing(this._folderTree.view.selection.currentIndex, + this._folderTree.columns.getFirstColumn()); + }, + + // nsIDOMEventListener + handleEvent: function EIO_nsIDOMEventListener(aEvent) { + switch (aEvent.type) { + case "CheckboxStateChange": + // Update the tags field when items are checked/unchecked in the listbox + var tags = this._getTagsArrayFromTagField(); + + if (aEvent.target.checked) { + if (tags.indexOf(aEvent.target.label) == -1) + tags.push(aEvent.target.label); + } + else { + var indexOfItem = tags.indexOf(aEvent.target.label); + if (indexOfItem != -1) + tags.splice(indexOfItem, 1); + } + this._element("tagsField").value = tags.join(", "); + this._updateTags(); + break; + case "blur": + let replaceFn = (str, firstLetter) => firstLetter.toUpperCase(); + let nodeName = aEvent.target.id.replace(/editBMPanel_(\w)/, replaceFn); + this["on" + nodeName + "Blur"](); + break; + case "unload": + this.uninitPanel(false); + break; + } + }, + + // nsINavBookmarkObserver + onItemChanged: function EIO_onItemChanged(aItemId, aProperty, + aIsAnnotationProperty, aValue, + aLastModified, aItemType) { + if (aProperty == "tags") { + // Tags case is special, since they should be updated if either: + // - the notification is for the edited bookmark + // - the notification is for the edited history entry + // - the notification is for one of edited uris + let shouldUpdateTagsField = this._itemId == aItemId; + if (this._itemId == -1 || this._multiEdit) { + // Check if the changed uri is part of the modified ones. + let changedURI = PlacesUtils.bookmarks.getBookmarkURI(aItemId); + let uris = this._multiEdit ? this._uris : [this._uri]; + uris.forEach(function (aURI, aIndex) { + if (aURI.equals(changedURI)) { + shouldUpdateTagsField = true; + if (this._multiEdit) { + this._tags[aIndex] = PlacesUtils.tagging.getTagsForURI(this._uris[aIndex]); + } + } + }, this); + } + + if (shouldUpdateTagsField) { + if (this._multiEdit) { + this._allTags = this._getCommonTags(); + this._initTextField("tagsField", this._allTags.join(", "), false); + } + else { + let tags = PlacesUtils.tagging.getTagsForURI(this._uri).join(", "); + this._initTextField("tagsField", tags, false); + } + } + + // Any tags change should be reflected in the tags selector. + this._rebuildTagsSelectorList(); + return; + } + + if (this._itemId != aItemId) { + if (aProperty == "title") { + // If the title of a folder which is listed within the folders + // menulist has been changed, we need to update the label of its + // representing element. + var menupopup = this._folderMenuList.menupopup; + for (let i = 0; i < menupopup.childNodes.length; i++) { + if ("folderId" in menupopup.childNodes[i] && + menupopup.childNodes[i].folderId == aItemId) { + menupopup.childNodes[i].label = aValue; + break; + } + } + } + + return; + } + + switch (aProperty) { + case "title": + var namePicker = this._element("namePicker"); + if (namePicker.value != aValue) { + namePicker.value = aValue; + this._editorTransactionManagerClear(namePicker); + } + break; + case "uri": + var locationField = this._element("locationField"); + if (locationField.value != aValue) { + this._uri = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService). + newURI(aValue, null, null); + this._initTextField("locationField", this._uri.spec); + this._initNamePicker(); + this._initTextField("tagsField", + PlacesUtils.tagging + .getTagsForURI(this._uri).join(", "), + false); + this._rebuildTagsSelectorList(); + } + break; + case "keyword": + this._keyword = PlacesUtils.bookmarks.getKeywordForBookmark(this._itemId); + this._initTextField("keywordField", this._keyword); + break; + case PlacesUIUtils.DESCRIPTION_ANNO: + this._initTextField("descriptionField", + PlacesUIUtils.getItemDescription(this._itemId)); + break; + case PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO: + this._element("loadInSidebarCheckbox").checked = + PlacesUtils.annotations.itemHasAnnotation(this._itemId, + PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO); + break; + case PlacesUtils.LMANNO_FEEDURI: + let feedURISpec = + PlacesUtils.annotations.getItemAnnotation(this._itemId, + PlacesUtils.LMANNO_FEEDURI); + this._initTextField("feedLocationField", feedURISpec, true); + break; + case PlacesUtils.LMANNO_SITEURI: + let siteURISpec = ""; + try { + siteURISpec = + PlacesUtils.annotations.getItemAnnotation(this._itemId, + PlacesUtils.LMANNO_SITEURI); + } catch (ex) {} + this._initTextField("siteLocationField", siteURISpec, true); + break; + } + }, + + onItemMoved: function EIO_onItemMoved(aItemId, aOldParent, aOldIndex, + aNewParent, aNewIndex, aItemType) { + if (aItemId != this._itemId || + aNewParent == this._getFolderIdFromMenuList()) + return; + + var folderItem = this._getFolderMenuItem(aNewParent); + + // just setting selectItem _does not_ trigger oncommand, so we don't + // recurse + this._folderMenuList.selectedItem = folderItem; + }, + + onItemAdded: function EIO_onItemAdded(aItemId, aParentId, aIndex, aItemType, + aURI) { + this._lastNewItem = aItemId; + }, + + onItemRemoved: function() { }, + onBeginUpdateBatch: function() { }, + onEndUpdateBatch: function() { }, + onItemVisited: function() { }, +}; diff --git a/components/places/content/editBookmarkOverlay.xul b/components/places/content/editBookmarkOverlay.xul new file mode 100644 index 0000000..196369d --- /dev/null +++ b/components/places/content/editBookmarkOverlay.xul @@ -0,0 +1,228 @@ + + + +%editBookmarkOverlayDTD; +]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +