@webcoder49/code-input 2.2.1 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -4
- package/code-input.css +91 -19
- package/code-input.d.ts +94 -13
- package/code-input.js +114 -40
- package/code-input.min.css +1 -1
- package/code-input.min.js +1 -1
- package/package.json +1 -1
- package/plugins/README.md +7 -0
- package/plugins/autocomplete.js +4 -2
- package/plugins/autocomplete.min.js +1 -1
- package/plugins/find-and-replace.js +136 -42
- package/plugins/find-and-replace.min.js +1 -1
- package/plugins/go-to-line.js +20 -2
- package/plugins/go-to-line.min.js +1 -1
- package/plugins/indent.js +97 -6
- package/plugins/indent.min.js +1 -1
- package/plugins/prism-line-numbers.css +10 -9
- package/plugins/prism-line-numbers.min.css +1 -1
- package/plugins/select-token-callbacks.js +289 -0
- package/plugins/select-token-callbacks.min.js +1 -0
- package/plugins/special-chars.css +1 -1
- package/plugins/special-chars.js +10 -8
- package/plugins/special-chars.min.css +1 -1
- package/plugins/special-chars.min.js +1 -1
- package/tests/hljs.html +2 -1
- package/tests/i18n.html +197 -0
- package/tests/prism-match-braces-compatibility.js +215 -0
- package/tests/prism-match-braces-compatibility.min.js +1 -0
- package/tests/prism.html +7 -8
- package/tests/tester.js +77 -13
- package/tests/tester.min.js +7 -4
package/plugins/indent.js
CHANGED
|
@@ -8,14 +8,24 @@ codeInput.plugins.Indent = class extends codeInput.Plugin {
|
|
|
8
8
|
bracketPairs = {}; // No bracket-auto-indentation used when {}
|
|
9
9
|
indentation = "\t";
|
|
10
10
|
indentationNumChars = 1;
|
|
11
|
+
tabIndentationEnabled = true; // Can be disabled for accessibility reasons to allow keyboard navigation
|
|
12
|
+
escTabToChangeFocus = true;
|
|
13
|
+
escJustPressed = false; // Becomes true when Escape key is pressed and false when another key is pressed
|
|
14
|
+
|
|
15
|
+
instructions = {
|
|
16
|
+
tabForIndentation: "Tab and Shift-Tab currently for indentation. Press Esc to enable keyboard navigation.",
|
|
17
|
+
tabForNavigation: "Tab and Shift-Tab currently for keyboard navigation. Type to return to indentation.",
|
|
18
|
+
};
|
|
11
19
|
|
|
12
20
|
/**
|
|
13
21
|
* Create an indentation plugin to pass into a template
|
|
14
|
-
* @param {
|
|
22
|
+
* @param {boolean} defaultSpaces Should the Tab key enter spaces rather than tabs? Defaults to false.
|
|
15
23
|
* @param {Number} numSpaces How many spaces is each tab character worth? Defaults to 4.
|
|
16
24
|
* @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.
|
|
25
|
+
* @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.
|
|
26
|
+
* @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.
|
|
17
27
|
*/
|
|
18
|
-
constructor(defaultSpaces=false, numSpaces=4, bracketPairs={"(": ")", "[": "]", "{": "}"}) {
|
|
28
|
+
constructor(defaultSpaces=false, numSpaces=4, bracketPairs={"(": ")", "[": "]", "{": "}"}, escTabToChangeFocus=true, instructionTranslations = {}) {
|
|
19
29
|
super([]); // No observed attributes
|
|
20
30
|
|
|
21
31
|
this.bracketPairs = bracketPairs;
|
|
@@ -26,20 +36,81 @@ codeInput.plugins.Indent = class extends codeInput.Plugin {
|
|
|
26
36
|
}
|
|
27
37
|
this.indentationNumChars = numSpaces;
|
|
28
38
|
}
|
|
39
|
+
|
|
40
|
+
this.escTabToChangeFocus = true;
|
|
41
|
+
|
|
42
|
+
this.addTranslations(this.instructions, instructionTranslations);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Make the Tab key
|
|
47
|
+
*/
|
|
48
|
+
disableTabIndentation() {
|
|
49
|
+
this.tabIndentationEnabled = false;
|
|
29
50
|
}
|
|
30
51
|
|
|
31
|
-
|
|
52
|
+
enableTabIndentation() {
|
|
53
|
+
this.tabIndentationEnabled = true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* Add keystroke events, and get the width of the indentation in pixels. */
|
|
32
57
|
afterElementsAdded(codeInput) {
|
|
58
|
+
|
|
33
59
|
let textarea = codeInput.textareaElement;
|
|
60
|
+
textarea.addEventListener('focus', (event) => { if(this.escTabToChangeFocus) codeInput.setKeyboardNavInstructions(this.instructions.tabForIndentation, true); })
|
|
34
61
|
textarea.addEventListener('keydown', (event) => { this.checkTab(codeInput, event); this.checkEnter(codeInput, event); this.checkBackspace(codeInput, event); });
|
|
35
62
|
textarea.addEventListener('beforeinput', (event) => { this.checkCloseBracket(codeInput, event); });
|
|
63
|
+
|
|
64
|
+
// Get the width of the indentation in pixels
|
|
65
|
+
let testIndentationWidthPre = document.createElement("pre");
|
|
66
|
+
testIndentationWidthPre.setAttribute("aria-hidden", "true"); // Hide for screen readers
|
|
67
|
+
let testIndentationWidthSpan = document.createElement("span");
|
|
68
|
+
if(codeInput.template.preElementStyled) {
|
|
69
|
+
testIndentationWidthPre.appendChild(testIndentationWidthSpan);
|
|
70
|
+
testIndentationWidthPre.classList.add("code-input_autocomplete_test-indentation-width");
|
|
71
|
+
codeInput.appendChild(testIndentationWidthPre); // Styled like first pre, but first pre found to update
|
|
72
|
+
} else {
|
|
73
|
+
let testIndentationWidthCode = document.createElement("code");
|
|
74
|
+
testIndentationWidthCode.appendChild(testIndentationWidthSpan);
|
|
75
|
+
testIndentationWidthCode.classList.add("code-input_autocomplete_test-indentation-width");
|
|
76
|
+
testIndentationWidthPre.appendChild(testIndentationWidthCode);
|
|
77
|
+
codeInput.appendChild(testIndentationWidthPre); // Styled like first pre, but first pre found to update
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
testIndentationWidthSpan.innerHTML = codeInput.escapeHtml(this.indentation);
|
|
81
|
+
let indentationWidthPx = testIndentationWidthSpan.offsetWidth;
|
|
82
|
+
codeInput.removeChild(testIndentationWidthPre);
|
|
83
|
+
|
|
84
|
+
codeInput.pluginData.indent = {indentationWidthPx: indentationWidthPx};
|
|
36
85
|
}
|
|
37
86
|
|
|
38
|
-
/* Deal with the Tab key causing indentation, and Tab+Selection indenting / Shift+Tab+Selection unindenting lines */
|
|
87
|
+
/* Deal with the Tab key causing indentation, and Tab+Selection indenting / Shift+Tab+Selection unindenting lines, and the mechanism through which Tab can be used to switch focus instead (accessibility). */
|
|
39
88
|
checkTab(codeInput, event) {
|
|
40
|
-
if(
|
|
89
|
+
if(!this.tabIndentationEnabled) return;
|
|
90
|
+
if(this.escTabToChangeFocus) {
|
|
91
|
+
// Accessibility - allow Tab for keyboard navigation when Esc pressed right before it.
|
|
92
|
+
if(event.key == "Escape") {
|
|
93
|
+
this.escJustPressed = true;
|
|
94
|
+
codeInput.setKeyboardNavInstructions(this.instructions.tabForNavigation, false);
|
|
95
|
+
return;
|
|
96
|
+
} else if(event.key != "Tab") {
|
|
97
|
+
if(event.key == "Shift") {
|
|
98
|
+
return; // Shift+Tab after Esc should still be keyboard navigation
|
|
99
|
+
}
|
|
100
|
+
codeInput.setKeyboardNavInstructions(this.instructions.tabForIndentation, false);
|
|
101
|
+
this.escJustPressed = false;
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if(!this.enableTabIndentation || this.escJustPressed) {
|
|
106
|
+
codeInput.setKeyboardNavInstructions("", false);
|
|
107
|
+
this.escJustPressed = false;
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
} else if(event.key != "Tab") {
|
|
41
111
|
return;
|
|
42
112
|
}
|
|
113
|
+
|
|
43
114
|
let inputElement = codeInput.textareaElement;
|
|
44
115
|
event.preventDefault(); // stop normal
|
|
45
116
|
|
|
@@ -93,6 +164,26 @@ codeInput.plugins.Indent = class extends codeInput.Plugin {
|
|
|
93
164
|
// move cursor
|
|
94
165
|
inputElement.selectionStart = selectionStartI;
|
|
95
166
|
inputElement.selectionEnd = selectionEndI;
|
|
167
|
+
|
|
168
|
+
// move scroll position to follow code
|
|
169
|
+
const textDirection = getComputedStyle(codeInput).direction;
|
|
170
|
+
if(textDirection == "rtl") {
|
|
171
|
+
if(event.shiftKey) {
|
|
172
|
+
// Scroll right
|
|
173
|
+
codeInput.scrollBy(codeInput.pluginData.indent.indentationWidthPx, 0);
|
|
174
|
+
} else {
|
|
175
|
+
// Scroll left
|
|
176
|
+
codeInput.scrollBy(-codeInput.pluginData.indent.indentationWidthPx, 0);
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
if(event.shiftKey) {
|
|
180
|
+
// Scroll left
|
|
181
|
+
codeInput.scrollBy(-codeInput.pluginData.indent.indentationWidthPx, 0);
|
|
182
|
+
} else {
|
|
183
|
+
// Scroll right
|
|
184
|
+
codeInput.scrollBy(codeInput.pluginData.indent.indentationWidthPx, 0);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
96
187
|
}
|
|
97
188
|
|
|
98
189
|
codeInput.value = inputElement.value;
|
|
@@ -235,4 +326,4 @@ codeInput.plugins.Indent = class extends codeInput.Plugin {
|
|
|
235
326
|
}
|
|
236
327
|
}
|
|
237
328
|
}
|
|
238
|
-
}
|
|
329
|
+
}
|
package/plugins/indent.min.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
codeInput.plugins.Indent=class extends codeInput.Plugin{bracketPairs={};indentation="\t";indentationNumChars=1;constructor(a=!1,b=4,c={"(":")","[":"]","{":"}"}){if(super([]),this.bracketPairs=c,a){this.indentation="";for(let a=0;a<b;a++)this.indentation+=" ";this.indentationNumChars=b}}afterElementsAdded(a){let b=a.textareaElement;b.addEventListener("keydown",b=>{this.checkTab(a,b),this.checkEnter(a,b),this.checkBackspace(a,b)}),b.addEventListener("beforeinput",b=>{this.checkCloseBracket(a,b)})}checkTab(a,b){var c=Math.max;if("Tab"==b.key)
|
|
1
|
+
codeInput.plugins.Indent=class extends codeInput.Plugin{bracketPairs={};indentation="\t";indentationNumChars=1;tabIndentationEnabled=!0;escTabToChangeFocus=!0;escJustPressed=!1;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."};constructor(a=!1,b=4,c={"(":")","[":"]","{":"}"},d=!0,e={}){if(super([]),this.bracketPairs=c,a){this.indentation="";for(let a=0;a<b;a++)this.indentation+=" ";this.indentationNumChars=b}this.escTabToChangeFocus=!0,this.addTranslations(this.instructions,e)}disableTabIndentation(){this.tabIndentationEnabled=!1}enableTabIndentation(){this.tabIndentationEnabled=!0}afterElementsAdded(a){let b=a.textareaElement;b.addEventListener("focus",()=>{this.escTabToChangeFocus&&a.setKeyboardNavInstructions(this.instructions.tabForIndentation,!0)}),b.addEventListener("keydown",b=>{this.checkTab(a,b),this.checkEnter(a,b),this.checkBackspace(a,b)}),b.addEventListener("beforeinput",b=>{this.checkCloseBracket(a,b)});let c=document.createElement("pre");c.setAttribute("aria-hidden","true");let d=document.createElement("span");if(a.template.preElementStyled)c.appendChild(d),c.classList.add("code-input_autocomplete_test-indentation-width"),a.appendChild(c);else{let b=document.createElement("code");b.appendChild(d),b.classList.add("code-input_autocomplete_test-indentation-width"),c.appendChild(b),a.appendChild(c)}d.innerHTML=a.escapeHtml(this.indentation);let e=d.offsetWidth;a.removeChild(c),a.pluginData.indent={indentationWidthPx:e}}checkTab(a,b){var c=Math.max;if(this.tabIndentationEnabled){if(this.escTabToChangeFocus){if("Escape"==b.key)return this.escJustPressed=!0,void a.setKeyboardNavInstructions(this.instructions.tabForNavigation,!1);if("Tab"!=b.key)return"Shift"==b.key?void 0:(a.setKeyboardNavInstructions(this.instructions.tabForIndentation,!1),void(this.escJustPressed=!1));if(!this.enableTabIndentation||this.escJustPressed)return a.setKeyboardNavInstructions("",!1),void(this.escJustPressed=!1)}else if("Tab"!=b.key)return;let d=a.textareaElement;if(b.preventDefault(),!b.shiftKey&&d.selectionStart==d.selectionEnd)document.execCommand("insertText",!1,this.indentation);else{let e=d.value.split("\n"),f=0,g=d.selectionStart,h=d.selectionEnd;for(let a=0;a<e.length;a++)(g<=f+e[a].length&&h>=f+1||g==h&&g<=f+e[a].length+1&&h>=f)&&(b.shiftKey?e[a].substring(0,this.indentationNumChars)==this.indentation&&(d.selectionStart=f,d.selectionEnd=f+this.indentationNumChars,document.execCommand("delete",!1,""),g>f&&(g=c(g-this.indentationNumChars,f)),h-=this.indentationNumChars,f-=this.indentationNumChars):(d.selectionStart=f,d.selectionEnd=f,document.execCommand("insertText",!1,this.indentation),g>f&&(g+=this.indentationNumChars),h+=this.indentationNumChars,f+=this.indentationNumChars)),f+=e[a].length+1;d.selectionStart=g,d.selectionEnd=h;const i=getComputedStyle(a).direction;"rtl"==i?b.shiftKey?a.scrollBy(a.pluginData.indent.indentationWidthPx,0):a.scrollBy(-a.pluginData.indent.indentationWidthPx,0):b.shiftKey?a.scrollBy(-a.pluginData.indent.indentationWidthPx,0):a.scrollBy(a.pluginData.indent.indentationWidthPx,0)}a.value=d.value}}checkEnter(a,b){if("Enter"!=b.key)return;b.preventDefault();let c=a.textareaElement,d=c.value.split("\n"),e=0,f=d.length-1,g="",h=0;for(let g=0;g<d.length;g++)if(e+=d[g].length+1,c.selectionEnd<=e){f=g;break}let j=d[f].length-(e-c.selectionEnd)+1;for(let c=0;c<j&&d[f].substring(c,c+this.indentationNumChars)==this.indentation;c+=this.indentationNumChars)h++;let k="";j!=d[f].length&&(k=d[f].substring(j),d[f]=d[f].substring(0,j));let l=!1,m="";if(null!=this.bracketPairs)for(let a in this.bracketPairs)if(d[f][d[f].length-1]==a){let b=this.bracketPairs[a];if(0<k.length&&k[0]==b){l=!0;for(let a=0;a<h+1;a++)m+=this.indentation}else h++;break}else{let b=this.bracketPairs[a];if(0<k.length&&k[0]==b){h--;break}}for(let c=0;c<h;c++)g+=this.indentation;let n=c.selectionStart;l&&(document.execCommand("insertText",!1,"\n"+m),h+=1),document.execCommand("insertText",!1,"\n"+g),c.selectionStart=n+h*this.indentationNumChars+1,c.selectionEnd=c.selectionStart;let o=+getComputedStyle(c).paddingTop.replace("px",""),p=+getComputedStyle(c).lineHeight.replace("px",""),q=+getComputedStyle(a).height.replace("px","");f*p+2*p+o>=c.scrollTop+q&&a.scrollBy(0,+getComputedStyle(c).lineHeight.replace("px","")),a.value=c.value}checkBackspace(a,b){if("Backspace"==b.key&&1!=this.indentationNumChars){let c=a.textareaElement;c.selectionStart==c.selectionEnd&&a.value.substring(c.selectionStart-this.indentationNumChars,c.selectionStart)==this.indentation&&(c.selectionStart-=this.indentationNumChars,b.preventDefault(),document.execCommand("delete",!1,""))}}checkCloseBracket(a,b){if(a.textareaElement.selectionStart==a.textareaElement.selectionEnd)for(let c in this.bracketPairs){let d=this.bracketPairs[c];b.data==d&&a.value.substring(a.textareaElement.selectionStart-this.indentationNumChars,a.textareaElement.selectionStart)==this.indentation&&(a.textareaElement.selectionStart-=this.indentationNumChars,document.execCommand("delete",!1,""))}}};
|
|
@@ -7,14 +7,15 @@
|
|
|
7
7
|
/* Update padding to match line-numbers plugin */
|
|
8
8
|
code-input.line-numbers textarea, code-input.line-numbers.code-input_pre-element-styled pre,
|
|
9
9
|
.line-numbers code-input textarea, .line-numbers code-input.code-input_pre-element-styled pre {
|
|
10
|
-
padding-
|
|
11
|
-
padding-bottom: 1em!important;
|
|
12
|
-
padding-right: 1em!important;
|
|
13
|
-
padding-left: 3.8em!important;
|
|
10
|
+
padding-left: max(3.8em, var(--padding, 16px))!important;
|
|
14
11
|
}
|
|
15
12
|
|
|
16
|
-
/*
|
|
17
|
-
code-input.line-numbers,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
13
|
+
/* Ensure pre code/textarea just wide enough to give 100% width with line numbers */
|
|
14
|
+
code-input.line-numbers, .line-numbers code-input {
|
|
15
|
+
grid-template-columns: calc(100% - max(0em, calc(3.8em - var(--padding, 16px))));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/* Make keyboard navigation still fill width */
|
|
19
|
+
code-input .code-input_dialog-container .code-input_keyboard-navigation-instructions {
|
|
20
|
+
width: calc(100% + max(3.8em, var(--padding, 16px)))!important;
|
|
21
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
.line-numbers code-input textarea,.line-numbers code-input.code-input_pre-element-styled pre,code-input.line-numbers textarea,code-input.line-numbers.code-input_pre-element-styled pre{padding-
|
|
1
|
+
.line-numbers code-input textarea,.line-numbers code-input.code-input_pre-element-styled pre,code-input.line-numbers textarea,code-input.line-numbers.code-input_pre-element-styled pre{padding-left:max(3.8em,var(--padding,16px))!important}.line-numbers code-input,code-input.line-numbers{grid-template-columns:calc(100% - max(0em,calc(3.8em - var(--padding,16px))))}code-input .code-input_dialog-container .code-input_keyboard-navigation-instructions{width:calc(100% + max(3.8em,var(--padding,16px)))!important}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Make tokens in the <pre><code> element that are included within the selected text of the <code-input>
|
|
3
|
+
* gain a CSS class while selected, or trigger JavaScript callbacks.
|
|
4
|
+
* Files: select-token-callbacks.js
|
|
5
|
+
*/
|
|
6
|
+
codeInput.plugins.SelectTokenCallbacks = class extends codeInput.Plugin {
|
|
7
|
+
/**
|
|
8
|
+
* Set up the behaviour of tokens text-selected in the `<code-input>` element, and the exact definition of a token being text-selected.
|
|
9
|
+
*
|
|
10
|
+
* 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.
|
|
11
|
+
*
|
|
12
|
+
* @param {codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks} tokenSelectorCallbacks What to do with text-selected tokens. See docstrings for the TokenSelectorCallbacks class.
|
|
13
|
+
* @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.
|
|
14
|
+
* @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.
|
|
15
|
+
* @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.
|
|
16
|
+
* @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.
|
|
17
|
+
* @param {boolean} partiallySelectedTokensAreSelected Whether tokens for which only some of their text is selected should be treated as selected. Default true.
|
|
18
|
+
* @param {boolean} parentTokensAreSelected Whether all parent tokens of selected tokens should be treated as selected. Default true.
|
|
19
|
+
*/
|
|
20
|
+
constructor(tokenSelectorCallbacks = codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks.createClassSynchronisation(), onlyCaretNotSelection = false, caretAtStartIsSelected = true, caretAtEndIsSelected = true, createSubTokens = false, partiallySelectedTokensAreSelected = true, parentTokensAreSelected = true) {
|
|
21
|
+
super([]); // No observed attributes
|
|
22
|
+
|
|
23
|
+
this.tokenSelectorCallbacks = tokenSelectorCallbacks;
|
|
24
|
+
this.onlyCaretNotSelection = onlyCaretNotSelection;
|
|
25
|
+
this.caretAtStartIsSelected = caretAtStartIsSelected;
|
|
26
|
+
this.caretAtEndIsSelected = caretAtEndIsSelected;
|
|
27
|
+
this.createSubTokens = createSubTokens;
|
|
28
|
+
this.partiallySelectedTokensAreSelected = partiallySelectedTokensAreSelected;
|
|
29
|
+
this.parentTokensAreSelected = parentTokensAreSelected;
|
|
30
|
+
}
|
|
31
|
+
/* Runs after code is highlighted; Params: codeInput element) */
|
|
32
|
+
afterHighlight(codeInputElement) {
|
|
33
|
+
this.syncSelection(codeInputElement);
|
|
34
|
+
}
|
|
35
|
+
/* Runs after elements are added into a `code-input` (useful for adding events to the textarea); Params: codeInput element) */
|
|
36
|
+
afterElementsAdded(codeInputElement) {
|
|
37
|
+
codeInputElement.pluginData.selectTokenCallbacks = {};
|
|
38
|
+
codeInputElement.pluginData.selectTokenCallbacks.lastSelectionStart = codeInputElement.textareaElement.selectionStart;
|
|
39
|
+
codeInputElement.pluginData.selectTokenCallbacks.lastSelectionEnd = codeInputElement.textareaElement.selectionEnd;
|
|
40
|
+
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);
|
|
41
|
+
this.syncSelection(codeInputElement);
|
|
42
|
+
|
|
43
|
+
// As of 2024-08, the selectionchange event is only supported on Firefox.
|
|
44
|
+
codeInputElement.textareaElement.addEventListener("selectionchange", () => {
|
|
45
|
+
this.checkSelectionChanged(codeInputElement)
|
|
46
|
+
});
|
|
47
|
+
// When selectionchange has complete support, the listeners below can be deleted.
|
|
48
|
+
codeInputElement.textareaElement.addEventListener("select", () => {
|
|
49
|
+
this.checkSelectionChanged(codeInputElement)
|
|
50
|
+
});
|
|
51
|
+
codeInputElement.textareaElement.addEventListener("keypress", () => {
|
|
52
|
+
this.checkSelectionChanged(codeInputElement)
|
|
53
|
+
});
|
|
54
|
+
codeInputElement.textareaElement.addEventListener("mousedown", () => {
|
|
55
|
+
this.checkSelectionChanged(codeInputElement)
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/* If the text selection has changed, run syncSelection. */
|
|
59
|
+
checkSelectionChanged(codeInputElement) {
|
|
60
|
+
if(
|
|
61
|
+
codeInputElement.textareaElement.selectionStart != codeInputElement.pluginData.selectTokenCallbacks.lastSelectionStart
|
|
62
|
+
|| codeInputElement.textareaElement.selectionEnd != codeInputElement.pluginData.selectTokenCallbacks.lastSelectionEnd
|
|
63
|
+
) {
|
|
64
|
+
this.syncSelection(codeInputElement);
|
|
65
|
+
codeInputElement.pluginData.selectTokenCallbacks.lastSelectionStart = codeInputElement.textareaElement.selectionStart;
|
|
66
|
+
codeInputElement.pluginData.selectTokenCallbacks.lastSelectionEnd = codeInputElement.textareaElement.selectionEnd;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/* Update which elements have the code-input_selected class. */
|
|
70
|
+
syncSelection(codeInputElement) {
|
|
71
|
+
codeInputElement.pluginData.selectTokenCallbacks.selectedTokenState.updateSelection(codeInputElement.textareaElement.selectionStart, codeInputElement.textareaElement.selectionEnd)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* A data structure specifying what should be done with tokens when they are selected, and also allows for previously selected
|
|
77
|
+
* tokens to be dealt with each time the selection changes. See the constructor and the createClassSynchronisation static method.
|
|
78
|
+
*/
|
|
79
|
+
codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks = class {
|
|
80
|
+
/**
|
|
81
|
+
* Pass any callbacks you want to customise the behaviour of selected tokens via JavaScript.
|
|
82
|
+
*
|
|
83
|
+
* (If the behaviour you want is just differently styling selected tokens _via CSS_, you should probably use the createClassSynchronisation static method.)
|
|
84
|
+
* @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.
|
|
85
|
+
* @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.
|
|
86
|
+
*/
|
|
87
|
+
constructor(tokenSelectedCallback, selectChangedCallback) {
|
|
88
|
+
this.tokenSelectedCallback = tokenSelectedCallback;
|
|
89
|
+
this.selectChangedCallback = selectChangedCallback;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 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.
|
|
94
|
+
*
|
|
95
|
+
* (If the behaviour you want requires more complex behaviour or JavaScript, you should use TokenSelectorCallbacks' constructor.)
|
|
96
|
+
*
|
|
97
|
+
* @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".
|
|
98
|
+
* @returns A new TokenSelectorCallbacks instance that encodes this behaviour.
|
|
99
|
+
*/
|
|
100
|
+
static createClassSynchronisation(selectedClass = "code-input_select-token-callbacks_selected") {
|
|
101
|
+
return new codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks(
|
|
102
|
+
(token) => {
|
|
103
|
+
token.classList.add(selectedClass);
|
|
104
|
+
},
|
|
105
|
+
(tokenContainer) => {
|
|
106
|
+
// Remove selected class
|
|
107
|
+
let selectedClassTokens = tokenContainer.getElementsByClassName(selectedClass);
|
|
108
|
+
// Use it like a queue, because as elements have their class name removed they are live-removed from the collection.
|
|
109
|
+
while(selectedClassTokens.length > 0) {
|
|
110
|
+
selectedClassTokens[0].classList.remove(selectedClass);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* Manages a single <code-input> element's selected tokens, and calling the correct functions on the selected tokens */
|
|
118
|
+
codeInput.plugins.SelectTokenCallbacks.SelectedTokenState = class {
|
|
119
|
+
constructor(codeElement, tokenSelectorCallbacks, onlyCaretNotSelection, caretAtStartIsSelected, caretAtEndIsSelected, createSubTokens, partiallySelectedTokensAreSelected, parentTokensAreSelected) {
|
|
120
|
+
this.tokenContainer = codeElement;
|
|
121
|
+
this.tokenSelectorCallbacks = tokenSelectorCallbacks;
|
|
122
|
+
this.onlyCaretNotSelection = onlyCaretNotSelection;
|
|
123
|
+
this.caretAtStartIsSelected = caretAtStartIsSelected;
|
|
124
|
+
this.caretAtEndIsSelected = caretAtEndIsSelected;
|
|
125
|
+
this.createSubTokens = createSubTokens;
|
|
126
|
+
this.partiallySelectedTokensAreSelected = partiallySelectedTokensAreSelected;
|
|
127
|
+
this.parentTokensAreSelected = parentTokensAreSelected;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/* Change the selected region to a new range from selectionStart to selectionEnd and run
|
|
131
|
+
the callbacks. */
|
|
132
|
+
updateSelection(selectionStart, selectionEnd) {
|
|
133
|
+
this.selectChanged()
|
|
134
|
+
if(!this.onlyCaretNotSelection || selectionStart == selectionEnd) { // Only deal with selected text if onlyCaretNotSelection is false.
|
|
135
|
+
this.updateSelectedTokens(this.tokenContainer, selectionStart, selectionEnd)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/* Runs when the text selection has changed, before any updateSelectedTokens call. */
|
|
139
|
+
selectChanged() {
|
|
140
|
+
if(this.createSubTokens) {
|
|
141
|
+
// Remove generated spans to hold selected partial tokens
|
|
142
|
+
let tempSpans = this.tokenContainer.getElementsByClassName("code-input_select-token-callbacks_temporary-span");
|
|
143
|
+
while(tempSpans.length > 0) {
|
|
144
|
+
// Replace with textContent as Text node
|
|
145
|
+
// Use it like a queue, because as elements have their class name removed they are live-removed from the collection.
|
|
146
|
+
tempSpans[0].parentElement.replaceChild(new Text(tempSpans[0].textContent), tempSpans[0]);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
this.tokenSelectorCallbacks.selectChangedCallback(this.tokenContainer);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/* Do the desired behaviour for selection to all tokens (elements in the currentElement)
|
|
154
|
+
from startIndex to endIndex in the text. Start from the currentElement as this function is recursive.
|
|
155
|
+
This code is similar to codeInput.plugins.FindAndReplace.FindMatchState.highlightMatch*/
|
|
156
|
+
updateSelectedTokens(currentElement, startIndex, endIndex) {
|
|
157
|
+
if(endIndex < 0 || endIndex == 0 && !this.caretAtStartIsSelected) {
|
|
158
|
+
return; // Nothing selected
|
|
159
|
+
}
|
|
160
|
+
if(this.parentTokensAreSelected && currentElement !== this.tokenContainer) {
|
|
161
|
+
this.tokenSelectorCallbacks.tokenSelectedCallback(currentElement); // Parent elements also marked with class / have callback called
|
|
162
|
+
}
|
|
163
|
+
for(let i = 0; i < currentElement.childNodes.length; i++) {
|
|
164
|
+
let childElement = currentElement.childNodes[i];
|
|
165
|
+
let childText = childElement.textContent;
|
|
166
|
+
|
|
167
|
+
let noInnerElements = false;
|
|
168
|
+
if(childElement.nodeType == 3) {
|
|
169
|
+
// Text node
|
|
170
|
+
if(this.createSubTokens) {
|
|
171
|
+
// Replace with token
|
|
172
|
+
if(i + 1 < currentElement.childNodes.length && currentElement.childNodes[i+1].nodeType == 3) {
|
|
173
|
+
// Can merge with next text node
|
|
174
|
+
currentElement.childNodes[i+1].textContent = childElement.textContent + currentElement.childNodes[i+1].textContent; // Merge textContent with next node
|
|
175
|
+
currentElement.removeChild(childElement); // Delete this node
|
|
176
|
+
i--; // As an element removed
|
|
177
|
+
continue; // Move to next node
|
|
178
|
+
}
|
|
179
|
+
noInnerElements = true;
|
|
180
|
+
|
|
181
|
+
let replacementElement = document.createElement("span");
|
|
182
|
+
replacementElement.textContent = childText;
|
|
183
|
+
replacementElement.classList.add("code-input_select-token-callbacks_temporary-span"); // Can remove span later
|
|
184
|
+
|
|
185
|
+
currentElement.replaceChild(replacementElement, childElement);
|
|
186
|
+
childElement = replacementElement;
|
|
187
|
+
} else {
|
|
188
|
+
// Skip text node
|
|
189
|
+
// Make indexes skip the element
|
|
190
|
+
startIndex -= childText.length;
|
|
191
|
+
endIndex -= childText.length;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if(startIndex <= 0) {
|
|
197
|
+
// Started selection
|
|
198
|
+
if(childText.length > endIndex) {
|
|
199
|
+
// Selection ends in childElement
|
|
200
|
+
if(this.partiallySelectedTokensAreSelected) {
|
|
201
|
+
if(noInnerElements) {
|
|
202
|
+
if(this.createSubTokens && startIndex != endIndex) { // Subtoken to create
|
|
203
|
+
// Text node - add selection class to first part
|
|
204
|
+
let startSpan = document.createElement("span");
|
|
205
|
+
this.tokenSelectorCallbacks.tokenSelectedCallback(startSpan); // Selected
|
|
206
|
+
startSpan.classList.add("code-input_select-token-callbacks_temporary-span"); // Can remove span later
|
|
207
|
+
startSpan.textContent = childText.substring(0, endIndex);
|
|
208
|
+
|
|
209
|
+
let endText = childText.substring(endIndex);
|
|
210
|
+
childElement.textContent = endText;
|
|
211
|
+
|
|
212
|
+
childElement.insertAdjacentElement('beforebegin', startSpan);
|
|
213
|
+
i++; // An extra element has been added
|
|
214
|
+
}
|
|
215
|
+
if(this.parentTokensAreSelected || !this.createSubTokens) {
|
|
216
|
+
this.tokenSelectorCallbacks.tokenSelectedCallback(childElement); // Selected
|
|
217
|
+
}
|
|
218
|
+
} else {
|
|
219
|
+
this.updateSelectedTokens(childElement, 0, endIndex);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Match ended - nothing to do after backtracking
|
|
224
|
+
return;
|
|
225
|
+
} else {
|
|
226
|
+
// Match goes through child element
|
|
227
|
+
this.tokenSelectorCallbacks.tokenSelectedCallback(childElement); // Selected
|
|
228
|
+
}
|
|
229
|
+
} else if(this.caretAtEndIsSelected && childText.length >= startIndex || childText.length > startIndex) {
|
|
230
|
+
// Match starts in childElement
|
|
231
|
+
if(this.partiallySelectedTokensAreSelected) {
|
|
232
|
+
if(noInnerElements) {
|
|
233
|
+
if(this.createSubTokens && startIndex != endIndex) { // Subtoken to create
|
|
234
|
+
if(childText.length > endIndex) {
|
|
235
|
+
// Match starts and ends in childElement - selection middle part
|
|
236
|
+
let startSpan = document.createElement("span");
|
|
237
|
+
startSpan.classList.add("code-input_select-token-callbacks_temporary-span"); // Can remove span later
|
|
238
|
+
startSpan.textContent = childText.substring(0, startIndex);
|
|
239
|
+
|
|
240
|
+
let middleText = childText.substring(startIndex, endIndex);
|
|
241
|
+
childElement.textContent = middleText;
|
|
242
|
+
this.tokenSelectorCallbacks.tokenSelectedCallback(childElement); // Selection
|
|
243
|
+
|
|
244
|
+
let endSpan = document.createElement("span");
|
|
245
|
+
endSpan.classList.add("code-input_select-token-callbacks_temporary-span"); // Can remove span later
|
|
246
|
+
endSpan.textContent = childText.substring(endIndex);
|
|
247
|
+
|
|
248
|
+
childElement.insertAdjacentElement('beforebegin', startSpan);
|
|
249
|
+
childElement.insertAdjacentElement('afterend', endSpan);
|
|
250
|
+
i++; // 2 extra elements have been added
|
|
251
|
+
} else {
|
|
252
|
+
// Match starts in element - highlight last part
|
|
253
|
+
let startText = childText.substring(0, startIndex);
|
|
254
|
+
childElement.textContent = startText;
|
|
255
|
+
|
|
256
|
+
let endSpan = document.createElement("span");
|
|
257
|
+
this.tokenSelectorCallbacks.tokenSelectedCallback(endSpan); // Selected
|
|
258
|
+
endSpan.classList.add("code-input_select-token-callbacks_temporary-span"); // Can remove span later
|
|
259
|
+
endSpan.textContent = childText.substring(startIndex);
|
|
260
|
+
|
|
261
|
+
childElement.insertAdjacentElement('afterend', endSpan);
|
|
262
|
+
i++; // An extra element has been added
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if(this.parentTokensAreSelected || !this.createSubTokens) {
|
|
266
|
+
this.tokenSelectorCallbacks.tokenSelectedCallback(childElement); // Selected
|
|
267
|
+
}
|
|
268
|
+
} else {
|
|
269
|
+
this.updateSelectedTokens(childElement, startIndex, endIndex);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if(this.caretAtStartIsSelected) {
|
|
274
|
+
if(childText.length > endIndex) {
|
|
275
|
+
// Match completely in childElement - nothing to do after backtracking
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
} else if(childText.length >= endIndex) {
|
|
279
|
+
// Match completely in childElement - nothing to do after backtracking
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Make indexes skip the element
|
|
285
|
+
startIndex -= childText.length;
|
|
286
|
+
endIndex -= childText.length;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
codeInput.plugins.SelectTokenCallbacks=class extends codeInput.Plugin{constructor(a=codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks.createClassSynchronisation(),b=!1,c=!0,d=!0,e=!1,f=!0,g=!0){super([]),this.tokenSelectorCallbacks=a,this.onlyCaretNotSelection=b,this.caretAtStartIsSelected=c,this.caretAtEndIsSelected=d,this.createSubTokens=e,this.partiallySelectedTokensAreSelected=f,this.parentTokensAreSelected=g}afterHighlight(a){this.syncSelection(a)}afterElementsAdded(a){a.pluginData.selectTokenCallbacks={},a.pluginData.selectTokenCallbacks.lastSelectionStart=a.textareaElement.selectionStart,a.pluginData.selectTokenCallbacks.lastSelectionEnd=a.textareaElement.selectionEnd,a.pluginData.selectTokenCallbacks.selectedTokenState=new codeInput.plugins.SelectTokenCallbacks.SelectedTokenState(a.codeElement,this.tokenSelectorCallbacks,this.onlyCaretNotSelection,this.caretAtStartIsSelected,this.caretAtEndIsSelected,this.createSubTokens,this.partiallySelectedTokensAreSelected,this.parentTokensAreSelected),this.syncSelection(a),a.textareaElement.addEventListener("selectionchange",()=>{this.checkSelectionChanged(a)}),a.textareaElement.addEventListener("select",()=>{this.checkSelectionChanged(a)}),a.textareaElement.addEventListener("keypress",()=>{this.checkSelectionChanged(a)}),a.textareaElement.addEventListener("mousedown",()=>{this.checkSelectionChanged(a)})}checkSelectionChanged(a){(a.textareaElement.selectionStart!=a.pluginData.selectTokenCallbacks.lastSelectionStart||a.textareaElement.selectionEnd!=a.pluginData.selectTokenCallbacks.lastSelectionEnd)&&(this.syncSelection(a),a.pluginData.selectTokenCallbacks.lastSelectionStart=a.textareaElement.selectionStart,a.pluginData.selectTokenCallbacks.lastSelectionEnd=a.textareaElement.selectionEnd)}syncSelection(a){a.pluginData.selectTokenCallbacks.selectedTokenState.updateSelection(a.textareaElement.selectionStart,a.textareaElement.selectionEnd)}},codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks=class{constructor(a,b){this.tokenSelectedCallback=a,this.selectChangedCallback=b}static createClassSynchronisation(a="code-input_select-token-callbacks_selected"){return new codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks(b=>{b.classList.add(a)},b=>{for(let c=b.getElementsByClassName(a);0<c.length;)c[0].classList.remove(a)})}},codeInput.plugins.SelectTokenCallbacks.SelectedTokenState=class{constructor(a,b,c,d,e,f,g,h){this.tokenContainer=a,this.tokenSelectorCallbacks=b,this.onlyCaretNotSelection=c,this.caretAtStartIsSelected=d,this.caretAtEndIsSelected=e,this.createSubTokens=f,this.partiallySelectedTokensAreSelected=g,this.parentTokensAreSelected=h}updateSelection(a,b){this.selectChanged(),this.onlyCaretNotSelection&&a!=b||this.updateSelectedTokens(this.tokenContainer,a,b)}selectChanged(){if(this.createSubTokens)for(let a=this.tokenContainer.getElementsByClassName("code-input_select-token-callbacks_temporary-span");0<a.length;)a[0].parentElement.replaceChild(new Text(a[0].textContent),a[0]);this.tokenSelectorCallbacks.selectChangedCallback(this.tokenContainer)}updateSelectedTokens(a,b,c){if(!(0>c)&&(0!=c||this.caretAtStartIsSelected)){this.parentTokensAreSelected&&a!==this.tokenContainer&&this.tokenSelectorCallbacks.tokenSelectedCallback(a);for(let d=0;d<a.childNodes.length;d++){let e=a.childNodes[d],f=e.textContent,g=!1;if(3==e.nodeType)if(this.createSubTokens){if(d+1<a.childNodes.length&&3==a.childNodes[d+1].nodeType){a.childNodes[d+1].textContent=e.textContent+a.childNodes[d+1].textContent,a.removeChild(e),d--;continue}g=!0;let b=document.createElement("span");b.textContent=f,b.classList.add("code-input_select-token-callbacks_temporary-span"),a.replaceChild(b,e),e=b}else{b-=f.length,c-=f.length;continue}if(0>=b){if(f.length>c){if(this.partiallySelectedTokensAreSelected)if(g){if(this.createSubTokens&&b!=c){let a=document.createElement("span");this.tokenSelectorCallbacks.tokenSelectedCallback(a),a.classList.add("code-input_select-token-callbacks_temporary-span"),a.textContent=f.substring(0,c);let b=f.substring(c);e.textContent=b,e.insertAdjacentElement("beforebegin",a),d++}(this.parentTokensAreSelected||!this.createSubTokens)&&this.tokenSelectorCallbacks.tokenSelectedCallback(e)}else this.updateSelectedTokens(e,0,c);return}this.tokenSelectorCallbacks.tokenSelectedCallback(e)}else if(this.caretAtEndIsSelected&&f.length>=b||f.length>b){if(this.partiallySelectedTokensAreSelected)if(g){if(this.createSubTokens&&b!=c)if(f.length>c){let a=document.createElement("span");a.classList.add("code-input_select-token-callbacks_temporary-span"),a.textContent=f.substring(0,b);let g=f.substring(b,c);e.textContent=g,this.tokenSelectorCallbacks.tokenSelectedCallback(e);let h=document.createElement("span");h.classList.add("code-input_select-token-callbacks_temporary-span"),h.textContent=f.substring(c),e.insertAdjacentElement("beforebegin",a),e.insertAdjacentElement("afterend",h),d++}else{let a=f.substring(0,b);e.textContent=a;let c=document.createElement("span");this.tokenSelectorCallbacks.tokenSelectedCallback(c),c.classList.add("code-input_select-token-callbacks_temporary-span"),c.textContent=f.substring(b),e.insertAdjacentElement("afterend",c),d++}(this.parentTokensAreSelected||!this.createSubTokens)&&this.tokenSelectorCallbacks.tokenSelectedCallback(e)}else this.updateSelectedTokens(e,b,c);if(this.caretAtStartIsSelected){if(f.length>c)return;}else if(f.length>=c)return}b-=f.length,c-=f.length}}}};
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
|
|
63
63
|
mask-image: var(--hex-0), var(--hex-1), var(--hex-2), var(--hex-3);
|
|
64
64
|
mask-repeat: no-repeat, no-repeat, no-repeat, no-repeat;
|
|
65
|
-
mask-size: 40%, 40%, 40%, 40
|
|
65
|
+
mask-size: min(40%, 0.25em), min(40%, 0.25em), min(40%, 0.25em), min(40%, 0.25em);
|
|
66
66
|
mask-position: 10% 10%, 90% 10%, 10% 90%, 90% 90%;
|
|
67
67
|
|
|
68
68
|
-webkit-mask-image: var(--hex-0), var(--hex-1), var(--hex-2), var(--hex-3);
|
package/plugins/special-chars.js
CHANGED
|
@@ -14,7 +14,7 @@ codeInput.plugins.SpecialChars = class extends codeInput.Plugin {
|
|
|
14
14
|
* Create a special characters plugin instance.
|
|
15
15
|
* Default = covers many non-renderable ASCII characters.
|
|
16
16
|
* @param {Boolean} colorInSpecialChars Whether or not to give special characters custom background colors based on their hex code
|
|
17
|
-
* @param {Boolean} inheritTextColor If `
|
|
17
|
+
* @param {Boolean} inheritTextColor If `inheritTextColor` is false, forces the color of the hex code to inherit from syntax highlighting. Otherwise, the base color of the `pre code` element is used to give contrast to the small characters.
|
|
18
18
|
* @param {RegExp} specialCharRegExp The regular expression which matches special characters
|
|
19
19
|
*/
|
|
20
20
|
constructor(colorInSpecialChars = false, inheritTextColor = false, specialCharRegExp = /(?!\n)(?!\t)[\u{0000}-\u{001F}]|[\u{007F}-\u{009F}]|[\u{0200}-\u{FFFF}]/ug) { // By default, covers many non-renderable ASCII characters
|
|
@@ -71,6 +71,7 @@ codeInput.plugins.SpecialChars = class extends codeInput.Plugin {
|
|
|
71
71
|
|
|
72
72
|
if(nextNode.textContent != "") {
|
|
73
73
|
let replacementElement = this.getStylisedSpecialChar(codeInput, nextNode.textContent);
|
|
74
|
+
// This next node will become the i+1th node so automatically iterated to
|
|
74
75
|
nextNode.parentNode.insertBefore(replacementElement, nextNode);
|
|
75
76
|
nextNode.textContent = "";
|
|
76
77
|
}
|
|
@@ -120,14 +121,15 @@ codeInput.plugins.SpecialChars = class extends codeInput.Plugin {
|
|
|
120
121
|
|
|
121
122
|
/* Get the colors a stylised representation of a given character must be shown in; lazy load and return [background color, text color] */
|
|
122
123
|
getCharacterColors(asciiCode) {
|
|
123
|
-
let backgroundColor;
|
|
124
124
|
let textColor;
|
|
125
125
|
if(!(asciiCode in this.cachedColors)) {
|
|
126
|
-
// Get background color
|
|
127
|
-
|
|
128
|
-
backgroundColor =
|
|
129
|
-
|
|
130
|
-
|
|
126
|
+
// Get background color
|
|
127
|
+
let asciiHex = asciiCode.toString(16);
|
|
128
|
+
let backgroundColor = "";
|
|
129
|
+
for(let i = 0; i < asciiHex.length; i++) {
|
|
130
|
+
backgroundColor += asciiHex[i] + asciiHex[i];
|
|
131
|
+
}
|
|
132
|
+
backgroundColor = ("000000" + backgroundColor).substring(backgroundColor.length); // So valid HEX color with 6 characters
|
|
131
133
|
|
|
132
134
|
// Get most suitable text color - white or black depending on background brightness
|
|
133
135
|
let colorBrightness = 0;
|
|
@@ -148,7 +150,7 @@ codeInput.plugins.SpecialChars = class extends codeInput.Plugin {
|
|
|
148
150
|
/* Get the width of a character in em (relative to font size), for use in creation of the stylised hexadecimal representation with the same width */
|
|
149
151
|
getCharacterWidthEm(codeInput, char) {
|
|
150
152
|
// Force zero-width characters
|
|
151
|
-
if(new RegExp("\u00AD|\
|
|
153
|
+
if(new RegExp("\u00AD|\u02DE|[\u0300-\u036F]|[\u0483-\u0489]|[\u200B-\u200D]|\uFEFF").test(char) ) { return 0 }
|
|
152
154
|
// Non-renderable ASCII characters should all be rendered at same size
|
|
153
155
|
if(char != "\u0096" && new RegExp("[\u{0000}-\u{001F}]|[\u{007F}-\u{009F}]", "g").test(char)) {
|
|
154
156
|
let fallbackWidth = this.getCharacterWidthEm(codeInput, "\u0096");
|
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
--code-input_special-chars_0);--hex-1:var(
|
|
3
3
|
--code-input_special-chars_0);--hex-2:var(
|
|
4
4
|
--code-input_special-chars_0);--hex-3:var(
|
|
5
|
-
--code-input_special-chars_0)}.code-input_special-char::before{margin-left:50%;transform:translate(-50%,0);content:" ";background-color:var(--code-input_special-char_color,currentColor);image-rendering:pixelated;display:inline-block;width:calc(100%-2px);height:100%;mask-image:var(--hex-0),var(--hex-1),var(--hex-2),var(--hex-3);mask-repeat:no-repeat,no-repeat,no-repeat,no-repeat;mask-size:40
|
|
5
|
+
--code-input_special-chars_0)}.code-input_special-char::before{margin-left:50%;transform:translate(-50%,0);content:" ";background-color:var(--code-input_special-char_color,currentColor);image-rendering:pixelated;display:inline-block;width:calc(100%-2px);height:100%;mask-image:var(--hex-0),var(--hex-1),var(--hex-2),var(--hex-3);mask-repeat:no-repeat,no-repeat,no-repeat,no-repeat;mask-size:min(40%,.25em),min(40%,.25em),min(40%,.25em),min(40%,.25em);mask-position:10% 10%,90% 10%,10% 90%,90% 90%;-webkit-mask-image:var(--hex-0),var(--hex-1),var(--hex-2),var(--hex-3);-webkit-mask-repeat:no-repeat,no-repeat,no-repeat,no-repeat;-webkit-mask-size:min(40%,.25em),min(40%,.25em),min(40%,.25em),min(40%,.25em);-webkit-mask-position:10% 10%,min(90%,.5em) 10%,10% 90%,min(90%,.5em) 90%}.code-input_special-char_zero-width{z-index:1;width:1em;margin-left:-.5em;margin-right:-.5em;position:relative;opacity:.75}.code-input_special-char_one-byte::before{height:1.5em;top:-1em;content:attr(data-hex2)}.code-input_special-char_one-byte::after{height:1.5em;bottom:-1em;content:attr(data-hex3)}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
codeInput.plugins.SpecialChars=class extends codeInput.Plugin{specialCharRegExp;cachedColors;cachedWidths;canvasContext;constructor(a=!1,b=!1,c=/(?!\n)(?!\t)[\u{0000}-\u{001F}]|[\u{007F}-\u{009F}]|[\u{0200}-\u{FFFF}]/ug){super([]),this.specialCharRegExp=c,this.colorInSpecialChars=a,this.inheritTextColor=b,this.cachedColors={},this.cachedWidths={};let d=document.createElement("canvas");this.canvasContext=d.getContext("2d")}afterElementsAdded(a){setTimeout(()=>{a.value=a.value},100)}afterHighlight(a){let b=a.codeElement;a.pluginData.specialChars={},a.pluginData.specialChars.contrastColor=window.getComputedStyle(b).color,this.recursivelyReplaceText(a,b),this.lastFont=window.getComputedStyle(a.textareaElement).font}recursivelyReplaceText(a,b){for(let c,d=0;d<b.childNodes.length;d++)if(c=b.childNodes[d],3==c.nodeType){let b=c.nodeValue;this.specialCharRegExp.lastIndex=0;let d=this.specialCharRegExp.exec(b);if(null!=d){let b=d.index;if(c=c.splitText(b+1).previousSibling,0<b&&(c=c.splitText(b)),""!=c.textContent){let b=this.getStylisedSpecialChar(a,c.textContent);c.parentNode.insertBefore(b,c),c.textContent=""}}}else 1==c.nodeType&&"code-input_special-char"!=c.className&&""!=c.nodeValue&&this.recursivelyReplaceText(a,c)}getStylisedSpecialChar(a,b){let c,d=b.codePointAt(0);this.colorInSpecialChars&&(c=this.getCharacterColors(d)),d=d.toString(16),d=("0000"+d).substring(d.length),d=d.toUpperCase();let e=this.getCharacterWidthEm(a,b),f=document.createElement("span");return f.classList.add("code-input_special-char"),f.style.setProperty("--hex-0","var(--code-input_special-chars_"+d[0]+")"),f.style.setProperty("--hex-1","var(--code-input_special-chars_"+d[1]+")"),f.style.setProperty("--hex-2","var(--code-input_special-chars_"+d[2]+")"),f.style.setProperty("--hex-3","var(--code-input_special-chars_"+d[3]+")"),0==e?f.classList.add("code-input_special-char_zero-width"):f.style.width=e+"em",this.colorInSpecialChars?(f.style.backgroundColor="#"+c[0],f.style.setProperty("--code-input_special-char_color",c[1])):!this.inheritTextColor&&f.style.setProperty("--code-input_special-char_color",a.pluginData.specialChars.contrastColor),f}getCharacterColors(a){let b
|
|
1
|
+
codeInput.plugins.SpecialChars=class extends codeInput.Plugin{specialCharRegExp;cachedColors;cachedWidths;canvasContext;constructor(a=!1,b=!1,c=/(?!\n)(?!\t)[\u{0000}-\u{001F}]|[\u{007F}-\u{009F}]|[\u{0200}-\u{FFFF}]/ug){super([]),this.specialCharRegExp=c,this.colorInSpecialChars=a,this.inheritTextColor=b,this.cachedColors={},this.cachedWidths={};let d=document.createElement("canvas");this.canvasContext=d.getContext("2d")}afterElementsAdded(a){setTimeout(()=>{a.value=a.value},100)}afterHighlight(a){let b=a.codeElement;a.pluginData.specialChars={},a.pluginData.specialChars.contrastColor=window.getComputedStyle(b).color,this.recursivelyReplaceText(a,b),this.lastFont=window.getComputedStyle(a.textareaElement).font}recursivelyReplaceText(a,b){for(let c,d=0;d<b.childNodes.length;d++)if(c=b.childNodes[d],3==c.nodeType){let b=c.nodeValue;this.specialCharRegExp.lastIndex=0;let d=this.specialCharRegExp.exec(b);if(null!=d){let b=d.index;if(c=c.splitText(b+1).previousSibling,0<b&&(c=c.splitText(b)),""!=c.textContent){let b=this.getStylisedSpecialChar(a,c.textContent);c.parentNode.insertBefore(b,c),c.textContent=""}}}else 1==c.nodeType&&"code-input_special-char"!=c.className&&""!=c.nodeValue&&this.recursivelyReplaceText(a,c)}getStylisedSpecialChar(a,b){let c,d=b.codePointAt(0);this.colorInSpecialChars&&(c=this.getCharacterColors(d)),d=d.toString(16),d=("0000"+d).substring(d.length),d=d.toUpperCase();let e=this.getCharacterWidthEm(a,b),f=document.createElement("span");return f.classList.add("code-input_special-char"),f.style.setProperty("--hex-0","var(--code-input_special-chars_"+d[0]+")"),f.style.setProperty("--hex-1","var(--code-input_special-chars_"+d[1]+")"),f.style.setProperty("--hex-2","var(--code-input_special-chars_"+d[2]+")"),f.style.setProperty("--hex-3","var(--code-input_special-chars_"+d[3]+")"),0==e?f.classList.add("code-input_special-char_zero-width"):f.style.width=e+"em",this.colorInSpecialChars?(f.style.backgroundColor="#"+c[0],f.style.setProperty("--code-input_special-char_color",c[1])):!this.inheritTextColor&&f.style.setProperty("--code-input_special-char_color",a.pluginData.specialChars.contrastColor),f}getCharacterColors(a){let b;if(!(a in this.cachedColors)){let c=a.toString(16),d="";for(let a=0;a<c.length;a++)d+=c[a]+c[a];d=("000000"+d).substring(d.length);let e=0;const f=[.299,.587,.114];for(let a=0;6>a;a+=2)e+=parseInt(d.substring(a,a+2),16)*f[a/2];return b=128>e?"white":"black",this.cachedColors[a]=[d,b],[d,b]}return this.cachedColors[a]}getCharacterWidthEm(a,b){if(/|˞|[̀-ͯ]|[҃-҉]|[-]|/.test(b))return 0;if("\x96"!=b&&/[\0-]|[-]/g.test(b)){let b=this.getCharacterWidthEm(a,"\x96");return b}let c=getComputedStyle(a.textareaElement).fontFamily+" "+getComputedStyle(a.textareaElement).fontStretch+" "+getComputedStyle(a.textareaElement).fontStyle+" "+getComputedStyle(a.textareaElement).fontVariant+" "+getComputedStyle(a.textareaElement).fontWeight+" "+getComputedStyle(a.textareaElement).lineHeight;if(null==this.cachedWidths[c]&&(this.cachedWidths[c]={}),null!=this.cachedWidths[c][b])return this.cachedWidths[c][b];this.canvasContext.font=getComputedStyle(a.textareaElement).font.replace(getComputedStyle(a.textareaElement).fontSize,"20px");let d=this.canvasContext.measureText(b).width/20;if(1<d)d/=2;else if(0==d&&"\x96"!=b){let b=this.getCharacterWidthEm(a,"\x96");return b}if(navigator.userAgent.includes("Mozilla")&&!navigator.userAgent.includes("Chrome")&&!navigator.userAgent.includes("Safari")){let b=+getComputedStyle(a.textareaElement).fontSize.substring(0,getComputedStyle(a.textareaElement).fontSize.length-2);20>b&&(d*=20/b)}return this.cachedWidths[c][b]=d,d}};
|
package/tests/hljs.html
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
<script src="../plugins/go-to-line.js"></script>
|
|
29
29
|
<link rel="stylesheet" href="../plugins/go-to-line.css">
|
|
30
30
|
<script src="../plugins/indent.js"></script>
|
|
31
|
+
<script src="../plugins/select-token-callbacks.js"></script>
|
|
31
32
|
<script src="../plugins/special-chars.js"></script>
|
|
32
33
|
<link rel="stylesheet" href="../plugins/special-chars.css">
|
|
33
34
|
|
|
@@ -41,7 +42,7 @@
|
|
|
41
42
|
|
|
42
43
|
<details id="collapse-results"><summary>Test Results (Click to Open)</summary><pre id="test-results"></pre></details>
|
|
43
44
|
<form method="GET" action="https://google.com/search" target="_blank">
|
|
44
|
-
<code-input
|
|
45
|
+
<code-input name="q">console.log("Hello, World!");
|
|
45
46
|
// A second line
|
|
46
47
|
// A third line with <html> tags</code-input>
|
|
47
48
|
<input type="submit" value="Search Google For Code"/>
|