/* -*- 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() { 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() { 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() { 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(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* () { 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(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() { if (this._batching) return; PlacesUtils.transactionManager.beginBatch(null); this._batching = true; }, _endBatch: function() { if (!this._batching) return; PlacesUtils.transactionManager.endBatch(false); this._batching = false; }, _fillEditProperties: function() { gEditItemOverlay.initPanel(this._itemId, { hiddenRows: this._hiddenRows, forceReadOnly: this._readOnly }); }, _fillAddProperties: Task.async(function* () { 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(aIID) { if (aIID.equals(Ci.nsIDOMEventListener) || aIID.equals(Ci.nsISupports)) return this; throw Cr.NS_NOINTERFACE; }, _element: function(aID) { return document.getElementById("editBMPanel_" + aID); }, onDialogUnload: function() { // 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() { // 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() { // 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() { 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(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() { 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(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() { 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(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(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* () { 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); }) };