From 9cf2338884b3c8152ad15596ae578b883f19dcd6 Mon Sep 17 00:00:00 2001 From: Niko Abeler Date: Thu, 19 Jun 2025 11:22:33 +0200 Subject: [PATCH 1/9] Update version in README.md to 2.4 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 87491da..149ab3b 100644 --- a/README.md +++ b/README.md @@ -54,8 +54,8 @@ From JSDelivr CDN (click) ```html - - + + ``` From 994e6033c703cb2f711aea1a7526509d51befbb9 Mon Sep 17 00:00:00 2001 From: Oliver Geer Date: Thu, 19 Jun 2025 18:15:38 +0100 Subject: [PATCH 2/9] Add internationalisation tests and support text bidirectionality Works: - Rendering of left-to-right and right-to-left text, if the HTML dir attribute is set on the code-input element or a parent - Rendering of left-to-right or right-to-left text inside a block of opposite-directionality text - Consistent widths of bold text in Latin, Han, Arabic scripts in Librewolf (Firefox, Gecko), GNOME Web (Safari-like, Webkit) and Chromium (Chrome, Blink) on Wayland, KDE Plasma, EndeavourOS (Arch Linux) GNU/Linux Doesn't work: - Consistent widths of bold text in Hebrew, Hindi scripts in Librewolf and GNOME Web - does work in Chromium --- code-input.css | 3 +- tests/i18n.html | 127 +++++++++++++++++++++++++++++++++++++++++++++++ tests/prism.html | 2 +- 3 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 tests/i18n.html diff --git a/code-input.css b/code-input.css index 89df115..05387fe 100644 --- a/code-input.css +++ b/code-input.css @@ -66,6 +66,7 @@ code-input textarea, code-input pre, code-input pre * { font-family: inherit!important; line-height: inherit!important; tab-size: inherit!important; + text-align: inherit!important; } code-input textarea, code-input pre { @@ -182,4 +183,4 @@ code-input:not(:has(.code-input_keyboard-navigation-instructions:empty)):has(tex code-input:not(:has(.code-input_keyboard-navigation-instructions:empty)):has(textarea:focus):not(.code-input_mouse-focused):not(.code-input_pre-element-styled) pre code, code-input:not(:has(.code-input_keyboard-navigation-instructions:empty)):has(textarea:focus):not(.code-input_mouse-focused).code-input_pre-element-styled pre { padding-top: calc(var(--padding) + 3em)!important; -} \ No newline at end of file +} diff --git a/tests/i18n.html b/tests/i18n.html new file mode 100644 index 0000000..1458125 --- /dev/null +++ b/tests/i18n.html @@ -0,0 +1,127 @@ + + + + Right-to-left text! + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/prism.html b/tests/prism.html index 0a8ae01..50fb510 100644 --- a/tests/prism.html +++ b/tests/prism.html @@ -51,4 +51,4 @@

Test for highlight.js

