tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

bookmarkProperties.js (17318B)


      1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
      2 /* This Source Code Form is subject to the terms of the Mozilla Public
      3 * License, v. 2.0. If a copy of the MPL was not distributed with this
      4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      5 
      6 /**
      7 * The panel is initialized based on data given in the js object passed
      8 * as window.arguments[0]. The object must have the following fields set:
      9 *   @ action (String). Possible values:
     10 *     - "add" - for adding a new item.
     11 *       @ type (String). Possible values:
     12 *         - "bookmark"
     13 *         - "folder"
     14 *           @ URIList (Array of nsIURI objects) - optional, list of uris to
     15 *             be bookmarked under the new folder.
     16 *       @ uri (nsIURI object) - optional, the default uri for the new item.
     17 *         The property is not used for the "folder with items" type.
     18 *       @ title (String) - optional, the default title for the new item.
     19 *       @ defaultInsertionPoint (InsertionPoint JS object) - optional, the
     20 *         default insertion point for the new item.
     21 *       @ keyword (String) - optional, the default keyword for the new item.
     22 *       @ postData (String) - optional, POST data to accompany the keyword.
     23 *       @ charSet (String) - optional, character-set to accompany the keyword.
     24 *      Notes:
     25 *        1) If |uri| is set for a bookmark and |title| isn't,
     26 *           the dialog will query the history tables for the title associated
     27 *           with the given uri. If the dialog is set to adding a folder with
     28 *           bookmark items under it (see URIList), a default static title is
     29 *           used ("[Folder Name]").
     30 *        2) The index field of the default insertion point is ignored if
     31 *           the folder picker is shown.
     32 *     - "edit" - for editing a bookmark item or a folder.
     33 *       @ type (String). Possible values:
     34 *         - "bookmark"
     35 *           @ node (an nsINavHistoryResultNode object) - a node representing
     36 *             the bookmark.
     37 *         - "folder"
     38 *           @ node (an nsINavHistoryResultNode object) - a node representing
     39 *             the folder.
     40 *   @ hiddenRows (Strings array) - optional, list of rows to be hidden
     41 *     regardless of the item edited or added by the dialog.
     42 *     Possible values:
     43 *     - "title"
     44 *     - "location"
     45 *     - "keyword"
     46 *     - "tags"
     47 *     - "folderPicker" - hides both the tree and the menu.
     48 *
     49 * window.arguments[0].bookmarkGuid is set to the guid of the item, if the
     50 * dialog is accepted.
     51 */
     52 
     53 /* import-globals-from editBookmark.js */
     54 
     55 /* Shared Places Import - change other consumers if you change this: */
     56 var { XPCOMUtils } = ChromeUtils.importESModule(
     57  "resource://gre/modules/XPCOMUtils.sys.mjs"
     58 );
     59 ChromeUtils.defineESModuleGetters(this, {
     60  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
     61 });
     62 XPCOMUtils.defineLazyScriptGetter(
     63  this,
     64  "PlacesTreeView",
     65  "chrome://browser/content/places/treeView.js"
     66 );
     67 XPCOMUtils.defineLazyScriptGetter(
     68  this,
     69  ["PlacesInsertionPoint", "PlacesController", "PlacesControllerDragHelper"],
     70  "chrome://browser/content/places/controller.js"
     71 );
     72 /* End Shared Places Import */
     73 
     74 const BOOKMARK_ITEM = 0;
     75 const BOOKMARK_FOLDER = 1;
     76 
     77 const ACTION_EDIT = 0;
     78 const ACTION_ADD = 1;
     79 
     80 var BookmarkPropertiesPanel = {
     81  /** UI Text Strings */
     82  __strings: null,
     83  get _strings() {
     84    if (!this.__strings) {
     85      this.__strings = document.getElementById("stringBundle");
     86    }
     87    return this.__strings;
     88  },
     89 
     90  _action: null,
     91  _itemType: null,
     92  _uri: null,
     93  _title: "",
     94  _URIs: [],
     95  _keyword: "",
     96  _postData: null,
     97  _charSet: "",
     98 
     99  _defaultInsertionPoint: null,
    100  _hiddenRows: [],
    101 
    102  /**
    103   * @returns {string}
    104   *   This method returns the correct label for the dialog's "accept"
    105   *   button based on the variant of the dialog.
    106   */
    107  _getAcceptLabel: function BPP__getAcceptLabel() {
    108    return this._strings.getString("dialogAcceptLabelSaveItem");
    109  },
    110 
    111  /**
    112   * @returns {string}
    113   *   This method returns the correct title for the current variant
    114   *   of this dialog.
    115   */
    116  _getDialogTitle: function BPP__getDialogTitle() {
    117    if (this._action == ACTION_ADD) {
    118      if (this._itemType == BOOKMARK_ITEM) {
    119        return this._strings.getString("dialogTitleAddNewBookmark2");
    120      }
    121 
    122      // add folder
    123      if (this._itemType != BOOKMARK_FOLDER) {
    124        throw new Error("Unknown item type");
    125      }
    126      if (this._URIs.length) {
    127        return this._strings.getString("dialogTitleAddMulti");
    128      }
    129 
    130      return this._strings.getString("dialogTitleAddBookmarkFolder");
    131    }
    132    if (this._action == ACTION_EDIT) {
    133      if (this._itemType === BOOKMARK_ITEM) {
    134        return this._strings.getString("dialogTitleEditBookmark2");
    135      }
    136 
    137      return this._strings.getString("dialogTitleEditBookmarkFolder");
    138    }
    139    return "";
    140  },
    141 
    142  /**
    143   * Determines the initial data for the item edited or added by this dialog
    144   */
    145  async _determineItemInfo() {
    146    let dialogInfo = window.arguments[0];
    147    this._action = dialogInfo.action == "add" ? ACTION_ADD : ACTION_EDIT;
    148    this._hiddenRows = dialogInfo.hiddenRows ? dialogInfo.hiddenRows : [];
    149    if (this._action == ACTION_ADD) {
    150      if (!("type" in dialogInfo)) {
    151        throw new Error("missing type property for add action");
    152      }
    153 
    154      if ("title" in dialogInfo) {
    155        this._title = dialogInfo.title;
    156      }
    157 
    158      if ("defaultInsertionPoint" in dialogInfo) {
    159        this._defaultInsertionPoint = dialogInfo.defaultInsertionPoint;
    160      } else {
    161        let parentGuid = await PlacesUIUtils.defaultParentGuid;
    162        this._defaultInsertionPoint = new PlacesInsertionPoint({
    163          parentGuid,
    164        });
    165      }
    166 
    167      switch (dialogInfo.type) {
    168        case "bookmark":
    169          this._itemType = BOOKMARK_ITEM;
    170          if ("uri" in dialogInfo) {
    171            if (!(dialogInfo.uri instanceof Ci.nsIURI)) {
    172              throw new Error("uri property should be a uri object");
    173            }
    174            this._uri = dialogInfo.uri;
    175            if (typeof this._title != "string") {
    176              this._title =
    177                (await PlacesUtils.history.fetch(this._uri)) || this._uri.spec;
    178            }
    179          } else {
    180            this._uri = Services.io.newURI("about:blank");
    181            this._title = this._strings.getString("newBookmarkDefault");
    182            this._dummyItem = true;
    183          }
    184 
    185          if ("keyword" in dialogInfo) {
    186            this._keyword = dialogInfo.keyword;
    187            this._isAddKeywordDialog = true;
    188            if ("postData" in dialogInfo) {
    189              this._postData = dialogInfo.postData;
    190            }
    191            if ("charSet" in dialogInfo) {
    192              this._charSet = dialogInfo.charSet;
    193            }
    194          }
    195          break;
    196 
    197        case "folder":
    198          this._itemType = BOOKMARK_FOLDER;
    199          if (!this._title) {
    200            if ("URIList" in dialogInfo) {
    201              this._title = this._strings.getString("bookmarkAllTabsDefault");
    202              this._URIs = dialogInfo.URIList;
    203            } else {
    204              this._title = this._strings.getString("newFolderDefault");
    205              this._dummyItem = true;
    206            }
    207          }
    208          break;
    209      }
    210    } else {
    211      // edit
    212      this._node = dialogInfo.node;
    213      this._title = this._node.title;
    214      if (PlacesUtils.nodeIsFolderOrShortcut(this._node)) {
    215        this._itemType = BOOKMARK_FOLDER;
    216      } else if (PlacesUtils.nodeIsURI(this._node)) {
    217        this._itemType = BOOKMARK_ITEM;
    218      }
    219    }
    220  },
    221 
    222  /**
    223   * This method should be called by the onload of the Bookmark Properties
    224   * dialog to initialize the state of the panel.
    225   */
    226  async onDialogLoad() {
    227    document.addEventListener("dialogaccept", () => this.onDialogAccept());
    228    document.addEventListener("dialogcancel", () => this.onDialogCancel());
    229    window.addEventListener("unload", () => this.onDialogUnload());
    230 
    231    // Disable the buttons until we have all the information required.
    232    let acceptButton = document
    233      .getElementById("bookmarkpropertiesdialog")
    234      .getButton("accept");
    235    acceptButton.disabled = true;
    236    await this._determineItemInfo();
    237    document.title = this._getDialogTitle();
    238 
    239    // Set adjustable title
    240    let title = { raw: document.title };
    241    document.documentElement.setAttribute("headertitle", JSON.stringify(title));
    242 
    243    let iconUrl = this._getIconUrl();
    244    if (iconUrl) {
    245      document.documentElement.style.setProperty(
    246        "--icon-url",
    247        `url(${iconUrl})`
    248      );
    249    }
    250 
    251    await this._initDialog();
    252  },
    253 
    254  _getIconUrl() {
    255    let url = "chrome://browser/skin/bookmark-hollow.svg";
    256 
    257    if (this._action === ACTION_EDIT && this._itemType === BOOKMARK_ITEM) {
    258      url = window.arguments[0]?.node?.icon;
    259    }
    260 
    261    return url;
    262  },
    263 
    264  /**
    265   * Initializes the dialog, gathering the required bookmark data. This function
    266   * will enable the accept button (if appropraite) when it is complete.
    267   */
    268  async _initDialog() {
    269    let acceptButton = document
    270      .getElementById("bookmarkpropertiesdialog")
    271      .getButton("accept");
    272    acceptButton.label = this._getAcceptLabel();
    273    let acceptButtonDisabled = false;
    274 
    275    // Since elements can be unhidden asynchronously, we must observe their
    276    // mutations and resize the dialog accordingly.
    277    this._mutationObserver = new MutationObserver(mutations => {
    278      for (let { target, oldValue } of mutations) {
    279        let hidden = target.hasAttribute("hidden");
    280        let wasHidden = oldValue !== null;
    281        if (target.classList.contains("hideable") && hidden != wasHidden) {
    282          // To support both kind of dialogs (window and dialog-box) we need
    283          // both resizeBy and sizeToContent, otherwise either the dialog
    284          // doesn't resize, or it gets empty unused space.
    285          if (hidden) {
    286            let diff = this._mutationObserver._heightsById.get(target.id);
    287            window.resizeBy(0, -diff);
    288          } else {
    289            let diff = target.getBoundingClientRect().height;
    290            this._mutationObserver._heightsById.set(target.id, diff);
    291            window.resizeBy(0, diff);
    292          }
    293          window.sizeToContent();
    294        }
    295      }
    296    });
    297    this._mutationObserver._heightsById = new Map();
    298    this._mutationObserver.observe(document, {
    299      subtree: true,
    300      attributeOldValue: true,
    301      attributeFilter: ["hidden"],
    302    });
    303 
    304    switch (this._action) {
    305      case ACTION_EDIT: {
    306        await gEditItemOverlay.initPanel({
    307          node: this._node,
    308          hiddenRows: this._hiddenRows,
    309          focusedElement: "first",
    310        });
    311        acceptButtonDisabled = gEditItemOverlay.readOnly;
    312        break;
    313      }
    314      case ACTION_ADD: {
    315        this._node = await this._promiseNewItem();
    316 
    317        // Edit the new item
    318        await gEditItemOverlay.initPanel({
    319          node: this._node,
    320          hiddenRows: this._hiddenRows,
    321          postData: this._postData,
    322          focusedElement: "first",
    323          addedMultipleBookmarks: this._node.children?.length > 1,
    324        });
    325 
    326        // Empty location field if the uri is about:blank, this way inserting a new
    327        // url will be easier for the user, Accept button will be automatically
    328        // disabled by the input listener until the user fills the field.
    329        let locationField = this._element("locationField");
    330        if (locationField.value == "about:blank") {
    331          locationField.value = "";
    332        }
    333 
    334        // if this is an uri related dialog disable accept button until
    335        // the user fills an uri value.
    336        if (this._itemType == BOOKMARK_ITEM) {
    337          acceptButtonDisabled = !this._inputIsValid();
    338        }
    339        break;
    340      }
    341    }
    342 
    343    if (!gEditItemOverlay.readOnly) {
    344      // Listen on uri fields to enable accept button if input is valid
    345      if (this._itemType == BOOKMARK_ITEM) {
    346        this._element("locationField").addEventListener("input", this);
    347        if (this._isAddKeywordDialog) {
    348          this._element("keywordField").addEventListener("input", this);
    349        }
    350      }
    351    }
    352    // Only enable the accept button once we've finished everything.
    353    acceptButton.disabled = acceptButtonDisabled;
    354  },
    355 
    356  // EventListener
    357  handleEvent: function BPP_handleEvent(aEvent) {
    358    var target = aEvent.target;
    359    switch (aEvent.type) {
    360      case "input":
    361        if (
    362          target.id == "editBMPanel_locationField" ||
    363          target.id == "editBMPanel_keywordField"
    364        ) {
    365          // Check uri fields to enable accept button if input is valid
    366          document
    367            .getElementById("bookmarkpropertiesdialog")
    368            .getButton("accept").disabled = !this._inputIsValid();
    369        }
    370        break;
    371    }
    372  },
    373 
    374  // nsISupports
    375  QueryInterface: ChromeUtils.generateQI([]),
    376 
    377  _element: function BPP__element(aID) {
    378    return document.getElementById("editBMPanel_" + aID);
    379  },
    380 
    381  onDialogUnload() {
    382    // gEditItemOverlay does not exist anymore here, so don't rely on it.
    383    this._mutationObserver.disconnect();
    384    delete this._mutationObserver;
    385 
    386    // Calling removeEventListener with arguments which do not identify any
    387    // currently registered EventListener on the EventTarget has no effect.
    388    this._element("locationField").removeEventListener("input", this);
    389    this._element("keywordField").removeEventListener("input", this);
    390  },
    391 
    392  onDialogAccept() {
    393    // We must blur current focused element to save its changes correctly
    394    document.commandDispatcher.focusedElement?.blur();
    395 
    396    // Get the states to compare bookmark and editedBookmark
    397    window.arguments[0].bookmarkState = gEditItemOverlay._bookmarkState;
    398 
    399    // We have to uninit the panel first, otherwise late changes could force it
    400    // to commit more transactions.
    401    gEditItemOverlay.uninitPanel(true);
    402 
    403    window.arguments[0].bookmarkGuid = this._node.bookmarkGuid;
    404  },
    405 
    406  onDialogCancel() {
    407    // We have to uninit the panel first, otherwise late changes could force it
    408    // to commit more transactions.
    409    gEditItemOverlay.uninitPanel(true);
    410  },
    411 
    412  /**
    413   * This method checks to see if the input fields are in a valid state.
    414   *
    415   * @returns {boolean} true if the input is valid, false otherwise
    416   */
    417  _inputIsValid: function BPP__inputIsValid() {
    418    if (
    419      this._itemType == BOOKMARK_ITEM &&
    420      !this._containsValidURI("locationField")
    421    ) {
    422      return false;
    423    }
    424    if (
    425      this._isAddKeywordDialog &&
    426      !this._element("keywordField").value.length
    427    ) {
    428      return false;
    429    }
    430 
    431    return true;
    432  },
    433 
    434  /**
    435   * Determines whether the input with the given ID contains a
    436   * string that can be converted into an nsIURI.
    437   *
    438   * @param {number} aTextboxID
    439   *        the ID of the textbox element whose contents we'll test
    440   *
    441   * @returns {boolean} true if the textbox contains a valid URI string, false otherwise
    442   */
    443  _containsValidURI: function BPP__containsValidURI(aTextboxID) {
    444    try {
    445      var value = this._element(aTextboxID).value;
    446      if (value) {
    447        Services.uriFixup.getFixupURIInfo(value);
    448        return true;
    449      }
    450    } catch (e) {}
    451    return false;
    452  },
    453 
    454  /**
    455   * [New Item Mode] Get the insertion point details for the new item, given
    456   * dialog state and opening arguments.
    457   *
    458   * @returns {Array}
    459   *   The container-identifier and insertion-index are returned separately in
    460   *   the form of [containerIdentifier, insertionIndex]
    461   */
    462  async _getInsertionPointDetails() {
    463    return [
    464      await this._defaultInsertionPoint.getIndex(),
    465      this._defaultInsertionPoint.guid,
    466    ];
    467  },
    468 
    469  async _promiseNewItem() {
    470    let [index, parentGuid] = await this._getInsertionPointDetails();
    471 
    472    let info = { parentGuid, index, title: this._title };
    473    if (this._itemType == BOOKMARK_ITEM) {
    474      info.url = this._uri;
    475      if (this._keyword) {
    476        info.keyword = this._keyword;
    477      }
    478      if (this._postData) {
    479        info.postData = this._postData;
    480      }
    481 
    482      if (this._charSet) {
    483        PlacesUIUtils.setCharsetForPage(this._uri, this._charSet, window).catch(
    484          console.error
    485        );
    486      }
    487    } else if (this._itemType == BOOKMARK_FOLDER) {
    488      // NewFolder requires a url rather than uri.
    489      info.children = this._URIs.map(item => {
    490        return { url: item.uri, title: item.title };
    491      });
    492    } else {
    493      throw new Error(`unexpected value for _itemType:  ${this._itemType}`);
    494    }
    495    return Object.freeze({
    496      index,
    497      bookmarkGuid: PlacesUtils.bookmarks.unsavedGuid,
    498      title: this._title,
    499      uri: this._uri ? this._uri.spec : "",
    500      type:
    501        this._itemType == BOOKMARK_ITEM
    502          ? Ci.nsINavHistoryResultNode.RESULT_TYPE_URI
    503          : Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER,
    504      parent: {
    505        bookmarkGuid: parentGuid,
    506        type: Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER,
    507      },
    508      children: info.children,
    509    });
    510  },
    511 };
    512 
    513 document.addEventListener("DOMContentLoaded", function () {
    514  // Content initialization is asynchronous, thus set mozSubdialogReady
    515  // immediately to properly wait for it.
    516  document.mozSubdialogReady = BookmarkPropertiesPanel.onDialogLoad()
    517    .catch(ex => console.error(`Failed to initialize dialog: ${ex}`))
    518    .then(() => window.sizeToContent());
    519 });