Skip to content

Commit 3dd4ad6

Browse files
authored
Merge pull request #120 from WebCoder49/116-select-triggers-token-hover
SelectTokenCallbacks plugin
2 parents 4a5324b + e6b218b commit 3dd4ad6

11 files changed

+608
-16
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ The next step is to set up a `template` to link `code-input` to your syntax-high
117117
</script>
118118
```
119119

120+
> ⚠️ 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.
121+
120122
To see a full list of plugins and their functions, please see [plugins/README.md](./plugins/README.md).
121123

122124
### 4. Using the component

code-input.d.ts

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ export namespace plugins {
9696
class Autocomplete extends Plugin {
9797
/**
9898
* Pass in a function to create a plugin that displays the popup that takes in (popup element, textarea, textarea.selectionEnd).
99-
* @param {function} updatePopupCallback a function to display the popup that takes in (popup element, textarea, textarea.selectionEnd).
99+
* @param {(popupElement: HTMLElement, textarea: HTMLTextAreaElement, selectionEnd: number) => void} updatePopupCallback a function to display the popup that takes in (popup element, textarea, textarea.selectionEnd).
100100
*/
101101
constructor(updatePopupCallback: (popupElem: HTMLElement, textarea: HTMLTextAreaElement, selectionEnd: number) => void);
102102
}
@@ -175,6 +175,55 @@ export namespace plugins {
175175
constructor(defaultSpaces?: boolean, numSpaces?: Number, bracketPairs?: Object, escTabToChangeFocus?: boolean);
176176
}
177177

178+
/**
179+
* Make tokens in the <pre><code> element that are included within the selected text of the <code-input>
180+
* gain a CSS class while selected, or trigger JavaScript callbacks.
181+
* Files: select-token-callbacks.js
182+
*/
183+
class SelectTokenCallbacks extends Plugin {
184+
/**
185+
* Set up the behaviour of tokens text-selected in the `<code-input>` element, and the exact definition of a token being text-selected.
186+
*
187+
* 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.
188+
*
189+
* @param {codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks} tokenSelectorCallbacks What to do with text-selected tokens. See docstrings for the TokenSelectorCallbacks class.
190+
* @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.
191+
* @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.
192+
* @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.
193+
* @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.
194+
* @param {boolean} partiallySelectedTokensAreSelected Whether tokens for which only some of their text is selected should be treated as selected. Default true.
195+
* @param {boolean} parentTokensAreSelected Whether all parent tokens of selected tokens should be treated as selected. Default true.
196+
*/
197+
constructor(tokenSelectorCallbacks?: codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks, onlyCaretNotSelection?: boolean, caretAtStartIsSelected?: boolean, caretAtEndIsSelected?: boolean, createSubTokens?: boolean, partiallySelectedTokensAreSelected?: boolean, parentTokensAreSelected?: boolean);
198+
}
199+
200+
namespace SelectTokenCallbacks {
201+
/**
202+
* A data structure specifying what should be done with tokens when they are selected, and also allows for previously selected
203+
* tokens to be dealt with each time the selection changes. See the constructor and the createClassSynchronisation static method.
204+
*/
205+
class TokenSelectorCallbacks {
206+
/**
207+
* Pass any callbacks you want to customise the behaviour of selected tokens via JavaScript.
208+
*
209+
* (If the behaviour you want is just differently styling selected tokens _via CSS_, you should probably use the createClassSynchronisation static method.)
210+
* @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.
211+
* @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.
212+
*/
213+
constructor(tokenSelectedCallback: (token: HTMLElement) => void, selectChangedCallback: (tokenContainer: HTMLElement) => void);
214+
215+
/**
216+
* 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.
217+
*
218+
* (If the behaviour you want requires more complex behaviour or JavaScript, you should use TokenSelectorCallbacks' constructor.)
219+
*
220+
* @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".
221+
* @returns {TokenSelectorCallbacks} A new TokenSelectorCallbacks instance that encodes this behaviour.
222+
*/
223+
static createClassSynchronisation(selectedClass: string): codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks;
224+
}
225+
}
226+
178227
/**
179228
* Render special characters and control characters as a symbol with their hex code.
180229
* Files: special-chars.js, special-chars.css
@@ -211,29 +260,29 @@ export class Template {
211260
*
212261
* Constructor to create a custom template instance. Pass this into `codeInput.registerTemplate` to use it.
213262
* I would strongly recommend using the built-in simpler template `codeInput.templates.prism` or `codeInput.templates.hljs`.
214-
* @param {Function} highlight - a callback to highlight the code, that takes an HTML `<code>` element inside a `<pre>` element as a parameter
263+
* @param {(codeElement: HTMLElement) => void} highlight - a callback to highlight the code, that takes an HTML `<code>` element inside a `<pre>` element as a parameter
215264
* @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.
216265
* @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]'.
217266
* @param {false} includeCodeInputInHighlightFunc - Setting this to true passes the `<code-input>` element as a second argument to the highlight function.
218267
* @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.
219268
* @param {codeInput.Plugin[]} plugins - An array of plugin objects to add extra features - see `codeInput.Plugin`
220269
* @returns template object
221270
*/
222-
constructor(highlight?: (code: HTMLElement) => void, preElementStyled?: boolean, isCode?: boolean, includeCodeInputInHighlightFunc?: false, autoDisableDuplicateSearching?: boolean, plugins?: Plugin[])
271+
constructor(highlight?: (codeElement: HTMLElement) => void, preElementStyled?: boolean, isCode?: boolean, includeCodeInputInHighlightFunc?: false, autoDisableDuplicateSearching?: boolean, plugins?: Plugin[])
223272
/**
224273
* **When `includeCodeInputInHighlightFunc` is `true`, `highlight` takes two parameters: the `<pre><code>` element, and the `<code-input>` element.**
225274
*
226275
* Constructor to create a custom template instance. Pass this into `codeInput.registerTemplate` to use it.
227276
* I would strongly recommend using the built-in simpler template `codeInput.templates.prism` or `codeInput.templates.hljs`.
228-
* @param {Function} highlight - a callback to highlight the code, that takes an HTML `<code>` element inside a `<pre>` element as a parameter
277+
* @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
229278
* @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.
230279
* @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]'.
231280
* @param {true} includeCodeInputInHighlightFunc - Setting this to true passes the `<code-input>` element as a second argument to the highlight function.
232281
* @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.
233282
* @param {codeInput.Plugin[]} plugins - An array of plugin objects to add extra features - see `codeInput.Plugin`
234283
* @returns template object
235284
*/
236-
constructor(highlight?: (code: HTMLElement, codeInput: CodeInput) => void, preElementStyled?: boolean, isCode?: boolean, includeCodeInputInHighlightFunc?: true, autoDisableDuplicateSearching?: boolean, plugins?: Plugin[])
285+
constructor(highlight?: (codeElement: HTMLElement, codeInput: CodeInput) => void, preElementStyled?: boolean, isCode?: boolean, includeCodeInputInHighlightFunc?: true, autoDisableDuplicateSearching?: boolean, plugins?: Plugin[])
237286
highlight: Function
238287
preElementStyled: boolean
239288
isCode: boolean

code-input.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,14 +158,14 @@ var codeInput = {
158158
/**
159159
* Constructor to create a custom template instance. Pass this into `codeInput.registerTemplate` to use it.
160160
* I would strongly recommend using the built-in simpler template `codeInput.templates.prism` or `codeInput.templates.hljs`.
161-
* @param {Function} highlight - a callback to highlight the code, that takes an HTML `<code>` element inside a `<pre>` element as a parameter
161+
* @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
162162
* @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.
163163
* @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]'.
164164
* @param {boolean} includeCodeInputInHighlightFunc - Setting this to true passes the `<code-input>` element as a second argument to the highlight function.
165165
* @param {codeInput.Plugin[]} plugins - An array of plugin objects to add extra features - see `codeInput.Plugin`
166166
* @returns {codeInput.Template} template object
167167
*/
168-
constructor(highlight = function () { }, preElementStyled = true, isCode = true, includeCodeInputInHighlightFunc = false, plugins = []) {
168+
constructor(highlight = function (codeElement) { }, preElementStyled = true, isCode = true, includeCodeInputInHighlightFunc = false, plugins = []) {
169169
this.highlight = highlight;
170170
this.preElementStyled = preElementStyled;
171171
this.isCode = isCode;

plugins/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,13 @@ Files: [special-chars.js](./special-chars.js) / [special-chars.css](./special-ch
6565

6666
[🚀 *CodePen Demo*](https://codepen.io/WebCoder49/pen/jOeYJbm)
6767

68+
### Select Token Callbacks
69+
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.
70+
71+
Files: select-token-callbacks.js
72+
73+
[🚀 *CodePen Demo*]()
74+
6875
## Using Plugins
6976
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:
7077
- 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.

plugins/autocomplete.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
codeInput.plugins.Autocomplete = class extends codeInput.Plugin {
66
/**
77
* Pass in a function to create a plugin that displays the popup that takes in (popup element, textarea, textarea.selectionEnd).
8-
* @param {function} updatePopupCallback a function to display the popup that takes in (popup element, textarea, textarea.selectionEnd).
8+
* @param {(popupElement: HTMLElement, textarea: HTMLTextAreaElement, selectionEnd: number) => void} updatePopupCallback a function to display the popup that takes in (popup element, textarea, textarea.selectionEnd).
99
*/
1010
constructor(updatePopupCallback) {
1111
super([]); // No observed attributes

plugins/find-and-replace.js

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -596,14 +596,16 @@ codeInput.plugins.FindAndReplace.FindMatchState = class {
596596

597597
/* Highlight a match from the find functionality given its start and end indexes in the text.
598598
Start from the currentElement as this function is recursive. Use the matchID in the class name
599-
of the match so different matches can be identified. */
599+
of the match so different matches can be identified.
600+
This code is similar to codeInput.plugins.SelectTokenCallbacks.SelectedTokenState.updateSelectedTokens*/
600601
highlightMatch(matchID, currentElement, startIndex, endIndex) {
601602
for(let i = 0; i < currentElement.childNodes.length; i++) {
602603
let childElement = currentElement.childNodes[i];
603604
let childText = childElement.textContent;
604605

605606
let noInnerElements = false;
606607
if(childElement.nodeType == 3) {
608+
// Text node
607609
if(i + 1 < currentElement.childNodes.length && currentElement.childNodes[i+1].nodeType == 3) {
608610
// Can merge with next text node
609611
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 {
616618

617619
let replacementElement = document.createElement("span");
618620
replacementElement.textContent = childText;
619-
replacementElement.classList.add("code-input_find-and-replace_temporary-span"); // Can remove
621+
replacementElement.classList.add("code-input_find-and-replace_temporary-span"); // Can remove span later
620622

621623
currentElement.replaceChild(replacementElement, childElement);
622624
childElement = replacementElement;
@@ -631,7 +633,7 @@ codeInput.plugins.FindAndReplace.FindMatchState = class {
631633
let startSpan = document.createElement("span");
632634
startSpan.classList.add("code-input_find-and-replace_find-match"); // Highlighted
633635
startSpan.setAttribute("data-code-input_find-and-replace_match-id", matchID);
634-
startSpan.classList.add("code-input_find-and-replace_temporary-span"); // Can remove
636+
startSpan.classList.add("code-input_find-and-replace_temporary-span"); // Can remove span later
635637
startSpan.textContent = childText.substring(0, endIndex);
636638
if(startSpan.textContent[0] == "\n") {
637639
// Newline at start - make clear
@@ -666,7 +668,7 @@ codeInput.plugins.FindAndReplace.FindMatchState = class {
666668
// Match starts and ends in childElement - highlight middle part
667669
// Text node - highlight last part
668670
let startSpan = document.createElement("span");
669-
startSpan.classList.add("code-input_find-and-replace_temporary-span"); // Can remove
671+
startSpan.classList.add("code-input_find-and-replace_temporary-span"); // Can remove span later
670672
startSpan.textContent = childText.substring(0, startIndex);
671673

672674
let middleText = childText.substring(startIndex, endIndex);
@@ -679,7 +681,7 @@ codeInput.plugins.FindAndReplace.FindMatchState = class {
679681
}
680682

681683
let endSpan = document.createElement("span");
682-
endSpan.classList.add("code-input_find-and-replace_temporary-span"); // Can remove
684+
endSpan.classList.add("code-input_find-and-replace_temporary-span"); // Can remove span later
683685
endSpan.textContent = childText.substring(endIndex);
684686

685687
childElement.insertAdjacentElement('beforebegin', startSpan);
@@ -693,7 +695,7 @@ codeInput.plugins.FindAndReplace.FindMatchState = class {
693695
let endSpan = document.createElement("span");
694696
endSpan.classList.add("code-input_find-and-replace_find-match"); // Highlighted
695697
endSpan.setAttribute("data-code-input_find-and-replace_match-id", matchID);
696-
endSpan.classList.add("code-input_find-and-replace_temporary-span"); // Can remove
698+
endSpan.classList.add("code-input_find-and-replace_temporary-span"); // Can remove span later
697699
endSpan.textContent = childText.substring(startIndex);
698700
if(endSpan.textContent[0] == "\n") {
699701
// Newline at start - make clear

0 commit comments

Comments
 (0)