beginTest(false); - \ No newline at end of file + From cdadecec68513bf1648a03a6638295d5109ed815 Mon Sep 17 00:00:00 2001 From: Oliver Geer Date: Fri, 20 Jun 2025 17:25:58 +0100 Subject: [PATCH 3/9] Make plugin UI localisable; Make keyboard navigation message look nicer, especially for right-to-left text --- code-input.css | 25 +++++++++--- code-input.d.ts | 41 ++++++++++++++++--- code-input.js | 11 ++++++ plugins/find-and-replace.js | 63 +++++++++++++++++++---------- plugins/go-to-line.js | 13 ++++-- plugins/indent.js | 37 +++++++++++++---- plugins/prism-line-numbers.css | 7 +++- tests/i18n.html | 72 +++++++++++++++++++++++++++++++++- 8 files changed, 226 insertions(+), 43 deletions(-) diff --git a/code-input.css b/code-input.css index 05387fe..d7a5bf3 100644 --- a/code-input.css +++ b/code-input.css @@ -139,6 +139,7 @@ code-input:not(.code-input_loaded) pre, code-input:not(.code-input_loaded) texta /* Contains dialog boxes that might appear as the result of a plugin. Sticks to the top of the code-input element */ + code-input .code-input_dialog-container { z-index: 2; @@ -146,30 +147,44 @@ code-input .code-input_dialog-container { grid-row: 1; grid-column: 1; - top: 0px; + top: 0; left: 0; + + margin: 0; + padding: 0; width: 100%; height: 0; - /* Dialog boxes' text is left-aligned */ - text-align: left; + /* Dialog boxes' text is based on text-direction */ + text-align: inherit; } +[dir=rtl] code-input .code-input_dialog-container, code-input[dir=rtl] .code-input_dialog-container { + left: unset; + right: 0; +} + /* Instructions specific to keyboard navigation set by plugins that override Tab functionality. */ code-input .code-input_dialog-container .code-input_keyboard-navigation-instructions { top: 0; - right: 0; + left: 0; + display: block; position: absolute; background-color: black; color: white; padding: 2px; padding-left: 10px; - text-wrap: pretty; + margin: 0; + text-wrap: balance; overflow: hidden; text-overflow: ellipsis; width: calc(100% - 12px); max-height: 3em; } +[dir=rtl] code-input .code-input_dialog-container .code-input_keyboard-navigation-instructions, code-input[dir=rtl] .code-input_dialog-container .code-input_keyboard-navigation-instructions { + left: unset; + right: 0; +} code-input:not(:has(textarea:focus)) .code-input_dialog-container .code-input_keyboard-navigation-instructions, code-input.code-input_mouse-focused .code-input_dialog-container .code-input_keyboard-navigation-instructions, diff --git a/code-input.d.ts b/code-input.d.ts index 43568aa..285caa4 100644 --- a/code-input.d.ts +++ b/code-input.d.ts @@ -132,14 +132,36 @@ export namespace plugins { * Create a find-and-replace command plugin to pass into a template * @param {boolean} useCtrlF Should Ctrl+F be overriden for find-and-replace find functionality? If not, you can 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? If not, you can 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. */ - constructor(useCtrlF?: boolean, useCtrlH?: boolean); + constructor(useCtrlF?: boolean, useCtrlH?: boolean, + instructionTranslations?: { + start?: string; + none?: string; + oneFound?: string; + matchIndex?: (index: Number, count: Number) => string; + error?: (message: string) => string; + infiniteLoopError?: string; + closeDialog?: string; + findPlaceholder?: string; + findCaseSensitive?: string; + findRegExp?: string; + replaceTitle?: string; + replacePlaceholder?: string; + findNext?: string; + findPrevious?: string; + replaceActionShort?: string; + replaceAction?: string; + replaceAllActionShort?: string; + replaceAllAction?: string + } + ); /** * Show a find-and-replace dialog. * @param {codeInput.CodeInput} codeInputElement the `` element. * @param {boolean} replacePartExpanded whether the replace part of the find-and-replace dialog should be expanded */ - showPrompt(codeInput: CodeInput, replacePartExpanded: boolean): void; + showPrompt(codeInputElement: CodeInput, replacePartExpanded: boolean): void; } /** @@ -150,8 +172,13 @@ export namespace plugins { /** * 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? If not, 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 English text. */ - constructor(useCtrlG: boolean); + constructor(useCtrlG: boolean, + instructionTranslations?: { + closeDialog?: string; + input?: string; + }); /** * Show a search-like dialog prompting line number. * @param {codeInput.CodeInput} codeInput the `` element. @@ -171,8 +198,12 @@ export namespace plugins { * @param {Number} numSpaces How many spaces is each tab character worth? Defaults to 4. * @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. */ - constructor(defaultSpaces?: boolean, numSpaces?: Number, bracketPairs?: Object, escTabToChangeFocus?: boolean); + constructor(defaultSpaces?: boolean, numSpaces?: Number, bracketPairs?: Object, escTabToChangeFocus?: boolean, instructionTranslations?: { + tabForIndentation?: string; + tabForNavigation?: string; + }); } /** @@ -355,4 +386,4 @@ export class CodeInput extends HTMLElement { } * @param {string} templateName - the name to register the template under * @param {Object} template - a Template object instance - see `codeInput.templates` */ -export function registerTemplate(templateName: string, template: Template): void; \ No newline at end of file +export function registerTemplate(templateName: string, template: Template): void; diff --git a/code-input.js b/code-input.js index 7bace3b..e724927 100644 --- a/code-input.js +++ b/code-input.js @@ -377,6 +377,17 @@ var codeInput = { }); } + /** + * Replace the keys in destination with any source + * @param {Object} destination Where to place the translated strings, already filled with the keys pointing to English strings. + * @param {Object} source The same keys, or some of them, mapped to translated strings. + */ + addTranslations(destination, source) { + for(const key in source) { + destination[key] = source[key]; + } + } + /** * Runs before code is highlighted. * @param {codeInput.CodeInput} codeInput - The codeInput element diff --git a/plugins/find-and-replace.js b/plugins/find-and-replace.js index 833c814..639c398 100644 --- a/plugins/find-and-replace.js +++ b/plugins/find-and-replace.js @@ -8,15 +8,38 @@ codeInput.plugins.FindAndReplace = class extends codeInput.Plugin { findMatchesOnValueChange = true; // Needed so the program can insert text to the find value and thus add it to Ctrl+Z without highlighting matches. + instructions = { + start: "Search for matches in your code.", + none: "No matches", + oneFound: "1 match found.", + matchIndex: (index, count) => `${index} of ${count} matches.`, + error: (message) => `Error: ${message}`, + infiniteLoopError: "Causes an infinite loop", + closeDialog: "Close Dialog and Return to Editor", + findPlaceholder: "Find", + findCaseSensitive: "Match Case Sensitive", + findRegExp: "Use JavaScript Regular Expression", + replaceTitle: "Replace", + replacePlaceholder: "Replace with", + findNext: "Find Next Occurrence", + findPrevious: "Find Previous Occurrence", + replaceActionShort: "Replace", + replaceAction: "Replace This Occurrence", + replaceAllActionShort: "Replace All", + replaceAllAction: "Replace All Occurrences" + }; + /** * Create a find-and-replace command plugin to pass into a template * @param {boolean} useCtrlF Should Ctrl+F be overriden for find-and-replace find functionality? If not, you can 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? If not, you can 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. */ - constructor(useCtrlF = true, useCtrlH = true) { + constructor(useCtrlF = true, useCtrlH = true, instructionTranslations = {}) { super([]); // No observed attributes this.useCtrlF = useCtrlF; this.useCtrlH = useCtrlH; + this.addTranslations(this.instructions, instructionTranslations); } /* Add keystroke events */ @@ -56,13 +79,13 @@ codeInput.plugins.FindAndReplace = class extends codeInput.Plugin { updateMatchDescription(dialog) { // 1-indexed if(dialog.findInput.value.length == 0) { - dialog.matchDescription.textContent = "Search for matches in your code."; + dialog.matchDescription.textContent = this.instructions.start; } else if(dialog.findMatchState.numMatches <= 0) { - dialog.matchDescription.textContent = "No matches."; + dialog.matchDescription.textContent = this.instructions.none; } else if(dialog.findMatchState.numMatches == 1) { - dialog.matchDescription.textContent = "1 match found."; + dialog.matchDescription.textContent = this.instructions.oneFound; } else { - dialog.matchDescription.textContent = `${dialog.findMatchState.focusedMatchID+1} of ${dialog.findMatchState.numMatches} matches.`; + dialog.matchDescription.textContent = this.instructions.matchIndex(dialog.findMatchState.focusedMatchID+1, dialog.findMatchState.numMatches); } } @@ -83,7 +106,7 @@ codeInput.plugins.FindAndReplace = class extends codeInput.Plugin { dialog.findInput.classList.add('code-input_find-and-replace_error'); // Only show last part of error message let messageParts = err.message.split(": "); - dialog.matchDescription.textContent = "Error: " + messageParts[messageParts.length-1]; // Show only last part of error. + dialog.matchDescription.textContent = this.instructions.error(messageParts[messageParts.length-1]); // Show only last part of error. return; } else { throw err; @@ -183,7 +206,7 @@ codeInput.plugins.FindAndReplace = class extends codeInput.Plugin { const replaceAllButton = document.createElement('button'); const cancel = document.createElement('span'); cancel.setAttribute("tabindex", 0); // Visible to keyboard navigation - cancel.setAttribute("title", "Close Dialog and Return to Editor"); + cancel.setAttribute("title", this.instructions.closeDialog); buttonContainer.appendChild(findNextButton); buttonContainer.appendChild(findPreviousButton); @@ -203,35 +226,35 @@ codeInput.plugins.FindAndReplace = class extends codeInput.Plugin { dialog.className = 'code-input_find-and-replace_dialog'; findInput.spellcheck = false; - findInput.placeholder = "Find"; + findInput.placeholder = this.instructions.findPlaceholder; findCaseSensitiveCheckbox.setAttribute("type", "checkbox"); - findCaseSensitiveCheckbox.title = "Match Case Sensitive"; + findCaseSensitiveCheckbox.title = this.instructions.findCaseSensitive; findCaseSensitiveCheckbox.classList.add("code-input_find-and-replace_case-sensitive-checkbox"); findRegExpCheckbox.setAttribute("type", "checkbox"); - findRegExpCheckbox.title = "Use JavaScript Regular Expression"; + findRegExpCheckbox.title = this.instructions.findRegExp; findRegExpCheckbox.classList.add("code-input_find-and-replace_reg-exp-checkbox"); matchDescription.textContent = "Search for matches in your code."; matchDescription.classList.add("code-input_find-and-replace_match-description"); - replaceSummary.innerText = "Replace"; + replaceSummary.innerText = this.instructions.replaceTitle; replaceInput.spellcheck = false; - replaceInput.placeholder = "Replace with"; + replaceInput.placeholder = this.instructions.replacePlaceholder; findNextButton.innerText = "↓"; - findNextButton.title = "Find Next Occurence"; + findNextButton.title = this.instructions.findNext; findPreviousButton.innerText = "↑"; - findPreviousButton.title = "Find Previous Occurence"; + findPreviousButton.title = this.instructions.findPrevious; replaceButton.className = 'code-input_find-and-replace_button-hidden'; - replaceButton.innerText = "Replace"; - replaceButton.title = "Replace This Occurence"; + replaceButton.innerText = this.instructions.replaceActionShort; + replaceButton.title = this.instructions.replaceAction; replaceButton.addEventListener("focus", () => { // Show replace section replaceDropdown.setAttribute("open", true); }); replaceAllButton.className = 'code-input_find-and-replace_button-hidden'; - replaceAllButton.innerText = "Replace All"; - replaceAllButton.title = "Replace All Occurences"; + replaceAllButton.innerText = this.instructions.replaceAllActionShort; + replaceAllButton.title = this.instructions.replaceAllAction; replaceAllButton.addEventListener("focus", () => { // Show replace section replaceDropdown.setAttribute("open", true); @@ -470,7 +493,7 @@ codeInput.plugins.FindAndReplace.FindMatchState = class { while ((match = searchRegexp.exec(this.codeInput.value)) !== null) { let matchText = match[0]; if(matchText.length == 0) { - throw SyntaxError("Causes an infinite loop"); + throw SyntaxError(this.instructions.infiniteLoopError); } // Add next match block if needed @@ -720,4 +743,4 @@ codeInput.plugins.FindAndReplace.FindMatchState = class { endIndex -= childText.length; } } -} \ No newline at end of file +} diff --git a/plugins/go-to-line.js b/plugins/go-to-line.js index 4156d16..b6ea68a 100644 --- a/plugins/go-to-line.js +++ b/plugins/go-to-line.js @@ -5,13 +5,20 @@ codeInput.plugins.GoToLine = class extends codeInput.Plugin { useCtrlG = false; + instructions = { + closeDialog: "Close Dialog and Return to Editor", + input: "Line:Column / Line no. then Enter", + }; + /** * 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? If not, 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. */ - constructor(useCtrlG = true) { + constructor(useCtrlG = true, instructionTranslations = {}) { super([]); // No observed attributes this.useCtrlG = useCtrlG; + this.addTranslations(this.instructions, instructionTranslations); } /* Add keystroke events */ @@ -85,14 +92,14 @@ codeInput.plugins.GoToLine = class extends codeInput.Plugin { const input = document.createElement('input'); const cancel = document.createElement('span'); cancel.setAttribute("tabindex", 0); // Visible to keyboard navigation - cancel.setAttribute("title", "Close Dialog and Return to Editor"); + cancel.setAttribute("title", this.instructions.closeDialog); dialog.appendChild(input); dialog.appendChild(cancel); dialog.className = 'code-input_go-to-line_dialog'; input.spellcheck = false; - input.placeholder = "Line:Column / Line no. then Enter"; + input.placeholder = this.instructions.input; dialog.codeInput = codeInput; dialog.textarea = textarea; dialog.input = input; diff --git a/plugins/indent.js b/plugins/indent.js index 4189e17..7ddc2fe 100644 --- a/plugins/indent.js +++ b/plugins/indent.js @@ -12,14 +12,20 @@ codeInput.plugins.Indent = class extends codeInput.Plugin { escTabToChangeFocus = true; escJustPressed = false; // Becomes true when Escape key is pressed and false when another key is pressed + instructions = { + tabForIndentation: "Tab and Shift-Tab currently for indentation. Press Esc to enable keyboard navigation.", + tabForNavigation: "Tab and Shift-Tab currently for keyboard navigation. Type to return to indentation.", + }; + /** * Create an indentation plugin to pass into a template * @param {boolean} defaultSpaces Should the Tab key enter spaces rather than tabs? Defaults to false. * @param {Number} numSpaces How many spaces is each tab character worth? Defaults to 4. * @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 Tab and 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 available keys and English text. */ - constructor(defaultSpaces=false, numSpaces=4, bracketPairs={"(": ")", "[": "]", "{": "}"}, escTabToChangeFocus=true) { + constructor(defaultSpaces=false, numSpaces=4, bracketPairs={"(": ")", "[": "]", "{": "}"}, escTabToChangeFocus=true, instructionTranslations = {}) { super([]); // No observed attributes this.bracketPairs = bracketPairs; @@ -32,6 +38,8 @@ codeInput.plugins.Indent = class extends codeInput.Plugin { } this.escTabToChangeFocus = true; + + this.addTranslations(this.instructions, instructionTranslations); } /** @@ -49,7 +57,7 @@ codeInput.plugins.Indent = class extends codeInput.Plugin { afterElementsAdded(codeInput) { let textarea = codeInput.textareaElement; - textarea.addEventListener('focus', (event) => { if(this.escTabToChangeFocus) codeInput.setKeyboardNavInstructions("Tab and Shift-Tab currently for indentation. Press Esc to enable keyboard navigation.", true); }) + textarea.addEventListener('focus', (event) => { if(this.escTabToChangeFocus) codeInput.setKeyboardNavInstructions(this.instructions.tabForIndentation, true); }) textarea.addEventListener('keydown', (event) => { this.checkTab(codeInput, event); this.checkEnter(codeInput, event); this.checkBackspace(codeInput, event); }); textarea.addEventListener('beforeinput', (event) => { this.checkCloseBracket(codeInput, event); }); @@ -83,13 +91,13 @@ codeInput.plugins.Indent = class extends codeInput.Plugin { // Accessibility - allow Tab for keyboard navigation when Esc pressed right before it. if(event.key == "Escape") { this.escJustPressed = true; - codeInput.setKeyboardNavInstructions("Tab and Shift-Tab currently for keyboard navigation. Type to return to indentation.", false); + codeInput.setKeyboardNavInstructions(this.instructions.tabForNavigation, false); return; } else if(event.key != "Tab") { if(event.key == "Shift") { return; // Shift+Tab after Esc should still be keyboard navigation } - codeInput.setKeyboardNavInstructions("Tab and Shift-Tab currently for indentation. Press Esc to enable keyboard navigation.", false); + codeInput.setKeyboardNavInstructions(this.instructions.tabForIndentation, false); this.escJustPressed = false; return; } @@ -158,10 +166,23 @@ codeInput.plugins.Indent = class extends codeInput.Plugin { inputElement.selectionEnd = selectionEndI; // move scroll position to follow code - if(event.shiftKey) { - codeInput.scrollBy(-codeInput.pluginData.indent.indentationWidthPx, 0); + const textDirection = getComputedStyle(codeInput).direction; + if(textDirection == "rtl") { + if(event.shiftKey) { + // Scroll right + codeInput.scrollBy(codeInput.pluginData.indent.indentationWidthPx, 0); + } else { + // Scroll left + codeInput.scrollBy(-codeInput.pluginData.indent.indentationWidthPx, 0); + } } else { - codeInput.scrollBy(codeInput.pluginData.indent.indentationWidthPx, 0); + if(event.shiftKey) { + // Scroll left + codeInput.scrollBy(-codeInput.pluginData.indent.indentationWidthPx, 0); + } else { + // Scroll right + codeInput.scrollBy(codeInput.pluginData.indent.indentationWidthPx, 0); + } } } @@ -305,4 +326,4 @@ codeInput.plugins.Indent = class extends codeInput.Plugin { } } } -} \ No newline at end of file +} diff --git a/plugins/prism-line-numbers.css b/plugins/prism-line-numbers.css index aab1835..036d373 100644 --- a/plugins/prism-line-numbers.css +++ b/plugins/prism-line-numbers.css @@ -13,4 +13,9 @@ code-input.line-numbers textarea, code-input.line-numbers.code-input_pre-element /* Ensure pre code/textarea just wide enough to give 100% width with line numbers */ code-input.line-numbers, .line-numbers code-input { grid-template-columns: calc(100% - max(0em, calc(3.8em - var(--padding, 16px)))); -} \ No newline at end of file +} + +/* Make keyboard navigation still fill width */ +code-input .code-input_dialog-container .code-input_keyboard-navigation-instructions { + width: calc(100% + max(3.8em, var(--padding, 16px)))!important; +} diff --git a/tests/i18n.html b/tests/i18n.html index 1458125..c6b72fc 100644 --- a/tests/i18n.html +++ b/tests/i18n.html @@ -1,7 +1,7 @@ - Right-to-left text! + code-input should be global @@ -79,6 +79,72 @@ new codeInput.plugins.SelectTokenCallbacks(new codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks(selectBrace, deselectAllBraces), true), new codeInput.plugins.SpecialChars(true),*/ ])); + + // Attribution: Translated by Oliver Geer with some help from English Wiktionary + let findAndReplaceTranslations = { + start: "Buscar términos en su código.", + none: "No hay sucesos", + oneFound: "1 suceso encontrado.", + matchIndex: (index, count) => `${index} de ${count} sucesos.`, + error: (message) => `Error: ${message}`, + infiniteLoopError: "Causa un ciclo infinito", + closeDialog: "Cerrar el Diálogo y Regresar al Editor", + findPlaceholder: "Buscar", + findCaseSensitive: "Prestar atención a las minúsculas/mayúsculas", + findRegExp: "Utilizar expresión regular de JavaScript", + replaceTitle: "Reemplazar", + replacePlaceholder: "Reemplazar con", + findNext: "Buscar Suceso Próximo", + findPrevious: "Buscar Suceso Previo", + replaceActionShort: "Reemplazar", + replaceAction: "Reemplazar este Suceso", + replaceAllActionShort: "Reemplazar Todos", + replaceAllAction: "Reemplazar Todos los Sucesos" + }; + let goToLineTranslations = { + closeDialog: "Cerrar el Diálogo y Regresar al Editor", + input: "Línea:Columno o Línea luego Retorno", + }; + let indentTranslations = { + tabForIndentation: "Tabulador y Mayús-Tabulador actualmente para la indentación. Tecla Escape para activar la navegación por el teclado.", + tabForNavigation: "Tabulador y Mayús-Tabulador actualmente para la navegación por el teclado. Tecla para activar la indentación.", + }; + + codeInput.registerTemplate("hljs+", codeInput.templates.hljs(hljs, [ + new codeInput.plugins.AutoCloseBrackets(), + new codeInput.plugins.Autocomplete(function(popupElem, textarea, selectionEnd) { + if(textarea.value.substring(selectionEnd-5, selectionEnd) == "popup") { + // Show popup + popupElem.style.display = "block"; + popupElem.innerHTML = "Here's your popup!"; + } else { + popupElem.style.display = "none"; + } + }), + new codeInput.plugins.Autodetect(), + new codeInput.plugins.FindAndReplace(true, true, findAndReplaceTranslations), + new codeInput.plugins.GoToLine(true, goToLineTranslations), + 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), + ])); + codeInput.registerTemplate("prism+", codeInput.templates.prism(Prism, [ + new codeInput.plugins.AutoCloseBrackets(), + new codeInput.plugins.Autocomplete(function(popupElem, textarea, selectionEnd) { + if(textarea.value.substring(selectionEnd-5, selectionEnd) == "popup") { + // Show popup + popupElem.style.display = "block"; + popupElem.innerHTML = "Here's your popup!"; + } else { + popupElem.style.display = "none"; + } + }), + new codeInput.plugins.FindAndReplace(true, true, findAndReplaceTranslations), + new codeInput.plugins.GoToLine(true, goToLineTranslations), + 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), + ])); @@ -88,6 +154,10 @@ + + + + - + + ``` diff --git a/package.json b/package.json index 4828ac6..8e185e2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@webcoder49/code-input", - "version": "2.4.0", + "version": "2.5.0", "description": "Fully customisable, editable syntax-highlighted textareas.", "browser": "code-input.js", "scripts": { From d53c19bf11d33c12204524ebad447e6be78a7117 Mon Sep 17 00:00:00 2001 From: Oliver Geer Date: Wed, 9 Jul 2025 18:40:36 +0100 Subject: [PATCH 8/9] Use classes for templates; keep but deprecate use of function template creators --- code-input.d.ts | 34 ++++++++++++++----- code-input.js | 88 ++++++++++++++++++++++++++++++++++--------------- 2 files changed, 88 insertions(+), 34 deletions(-) diff --git a/code-input.d.ts b/code-input.d.ts index 285caa4..284b2c2 100644 --- a/code-input.d.ts +++ b/code-input.d.ts @@ -345,17 +345,35 @@ export class Template { */ export namespace templates { /** - * Constructor to create a template that uses Prism.js syntax highlighting (https://prismjs.com/) - * @param {Object} prism Import Prism.js, then after that import pass the `Prism` object as this parameter. - * @param {codeInput.Plugin[]} plugins - An array of plugin objects to add extra features - see `codeInput.plugins` - * @returns template object + * A template that uses Prism.js syntax highlighting (https://prismjs.com/). + */ + class Prism extends Template { + /** + * Constructor to create a template that uses Prism.js syntax highlighting (https://prismjs.com/) + * @param {Object} prism Import Prism.js, then after that import pass the `Prism` object as this parameter. + * @param {codeInput.Plugin[]} plugins - An array of plugin objects to add extra features - see `codeInput.plugins` + * @returns template object + */ + constructor(prism: Object, plugins?: Plugin[]) + } + /** + * @deprecated Please use `new codeInput.templates.Prism(...)` */ function prism(prism: Object, plugins?: Plugin[]): Template /** - * Constructor to create a template that uses highlight.js syntax highlighting (https://highlightjs.org/) - * @param {Object} hljs Import highlight.js, then after that import pass the `hljs` object as this parameter. - * @param {codeInput.Plugin[]} plugins - An array of plugin objects to add extra features - see `codeInput.plugins` - * @returns template object + * A template that uses highlight.js syntax highlighting (https://highlightjs.org/). + */ + class Hljs extends Template { + /** + * Constructor to create a template that uses highlight.js syntax highlighting (https://highlightjs.org/) + * @param {Object} hljs Import highlight.js, then after that import pass the `hljs` object as this parameter. + * @param {codeInput.Plugin[]} plugins - An array of plugin objects to add extra features - see `codeInput.plugins` + * @returns template object + */ + constructor(hljs: Object, plugins?: Plugin[]) + } + /** + * @deprecated Please use `new codeInput.templates.Hljs(...)` */ function hljs(hljs: Object, plugins?: Plugin[]): Template /** diff --git a/code-input.js b/code-input.js index e724927..a9fc914 100644 --- a/code-input.js +++ b/code-input.js @@ -222,38 +222,19 @@ var codeInput = { * For adding small pieces of functionality, please see `codeInput.plugins`. */ templates: { + // (Source code for class templates after var codeInput = ... so they can extend the codeInput.Template class) /** - * Constructor to create a template that uses Prism.js syntax highlighting (https://prismjs.com/) - * @param {Object} prism Import Prism.js, then after that import pass the `Prism` object as this parameter. - * @param {codeInput.Plugin[]} plugins - An array of plugin objects to add extra features - see `codeInput.plugins` - * @returns {codeInput.Template} template object + * @deprecated Please use `new codeInput.templates.Prism(...)` */ prism(prism, plugins = []) { // Dependency: Prism.js (https://prismjs.com/) - return new codeInput.Template( - prism.highlightElement, // highlight - true, // preElementStyled - true, // isCode - false, // includeCodeInputInHighlightFunc - plugins - ); + return new codeInput.templates.Prism(prism, plugins); }, + /** - * Constructor to create a template that uses highlight.js syntax highlighting (https://highlightjs.org/) - * @param {Object} hljs Import highlight.js, then after that import pass the `hljs` object as this parameter. - * @param {codeInput.Plugin[]} plugins - An array of plugin objects to add extra features - see `codeInput.plugins` - * @returns {codeInput.Template} template object + * @deprecated Please use `new codeInput.templates.Hljs(...)` */ hljs(hljs, plugins = []) { // Dependency: Highlight.js (https://highlightjs.org/) - return new codeInput.Template( - function(codeElement) { - codeElement.removeAttribute("data-highlighted"); - hljs.highlightElement(codeElement); - }, // highlight - false, // preElementStyled - true, // isCode - false, // includeCodeInputInHighlightFunc - plugins - ); + return new codeInput.templates.Hljs(hljs, plugins); }, /** @@ -318,7 +299,7 @@ var codeInput = { }, /** - * @deprecated Please use `new codeInput.Template()` + * @deprecated Please use `new codeInput.Template(...)` */ custom(highlight = function () { }, preElementStyled = true, isCode = true, includeCodeInputInHighlightFunc = false, plugins = []) { return { @@ -1058,4 +1039,59 @@ var codeInput = { } } +{ + // Templates are defined here after the codeInput variable is set, because they reference it by extending codeInput.Template. + + // ESM-SUPPORT-START-TEMPLATE-prism Do not (re)move this - it's needed for ESM generation! + /** + * A template that uses Prism.js syntax highlighting (https://prismjs.com/). + */ + class Prism extends codeInput.Template { // Dependency: Prism.js (https://prismjs.com/) + /** + * Constructor to create a template that uses Prism.js syntax highlighting (https://prismjs.com/) + * @param {Object} prism Import Prism.js, then after that import pass the `Prism` object as this parameter. + * @param {codeInput.Plugin[]} plugins - An array of plugin objects to add extra features - see `codeInput.plugins` + * @returns {codeInput.Template} template object + */ + constructor(prism, plugins = []) { + super( + prism.highlightElement, // highlight + true, // preElementStyled + true, // isCode + false, // includeCodeInputInHighlightFunc + plugins + ); + } + }; + // ESM-SUPPORT-END-TEMPLATE-prism Do not (re)move this - it's needed for ESM generation! + codeInput.templates.Prism = Prism; + + // ESM-SUPPORT-START-TEMPLATE-hljs Do not (re)move this - it's needed for ESM generation! + /** + * A template that uses highlight.js syntax highlighting (https://highlightjs.org/). + */ + class Hljs extends codeInput.Template { // Dependency: Highlight.js (https://highlightjs.org/) + /** + * Constructor to create a template that uses highlight.js syntax highlighting (https://highlightjs.org/) + * @param {Object} hljs Import highlight.js, then after that import pass the `hljs` object as this parameter. + * @param {codeInput.Plugin[]} plugins - An array of plugin objects to add extra features - see `codeInput.plugins` + * @returns {codeInput.Template} template object + */ + constructor(hljs, plugins = []) { + super( + function(codeElement) { + codeElement.removeAttribute("data-highlighted"); + hljs.highlightElement(codeElement); + }, // highlight + false, // preElementStyled + true, // isCode + false, // includeCodeInputInHighlightFunc + plugins + ); + } + }; + // ESM-SUPPORT-END-TEMPLATE-hljs Do not (re)move this - it's needed for ESM generation! + codeInput.templates.Hljs = Hljs; +} + customElements.define("code-input", codeInput.CodeInput); From fe408d3cb488a8712d4a602101857587d12af38a Mon Sep 17 00:00:00 2001 From: Oliver Geer Date: Fri, 11 Jul 2025 13:45:29 +0100 Subject: [PATCH 9/9] Use classes for templates in README and tests --- README.md | 6 +++--- tests/tester.js | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index f336e9a..2607e46 100644 --- a/README.md +++ b/README.md @@ -64,12 +64,12 @@ The next step is to set up a `template` to link `code-input` to your syntax-high - *Highlight.js:* ```js - codeInput.registerTemplate("syntax-highlighted", codeInput.templates.hljs(hljs, [] /* Array of plugins (see below) */)); + codeInput.registerTemplate("syntax-highlighted", new codeInput.templates.Hljs(hljs, [] /* Array of plugins (see below) */)); ``` - *Prism.js:* ```js - codeInput.registerTemplate("syntax-highlighted", codeInput.templates.prism(Prism, [] /* Array of plugins (see below) */)); + codeInput.registerTemplate("syntax-highlighted", new codeInput.templates.Prism(Prism, [] /* Array of plugins (see below) */)); ``` - *Custom:* @@ -106,7 +106,7 @@ The next step is to set up a `template` to link `code-input` to your syntax-high