521 lines
17 KiB
JavaScript
521 lines
17 KiB
JavaScript
/* -*- 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.nodeIsFolderOrShortcut(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", () => this.onDialogAccept());
|
|
document.addEventListener("dialogcancel", () => this.onDialogCancel());
|
|
window.addEventListener("unload", () => this.onDialogUnload());
|
|
|
|
// 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",
|
|
addedMultipleBookmarks: this._node.children?.length > 1,
|
|
});
|
|
|
|
// 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.
|
|
*
|
|
* @returns {Array}
|
|
* 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());
|
|
});
|