summaryrefslogtreecommitdiffstats
path: root/browser/components/places/content/bookmarkProperties.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/places/content/bookmarkProperties.js')
-rw-r--r--browser/components/places/content/bookmarkProperties.js519
1 files changed, 519 insertions, 0 deletions
diff --git a/browser/components/places/content/bookmarkProperties.js b/browser/components/places/content/bookmarkProperties.js
new file mode 100644
index 0000000000..bc98820a7b
--- /dev/null
+++ b/browser/components/places/content/bookmarkProperties.js
@@ -0,0 +1,519 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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"
+ * - "folder"
+ * @ URIList (Array of nsIURI objects) - optional, list of uris to
+ * be bookmarked under the new folder.
+ * @ 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.
+ * @ 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 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"
+ * @ node (an nsINavHistoryResultNode object) - a node representing
+ * the bookmark.
+ * - "folder"
+ * @ node (an nsINavHistoryResultNode object) - a node representing
+ * 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"
+ * - "keyword"
+ * - "tags"
+ * - "folderPicker" - hides both the tree and the menu.
+ *
+ * window.arguments[0].bookmarkGuid is set to the guid of the item, if the
+ * dialog is accepted.
+ */
+
+/* import-globals-from editBookmark.js */
+
+/* Shared Places Import - change other consumers if you change this: */
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+ChromeUtils.defineESModuleGetters(this, {
+ PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
+});
+XPCOMUtils.defineLazyScriptGetter(
+ this,
+ "PlacesTreeView",
+ "chrome://browser/content/places/treeView.js"
+);
+XPCOMUtils.defineLazyScriptGetter(
+ this,
+ ["PlacesInsertionPoint", "PlacesController", "PlacesControllerDragHelper"],
+ "chrome://browser/content/places/controller.js"
+);
+/* End Shared Places Import */
+
+const BOOKMARK_ITEM = 0;
+const BOOKMARK_FOLDER = 1;
+
+const ACTION_EDIT = 0;
+const ACTION_ADD = 1;
+
+var BookmarkPropertiesPanel = {
+ /** UI Text Strings */
+ __strings: null,
+ get _strings() {
+ if (!this.__strings) {
+ this.__strings = document.getElementById("stringBundle");
+ }
+ return this.__strings;
+ },
+
+ _action: null,
+ _itemType: null,
+ _uri: null,
+ _title: "",
+ _URIs: [],
+ _keyword: "",
+ _postData: null,
+ _charSet: "",
+
+ _defaultInsertionPoint: null,
+ _hiddenRows: [],
+
+ /**
+ * @returns {string}
+ * This method returns the correct label for the dialog's "accept"
+ * button based on the variant of the dialog.
+ */
+ _getAcceptLabel: function BPP__getAcceptLabel() {
+ return this._strings.getString("dialogAcceptLabelSaveItem");
+ },
+
+ /**
+ * @returns {string}
+ * 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("dialogTitleAddNewBookmark2");
+ }
+
+ // add folder
+ if (this._itemType != BOOKMARK_FOLDER) {
+ throw new Error("Unknown item type");
+ }
+ if (this._URIs.length) {
+ return this._strings.getString("dialogTitleAddMulti");
+ }
+
+ return this._strings.getString("dialogTitleAddBookmarkFolder");
+ }
+ if (this._action == ACTION_EDIT) {
+ if (this._itemType === BOOKMARK_ITEM) {
+ return this._strings.getString("dialogTitleEditBookmark2");
+ }
+
+ return this._strings.getString("dialogTitleEditBookmarkFolder");
+ }
+ return "";
+ },
+
+ /**
+ * Determines the initial data for the item edited or added by this dialog
+ */
+ async _determineItemInfo() {
+ let dialogInfo = window.arguments[0];
+ this._action = dialogInfo.action == "add" ? ACTION_ADD : ACTION_EDIT;
+ this._hiddenRows = dialogInfo.hiddenRows ? dialogInfo.hiddenRows : [];
+ if (this._action == ACTION_ADD) {
+ if (!("type" in dialogInfo)) {
+ throw new Error("missing type property for add action");
+ }
+
+ if ("title" in dialogInfo) {
+ this._title = dialogInfo.title;
+ }
+
+ if ("defaultInsertionPoint" in dialogInfo) {
+ this._defaultInsertionPoint = dialogInfo.defaultInsertionPoint;
+ } else {
+ let parentGuid = await PlacesUIUtils.defaultParentGuid;
+ this._defaultInsertionPoint = new PlacesInsertionPoint({
+ parentGuid,
+ });
+ }
+
+ switch (dialogInfo.type) {
+ case "bookmark":
+ this._itemType = BOOKMARK_ITEM;
+ if ("uri" in dialogInfo) {
+ if (!(dialogInfo.uri instanceof Ci.nsIURI)) {
+ throw new Error("uri property should be a uri object");
+ }
+ this._uri = dialogInfo.uri;
+ if (typeof this._title != "string") {
+ this._title =
+ (await PlacesUtils.history.fetch(this._uri)) || this._uri.spec;
+ }
+ } else {
+ this._uri = Services.io.newURI("about:blank");
+ this._title = this._strings.getString("newBookmarkDefault");
+ this._dummyItem = true;
+ }
+
+ 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;
+ }
+ } else {
+ // edit
+ this._node = dialogInfo.node;
+ this._title = this._node.title;
+ if (PlacesUtils.nodeIsFolder(this._node)) {
+ this._itemType = BOOKMARK_FOLDER;
+ } else if (PlacesUtils.nodeIsURI(this._node)) {
+ this._itemType = BOOKMARK_ITEM;
+ }
+ }
+ },
+
+ /**
+ * This method should be called by the onload of the Bookmark Properties
+ * dialog to initialize the state of the panel.
+ */
+ async onDialogLoad() {
+ document.addEventListener("dialogaccept", function () {
+ BookmarkPropertiesPanel.onDialogAccept();
+ });
+ document.addEventListener("dialogcancel", function () {
+ BookmarkPropertiesPanel.onDialogCancel();
+ });
+
+ // Disable the buttons until we have all the information required.
+ let acceptButton = document
+ .getElementById("bookmarkpropertiesdialog")
+ .getButton("accept");
+ acceptButton.disabled = true;
+ await this._determineItemInfo();
+ document.title = this._getDialogTitle();
+
+ // Set adjustable title
+ let title = { raw: document.title };
+ document.documentElement.setAttribute("headertitle", JSON.stringify(title));
+
+ let iconUrl = this._getIconUrl();
+ if (iconUrl) {
+ document.documentElement.style.setProperty(
+ "--icon-url",
+ `url(${iconUrl})`
+ );
+ }
+
+ await this._initDialog();
+ },
+
+ _getIconUrl() {
+ let url = "chrome://browser/skin/bookmark-hollow.svg";
+
+ if (this._action === ACTION_EDIT && this._itemType === BOOKMARK_ITEM) {
+ url = window.arguments[0]?.node?.icon;
+ }
+
+ return url;
+ },
+
+ /**
+ * Initializes the dialog, gathering the required bookmark data. This function
+ * will enable the accept button (if appropraite) when it is complete.
+ */
+ async _initDialog() {
+ let acceptButton = document
+ .getElementById("bookmarkpropertiesdialog")
+ .getButton("accept");
+ acceptButton.label = this._getAcceptLabel();
+ let acceptButtonDisabled = false;
+
+ // Since elements can be unhidden asynchronously, we must observe their
+ // mutations and resize the dialog accordingly.
+ this._mutationObserver = new MutationObserver(mutations => {
+ for (let { target, oldValue } of mutations) {
+ let hidden = target.getAttribute("hidden") == "true";
+ if (
+ target.classList.contains("hideable") &&
+ hidden != (oldValue == "true")
+ ) {
+ // To support both kind of dialogs (window and dialog-box) we need
+ // both resizeBy and sizeToContent, otherwise either the dialog
+ // doesn't resize, or it gets empty unused space.
+ if (hidden) {
+ let diff = this._mutationObserver._heightsById.get(target.id);
+ window.resizeBy(0, -diff);
+ } else {
+ let diff = target.getBoundingClientRect().height;
+ this._mutationObserver._heightsById.set(target.id, diff);
+ window.resizeBy(0, diff);
+ }
+ window.sizeToContent();
+ }
+ }
+ });
+ this._mutationObserver._heightsById = new Map();
+ this._mutationObserver.observe(document, {
+ subtree: true,
+ attributeOldValue: true,
+ attributeFilter: ["hidden"],
+ });
+
+ switch (this._action) {
+ case ACTION_EDIT:
+ await gEditItemOverlay.initPanel({
+ node: this._node,
+ hiddenRows: this._hiddenRows,
+ focusedElement: "first",
+ });
+ acceptButtonDisabled = gEditItemOverlay.readOnly;
+ break;
+ case ACTION_ADD:
+ this._node = await this._promiseNewItem();
+ // Edit the new item
+ await gEditItemOverlay.initPanel({
+ node: this._node,
+ hiddenRows: this._hiddenRows,
+ postData: this._postData,
+ focusedElement: "first",
+ });
+
+ // 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.
+ let locationField = this._element("locationField");
+ if (locationField.value == "about:blank") {
+ locationField.value = "";
+ }
+
+ // if this is an uri related dialog disable accept button until
+ // the user fills an uri value.
+ if (this._itemType == BOOKMARK_ITEM) {
+ acceptButtonDisabled = !this._inputIsValid();
+ }
+ break;
+ }
+
+ if (!gEditItemOverlay.readOnly) {
+ // Listen on uri fields to enable accept button if input is valid
+ if (this._itemType == BOOKMARK_ITEM) {
+ this._element("locationField").addEventListener("input", this);
+ if (this._isAddKeywordDialog) {
+ this._element("keywordField").addEventListener("input", this);
+ }
+ }
+ }
+ // Only enable the accept button once we've finished everything.
+ acceptButton.disabled = acceptButtonDisabled;
+ },
+
+ // EventListener
+ handleEvent: function BPP_handleEvent(aEvent) {
+ var target = aEvent.target;
+ switch (aEvent.type) {
+ case "input":
+ if (
+ target.id == "editBMPanel_locationField" ||
+ target.id == "editBMPanel_keywordField"
+ ) {
+ // Check uri fields to enable accept button if input is valid
+ document
+ .getElementById("bookmarkpropertiesdialog")
+ .getButton("accept").disabled = !this._inputIsValid();
+ }
+ break;
+ }
+ },
+
+ // nsISupports
+ QueryInterface: ChromeUtils.generateQI([]),
+
+ _element: function BPP__element(aID) {
+ return document.getElementById("editBMPanel_" + aID);
+ },
+
+ onDialogUnload() {
+ // gEditItemOverlay does not exist anymore here, so don't rely on it.
+ this._mutationObserver.disconnect();
+ delete this._mutationObserver;
+
+ // Calling removeEventListener with arguments which do not identify any
+ // currently registered EventListener on the EventTarget has no effect.
+ this._element("locationField").removeEventListener("input", this);
+ this._element("keywordField").removeEventListener("input", this);
+ },
+
+ onDialogAccept() {
+ // We must blur current focused element to save its changes correctly
+ document.commandDispatcher.focusedElement?.blur();
+
+ // Get the states to compare bookmark and editedBookmark
+ window.arguments[0].bookmarkState = gEditItemOverlay._bookmarkState;
+
+ // We have to uninit the panel first, otherwise late changes could force it
+ // to commit more transactions.
+ gEditItemOverlay.uninitPanel(true);
+
+ window.arguments[0].bookmarkGuid = this._node.bookmarkGuid;
+ },
+
+ onDialogCancel() {
+ // We have to uninit the panel first, otherwise late changes could force it
+ // to commit more transactions.
+ gEditItemOverlay.uninitPanel(true);
+ },
+
+ /**
+ * This method checks to see if the input fields are in a valid state.
+ *
+ * @returns {boolean} 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 input with the given ID contains a
+ * string that can be converted into an nsIURI.
+ *
+ * @param {number} aTextboxID
+ * the ID of the textbox element whose contents we'll test
+ *
+ * @returns {boolean} 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) {
+ Services.uriFixup.getFixupURIInfo(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]
+ */
+ async _getInsertionPointDetails() {
+ return [
+ await this._defaultInsertionPoint.getIndex(),
+ this._defaultInsertionPoint.guid,
+ ];
+ },
+
+ async _promiseNewItem() {
+ let [index, parentGuid] = await this._getInsertionPointDetails();
+
+ let info = { parentGuid, index, title: this._title };
+ if (this._itemType == BOOKMARK_ITEM) {
+ info.url = this._uri;
+ if (this._keyword) {
+ info.keyword = this._keyword;
+ }
+ if (this._postData) {
+ info.postData = this._postData;
+ }
+
+ if (this._charSet) {
+ PlacesUIUtils.setCharsetForPage(this._uri, this._charSet, window).catch(
+ console.error
+ );
+ }
+ } else if (this._itemType == BOOKMARK_FOLDER) {
+ // NewFolder requires a url rather than uri.
+ info.children = this._URIs.map(item => {
+ return { url: item.uri, title: item.title };
+ });
+ } else {
+ throw new Error(`unexpected value for _itemType: ${this._itemType}`);
+ }
+ return Object.freeze({
+ index,
+ bookmarkGuid: PlacesUtils.bookmarks.unsavedGuid,
+ title: this._title,
+ uri: this._uri ? this._uri.spec : "",
+ type:
+ this._itemType == BOOKMARK_ITEM
+ ? Ci.nsINavHistoryResultNode.RESULT_TYPE_URI
+ : Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER,
+ parent: {
+ bookmarkGuid: parentGuid,
+ type: Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER,
+ },
+ children: info.children,
+ });
+ },
+};
+
+document.addEventListener("DOMContentLoaded", function () {
+ // Content initialization is asynchronous, thus set mozSubdialogReady
+ // immediately to properly wait for it.
+ document.mozSubdialogReady = BookmarkPropertiesPanel.onDialogLoad()
+ .catch(ex => console.error(`Failed to initialize dialog: ${ex}`))
+ .then(() => window.sizeToContent());
+});