From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- .../places/content/bookmarkProperties.js | 519 +++++++++++++++++++++ 1 file changed, 519 insertions(+) create mode 100644 browser/components/places/content/bookmarkProperties.js (limited to 'browser/components/places/content/bookmarkProperties.js') 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()); +}); -- cgit v1.2.3