123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676 |
- /* -*- 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);
- })
- };
|