tor-browser

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

customkeys.js (7603B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
      3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 const table = document.getElementById("table");
      6 
      7 function setTextContent(element, content) {
      8  if (content.startsWith("customkeys-")) {
      9    element.setAttribute("data-l10n-id", content);
     10  } else {
     11    element.textContent = content;
     12  }
     13 }
     14 
     15 function notifyUpdate() {
     16  window.dispatchEvent(new CustomEvent("CustomKeysUpdate"));
     17 }
     18 
     19 async function buildTable() {
     20  const keys = await RPMSendQuery("CustomKeys:GetKeys");
     21  for (const category in keys) {
     22    const tbody = document.createElement("tbody");
     23    table.append(tbody);
     24    let row = document.createElement("tr");
     25    row.className = "category";
     26    tbody.append(row);
     27    let cell = document.createElement("td");
     28    row.append(cell);
     29    cell.setAttribute("colspan", 5);
     30    const heading = document.createElement("h1");
     31    setTextContent(heading, category);
     32    cell.append(heading);
     33    const categoryKeys = keys[category];
     34    for (const keyId in categoryKeys) {
     35      row = document.createElement("tr");
     36      row.className = "key";
     37      tbody.append(row);
     38      row.setAttribute("data-id", keyId);
     39      cell = document.createElement("th");
     40      const key = categoryKeys[keyId];
     41      setTextContent(cell, key.title);
     42      row.append(cell);
     43      cell = document.createElement("td");
     44      cell.textContent = key.shortcut;
     45      row.append(cell);
     46      cell = document.createElement("td");
     47      let button = document.createElement("button");
     48      button.className = "change";
     49      button.setAttribute("data-l10n-id", "customkeys-change");
     50      cell.append(button);
     51      let label = document.createElement("label");
     52      label.className = "newLabel";
     53      let span = document.createElement("span");
     54      span.setAttribute("data-l10n-id", "customkeys-new-key");
     55      label.append(span);
     56      let input = document.createElement("input");
     57      input.className = "new";
     58      label.append(input);
     59      cell.append(label);
     60      row.append(cell);
     61      cell = document.createElement("td");
     62      button = document.createElement("button");
     63      button.className = "clear";
     64      button.setAttribute("data-l10n-id", "customkeys-clear");
     65      cell.append(button);
     66      row.append(cell);
     67      cell = document.createElement("td");
     68      button = document.createElement("button");
     69      button.className = "reset";
     70      button.setAttribute("data-l10n-id", "customkeys-reset");
     71      cell.append(button);
     72      row.append(cell);
     73      updateKey(row, key);
     74    }
     75  }
     76  notifyUpdate();
     77 }
     78 
     79 function updateKey(row, data) {
     80  row.children[1].textContent = data.shortcut;
     81  row.classList.toggle("customized", data.isCustomized);
     82  row.classList.toggle("assigned", !!data.shortcut);
     83 }
     84 
     85 // Returns false if the assignment should be cancelled.
     86 async function maybeHandleConflict(data) {
     87  for (const row of table.querySelectorAll(".key")) {
     88    if (data.shortcut != row.children[1].textContent) {
     89      continue; // Not a conflict.
     90    }
     91    const conflictId = row.dataset.id;
     92    if (conflictId == data.id) {
     93      // We're trying to assign this key to the shortcut it is already
     94      // assigned to. We don't need to do anything.
     95      return false;
     96    }
     97    const conflictDesc = row.children[0].textContent;
     98    if (
     99      window.confirm(
    100        await document.l10n.formatValue("customkeys-conflict-confirm", {
    101          conflict: conflictDesc,
    102        })
    103      )
    104    ) {
    105      // Clear the conflicting key.
    106      const newData = await RPMSendQuery("CustomKeys:ClearKey", conflictId);
    107      updateKey(row, newData);
    108      return true;
    109    }
    110    return false;
    111  }
    112  return true;
    113 }
    114 
    115 async function onAction(event) {
    116  const row = event.target.closest("tr");
    117  const keyId = row.dataset.id;
    118  if (event.target.className == "reset") {
    119    Glean.browserCustomkeys.actions.reset.add();
    120    const data = await RPMSendQuery("CustomKeys:GetDefaultKey", keyId);
    121    if (await maybeHandleConflict(data)) {
    122      const newData = await RPMSendQuery("CustomKeys:ResetKey", keyId);
    123      updateKey(row, newData);
    124      notifyUpdate();
    125    }
    126  } else if (event.target.className == "change") {
    127    Glean.browserCustomkeys.actions.change.add();
    128    // The "editing" class will cause the Change button to be replaced by a
    129    // labelled input for the new key.
    130    row.classList.add("editing");
    131    // We need to listen for keys in the parent process because we want to
    132    // intercept reserved keys, which we can't do in the content process.
    133    RPMSendAsyncMessage("CustomKeys:CaptureKey", true);
    134    row.querySelector(".new").focus();
    135  } else if (event.target.className == "clear") {
    136    Glean.browserCustomkeys.actions.clear.add();
    137    const newData = await RPMSendQuery("CustomKeys:ClearKey", keyId);
    138    updateKey(row, newData);
    139    notifyUpdate();
    140  }
    141 }
    142 
    143 async function onKey({ data }) {
    144  const input = document.activeElement;
    145  const row = input.closest("tr");
    146  data.id = row.dataset.id;
    147  if (data.isModifier) {
    148    // This is a modifier. Display it, but don't assign yet. We assign when the
    149    // main key is pressed (below).
    150    input.value = data.modifierString;
    151    // Select the input's text so screen readers will report it.
    152    input.select();
    153    return;
    154  }
    155  if (!data.isValid) {
    156    input.value = await document.l10n.formatValue("customkeys-key-invalid");
    157    input.select();
    158    return;
    159  }
    160  if (await maybeHandleConflict(data)) {
    161    const newData = await RPMSendQuery("CustomKeys:ChangeKey", data);
    162    updateKey(row, newData);
    163  }
    164  RPMSendAsyncMessage("CustomKeys:CaptureKey", false);
    165  row.classList.remove("editing");
    166  row.querySelector(".change").focus();
    167  notifyUpdate();
    168 }
    169 
    170 function onFocusLost(event) {
    171  if (event.target.className == "new") {
    172    // If the input loses focus, cancel editing of the key.
    173    RPMSendAsyncMessage("CustomKeys:CaptureKey", false);
    174    const row = event.target.closest("tr");
    175    row.classList.remove("editing");
    176    // Clear any modifiers that were displayed, ready for the next edit.
    177    event.target.value = "";
    178  }
    179 }
    180 
    181 function onSearchInput(event) {
    182  const query = event.target.value.toLowerCase();
    183  for (const row of table.querySelectorAll(".key")) {
    184    row.hidden =
    185      query && !row.children[0].textContent.toLowerCase().includes(query);
    186  }
    187  for (const tbody of table.tBodies) {
    188    // Show a category only if it has at least 1 shown key.
    189    tbody.hidden = !tbody.querySelector(".key:not([hidden])");
    190  }
    191  notifyUpdate();
    192 }
    193 
    194 async function onResetAll() {
    195  Glean.browserCustomkeys.actions.reset_all.add();
    196  if (
    197    !window.confirm(
    198      await document.l10n.formatValue("customkeys-reset-all-confirm")
    199    )
    200  ) {
    201    return;
    202  }
    203  await RPMSendQuery("CustomKeys:ResetAll");
    204  const keysByCat = await RPMSendQuery("CustomKeys:GetKeys");
    205  const keysById = {};
    206  for (const category in keysByCat) {
    207    const categoryKeys = keysByCat[category];
    208    for (const keyId in categoryKeys) {
    209      keysById[keyId] = categoryKeys[keyId];
    210    }
    211  }
    212  for (const row of table.querySelectorAll(".key")) {
    213    const data = keysById[row.dataset.id];
    214    if (data) {
    215      updateKey(row, data);
    216    }
    217  }
    218  notifyUpdate();
    219 }
    220 
    221 buildTable();
    222 table.addEventListener("click", onAction);
    223 RPMAddMessageListener("CustomKeys:CapturedKey", onKey);
    224 table.addEventListener("focusout", onFocusLost);
    225 document.getElementById("search").addEventListener("input", onSearchInput);
    226 document.getElementById("resetAll").addEventListener("click", onResetAll);
    227 Glean.browserCustomkeys.opened.add();