/* 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/. */ 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();