From 92da817a90ba6c3d5a321ba136c3b9cd861a4883 Mon Sep 17 00:00:00 2001 From: Oliver Geer Date: Fri, 7 Nov 2025 11:52:20 +0000 Subject: [PATCH 1/3] Add plugin support for Cmd on Macs rather than Ctrl (optional) Tested with i18n-hljs.html, but undocumented --- code-input.d.ts | 9 ++++-- plugins/find-and-replace.js | 20 +++++++++++-- plugins/go-to-line.js | 19 +++++++++++-- tests/i18n-hljs.html | 4 +-- tests/i18n-prism.html | 4 +-- tests/tester.js | 57 ++++++++++++++++++++++++++----------- 6 files changed, 84 insertions(+), 29 deletions(-) diff --git a/code-input.d.ts b/code-input.d.ts index e97206a..8465673 100644 --- a/code-input.d.ts +++ b/code-input.d.ts @@ -138,6 +138,7 @@ export namespace plugins { * @param {boolean} useCtrlF Should Ctrl+F be overriden for find-and-replace find functionality? Either way, you can also trigger it yourself using (instance of this plugin)`.showPrompt(code-input element, false)`. * @param {boolean} useCtrlH Should Ctrl+H be overriden for find-and-replace replace functionality? Either way, you can also trigger it yourself using (instance of this plugin)`.showPrompt(code-input element, true)`. * @param {Object} instructionTranslations: user interface string keys mapped to translated versions for localisation. Look at the find-and-replace.js source code for the English text. + * @param {boolean} alwaysCtrl: if true always use Ctrl+F/H as the keyboard shortcut; if false use Cmd+F/H on Apple devices and Ctrl+F/H elsewhere. False highly recommended; defaults to true for backwards compatiblity. */ constructor(useCtrlF?: boolean, useCtrlH?: boolean, instructionTranslations?: { @@ -159,7 +160,8 @@ export namespace plugins { replaceAction?: string; replaceAllActionShort?: string; replaceAllAction?: string - } + }, + alwaysCtrl?: boolean ); /** * Show a find-and-replace dialog. @@ -190,7 +192,9 @@ export namespace plugins { guidanceColumnRange?: (line: Number, current: Number, max: Number) => string; guidanceValidLine?: (line: Number) => string; guidanceValidColumn?: (line: Number, column: Number) => string; - }); + }, + alwaysCtrl?: boolean + ); /** * Show a search-like dialog prompting line number. * @param {codeInput.CodeInput} codeInput the `` element. @@ -213,6 +217,7 @@ export namespace plugins { * @param {Object} bracketPairs Opening brackets mapped to closing brackets, default and example {"(": ")", "[": "]", "{": "}"}. All brackets must only be one character, and this can be left as null to remove bracket-based indentation behaviour. * @param {boolean} escTabToChangeFocus Whether pressing the Escape key before (Shift+)Tab should make this keypress focus on a different element (Tab's default behaviour). You should always either enable this or use this plugin's disableTabIndentation and enableTabIndentation methods linked to other keyboard shortcuts, for accessibility. * @param {Object} instructionTranslations: user interface string keys mapped to translated versions for localisation. Look at the go-to-line.js source code for the English text. + * @param {boolean} alwaysCtrl: if true always use Ctrl+G as the keyboard shortcut; if false use Cmd+G on Apple devices and Ctrl+G elsewhere. False highly recommended; defaults to true for backwards compatiblity. */ constructor(defaultSpaces?: boolean, numSpaces?: Number, bracketPairs?: Object, escTabToChangeFocus?: boolean, instructionTranslations?: { tabForIndentation?: string; diff --git a/plugins/find-and-replace.js b/plugins/find-and-replace.js index 3d4417c..2b8da87 100644 --- a/plugins/find-and-replace.js +++ b/plugins/find-and-replace.js @@ -36,11 +36,13 @@ codeInput.plugins.FindAndReplace = class extends codeInput.Plugin { * @param {boolean} useCtrlF Should Ctrl+F be overriden for find-and-replace find functionality? Either way, you can also trigger it yourself using (instance of this plugin)`.showPrompt(code-input element, false)`. * @param {boolean} useCtrlH Should Ctrl+H be overriden for find-and-replace replace functionality? Either way, you can also trigger it yourself using (instance of this plugin)`.showPrompt(code-input element, true)`. * @param {Object} instructionTranslations: user interface string keys mapped to translated versions for localisation. Look at the find-and-replace.js source code for the English text and available keys. + * @param {boolean} alwaysCtrl: if true always use Ctrl+F/H as the keyboard shortcut; if false use Cmd+F/H on Apple devices and Ctrl+F/H elsewhere. False highly recommended; defaults to true for backwards compatiblity. */ - constructor(useCtrlF = true, useCtrlH = true, instructionTranslations = {}) { + constructor(useCtrlF = true, useCtrlH = true, instructionTranslations = {}, alwaysCtrl = true) { super([]); // No observed attributes this.useCtrlF = useCtrlF; this.useCtrlH = useCtrlH; + this.alwaysCtrl = alwaysCtrl; this.addTranslations(this.instructions, instructionTranslations); } @@ -420,9 +422,21 @@ codeInput.plugins.FindAndReplace = class extends codeInput.Plugin { this.updateFindMatches(dialog); } + hasModifier(event) { + if(this.alwaysCtrl) return event.ctrlKey; + // Thanks to https://developer.mozilla.org/en-US/docs/Web/API/Navigator/platform + if(navigator.platform.startsWith("Mac") || navigator.platform === "iPhone") { + // Command + return event.metaKey; + } else { + // Control + return event.ctrlKey; + } + } + /* Event handler for keydown event that makes Ctrl+F open find dialog */ checkCtrlF(codeInput, event) { - if (event.ctrlKey && event.key == 'f') { + if (this.hasModifier(event) && event.key == 'f') { event.preventDefault(); this.showPrompt(codeInput, false); } @@ -430,7 +444,7 @@ codeInput.plugins.FindAndReplace = class extends codeInput.Plugin { /* Event handler for keydown event that makes Ctrl+H open find+replace dialog */ checkCtrlH(codeInput, event) { - if (event.ctrlKey && event.key == 'h') { + if (this.hasModifier(event) && event.key == 'h') { event.preventDefault(); this.showPrompt(codeInput, true); } diff --git a/plugins/go-to-line.js b/plugins/go-to-line.js index 54182da..ec24a05 100644 --- a/plugins/go-to-line.js +++ b/plugins/go-to-line.js @@ -19,10 +19,11 @@ codeInput.plugins.GoToLine = class extends codeInput.Plugin { /** * Create a go-to-line command plugin to pass into a template - * @param {boolean} useCtrlG Should Ctrl+G be overriden for go-to-line functionality? Either way, you can trigger it yourself using (instance of this plugin)`.showPrompt(code-input element)`. + * @param {boolean} useCtrlG Should Ctrl/Cmd+G be overriden for go-to-line functionality? Either way, you can trigger it yourself using (instance of this plugin)`.showPrompt(code-input element)`. * @param {Object} instructionTranslations: user interface string keys mapped to translated versions for localisation. Look at the go-to-line.js source code for the available keys and English text. + * @param {boolean} alwaysCtrl: if true always use Ctrl+G as the keyboard shortcut; if false use Cmd+G on Apple devices and Ctrl+G elsewhere. False highly recommended; defaults to true for backwards compatiblity. */ - constructor(useCtrlG = true, instructionTranslations = {}) { + constructor(useCtrlG = true, instructionTranslations = {}, alwaysCtrl = true) { super([]); // No observed attributes this.useCtrlG = useCtrlG; this.addTranslations(this.instructions, instructionTranslations); @@ -202,9 +203,21 @@ codeInput.plugins.GoToLine = class extends codeInput.Plugin { } } + hasModifier(event) { + if(this.alwaysCtrl) return event.ctrlKey; + // Thanks to https://developer.mozilla.org/en-US/docs/Web/API/Navigator/platform + if(navigator.platform.startsWith("Mac") || navigator.platform === "iPhone") { + // Command + return event.metaKey; + } else { + // Control + return event.ctrlKey; + } + } + /* Event handler for keydown event that makes Ctrl+G open go to line dialog */ checkCtrlG(codeInput, event) { - if (event.ctrlKey && event.key == 'g') { + if (this.hasModifier(event) && event.key == 'g') { event.preventDefault(); this.showPrompt(codeInput); } diff --git a/tests/i18n-hljs.html b/tests/i18n-hljs.html index a3a83f9..70c64cc 100644 --- a/tests/i18n-hljs.html +++ b/tests/i18n-hljs.html @@ -62,8 +62,8 @@ } }), new codeInput.plugins.Autodetect(), - new codeInput.plugins.FindAndReplace(true, true, findAndReplaceTranslations), - new codeInput.plugins.GoToLine(true, goToLineTranslations), + new codeInput.plugins.FindAndReplace(true, true, findAndReplaceTranslations, false), + new codeInput.plugins.GoToLine(true, goToLineTranslations, false), new codeInput.plugins.Indent(true, 2, {"(": ")", "[": "]", "{": "}"}, true, indentTranslations), new codeInput.plugins.SelectTokenCallbacks(codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks.createClassSynchronisation("in-selection"), false, true, true, true, true, false), //new codeInput.plugins.SpecialChars(true), diff --git a/tests/i18n-prism.html b/tests/i18n-prism.html index 3697c0c..7778d02 100644 --- a/tests/i18n-prism.html +++ b/tests/i18n-prism.html @@ -61,8 +61,8 @@ popupElem.style.display = "none"; } }), - new codeInput.plugins.FindAndReplace(true, true, findAndReplaceTranslations), - new codeInput.plugins.GoToLine(true, goToLineTranslations), + new codeInput.plugins.FindAndReplace(true, true, findAndReplaceTranslations, false), + new codeInput.plugins.GoToLine(true, goToLineTranslations, false), new codeInput.plugins.Indent(true, 2, {"(": ")", "[": "]", "{": "}"}, true, indentTranslations), new codeInput.plugins.SelectTokenCallbacks(new codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks(selectBrace, deselectAllBraces), true), //new codeInput.plugins.SpecialChars(true), diff --git a/tests/tester.js b/tests/tester.js index ed5b293..b15899c 100644 --- a/tests/tester.js +++ b/tests/tester.js @@ -120,8 +120,8 @@ function beginTest(isHLJS) { } }), new codeInput.plugins.Autodetect(), - new codeInput.plugins.FindAndReplace(), - new codeInput.plugins.GoToLine(), + new codeInput.plugins.FindAndReplace(true, true, {}, false), + new codeInput.plugins.GoToLine(true, {}, false), new codeInput.plugins.Indent(true, 2), new codeInput.plugins.SelectTokenCallbacks(codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks.createClassSynchronisation("in-selection"), false, true, true, true, true, false), new codeInput.plugins.SpecialChars(true), @@ -138,8 +138,8 @@ function beginTest(isHLJS) { popupElem.style.display = "none"; } }), - new codeInput.plugins.FindAndReplace(), - new codeInput.plugins.GoToLine(), + new codeInput.plugins.FindAndReplace(true, true, {}, false), + new codeInput.plugins.GoToLine(true, {}, false), new codeInput.plugins.Indent(true, 2), new codeInput.plugins.SelectTokenCallbacks(new codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks(selectBrace, deselectAllBraces), true), new codeInput.plugins.SpecialChars(true), @@ -489,7 +489,12 @@ console.log("I've got another line!", 2 < 3, "should be true."); await waitAsync(50); // Wait for highlighting so text updates // Open dialog and get interactive elements - textarea.dispatchEvent(new KeyboardEvent("keydown", { "cancelable": true, "key": "f", "ctrlKey": true })); + // Thanks to https://developer.mozilla.org/en-US/docs/Web/API/Navigator/platform + if(navigator.platform.startsWith("Mac") || navigator.platform === "iPhone") { + textarea.dispatchEvent(new KeyboardEvent("keydown", { "cancelable": true, "key": "f", "metaKey": true })); + } else { + textarea.dispatchEvent(new KeyboardEvent("keydown", { "cancelable": true, "key": "f", "ctrlKey": true })); + } let inputBoxes = codeInputElement.querySelectorAll(".code-input_find-and-replace_dialog input"); let findInput = inputBoxes[0]; let regExpCheckbox = inputBoxes[1]; @@ -534,8 +539,11 @@ console.log("I've got another line!", 2 < 3, "should be true."); assertEqual("FindAndReplace", "Selection End on Focused Match when Dialog Exited", textarea.selectionEnd, 8); // Open replace dialog; conduct a find and replace - textarea.dispatchEvent(new KeyboardEvent("keydown", { "cancelable": true, "key": "h", "ctrlKey": true })); - findInput.value = ""; + if(navigator.platform.startsWith("Mac") || navigator.platform === "iPhone") { + textarea.dispatchEvent(new KeyboardEvent("keydown", { "cancelable": true, "key": "h", "metaKey": true })); + } else { + textarea.dispatchEvent(new KeyboardEvent("keydown", { "cancelable": true, "key": "h", "ctrlKey": true })); + } findInput.value = ""; findInput.focus(); allowInputEvents(findInput); addText(findInput, "hello"); @@ -575,33 +583,48 @@ console.log("I've got another line!", 2 < 3, "should be true."); backspace(textarea); addText(textarea, "// 7 times table\nlet i = 1;\nwhile(i <= 12) { console.log(`7 x ${i} = ${7*i}`) }\n// That's my code.\n// This is another comment\n// Another\n// Line"); - textarea.dispatchEvent(new KeyboardEvent("keydown", { "cancelable": true, "key": "g", "ctrlKey": true })); - let lineInput = codeInputElement.querySelector(".code-input_go-to-line_dialog input"); + if(navigator.platform.startsWith("Mac") || navigator.platform === "iPhone") { + textarea.dispatchEvent(new KeyboardEvent("keydown", { "cancelable": true, "key": "g", "metaKey": true })); + } else { + textarea.dispatchEvent(new KeyboardEvent("keydown", { "cancelable": true, "key": "g", "ctrlKey": true })); + } let lineInput = codeInputElement.querySelector(".code-input_go-to-line_dialog input"); lineInput.value = "1"; lineInput.dispatchEvent(new KeyboardEvent("keydown", { "key": "Enter" })); lineInput.dispatchEvent(new KeyboardEvent("keyup", { "key": "Enter" })); assertEqual("GoToLine", "Line Only", textarea.selectionStart, 0); - textarea.dispatchEvent(new KeyboardEvent("keydown", { "cancelable": true, "key": "g", "ctrlKey": true })); - lineInput.value = "3:18"; + if(navigator.platform.startsWith("Mac") || navigator.platform === "iPhone") { + textarea.dispatchEvent(new KeyboardEvent("keydown", { "cancelable": true, "key": "g", "metaKey": true })); + } else { + textarea.dispatchEvent(new KeyboardEvent("keydown", { "cancelable": true, "key": "g", "ctrlKey": true })); + } lineInput.value = "3:18"; lineInput.dispatchEvent(new KeyboardEvent("keydown", { "key": "Enter" })); lineInput.dispatchEvent(new KeyboardEvent("keyup", { "key": "Enter" })); assertEqual("GoToLine", "Line and Column", textarea.selectionStart, 45); - textarea.dispatchEvent(new KeyboardEvent("keydown", { "cancelable": true, "key": "g", "ctrlKey": true })); - lineInput.value = "10"; + if(navigator.platform.startsWith("Mac") || navigator.platform === "iPhone") { + textarea.dispatchEvent(new KeyboardEvent("keydown", { "cancelable": true, "key": "g", "metaKey": true })); + } else { + textarea.dispatchEvent(new KeyboardEvent("keydown", { "cancelable": true, "key": "g", "ctrlKey": true })); + } lineInput.value = "10"; lineInput.dispatchEvent(new KeyboardEvent("keydown", { "key": "Enter" })); lineInput.dispatchEvent(new KeyboardEvent("keyup", { "key": "Enter" })); assertEqual("GoToLine", "Rejects Out-of-range Line", lineInput.classList.contains("code-input_go-to-line_error"), true); - textarea.dispatchEvent(new KeyboardEvent("keydown", { "cancelable": true, "key": "g", "ctrlKey": true })); - lineInput.value = "2:12"; + if(navigator.platform.startsWith("Mac") || navigator.platform === "iPhone") { + textarea.dispatchEvent(new KeyboardEvent("keydown", { "cancelable": true, "key": "g", "metaKey": true })); + } else { + textarea.dispatchEvent(new KeyboardEvent("keydown", { "cancelable": true, "key": "g", "ctrlKey": true })); + } lineInput.value = "2:12"; lineInput.dispatchEvent(new KeyboardEvent("keydown", { "key": "Enter" })); lineInput.dispatchEvent(new KeyboardEvent("keyup", { "key": "Enter" })); assertEqual("GoToLine", "Rejects Out-of-range Column", lineInput.classList.contains("code-input_go-to-line_error"), true); - textarea.dispatchEvent(new KeyboardEvent("keydown", { "cancelable": true, "key": "g", "ctrlKey": true })); - lineInput.value = "sausages"; + if(navigator.platform.startsWith("Mac") || navigator.platform === "iPhone") { + textarea.dispatchEvent(new KeyboardEvent("keydown", { "cancelable": true, "key": "g", "metaKey": true })); + } else { + textarea.dispatchEvent(new KeyboardEvent("keydown", { "cancelable": true, "key": "g", "ctrlKey": true })); + } lineInput.value = "sausages"; lineInput.dispatchEvent(new KeyboardEvent("keydown", { "key": "Enter" })); lineInput.dispatchEvent(new KeyboardEvent("keyup", { "key": "Enter" })); assertEqual("GoToLine", "Rejects Invalid Input", lineInput.classList.contains("code-input_go-to-line_error"), true); From b82231895964b73bfe7546e4301d3d929836a4e5 Mon Sep 17 00:00:00 2001 From: Oliver Geer Date: Fri, 7 Nov 2025 13:28:04 +0000 Subject: [PATCH 2/3] Document recommended Ctrl/Cmd interoperability for FindAndReplace and GoToLine --- docs/plugins/_index.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/plugins/_index.md b/docs/plugins/_index.md index 23deee2..e3b538e 100644 --- a/docs/plugins/_index.md +++ b/docs/plugins/_index.md @@ -146,6 +146,8 @@ Right now, you can only add one plugin of each type (e.g. one SelectTokenCallbac let findAndReplacePlugin = new codeInput.plugins.FindAndReplace( true, // Should Ctrl+F be overriden for find-and-replace find functionality? Either way, you can also trigger it yourself using (instance of this plugin)`.showPrompt(code-input element, false)`. true, // Should Ctrl+H be overriden for find-and-replace replace functionality? Either way, you can also trigger it yourself using (instance of this plugin)`.showPrompt(code-input element, true)`. + {}, // Keep this as an empty object for the English UI, or add translations as in https://code-input-js.org/i18n/ + false // Should Ctrl key be forced for keyboard shortcuts; setting this to false makes the keyboard shortcut follow the operating system (i.e. Cmd on Apple). ); // Programatically opening the dialogs, to integrate with your user interface function find() { @@ -196,6 +198,8 @@ Hickory dickory dock.