Source code
Revision control
Copy as Markdown
Other Tools
/* 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,
const table = document.getElementById("table");
function setTextContent(element, content) {
if (content.startsWith("customkeys-")) {
element.setAttribute("data-l10n-id", content);
} else {
element.textContent = content;
}
}
function notifyUpdate() {
window.dispatchEvent(new CustomEvent("CustomKeysUpdate"));
}
async function buildTable() {
const keys = await RPMSendQuery("CustomKeys:GetKeys");
for (const category in keys) {
const tbody = document.createElement("tbody");
table.append(tbody);
let row = document.createElement("tr");
row.className = "category";
tbody.append(row);
let cell = document.createElement("td");
row.append(cell);
cell.setAttribute("colspan", 5);
const heading = document.createElement("h1");
setTextContent(heading, category);
cell.append(heading);
const categoryKeys = keys[category];
for (const keyId in categoryKeys) {
row = document.createElement("tr");
row.className = "key";
tbody.append(row);
row.setAttribute("data-id", keyId);
cell = document.createElement("th");
const key = categoryKeys[keyId];
setTextContent(cell, key.title);
row.append(cell);
cell = document.createElement("td");
cell.textContent = key.shortcut;
row.append(cell);
cell = document.createElement("td");
let button = document.createElement("button");
button.className = "change";
button.setAttribute("data-l10n-id", "customkeys-change");
cell.append(button);
let label = document.createElement("label");
label.className = "newLabel";
let span = document.createElement("span");
span.setAttribute("data-l10n-id", "customkeys-new-key");
label.append(span);
let input = document.createElement("input");
input.className = "new";
label.append(input);
cell.append(label);
row.append(cell);
cell = document.createElement("td");
button = document.createElement("button");
button.className = "clear";
button.setAttribute("data-l10n-id", "customkeys-clear");
cell.append(button);
row.append(cell);
cell = document.createElement("td");
button = document.createElement("button");
button.className = "reset";
button.setAttribute("data-l10n-id", "customkeys-reset");
cell.append(button);
row.append(cell);
updateKey(row, key);
}
}
notifyUpdate();
}
function updateKey(row, data) {
row.children[1].textContent = data.shortcut;
row.classList.toggle("customized", data.isCustomized);
row.classList.toggle("assigned", !!data.shortcut);
}
// Returns false if the assignment should be cancelled.
async function maybeHandleConflict(data) {
for (const row of table.querySelectorAll(".key")) {
if (data.shortcut != row.children[1].textContent) {
continue; // Not a conflict.
}
const conflictId = row.dataset.id;
if (conflictId == data.id) {
// We're trying to assign this key to the shortcut it is already
// assigned to. We don't need to do anything.
return false;
}
const conflictDesc = row.children[0].textContent;
if (
window.confirm(
await document.l10n.formatValue("customkeys-conflict-confirm", {
conflict: conflictDesc,
})
)
) {
// Clear the conflicting key.
const newData = await RPMSendQuery("CustomKeys:ClearKey", conflictId);
updateKey(row, newData);
return true;
}
return false;
}
return true;
}
async function onAction(event) {
const row = event.target.closest("tr");
const keyId = row.dataset.id;
if (event.target.className == "reset") {
Glean.browserCustomkeys.actions.reset.add();
const data = await RPMSendQuery("CustomKeys:GetDefaultKey", keyId);
if (await maybeHandleConflict(data)) {
const newData = await RPMSendQuery("CustomKeys:ResetKey", keyId);
updateKey(row, newData);
notifyUpdate();
}
} else if (event.target.className == "change") {
Glean.browserCustomkeys.actions.change.add();
// The "editing" class will cause the Change button to be replaced by a
// labelled input for the new key.
row.classList.add("editing");
// We need to listen for keys in the parent process because we want to
// intercept reserved keys, which we can't do in the content process.
RPMSendAsyncMessage("CustomKeys:CaptureKey", true);
row.querySelector(".new").focus();
} else if (event.target.className == "clear") {
Glean.browserCustomkeys.actions.clear.add();
const newData = await RPMSendQuery("CustomKeys:ClearKey", keyId);
updateKey(row, newData);
notifyUpdate();
}
}
async function onKey({ data }) {
const input = document.activeElement;
const row = input.closest("tr");
data.id = row.dataset.id;
if (data.isModifier) {
// This is a modifier. Display it, but don't assign yet. We assign when the
// main key is pressed (below).
input.value = data.modifierString;
// Select the input's text so screen readers will report it.
input.select();
return;
}
if (!data.isValid) {
input.value = await document.l10n.formatValue("customkeys-key-invalid");
input.select();
return;
}
if (await maybeHandleConflict(data)) {
const newData = await RPMSendQuery("CustomKeys:ChangeKey", data);
updateKey(row, newData);
}
RPMSendAsyncMessage("CustomKeys:CaptureKey", false);
row.classList.remove("editing");
row.querySelector(".change").focus();
notifyUpdate();
}
function onFocusLost(event) {
if (event.target.className == "new") {
// If the input loses focus, cancel editing of the key.
RPMSendAsyncMessage("CustomKeys:CaptureKey", false);
const row = event.target.closest("tr");
row.classList.remove("editing");
// Clear any modifiers that were displayed, ready for the next edit.
event.target.value = "";
}
}
function onSearchInput(event) {
const query = event.target.value.toLowerCase();
for (const row of table.querySelectorAll(".key")) {
row.hidden =
query && !row.children[0].textContent.toLowerCase().includes(query);
}
for (const tbody of table.tBodies) {
// Show a category only if it has at least 1 shown key.
tbody.hidden = !tbody.querySelector(".key:not([hidden])");
}
notifyUpdate();
}
async function onResetAll() {
Glean.browserCustomkeys.actions.reset_all.add();
if (
!window.confirm(
await document.l10n.formatValue("customkeys-reset-all-confirm")
)
) {
return;
}
await RPMSendQuery("CustomKeys:ResetAll");
const keysByCat = await RPMSendQuery("CustomKeys:GetKeys");
const keysById = {};
for (const category in keysByCat) {
const categoryKeys = keysByCat[category];
for (const keyId in categoryKeys) {
keysById[keyId] = categoryKeys[keyId];
}
}
for (const row of table.querySelectorAll(".key")) {
const data = keysById[row.dataset.id];
if (data) {
updateKey(row, data);
}
}
notifyUpdate();
}
buildTable();
table.addEventListener("click", onAction);
RPMAddMessageListener("CustomKeys:CapturedKey", onKey);
table.addEventListener("focusout", onFocusLost);
document.getElementById("search").addEventListener("input", onSearchInput);
document.getElementById("resetAll").addEventListener("click", onResetAll);
Glean.browserCustomkeys.opened.add();