Skip to content

SelectTokenCallbacks plugin #120

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ The next step is to set up a `template` to link `code-input` to your syntax-high
</script>
```

> ⚠️ 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
Expand Down
59 changes: 54 additions & 5 deletions code-input.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -175,6 +175,55 @@ export namespace plugins {
constructor(defaultSpaces?: boolean, numSpaces?: Number, bracketPairs?: Object, escTabToChangeFocus?: boolean);
}

/**
* Make tokens in the <pre><code> element that are included within the selected text of the <code-input>
* 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 `<code-input>` 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 `<span>` 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 `<pre><code>`) 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 `<pre><code>`'s `<code>` 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 `<code-input>`, 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 `<code-input>` 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
Expand Down Expand Up @@ -211,29 +260,29 @@ 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 `<code>` element inside a `<pre>` element as a parameter
* @param {(codeElement: HTMLElement) => void} highlight - a callback to highlight the code, that takes an HTML `<code>` element inside a `<pre>` element as a parameter
* @param {boolean} preElementStyled - is the `<pre>` element CSS-styled as well as the `<code>` element? If true, `<pre>` element's scrolling is synchronised; if false, `<code>` 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 `<code>` element will be given the class name 'language-[lang attribute's value]'.
* @param {false} includeCodeInputInHighlightFunc - Setting this to true passes the `<code-input>` element as a second argument to the highlight function.
* @param {boolean} autoDisableDuplicateSearching - Leaving this as true uses code-input's default fix for preventing duplicate results in Ctrl+F searching from the input and result elements, and setting this to false indicates your highlighting function implements its own fix. The default fix works by moving text content from elements to CSS `::before` pseudo-elements after highlighting.
* @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 `<pre><code>` element, and the `<code-input>` 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 `<code>` element inside a `<pre>` element as a parameter
* @param {(codeElement: HTMLElement, codeInput: CodeInput) => void} highlight - a callback to highlight the code, that takes an HTML `<code>` element inside a `<pre>` element as a parameter
* @param {boolean} preElementStyled - is the `<pre>` element CSS-styled as well as the `<code>` element? If true, `<pre>` element's scrolling is synchronised; if false, `<code>` 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 `<code>` element will be given the class name 'language-[lang attribute's value]'.
* @param {true} includeCodeInputInHighlightFunc - Setting this to true passes the `<code-input>` element as a second argument to the highlight function.
* @param {boolean} autoDisableDuplicateSearching - Leaving this as true uses code-input's default fix for preventing duplicate results in Ctrl+F searching from the input and result elements, and setting this to false indicates your highlighting function implements its own fix. The default fix works by moving text content from elements to CSS `::before` pseudo-elements after highlighting.
* @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
Expand Down
4 changes: 2 additions & 2 deletions code-input.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<code>` element inside a `<pre>` element as a parameter
* @param {(codeElement: HTMLCodeElement, codeInput?: codeInput.CodeInput) => void} highlight - a callback to highlight the code, that takes an HTML `<code>` element inside a `<pre>` element as a parameter
* @param {boolean} preElementStyled - is the `<pre>` element CSS-styled as well as the `<code>` element? If true, `<pre>` element's scrolling is synchronised; if false, `<code>` 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 `<code>` element will be given the class name 'language-[lang attribute's value]'.
* @param {boolean} includeCodeInputInHighlightFunc - Setting this to true passes the `<code-input>` 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;
Expand Down
7 changes: 7 additions & 0 deletions plugins/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<pre><code>` element that are included within the selected text of the `<code-input>` 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.
Expand Down
2 changes: 1 addition & 1 deletion plugins/autocomplete.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 8 additions & 6 deletions plugins/find-and-replace.js
Original file line number Diff line number Diff line change
Expand Up @@ -596,14 +596,16 @@ 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];
let childText = childElement.textContent;

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
Expand All @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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
Expand Down
Loading