diff --git a/README.md b/README.md index cc00883..c9ed52c 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,8 @@ The next step is to set up a `template` to link `code-input` to your syntax-high ``` +> ⚠️ Unfortunately placing multiple plugins of the same type in a template can currently cause errors and undefined behaviour, even if such a configuration makes logical sense. [This is issue #118](https://github.com/WebCoder49/code-input/issues/118) and will be fixed as soon as possible - if you'd like to help and have the time you're welcome, but it's also at the top of the maintainer's To-Do list. + To see a full list of plugins and their functions, please see [plugins/README.md](./plugins/README.md). ### 4. Using the component diff --git a/code-input.d.ts b/code-input.d.ts index 1f0ea6b..43568aa 100644 --- a/code-input.d.ts +++ b/code-input.d.ts @@ -96,7 +96,7 @@ export namespace plugins { class Autocomplete extends Plugin { /** * Pass in a function to create a plugin that displays the popup that takes in (popup element, textarea, textarea.selectionEnd). - * @param {function} updatePopupCallback a function to display the popup that takes in (popup element, textarea, textarea.selectionEnd). + * @param {(popupElement: HTMLElement, textarea: HTMLTextAreaElement, selectionEnd: number) => void} updatePopupCallback a function to display the popup that takes in (popup element, textarea, textarea.selectionEnd). */ constructor(updatePopupCallback: (popupElem: HTMLElement, textarea: HTMLTextAreaElement, selectionEnd: number) => void); } @@ -175,6 +175,55 @@ export namespace plugins { constructor(defaultSpaces?: boolean, numSpaces?: Number, bracketPairs?: Object, escTabToChangeFocus?: boolean); } + /** + * Make tokens in the
element that are included within the selected text of the
+ * gain a CSS class while selected, or trigger JavaScript callbacks.
+ * Files: select-token-callbacks.js
+ */
+ class SelectTokenCallbacks extends Plugin {
+ /**
+ * Set up the behaviour of tokens text-selected in the `` element, and the exact definition of a token being text-selected.
+ *
+ * All parameters are optional. If you provide no arguments to the constructor, this will dynamically apply the "code-input_select-token-callbacks_selected" class to selected tokens only, for you to style via CSS.
+ *
+ * @param {codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks} tokenSelectorCallbacks What to do with text-selected tokens. See docstrings for the TokenSelectorCallbacks class.
+ * @param {boolean} onlyCaretNotSelection If true, tokens will only be marked as selected when no text is selected but rather the caret is inside them (start of selection == end of selection). Default false.
+ * @param {boolean} caretAtStartIsSelected Whether the caret or text selection's end being just before the first character of a token means said token is selected. Default true.
+ * @param {boolean} caretAtEndIsSelected Whether the caret or text selection's start being just after the last character of a token means said token is selected. Default true.
+ * @param {boolean} createSubTokens Whether temporary `` elements should be created inside partially-selected tokens containing just the selected text and given the selected class. Default false.
+ * @param {boolean} partiallySelectedTokensAreSelected Whether tokens for which only some of their text is selected should be treated as selected. Default true.
+ * @param {boolean} parentTokensAreSelected Whether all parent tokens of selected tokens should be treated as selected. Default true.
+ */
+ constructor(tokenSelectorCallbacks?: codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks, onlyCaretNotSelection?: boolean, caretAtStartIsSelected?: boolean, caretAtEndIsSelected?: boolean, createSubTokens?: boolean, partiallySelectedTokensAreSelected?: boolean, parentTokensAreSelected?: boolean);
+ }
+
+ namespace SelectTokenCallbacks {
+ /**
+ * A data structure specifying what should be done with tokens when they are selected, and also allows for previously selected
+ * tokens to be dealt with each time the selection changes. See the constructor and the createClassSynchronisation static method.
+ */
+ class TokenSelectorCallbacks {
+ /**
+ * Pass any callbacks you want to customise the behaviour of selected tokens via JavaScript.
+ *
+ * (If the behaviour you want is just differently styling selected tokens _via CSS_, you should probably use the createClassSynchronisation static method.)
+ * @param {(token: HTMLElement) => void} tokenSelectedCallback Runs multiple times when the text selection inside the code-input changes, each time inputting a single (part of the highlighted ``) token element that is selected in the new text selection.
+ * @param {(tokenContainer: HTMLElement) => void} selectChangedCallback Each time the text selection inside the code-input changes, runs once before any tokenSelectedCallback calls, inputting the highlighted ``'s `` element that contains all token elements.
+ */
+ constructor(tokenSelectedCallback: (token: HTMLElement) => void, selectChangedCallback: (tokenContainer: HTMLElement) => void);
+
+ /**
+ * Use preset callbacks which ensure all tokens in the selected text range in the ``, and only such tokens, are given a certain CSS class.
+ *
+ * (If the behaviour you want requires more complex behaviour or JavaScript, you should use TokenSelectorCallbacks' constructor.)
+ *
+ * @param {string} selectedClass The CSS class that will be present on tokens only when they are part of the selected text in the `` element. Defaults to "code-input_select-token-callbacks_selected".
+ * @returns {TokenSelectorCallbacks} A new TokenSelectorCallbacks instance that encodes this behaviour.
+ */
+ static createClassSynchronisation(selectedClass: string): codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks;
+ }
+ }
+
/**
* Render special characters and control characters as a symbol with their hex code.
* Files: special-chars.js, special-chars.css
@@ -211,7 +260,7 @@ export class Template {
*
* Constructor to create a custom template instance. Pass this into `codeInput.registerTemplate` to use it.
* I would strongly recommend using the built-in simpler template `codeInput.templates.prism` or `codeInput.templates.hljs`.
- * @param {Function} highlight - a callback to highlight the code, that takes an HTML `` element inside a `` element as a parameter
+ * @param {(codeElement: HTMLElement) => void} highlight - a callback to highlight the code, that takes an HTML `` element inside a `` element as a parameter
* @param {boolean} preElementStyled - is the `` element CSS-styled as well as the `` element? If true, `` element's scrolling is synchronised; if false, `` element's scrolling is synchronised.
* @param {boolean} isCode - is this for writing code? If true, the code-input's lang HTML attribute can be used, and the `` element will be given the class name 'language-[lang attribute's value]'.
* @param {false} includeCodeInputInHighlightFunc - Setting this to true passes the `` element as a second argument to the highlight function.
@@ -219,13 +268,13 @@ export class Template {
* @param {codeInput.Plugin[]} plugins - An array of plugin objects to add extra features - see `codeInput.Plugin`
* @returns template object
*/
- constructor(highlight?: (code: HTMLElement) => void, preElementStyled?: boolean, isCode?: boolean, includeCodeInputInHighlightFunc?: false, autoDisableDuplicateSearching?: boolean, plugins?: Plugin[])
+ constructor(highlight?: (codeElement: HTMLElement) => void, preElementStyled?: boolean, isCode?: boolean, includeCodeInputInHighlightFunc?: false, autoDisableDuplicateSearching?: boolean, plugins?: Plugin[])
/**
* **When `includeCodeInputInHighlightFunc` is `true`, `highlight` takes two parameters: the `` element, and the `` element.**
*
* Constructor to create a custom template instance. Pass this into `codeInput.registerTemplate` to use it.
* I would strongly recommend using the built-in simpler template `codeInput.templates.prism` or `codeInput.templates.hljs`.
- * @param {Function} highlight - a callback to highlight the code, that takes an HTML `` element inside a `` element as a parameter
+ * @param {(codeElement: HTMLElement, codeInput: CodeInput) => void} highlight - a callback to highlight the code, that takes an HTML `` element inside a `` element as a parameter
* @param {boolean} preElementStyled - is the `` element CSS-styled as well as the `` element? If true, `` element's scrolling is synchronised; if false, `` element's scrolling is synchronised.
* @param {boolean} isCode - is this for writing code? If true, the code-input's lang HTML attribute can be used, and the `` element will be given the class name 'language-[lang attribute's value]'.
* @param {true} includeCodeInputInHighlightFunc - Setting this to true passes the `` element as a second argument to the highlight function.
@@ -233,7 +282,7 @@ export class Template {
* @param {codeInput.Plugin[]} plugins - An array of plugin objects to add extra features - see `codeInput.Plugin`
* @returns template object
*/
- constructor(highlight?: (code: HTMLElement, codeInput: CodeInput) => void, preElementStyled?: boolean, isCode?: boolean, includeCodeInputInHighlightFunc?: true, autoDisableDuplicateSearching?: boolean, plugins?: Plugin[])
+ constructor(highlight?: (codeElement: HTMLElement, codeInput: CodeInput) => void, preElementStyled?: boolean, isCode?: boolean, includeCodeInputInHighlightFunc?: true, autoDisableDuplicateSearching?: boolean, plugins?: Plugin[])
highlight: Function
preElementStyled: boolean
isCode: boolean
diff --git a/code-input.js b/code-input.js
index b0458bc..7bace3b 100644
--- a/code-input.js
+++ b/code-input.js
@@ -158,14 +158,14 @@ var codeInput = {
/**
* Constructor to create a custom template instance. Pass this into `codeInput.registerTemplate` to use it.
* I would strongly recommend using the built-in simpler template `codeInput.templates.prism` or `codeInput.templates.hljs`.
- * @param {Function} highlight - a callback to highlight the code, that takes an HTML `` element inside a `` element as a parameter
+ * @param {(codeElement: HTMLCodeElement, codeInput?: codeInput.CodeInput) => void} highlight - a callback to highlight the code, that takes an HTML `` element inside a `` element as a parameter
* @param {boolean} preElementStyled - is the `` element CSS-styled as well as the `` element? If true, `` element's scrolling is synchronised; if false, `` element's scrolling is synchronised.
* @param {boolean} isCode - is this for writing code? If true, the code-input's lang HTML attribute can be used, and the `` element will be given the class name 'language-[lang attribute's value]'.
* @param {boolean} includeCodeInputInHighlightFunc - Setting this to true passes the `` element as a second argument to the highlight function.
* @param {codeInput.Plugin[]} plugins - An array of plugin objects to add extra features - see `codeInput.Plugin`
* @returns {codeInput.Template} template object
*/
- constructor(highlight = function () { }, preElementStyled = true, isCode = true, includeCodeInputInHighlightFunc = false, plugins = []) {
+ constructor(highlight = function (codeElement) { }, preElementStyled = true, isCode = true, includeCodeInputInHighlightFunc = false, plugins = []) {
this.highlight = highlight;
this.preElementStyled = preElementStyled;
this.isCode = isCode;
diff --git a/plugins/README.md b/plugins/README.md
index 9c26a26..a2edb32 100644
--- a/plugins/README.md
+++ b/plugins/README.md
@@ -65,6 +65,13 @@ Files: [special-chars.js](./special-chars.js) / [special-chars.css](./special-ch
[🚀 *CodePen Demo*](https://codepen.io/WebCoder49/pen/jOeYJbm)
+### Select Token Callbacks
+Make tokens in the `` element that are included within the selected text of the `` gain a CSS class while selected, or trigger JavaScript callbacks.
+
+Files: select-token-callbacks.js
+
+[🚀 *CodePen Demo*]()
+
## Using Plugins
Plugins allow you to add extra features to a template, like [automatic indentation](./indent.js) or [support for highlight.js's language autodetection](./autodetect.js). To use them, just:
- Import the plugins' JS/CSS files (there may only be one of these; import all of the files that exist) after you have imported `code-input` and before registering the template.
diff --git a/plugins/autocomplete.js b/plugins/autocomplete.js
index f4d1efd..ba9b8eb 100644
--- a/plugins/autocomplete.js
+++ b/plugins/autocomplete.js
@@ -5,7 +5,7 @@
codeInput.plugins.Autocomplete = class extends codeInput.Plugin {
/**
* Pass in a function to create a plugin that displays the popup that takes in (popup element, textarea, textarea.selectionEnd).
- * @param {function} updatePopupCallback a function to display the popup that takes in (popup element, textarea, textarea.selectionEnd).
+ * @param {(popupElement: HTMLElement, textarea: HTMLTextAreaElement, selectionEnd: number) => void} updatePopupCallback a function to display the popup that takes in (popup element, textarea, textarea.selectionEnd).
*/
constructor(updatePopupCallback) {
super([]); // No observed attributes
diff --git a/plugins/find-and-replace.js b/plugins/find-and-replace.js
index 145cbe7..833c814 100644
--- a/plugins/find-and-replace.js
+++ b/plugins/find-and-replace.js
@@ -596,7 +596,8 @@ codeInput.plugins.FindAndReplace.FindMatchState = class {
/* Highlight a match from the find functionality given its start and end indexes in the text.
Start from the currentElement as this function is recursive. Use the matchID in the class name
- of the match so different matches can be identified. */
+ of the match so different matches can be identified.
+ This code is similar to codeInput.plugins.SelectTokenCallbacks.SelectedTokenState.updateSelectedTokens*/
highlightMatch(matchID, currentElement, startIndex, endIndex) {
for(let i = 0; i < currentElement.childNodes.length; i++) {
let childElement = currentElement.childNodes[i];
@@ -604,6 +605,7 @@ codeInput.plugins.FindAndReplace.FindMatchState = class {
let noInnerElements = false;
if(childElement.nodeType == 3) {
+ // Text node
if(i + 1 < currentElement.childNodes.length && currentElement.childNodes[i+1].nodeType == 3) {
// Can merge with next text node
currentElement.childNodes[i+1].textContent = childElement.textContent + currentElement.childNodes[i+1].textContent; // Merge textContent with next node
@@ -616,7 +618,7 @@ codeInput.plugins.FindAndReplace.FindMatchState = class {
let replacementElement = document.createElement("span");
replacementElement.textContent = childText;
- replacementElement.classList.add("code-input_find-and-replace_temporary-span"); // Can remove
+ replacementElement.classList.add("code-input_find-and-replace_temporary-span"); // Can remove span later
currentElement.replaceChild(replacementElement, childElement);
childElement = replacementElement;
@@ -631,7 +633,7 @@ codeInput.plugins.FindAndReplace.FindMatchState = class {
let startSpan = document.createElement("span");
startSpan.classList.add("code-input_find-and-replace_find-match"); // Highlighted
startSpan.setAttribute("data-code-input_find-and-replace_match-id", matchID);
- startSpan.classList.add("code-input_find-and-replace_temporary-span"); // Can remove
+ startSpan.classList.add("code-input_find-and-replace_temporary-span"); // Can remove span later
startSpan.textContent = childText.substring(0, endIndex);
if(startSpan.textContent[0] == "\n") {
// Newline at start - make clear
@@ -666,7 +668,7 @@ codeInput.plugins.FindAndReplace.FindMatchState = class {
// Match starts and ends in childElement - highlight middle part
// Text node - highlight last part
let startSpan = document.createElement("span");
- startSpan.classList.add("code-input_find-and-replace_temporary-span"); // Can remove
+ startSpan.classList.add("code-input_find-and-replace_temporary-span"); // Can remove span later
startSpan.textContent = childText.substring(0, startIndex);
let middleText = childText.substring(startIndex, endIndex);
@@ -679,7 +681,7 @@ codeInput.plugins.FindAndReplace.FindMatchState = class {
}
let endSpan = document.createElement("span");
- endSpan.classList.add("code-input_find-and-replace_temporary-span"); // Can remove
+ endSpan.classList.add("code-input_find-and-replace_temporary-span"); // Can remove span later
endSpan.textContent = childText.substring(endIndex);
childElement.insertAdjacentElement('beforebegin', startSpan);
@@ -693,7 +695,7 @@ codeInput.plugins.FindAndReplace.FindMatchState = class {
let endSpan = document.createElement("span");
endSpan.classList.add("code-input_find-and-replace_find-match"); // Highlighted
endSpan.setAttribute("data-code-input_find-and-replace_match-id", matchID);
- endSpan.classList.add("code-input_find-and-replace_temporary-span"); // Can remove
+ endSpan.classList.add("code-input_find-and-replace_temporary-span"); // Can remove span later
endSpan.textContent = childText.substring(startIndex);
if(endSpan.textContent[0] == "\n") {
// Newline at start - make clear
diff --git a/plugins/select-token-callbacks.js b/plugins/select-token-callbacks.js
new file mode 100644
index 0000000..8f158d3
--- /dev/null
+++ b/plugins/select-token-callbacks.js
@@ -0,0 +1,289 @@
+/**
+ * Make tokens in the element that are included within the selected text of the
+ * gain a CSS class while selected, or trigger JavaScript callbacks.
+ * Files: select-token-callbacks.js
+ */
+codeInput.plugins.SelectTokenCallbacks = class extends codeInput.Plugin {
+ /**
+ * Set up the behaviour of tokens text-selected in the `` element, and the exact definition of a token being text-selected.
+ *
+ * All parameters are optional. If you provide no arguments to the constructor, this will dynamically apply the "code-input_select-token-callbacks_selected" class to selected tokens only, for you to style via CSS.
+ *
+ * @param {codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks} tokenSelectorCallbacks What to do with text-selected tokens. See docstrings for the TokenSelectorCallbacks class.
+ * @param {boolean} onlyCaretNotSelection If true, tokens will only be marked as selected when no text is selected but rather the caret is inside them (start of selection == end of selection). Default false.
+ * @param {boolean} caretAtStartIsSelected Whether the caret or text selection's end being just before the first character of a token means said token is selected. Default true.
+ * @param {boolean} caretAtEndIsSelected Whether the caret or text selection's start being just after the last character of a token means said token is selected. Default true.
+ * @param {boolean} createSubTokens Whether temporary `` elements should be created inside partially-selected tokens containing just the selected text and given the selected class. Default false.
+ * @param {boolean} partiallySelectedTokensAreSelected Whether tokens for which only some of their text is selected should be treated as selected. Default true.
+ * @param {boolean} parentTokensAreSelected Whether all parent tokens of selected tokens should be treated as selected. Default true.
+ */
+ constructor(tokenSelectorCallbacks = codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks.createClassSynchronisation(), onlyCaretNotSelection = false, caretAtStartIsSelected = true, caretAtEndIsSelected = true, createSubTokens = false, partiallySelectedTokensAreSelected = true, parentTokensAreSelected = true) {
+ super([]); // No observed attributes
+
+ this.tokenSelectorCallbacks = tokenSelectorCallbacks;
+ this.onlyCaretNotSelection = onlyCaretNotSelection;
+ this.caretAtStartIsSelected = caretAtStartIsSelected;
+ this.caretAtEndIsSelected = caretAtEndIsSelected;
+ this.createSubTokens = createSubTokens;
+ this.partiallySelectedTokensAreSelected = partiallySelectedTokensAreSelected;
+ this.parentTokensAreSelected = parentTokensAreSelected;
+ }
+ /* Runs after code is highlighted; Params: codeInput element) */
+ afterHighlight(codeInputElement) {
+ this.syncSelection(codeInputElement);
+ }
+ /* Runs after elements are added into a `code-input` (useful for adding events to the textarea); Params: codeInput element) */
+ afterElementsAdded(codeInputElement) {
+ codeInputElement.pluginData.selectTokenCallbacks = {};
+ codeInputElement.pluginData.selectTokenCallbacks.lastSelectionStart = codeInputElement.textareaElement.selectionStart;
+ codeInputElement.pluginData.selectTokenCallbacks.lastSelectionEnd = codeInputElement.textareaElement.selectionEnd;
+ codeInputElement.pluginData.selectTokenCallbacks.selectedTokenState = new codeInput.plugins.SelectTokenCallbacks.SelectedTokenState(codeInputElement.codeElement, this.tokenSelectorCallbacks, this.onlyCaretNotSelection, this.caretAtStartIsSelected, this.caretAtEndIsSelected, this.createSubTokens, this.partiallySelectedTokensAreSelected, this.parentTokensAreSelected);
+ this.syncSelection(codeInputElement);
+
+ // As of 2024-08, the selectionchange event is only supported on Firefox.
+ codeInputElement.textareaElement.addEventListener("selectionchange", () => {
+ this.checkSelectionChanged(codeInputElement)
+ });
+ // When selectionchange has complete support, the listeners below can be deleted.
+ codeInputElement.textareaElement.addEventListener("select", () => {
+ this.checkSelectionChanged(codeInputElement)
+ });
+ codeInputElement.textareaElement.addEventListener("keypress", () => {
+ this.checkSelectionChanged(codeInputElement)
+ });
+ codeInputElement.textareaElement.addEventListener("mousedown", () => {
+ this.checkSelectionChanged(codeInputElement)
+ });
+ }
+ /* If the text selection has changed, run syncSelection. */
+ checkSelectionChanged(codeInputElement) {
+ if(
+ codeInputElement.textareaElement.selectionStart != codeInputElement.pluginData.selectTokenCallbacks.lastSelectionStart
+ || codeInputElement.textareaElement.selectionEnd != codeInputElement.pluginData.selectTokenCallbacks.lastSelectionEnd
+ ) {
+ this.syncSelection(codeInputElement);
+ codeInputElement.pluginData.selectTokenCallbacks.lastSelectionStart = codeInputElement.textareaElement.selectionStart;
+ codeInputElement.pluginData.selectTokenCallbacks.lastSelectionEnd = codeInputElement.textareaElement.selectionEnd;
+ }
+ }
+ /* Update which elements have the code-input_selected class. */
+ syncSelection(codeInputElement) {
+ codeInputElement.pluginData.selectTokenCallbacks.selectedTokenState.updateSelection(codeInputElement.textareaElement.selectionStart, codeInputElement.textareaElement.selectionEnd)
+ }
+}
+
+/**
+ * A data structure specifying what should be done with tokens when they are selected, and also allows for previously selected
+ * tokens to be dealt with each time the selection changes. See the constructor and the createClassSynchronisation static method.
+ */
+codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks = class {
+ /**
+ * Pass any callbacks you want to customise the behaviour of selected tokens via JavaScript.
+ *
+ * (If the behaviour you want is just differently styling selected tokens _via CSS_, you should probably use the createClassSynchronisation static method.)
+ * @param {(token: HTMLElement) => void} tokenSelectedCallback Runs multiple times when the text selection inside the code-input changes, each time inputting a single (part of the highlighted ``) token element that is selected in the new text selection.
+ * @param {(tokenContainer: HTMLElement) => void} selectChangedCallback Each time the text selection inside the code-input changes, runs once before any tokenSelectedCallback calls, inputting the highlighted ``'s `` element that contains all token elements.
+ */
+ constructor(tokenSelectedCallback, selectChangedCallback) {
+ this.tokenSelectedCallback = tokenSelectedCallback;
+ this.selectChangedCallback = selectChangedCallback;
+ }
+
+ /**
+ * Use preset callbacks which ensure all tokens in the selected text range in the ``, and only such tokens, are given a certain CSS class.
+ *
+ * (If the behaviour you want requires more complex behaviour or JavaScript, you should use TokenSelectorCallbacks' constructor.)
+ *
+ * @param {string} selectedClass The CSS class that will be present on tokens only when they are part of the selected text in the `` element. Defaults to "code-input_select-token-callbacks_selected".
+ * @returns A new TokenSelectorCallbacks instance that encodes this behaviour.
+ */
+ static createClassSynchronisation(selectedClass = "code-input_select-token-callbacks_selected") {
+ return new codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks(
+ (token) => {
+ token.classList.add(selectedClass);
+ },
+ (tokenContainer) => {
+ // Remove selected class
+ let selectedClassTokens = tokenContainer.getElementsByClassName(selectedClass);
+ // Use it like a queue, because as elements have their class name removed they are live-removed from the collection.
+ while(selectedClassTokens.length > 0) {
+ selectedClassTokens[0].classList.remove(selectedClass);
+ }
+ }
+ );
+ }
+}
+
+/* Manages a single element's selected tokens, and calling the correct functions on the selected tokens */
+codeInput.plugins.SelectTokenCallbacks.SelectedTokenState = class {
+ constructor(codeElement, tokenSelectorCallbacks, onlyCaretNotSelection, caretAtStartIsSelected, caretAtEndIsSelected, createSubTokens, partiallySelectedTokensAreSelected, parentTokensAreSelected) {
+ this.tokenContainer = codeElement;
+ this.tokenSelectorCallbacks = tokenSelectorCallbacks;
+ this.onlyCaretNotSelection = onlyCaretNotSelection;
+ this.caretAtStartIsSelected = caretAtStartIsSelected;
+ this.caretAtEndIsSelected = caretAtEndIsSelected;
+ this.createSubTokens = createSubTokens;
+ this.partiallySelectedTokensAreSelected = partiallySelectedTokensAreSelected;
+ this.parentTokensAreSelected = parentTokensAreSelected;
+ }
+
+ /* Change the selected region to a new range from selectionStart to selectionEnd and run
+ the callbacks. */
+ updateSelection(selectionStart, selectionEnd) {
+ this.selectChanged()
+ if(!this.onlyCaretNotSelection || selectionStart == selectionEnd) { // Only deal with selected text if onlyCaretNotSelection is false.
+ this.updateSelectedTokens(this.tokenContainer, selectionStart, selectionEnd)
+ }
+ }
+ /* Runs when the text selection has changed, before any updateSelectedTokens call. */
+ selectChanged() {
+ if(this.createSubTokens) {
+ // Remove generated spans to hold selected partial tokens
+ let tempSpans = this.tokenContainer.getElementsByClassName("code-input_select-token-callbacks_temporary-span");
+ while(tempSpans.length > 0) {
+ // Replace with textContent as Text node
+ // Use it like a queue, because as elements have their class name removed they are live-removed from the collection.
+ tempSpans[0].parentElement.replaceChild(new Text(tempSpans[0].textContent), tempSpans[0]);
+ }
+ }
+
+ this.tokenSelectorCallbacks.selectChangedCallback(this.tokenContainer);
+ }
+
+ /* Do the desired behaviour for selection to all tokens (elements in the currentElement)
+ from startIndex to endIndex in the text. Start from the currentElement as this function is recursive.
+ This code is similar to codeInput.plugins.FindAndReplace.FindMatchState.highlightMatch*/
+ updateSelectedTokens(currentElement, startIndex, endIndex) {
+ if(endIndex < 0 || endIndex == 0 && !this.caretAtStartIsSelected) {
+ return; // Nothing selected
+ }
+ if(this.parentTokensAreSelected && currentElement !== this.tokenContainer) {
+ this.tokenSelectorCallbacks.tokenSelectedCallback(currentElement); // Parent elements also marked with class / have callback called
+ }
+ for(let i = 0; i < currentElement.childNodes.length; i++) {
+ let childElement = currentElement.childNodes[i];
+ let childText = childElement.textContent;
+
+ let noInnerElements = false;
+ if(childElement.nodeType == 3) {
+ // Text node
+ if(this.createSubTokens) {
+ // Replace with token
+ if(i + 1 < currentElement.childNodes.length && currentElement.childNodes[i+1].nodeType == 3) {
+ // Can merge with next text node
+ currentElement.childNodes[i+1].textContent = childElement.textContent + currentElement.childNodes[i+1].textContent; // Merge textContent with next node
+ currentElement.removeChild(childElement); // Delete this node
+ i--; // As an element removed
+ continue; // Move to next node
+ }
+ noInnerElements = true;
+
+ let replacementElement = document.createElement("span");
+ replacementElement.textContent = childText;
+ replacementElement.classList.add("code-input_select-token-callbacks_temporary-span"); // Can remove span later
+
+ currentElement.replaceChild(replacementElement, childElement);
+ childElement = replacementElement;
+ } else {
+ // Skip text node
+ // Make indexes skip the element
+ startIndex -= childText.length;
+ endIndex -= childText.length;
+ continue;
+ }
+ }
+
+ if(startIndex <= 0) {
+ // Started selection
+ if(childText.length > endIndex) {
+ // Selection ends in childElement
+ if(this.partiallySelectedTokensAreSelected) {
+ if(noInnerElements) {
+ if(this.createSubTokens && startIndex != endIndex) { // Subtoken to create
+ // Text node - add selection class to first part
+ let startSpan = document.createElement("span");
+ this.tokenSelectorCallbacks.tokenSelectedCallback(startSpan); // Selected
+ startSpan.classList.add("code-input_select-token-callbacks_temporary-span"); // Can remove span later
+ startSpan.textContent = childText.substring(0, endIndex);
+
+ let endText = childText.substring(endIndex);
+ childElement.textContent = endText;
+
+ childElement.insertAdjacentElement('beforebegin', startSpan);
+ i++; // An extra element has been added
+ }
+ if(this.parentTokensAreSelected || !this.createSubTokens) {
+ this.tokenSelectorCallbacks.tokenSelectedCallback(childElement); // Selected
+ }
+ } else {
+ this.updateSelectedTokens(childElement, 0, endIndex);
+ }
+ }
+
+ // Match ended - nothing to do after backtracking
+ return;
+ } else {
+ // Match goes through child element
+ this.tokenSelectorCallbacks.tokenSelectedCallback(childElement); // Selected
+ }
+ } else if(this.caretAtEndIsSelected && childText.length >= startIndex || childText.length > startIndex) {
+ // Match starts in childElement
+ if(this.partiallySelectedTokensAreSelected) {
+ if(noInnerElements) {
+ if(this.createSubTokens && startIndex != endIndex) { // Subtoken to create
+ if(childText.length > endIndex) {
+ // Match starts and ends in childElement - selection middle part
+ let startSpan = document.createElement("span");
+ startSpan.classList.add("code-input_select-token-callbacks_temporary-span"); // Can remove span later
+ startSpan.textContent = childText.substring(0, startIndex);
+
+ let middleText = childText.substring(startIndex, endIndex);
+ childElement.textContent = middleText;
+ this.tokenSelectorCallbacks.tokenSelectedCallback(childElement); // Selection
+
+ let endSpan = document.createElement("span");
+ endSpan.classList.add("code-input_select-token-callbacks_temporary-span"); // Can remove span later
+ endSpan.textContent = childText.substring(endIndex);
+
+ childElement.insertAdjacentElement('beforebegin', startSpan);
+ childElement.insertAdjacentElement('afterend', endSpan);
+ i++; // 2 extra elements have been added
+ } else {
+ // Match starts in element - highlight last part
+ let startText = childText.substring(0, startIndex);
+ childElement.textContent = startText;
+
+ let endSpan = document.createElement("span");
+ this.tokenSelectorCallbacks.tokenSelectedCallback(endSpan); // Selected
+ endSpan.classList.add("code-input_select-token-callbacks_temporary-span"); // Can remove span later
+ endSpan.textContent = childText.substring(startIndex);
+
+ childElement.insertAdjacentElement('afterend', endSpan);
+ i++; // An extra element has been added
+ }
+ }
+ if(this.parentTokensAreSelected || !this.createSubTokens) {
+ this.tokenSelectorCallbacks.tokenSelectedCallback(childElement); // Selected
+ }
+ } else {
+ this.updateSelectedTokens(childElement, startIndex, endIndex);
+ }
+ }
+
+ if(this.caretAtStartIsSelected) {
+ if(childText.length > endIndex) {
+ // Match completely in childElement - nothing to do after backtracking
+ return;
+ }
+ } else if(childText.length >= endIndex) {
+ // Match completely in childElement - nothing to do after backtracking
+ return;
+ }
+ }
+
+ // Make indexes skip the element
+ startIndex -= childText.length;
+ endIndex -= childText.length;
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/hljs.html b/tests/hljs.html
index 4c9238f..1aeb729 100644
--- a/tests/hljs.html
+++ b/tests/hljs.html
@@ -28,6 +28,7 @@
+
diff --git a/tests/prism-match-braces-compatibility.js b/tests/prism-match-braces-compatibility.js
new file mode 100644
index 0000000..73909a1
--- /dev/null
+++ b/tests/prism-match-braces-compatibility.js
@@ -0,0 +1,215 @@
+/* Modified from https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/match-braces/prism-match-braces.js
+to enable codeInput SelectTokenCallbacks compatibility. Use:
+new codeInput.plugins.SelectTokenCallbacks(new codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks(selectBrace, deselectAllBraces), true) */
+// Additions on lines 6-10, and lines 84-98.
+
+// code-input modification: ADD
+// Callbacks
+let selectBrace;
+let deselectAllBraces;
+// END code-input modification
+(function () {
+
+if (typeof Prism === 'undefined' || typeof document === 'undefined') {
+ return;
+}
+
+function mapClassName(name) {
+ var customClass = Prism.plugins.customClass;
+ if (customClass) {
+ return customClass.apply(name, 'none');
+ } else {
+ return name;
+ }
+}
+
+var PARTNER = {
+ '(': ')',
+ '[': ']',
+ '{': '}',
+};
+
+// The names for brace types.
+// These names have two purposes: 1) they can be used for styling and 2) they are used to pair braces. Only braces
+// of the same type are paired.
+var NAMES = {
+ '(': 'brace-round',
+ '[': 'brace-square',
+ '{': 'brace-curly',
+};
+
+// A map for brace aliases.
+// This is useful for when some braces have a prefix/suffix as part of the punctuation token.
+var BRACE_ALIAS_MAP = {
+ '${': '{', // JS template punctuation (e.g. `foo ${bar + 1}`)
+};
+
+var LEVEL_WARP = 12;
+
+var pairIdCounter = 0;
+
+var BRACE_ID_PATTERN = /^(pair-\d+-)(close|open)$/;
+
+/**
+ * Returns the brace partner given one brace of a brace pair.
+ *
+ * @param {HTMLElement} brace
+ * @returns {HTMLElement}
+ */
+function getPartnerBrace(brace) {
+ var match = BRACE_ID_PATTERN.exec(brace.id);
+ return document.querySelector('#' + match[1] + (match[2] == 'open' ? 'close' : 'open'));
+}
+
+/**
+ * @this {HTMLElement}
+ */
+function hoverBrace() {
+ if (!Prism.util.isActive(this, 'brace-hover', true)) {
+ return;
+ }
+
+ [this, getPartnerBrace(this)].forEach(function (e) {
+ e.classList.add(mapClassName('brace-hover'));
+ });
+}
+/**
+ * @this {HTMLElement}
+ */
+function leaveBrace() {
+ [this, getPartnerBrace(this)].forEach(function (e) {
+ e.classList.remove(mapClassName('brace-hover'));
+ });
+}
+// code-input modification: ADD
+selectBrace = (token) => {
+ if(BRACE_ID_PATTERN.test(token.id)) { // Check it's a brace
+ hoverBrace.apply(token); // Move the brace from a this to a parameter
+ }
+};
+deselectAllBraces = (tokenContainer) => {
+ // Remove selected class
+ let selectedClassTokens = tokenContainer.getElementsByClassName(mapClassName('brace-hover'));
+ // Use it like a queue, because as elements have their class name removed they are live-removed from the collection.
+ while(selectedClassTokens.length > 0) {
+ selectedClassTokens[0].classList.remove(mapClassName('brace-hover'));
+ }
+}; // Moves the brace from a this to a parameter
+// end code-input modification
+/**
+ * @this {HTMLElement}
+ */
+function clickBrace() {
+ if (!Prism.util.isActive(this, 'brace-select', true)) {
+ return;
+ }
+
+ [this, getPartnerBrace(this)].forEach(function (e) {
+ e.classList.add(mapClassName('brace-selected'));
+ });
+}
+
+Prism.hooks.add('complete', function (env) {
+
+ /** @type {HTMLElement} */
+ var code = env.element;
+ var pre = code.parentElement;
+
+ if (!pre || pre.tagName != 'PRE') {
+ return;
+ }
+
+ // find the braces to match
+ /** @type {string[]} */
+ var toMatch = [];
+ if (Prism.util.isActive(code, 'match-braces')) {
+ toMatch.push('(', '[', '{');
+ }
+
+ if (toMatch.length == 0) {
+ // nothing to match
+ return;
+ }
+
+ if (!pre.__listenerAdded) {
+ // code blocks might be highlighted more than once
+ pre.addEventListener('mousedown', function removeBraceSelected() {
+ // the code element might have been replaced
+ var code = pre.querySelector('code');
+ var className = mapClassName('brace-selected');
+ Array.prototype.slice.call(code.querySelectorAll('.' + className)).forEach(function (e) {
+ e.classList.remove(className);
+ });
+ });
+ Object.defineProperty(pre, '__listenerAdded', { value: true });
+ }
+
+ /** @type {HTMLSpanElement[]} */
+ var punctuation = Array.prototype.slice.call(
+ code.querySelectorAll('span.' + mapClassName('token') + '.' + mapClassName('punctuation'))
+ );
+
+ /** @type {{ index: number, open: boolean, element: HTMLElement }[]} */
+ var allBraces = [];
+
+ toMatch.forEach(function (open) {
+ var close = PARTNER[open];
+ var name = mapClassName(NAMES[open]);
+
+ /** @type {[number, number][]} */
+ var pairs = [];
+ /** @type {number[]} */
+ var openStack = [];
+
+ for (var i = 0; i < punctuation.length; i++) {
+ var element = punctuation[i];
+ if (element.childElementCount == 0) {
+ var text = element.textContent;
+ text = BRACE_ALIAS_MAP[text] || text;
+ if (text === open) {
+ allBraces.push({ index: i, open: true, element: element });
+ element.classList.add(name);
+ element.classList.add(mapClassName('brace-open'));
+ openStack.push(i);
+ } else if (text === close) {
+ allBraces.push({ index: i, open: false, element: element });
+ element.classList.add(name);
+ element.classList.add(mapClassName('brace-close'));
+ if (openStack.length) {
+ pairs.push([i, openStack.pop()]);
+ }
+ }
+ }
+ }
+
+ pairs.forEach(function (pair) {
+ var pairId = 'pair-' + (pairIdCounter++) + '-';
+
+ var opening = punctuation[pair[0]];
+ var closing = punctuation[pair[1]];
+
+ opening.id = pairId + 'open';
+ closing.id = pairId + 'close';
+
+ [opening, closing].forEach(function (e) {
+ e.addEventListener('mouseenter', hoverBrace);
+ e.addEventListener('mouseleave', leaveBrace);
+ e.addEventListener('click', clickBrace);
+ });
+ });
+ });
+
+ var level = 0;
+ allBraces.sort(function (a, b) { return a.index - b.index; });
+ allBraces.forEach(function (brace) {
+ if (brace.open) {
+ brace.element.classList.add(mapClassName('brace-level-' + (level % LEVEL_WARP + 1)));
+ level++;
+ } else {
+ level = Math.max(0, level - 1);
+ brace.element.classList.add(mapClassName('brace-level-' + (level % LEVEL_WARP + 1)));
+ }
+ });
+});
+
+}());
\ No newline at end of file
diff --git a/tests/prism.html b/tests/prism.html
index 7da62fb..0a8ae01 100644
--- a/tests/prism.html
+++ b/tests/prism.html
@@ -4,13 +4,14 @@
code-input Tester
-
+
+
@@ -26,6 +27,7 @@
+
@@ -38,7 +40,7 @@ Test for highlight.js
This page carries out automated tests for the code-input library to check that both the core components and the plugins work in some ways. It doesn't fully cover every scenario so you should test any code you change by hand, but it's good for quickly checking a wide range of functionality works.
Test Results (Click to Open)
-