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();