@webcoder49/code-input 2.6.0 → 2.6.2
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/docs/_index.md +1 -1
- package/esm/code-input.d.mts +154 -0
- package/esm/code-input.mjs +995 -0
- package/esm/plugins/auto-close-brackets.d.mts +15 -0
- package/esm/plugins/auto-close-brackets.mjs +84 -0
- package/esm/plugins/autocomplete.d.mts +14 -0
- package/esm/plugins/autocomplete.mjs +93 -0
- package/esm/plugins/autodetect.d.mts +11 -0
- package/esm/plugins/autodetect.mjs +35 -0
- package/esm/plugins/find-and-replace.d.mts +43 -0
- package/esm/plugins/find-and-replace.mjs +777 -0
- package/esm/plugins/go-to-line.d.mts +29 -0
- package/esm/plugins/go-to-line.mjs +217 -0
- package/esm/plugins/indent.d.mts +22 -0
- package/esm/plugins/indent.mjs +359 -0
- package/esm/plugins/select-token-callbacks.d.mts +51 -0
- package/esm/plugins/select-token-callbacks.mjs +296 -0
- package/esm/plugins/special-chars.d.mts +25 -0
- package/esm/plugins/special-chars.mjs +207 -0
- package/esm/plugins/test.d.mts +16 -0
- package/esm/plugins/test.mjs +56 -0
- package/esm/templates/hljs.d.mts +16 -0
- package/esm/templates/hljs.mjs +28 -0
- package/esm/templates/prism.d.mts +16 -0
- package/esm/templates/prism.mjs +25 -0
- package/package.json +1 -1
- package/esm/.code-input.mjs.kate-swp +0 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// NOTICE: This code is @generated from code outside the esm directory. Please do not edit it to contribute!
|
|
2
|
+
|
|
3
|
+
import { Plugin, CodeInput } from "../code-input.d.mts";
|
|
4
|
+
/**
|
|
5
|
+
* Add basic Go-To-Line (ctrl-G by default) functionality to the code editor.
|
|
6
|
+
* Files: go-to-line.js / go-to-line.css
|
|
7
|
+
*/
|
|
8
|
+
export default class GoToLine extends Plugin {
|
|
9
|
+
/**
|
|
10
|
+
* Create a go-to-line command plugin to pass into a template
|
|
11
|
+
* @param {boolean} useCtrlG Should Ctrl+G be overriden for go-to-line functionality? Either way, you can trigger it yourself using (instance of this plugin)`.showPrompt(code-input element)`.
|
|
12
|
+
* @param {Object} instructionTranslations: user interface string keys mapped to translated versions for localisation. Look at the go-to-line.js source code for the English text.
|
|
13
|
+
*/
|
|
14
|
+
constructor(useCtrlG?: boolean,
|
|
15
|
+
instructionTranslations?: {
|
|
16
|
+
closeDialog?: string;
|
|
17
|
+
input?: string;
|
|
18
|
+
guidanceFormat?: string;
|
|
19
|
+
guidanceLineRange?: (current:Number, max: Number) => string;
|
|
20
|
+
guidanceColumnRange?: (line: Number, current: Number, max: Number) => string;
|
|
21
|
+
guidanceValidLine?: (line: Number) => string;
|
|
22
|
+
guidanceValidColumn?: (line: Number, column: Number) => string;
|
|
23
|
+
});
|
|
24
|
+
/**
|
|
25
|
+
* Show a search-like dialog prompting line number.
|
|
26
|
+
* @param {CodeInput} codeInput the `<code-input>` element.
|
|
27
|
+
*/
|
|
28
|
+
showPrompt(codeInput: CodeInput): void;
|
|
29
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// NOTICE: This code is @generated from code outside the esm directory. Please do not edit it to contribute!
|
|
2
|
+
|
|
3
|
+
import { Plugin } from "../code-input.mjs";
|
|
4
|
+
const plugins = {};
|
|
5
|
+
/**
|
|
6
|
+
* Add basic Go-To-Line (Ctrl+G by default) functionality to the code editor.
|
|
7
|
+
* Files: go-to-line.js / go-to-line.css
|
|
8
|
+
*/
|
|
9
|
+
"use strict";
|
|
10
|
+
|
|
11
|
+
plugins.GoToLine = class extends Plugin {
|
|
12
|
+
useCtrlG = false;
|
|
13
|
+
|
|
14
|
+
instructions = {
|
|
15
|
+
closeDialog: "Close Dialog and Return to Editor",
|
|
16
|
+
input: "Line:Column / Line no. then Enter",
|
|
17
|
+
guidanceFormat: "Wrong format. Enter a line number (e.g. 1) or a line number then colon then column number (e.g. 1:3).",
|
|
18
|
+
guidanceLineRange: (current, max) => `Line number (currently ${current}) should be between 1 and ${max} inclusive.`,
|
|
19
|
+
guidanceColumnRange: (line, current, max) => `On line ${line}, column number (currently ${current}) should be between 1 and ${max} inclusive.`,
|
|
20
|
+
guidanceValidLine: (line) => `Press Enter to go to line ${line}.`,
|
|
21
|
+
guidanceValidColumn: (line, column) => `Press Enter to go to line ${line}, column ${column}.`,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Create a go-to-line command plugin to pass into a template
|
|
26
|
+
* @param {boolean} useCtrlG Should Ctrl+G be overriden for go-to-line functionality? Either way, you can trigger it yourself using (instance of this plugin)`.showPrompt(code-input element)`.
|
|
27
|
+
* @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.
|
|
28
|
+
*/
|
|
29
|
+
constructor(useCtrlG = true, instructionTranslations = {}) {
|
|
30
|
+
super([]); // No observed attributes
|
|
31
|
+
this.useCtrlG = useCtrlG;
|
|
32
|
+
this.addTranslations(this.instructions, instructionTranslations);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* Add keystroke events */
|
|
36
|
+
afterElementsAdded(codeInput) {
|
|
37
|
+
const textarea = codeInput.textareaElement;
|
|
38
|
+
if(this.useCtrlG) {
|
|
39
|
+
textarea.addEventListener('keydown', (event) => { this.checkCtrlG(codeInput, event); });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* Called with a dialog box keyup event to check the validity of the line number entered and submit the dialog if Enter is pressed */
|
|
44
|
+
checkPrompt(dialog, event) {
|
|
45
|
+
if (event.key == 'Escape') return this.cancelPrompt(dialog, event);
|
|
46
|
+
|
|
47
|
+
// Line number(:column number)
|
|
48
|
+
const lines = dialog.textarea.value.split('\n');
|
|
49
|
+
const maxLineNo = lines.length;
|
|
50
|
+
const lineNo = Number(dialog.input.value.split(':')[0]);
|
|
51
|
+
let columnNo = 0; // Means go to start of indented line
|
|
52
|
+
let maxColumnNo = 1;
|
|
53
|
+
const querySplitByColons = dialog.input.value.split(':');
|
|
54
|
+
|
|
55
|
+
// Invalid format
|
|
56
|
+
if(querySplitByColons.length > 2 || !/^[0-9:]*$/.test(dialog.input.value)) {
|
|
57
|
+
dialog.guidance.textContent = this.instructions.guidanceFormat;
|
|
58
|
+
return dialog.input.classList.add('code-input_go-to-line_error');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Number(s) present
|
|
62
|
+
if (dialog.input.value) {
|
|
63
|
+
if (lineNo < 1 || lineNo > maxLineNo) {
|
|
64
|
+
// Out-of-range line number
|
|
65
|
+
dialog.guidance.textContent = this.instructions.guidanceLineRange(lineNo, maxLineNo);
|
|
66
|
+
return dialog.input.classList.add('code-input_go-to-line_error');
|
|
67
|
+
} else {
|
|
68
|
+
// Check if line:column - if so calculate column number
|
|
69
|
+
if(querySplitByColons.length >= 2) {
|
|
70
|
+
columnNo = Number(querySplitByColons[1]);
|
|
71
|
+
maxColumnNo = lines[lineNo-1].length+1; // column 1 always works since at start of line
|
|
72
|
+
}
|
|
73
|
+
if(columnNo < 0 || columnNo > maxColumnNo) {
|
|
74
|
+
dialog.guidance.textContent = this.instructions.guidanceColumnRange(lineNo, columnNo, maxColumnNo);
|
|
75
|
+
return dialog.input.classList.add('code-input_go-to-line_error');
|
|
76
|
+
} else {
|
|
77
|
+
if(columnNo === 0) {
|
|
78
|
+
// No column specified, or 0 which for backwards compatibility acts
|
|
79
|
+
// like none selected
|
|
80
|
+
dialog.guidance.textContent = this.instructions.guidanceValidLine(lineNo);
|
|
81
|
+
} else {
|
|
82
|
+
dialog.guidance.textContent = this.instructions.guidanceValidColumn(lineNo, columnNo);
|
|
83
|
+
}
|
|
84
|
+
dialog.input.classList.remove('code-input_go-to-line_error');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
// No value
|
|
89
|
+
dialog.guidance.textContent = "";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (event.key == 'Enter') {
|
|
93
|
+
this.goTo(dialog.textarea, lineNo, columnNo);
|
|
94
|
+
this.cancelPrompt(dialog, event);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* Called with a dialog box keyup event to close and clear the dialog box */
|
|
99
|
+
cancelPrompt(dialog, event) {
|
|
100
|
+
event.preventDefault();
|
|
101
|
+
dialog.codeInput.handleEventsFromTextarea = false;
|
|
102
|
+
dialog.textarea.focus();
|
|
103
|
+
dialog.codeInput.handleEventsFromTextarea = true;
|
|
104
|
+
dialog.setAttribute("inert", true); // Hide from keyboard navigation when closed.
|
|
105
|
+
dialog.setAttribute("tabindex", -1); // Hide from keyboard navigation when closed.
|
|
106
|
+
dialog.setAttribute("aria-hidden", true); // Hide from screen reader when closed.
|
|
107
|
+
|
|
108
|
+
// Remove dialog after animation
|
|
109
|
+
dialog.classList.add('code-input_go-to-line_hidden-dialog');
|
|
110
|
+
dialog.input.value = "";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Show a search-like dialog prompting line number.
|
|
115
|
+
* @param {codeInput.CodeInput} codeInput the `<code-input>` element.
|
|
116
|
+
*/
|
|
117
|
+
showPrompt(codeInput) {
|
|
118
|
+
if(codeInput.pluginData.goToLine == undefined || codeInput.pluginData.goToLine.dialog == undefined) {
|
|
119
|
+
const textarea = codeInput.textareaElement;
|
|
120
|
+
|
|
121
|
+
const dialog = document.createElement('div');
|
|
122
|
+
|
|
123
|
+
const input = document.createElement('input');
|
|
124
|
+
|
|
125
|
+
// TODO: Make a button element (semantic HTML for accessibility) in next major version
|
|
126
|
+
const cancel = document.createElement('span');
|
|
127
|
+
cancel.setAttribute("role", "button");
|
|
128
|
+
cancel.setAttribute("aria-label", this.instructions.closeDialog);
|
|
129
|
+
cancel.setAttribute("tabindex", 0); // Visible to keyboard navigation
|
|
130
|
+
cancel.setAttribute("title", this.instructions.closeDialog);
|
|
131
|
+
|
|
132
|
+
const guidance = document.createElement('p');
|
|
133
|
+
guidance.setAttribute("aria-live", "assertive"); // Screen reader must read the status message.
|
|
134
|
+
guidance.textContent = "";
|
|
135
|
+
|
|
136
|
+
dialog.appendChild(input);
|
|
137
|
+
dialog.appendChild(cancel);
|
|
138
|
+
dialog.appendChild(guidance);
|
|
139
|
+
|
|
140
|
+
dialog.className = 'code-input_go-to-line_dialog';
|
|
141
|
+
input.spellcheck = false;
|
|
142
|
+
input.placeholder = this.instructions.input;
|
|
143
|
+
dialog.codeInput = codeInput;
|
|
144
|
+
dialog.textarea = textarea;
|
|
145
|
+
dialog.input = input;
|
|
146
|
+
dialog.guidance = guidance;
|
|
147
|
+
|
|
148
|
+
input.addEventListener('keypress', (event) => {
|
|
149
|
+
/* Stop enter from submitting form */
|
|
150
|
+
if (event.key == 'Enter') event.preventDefault();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
input.addEventListener('keyup', (event) => { return this.checkPrompt(dialog, event); });
|
|
154
|
+
cancel.addEventListener('click', (event) => { this.cancelPrompt(dialog, event); });
|
|
155
|
+
cancel.addEventListener('keypress', (event) => { if (event.key == "Space" || event.key == "Enter") this.cancelPrompt(dialog, event); });
|
|
156
|
+
|
|
157
|
+
codeInput.dialogContainerElement.appendChild(dialog);
|
|
158
|
+
codeInput.pluginData.goToLine = {dialog: dialog};
|
|
159
|
+
input.focus();
|
|
160
|
+
} else {
|
|
161
|
+
codeInput.pluginData.goToLine.dialog.classList.remove("code-input_go-to-line_hidden-dialog");
|
|
162
|
+
codeInput.pluginData.goToLine.dialog.removeAttribute("inert"); // Show to keyboard navigation when open.
|
|
163
|
+
codeInput.pluginData.goToLine.dialog.setAttribute("tabindex", 0); // Show to keyboard navigation when open.
|
|
164
|
+
codeInput.pluginData.goToLine.dialog.removeAttribute("aria-hidden"); // Show to screen reader when open.
|
|
165
|
+
codeInput.pluginData.goToLine.dialog.input.focus();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/* Set the cursor on the first non-space char of textarea's nth line, or to the columnNo-numbered character in the line if it's not 0; and scroll it into view */
|
|
170
|
+
goTo(textarea, lineNo, columnNo = 0) {
|
|
171
|
+
let fontSize;
|
|
172
|
+
let lineHeight;
|
|
173
|
+
let scrollAmount;
|
|
174
|
+
let topPadding;
|
|
175
|
+
let cursorPos = -1;
|
|
176
|
+
let lines = textarea.value.split('\n');
|
|
177
|
+
|
|
178
|
+
if (lineNo > 0 && lineNo <= lines.length) {
|
|
179
|
+
if (textarea.computedStyleMap) {
|
|
180
|
+
fontSize = textarea.computedStyleMap().get('font-size').value;
|
|
181
|
+
lineHeight = fontSize * textarea.computedStyleMap().get('line-height').value;
|
|
182
|
+
} else {
|
|
183
|
+
fontSize = document.defaultView.getComputedStyle(textarea, null).getPropertyValue('font-size').split('px')[0];
|
|
184
|
+
lineHeight = document.defaultView.getComputedStyle(textarea, null).getPropertyValue('line-height').split('px')[0];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// scroll amount and initial top padding (3 lines above, if possible)
|
|
188
|
+
scrollAmount = (lineNo > 3 ? lineNo - 3 : 1) * lineHeight;
|
|
189
|
+
topPadding = (lineHeight - fontSize) / 2;
|
|
190
|
+
|
|
191
|
+
if (lineNo > 1) {
|
|
192
|
+
// cursor positon just after n - 1 full lines
|
|
193
|
+
cursorPos = lines.slice(0, lineNo - 1).join('\n').length;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// scan first non-space char in nth line
|
|
197
|
+
if (columnNo == 0) {
|
|
198
|
+
do cursorPos++; while (textarea.value[cursorPos] != '\n' && /\s/.test(textarea.value[cursorPos]));
|
|
199
|
+
} else {
|
|
200
|
+
cursorPos += 1 + columnNo - 1;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
textarea.scrollTop = scrollAmount - topPadding;
|
|
204
|
+
textarea.setSelectionRange(cursorPos, cursorPos);
|
|
205
|
+
textarea.click();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/* Event handler for keydown event that makes Ctrl+G open go to line dialog */
|
|
210
|
+
checkCtrlG(codeInput, event) {
|
|
211
|
+
if (event.ctrlKey && event.key == 'g') {
|
|
212
|
+
event.preventDefault();
|
|
213
|
+
this.showPrompt(codeInput);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
export default plugins.GoToLine;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// NOTICE: This code is @generated from code outside the esm directory. Please do not edit it to contribute!
|
|
2
|
+
|
|
3
|
+
import { Plugin, CodeInput } from "../code-input.d.mts";
|
|
4
|
+
/**
|
|
5
|
+
* Adds indentation using the `Tab` key, and auto-indents after a newline, as well as making it
|
|
6
|
+
* possible to indent/unindent multiple lines using Tab/Shift+Tab
|
|
7
|
+
* Files: indent.js
|
|
8
|
+
*/
|
|
9
|
+
export default class Indent extends Plugin {
|
|
10
|
+
/**
|
|
11
|
+
* Create an indentation plugin to pass into a template
|
|
12
|
+
* @param {boolean} defaultSpaces Should the Tab key enter spaces rather than tabs? Defaults to false.
|
|
13
|
+
* @param {Number} numSpaces How many spaces is each tab character worth? Defaults to 4.
|
|
14
|
+
* @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.
|
|
15
|
+
* @param {boolean} escTabToChangeFocus Whether pressing the Escape key before (Shift+)Tab should make this keypress focus on a different element (Tab's default behaviour). You should always either enable this or use this plugin's disableTabIndentation and enableTabIndentation methods linked to other keyboard shortcuts, for accessibility.
|
|
16
|
+
* @param {Object} instructionTranslations: user interface string keys mapped to translated versions for localisation. Look at the go-to-line.js source code for the English text.
|
|
17
|
+
*/
|
|
18
|
+
constructor(defaultSpaces?: boolean, numSpaces?: Number, bracketPairs?: Object, escTabToChangeFocus?: boolean, instructionTwranslations?: {
|
|
19
|
+
tabForIndentation?: string;
|
|
20
|
+
tabForNavigation?: string;
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
// NOTICE: This code is @generated from code outside the esm directory. Please do not edit it to contribute!
|
|
2
|
+
|
|
3
|
+
import { Plugin } from "../code-input.mjs";
|
|
4
|
+
const plugins = {};
|
|
5
|
+
/**
|
|
6
|
+
* Add indentation using the `Tab` key, and auto-indents after a newline, as well as making it
|
|
7
|
+
* possible to indent/unindent multiple lines using Tab/Shift+Tab
|
|
8
|
+
* Files: indent.js
|
|
9
|
+
*/
|
|
10
|
+
"use strict";
|
|
11
|
+
|
|
12
|
+
plugins.Indent = class extends Plugin {
|
|
13
|
+
|
|
14
|
+
bracketPairs = {}; // No bracket-auto-indentation used when {}
|
|
15
|
+
indentation = "\t";
|
|
16
|
+
indentationNumChars = 1;
|
|
17
|
+
tabIndentationEnabled = true; // Can be disabled for accessibility reasons to allow keyboard navigation
|
|
18
|
+
escTabToChangeFocus = true;
|
|
19
|
+
escJustPressed = false; // Becomes true when Escape key is pressed and false when another key is pressed
|
|
20
|
+
|
|
21
|
+
instructions = {
|
|
22
|
+
tabForIndentation: "Tab and Shift-Tab currently for indentation. Press Esc to enable keyboard navigation.",
|
|
23
|
+
tabForNavigation: "Tab and Shift-Tab currently for keyboard navigation. Type to return to indentation.",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Create an indentation plugin to pass into a template
|
|
28
|
+
* @param {boolean} defaultSpaces Should the Tab key enter spaces rather than tabs? Defaults to false.
|
|
29
|
+
* @param {Number} numSpaces How many spaces is each tab character worth? Defaults to 4.
|
|
30
|
+
* @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.
|
|
31
|
+
* @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.
|
|
32
|
+
* @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.
|
|
33
|
+
*/
|
|
34
|
+
constructor(defaultSpaces=false, numSpaces=4, bracketPairs={"(": ")", "[": "]", "{": "}"}, escTabToChangeFocus=true, instructionTranslations = {}) {
|
|
35
|
+
super([]); // No observed attributes
|
|
36
|
+
|
|
37
|
+
this.bracketPairs = bracketPairs;
|
|
38
|
+
if(defaultSpaces) {
|
|
39
|
+
this.indentation = "";
|
|
40
|
+
for(let i = 0; i < numSpaces; i++) {
|
|
41
|
+
this.indentation += " ";
|
|
42
|
+
}
|
|
43
|
+
this.indentationNumChars = numSpaces;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
this.escTabToChangeFocus = true;
|
|
47
|
+
|
|
48
|
+
this.addTranslations(this.instructions, instructionTranslations);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Make the Tab key
|
|
53
|
+
*/
|
|
54
|
+
disableTabIndentation() {
|
|
55
|
+
this.tabIndentationEnabled = false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
enableTabIndentation() {
|
|
59
|
+
this.tabIndentationEnabled = true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* Add keystroke events, and get the width of the indentation in pixels. */
|
|
63
|
+
afterElementsAdded(codeInput) {
|
|
64
|
+
|
|
65
|
+
let textarea = codeInput.textareaElement;
|
|
66
|
+
textarea.addEventListener('focus', (event) => { if(this.escTabToChangeFocus) codeInput.setKeyboardNavInstructions(this.instructions.tabForIndentation, true); })
|
|
67
|
+
textarea.addEventListener('keydown', (event) => { this.checkTab(codeInput, event); this.checkEnter(codeInput, event); this.checkBackspace(codeInput, event); });
|
|
68
|
+
textarea.addEventListener('beforeinput', (event) => { this.checkCloseBracket(codeInput, event); });
|
|
69
|
+
|
|
70
|
+
// Get the width of the indentation in pixels
|
|
71
|
+
let testIndentationWidthPre = document.createElement("pre");
|
|
72
|
+
testIndentationWidthPre.setAttribute("aria-hidden", "true"); // Hide for screen readers
|
|
73
|
+
let testIndentationWidthSpan = document.createElement("span");
|
|
74
|
+
if(codeInput.templateObject.preElementStyled) {
|
|
75
|
+
testIndentationWidthPre.appendChild(testIndentationWidthSpan);
|
|
76
|
+
testIndentationWidthPre.classList.add("code-input_autocomplete_test-indentation-width");
|
|
77
|
+
codeInput.appendChild(testIndentationWidthPre); // Styled like first pre, but first pre found to update
|
|
78
|
+
} else {
|
|
79
|
+
let testIndentationWidthCode = document.createElement("code");
|
|
80
|
+
testIndentationWidthCode.appendChild(testIndentationWidthSpan);
|
|
81
|
+
testIndentationWidthCode.classList.add("code-input_autocomplete_test-indentation-width");
|
|
82
|
+
testIndentationWidthPre.appendChild(testIndentationWidthCode);
|
|
83
|
+
codeInput.appendChild(testIndentationWidthPre); // Styled like first pre, but first pre found to update
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
testIndentationWidthSpan.innerHTML = codeInput.escapeHtml(this.indentation);
|
|
87
|
+
let indentationWidthPx = testIndentationWidthSpan.offsetWidth;
|
|
88
|
+
codeInput.removeChild(testIndentationWidthPre);
|
|
89
|
+
|
|
90
|
+
codeInput.pluginData.indent = {automatedKeypresses: false, indentationWidthPx: indentationWidthPx};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* 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). */
|
|
94
|
+
checkTab(codeInput, event) {
|
|
95
|
+
if(codeInput.pluginData.indent.automatedKeypresses) return;
|
|
96
|
+
if(!this.tabIndentationEnabled) return;
|
|
97
|
+
if(this.escTabToChangeFocus) {
|
|
98
|
+
// Accessibility - allow Tab for keyboard navigation when Esc pressed right before it.
|
|
99
|
+
if(event.key == "Escape") {
|
|
100
|
+
this.escJustPressed = true;
|
|
101
|
+
codeInput.setKeyboardNavInstructions(this.instructions.tabForNavigation, false);
|
|
102
|
+
return;
|
|
103
|
+
} else if(event.key != "Tab") {
|
|
104
|
+
if(event.key == "Shift") {
|
|
105
|
+
return; // Shift+Tab after Esc should still be keyboard navigation
|
|
106
|
+
}
|
|
107
|
+
codeInput.setKeyboardNavInstructions(this.instructions.tabForIndentation, false);
|
|
108
|
+
this.escJustPressed = false;
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if(!this.enableTabIndentation || this.escJustPressed) {
|
|
113
|
+
codeInput.setKeyboardNavInstructions("", false);
|
|
114
|
+
this.escJustPressed = false;
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
} else if(event.key != "Tab") {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let inputElement = codeInput.textareaElement;
|
|
122
|
+
event.preventDefault(); // stop normal
|
|
123
|
+
|
|
124
|
+
if(!event.shiftKey && inputElement.selectionStart == inputElement.selectionEnd) {
|
|
125
|
+
// Just place a tab/spaces here.
|
|
126
|
+
// automatedKeypresses property to prevent keypresses being captured
|
|
127
|
+
// by this plugin during automated input as some browsers
|
|
128
|
+
// (e.g. GNOME Web) do.
|
|
129
|
+
codeInput.pluginData.indent.automatedKeypresses = true;
|
|
130
|
+
document.execCommand("insertText", false, this.indentation);
|
|
131
|
+
codeInput.pluginData.indent.automatedKeypresses = false;
|
|
132
|
+
|
|
133
|
+
} else {
|
|
134
|
+
let lines = inputElement.value.split("\n");
|
|
135
|
+
let letterI = 0;
|
|
136
|
+
|
|
137
|
+
let selectionStartI = inputElement.selectionStart; // where cursor moves after tab - moving forward by 1 indent
|
|
138
|
+
let selectionEndI = inputElement.selectionEnd; // where cursor moves after tab - moving forward by 1 indent
|
|
139
|
+
|
|
140
|
+
for (let i = 0; i < lines.length; i++) {
|
|
141
|
+
if((selectionStartI <= letterI+lines[i].length && selectionEndI >= letterI + 1)
|
|
142
|
+
|| (selectionStartI == selectionEndI && selectionStartI <= letterI+lines[i].length+1 && selectionEndI >= letterI)) { // + 1 so newlines counted
|
|
143
|
+
// Starts before or at last char and ends after or at first char
|
|
144
|
+
if(event.shiftKey) {
|
|
145
|
+
if(lines[i].substring(0, this.indentationNumChars) == this.indentation) {
|
|
146
|
+
// Remove first indent
|
|
147
|
+
inputElement.selectionStart = letterI;
|
|
148
|
+
inputElement.selectionEnd = letterI+this.indentationNumChars;
|
|
149
|
+
document.execCommand("delete", false, "");
|
|
150
|
+
|
|
151
|
+
// Change selection
|
|
152
|
+
if(selectionStartI > letterI) { // Indented outside selection
|
|
153
|
+
selectionStartI = Math.max(selectionStartI - this.indentationNumChars, letterI); // Don't move to before indent
|
|
154
|
+
}
|
|
155
|
+
selectionEndI -= this.indentationNumChars;
|
|
156
|
+
letterI -= this.indentationNumChars;
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
// Add tab at start
|
|
160
|
+
inputElement.selectionStart = letterI;
|
|
161
|
+
inputElement.selectionEnd = letterI;
|
|
162
|
+
// automatedKeypresses property to prevent keypresses being captured
|
|
163
|
+
// by this plugin during automated input as some browsers
|
|
164
|
+
// (e.g. GNOME Web) do.
|
|
165
|
+
codeInput.pluginData.indent.f = true;
|
|
166
|
+
document.execCommand("insertText", false, this.indentation);
|
|
167
|
+
codeInput.pluginData.indent.automatedKeypresses = false;
|
|
168
|
+
|
|
169
|
+
// Change selection
|
|
170
|
+
if(selectionStartI > letterI) { // Indented outside selection
|
|
171
|
+
selectionStartI += this.indentationNumChars;
|
|
172
|
+
}
|
|
173
|
+
selectionEndI += this.indentationNumChars;
|
|
174
|
+
letterI += this.indentationNumChars;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
letterI += lines[i].length+1; // newline counted
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// move cursor
|
|
182
|
+
inputElement.selectionStart = selectionStartI;
|
|
183
|
+
inputElement.selectionEnd = selectionEndI;
|
|
184
|
+
|
|
185
|
+
// move scroll position to follow code
|
|
186
|
+
const textDirection = getComputedStyle(codeInput).direction;
|
|
187
|
+
if(textDirection == "rtl") {
|
|
188
|
+
if(event.shiftKey) {
|
|
189
|
+
// Scroll right
|
|
190
|
+
codeInput.scrollBy(codeInput.pluginData.indent.indentationWidthPx, 0);
|
|
191
|
+
} else {
|
|
192
|
+
// Scroll left
|
|
193
|
+
codeInput.scrollBy(-codeInput.pluginData.indent.indentationWidthPx, 0);
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
if(event.shiftKey) {
|
|
197
|
+
// Scroll left
|
|
198
|
+
codeInput.scrollBy(-codeInput.pluginData.indent.indentationWidthPx, 0);
|
|
199
|
+
} else {
|
|
200
|
+
// Scroll right
|
|
201
|
+
codeInput.scrollBy(codeInput.pluginData.indent.indentationWidthPx, 0);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
codeInput.value = inputElement.value;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/* Deal with new lines retaining indentation */
|
|
210
|
+
checkEnter(codeInput, event) {
|
|
211
|
+
if(codeInput.pluginData.indent.automatedKeypresses) return;
|
|
212
|
+
if(event.key != "Enter") {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
event.preventDefault(); // Stop normal \n only
|
|
216
|
+
|
|
217
|
+
let inputElement = codeInput.textareaElement;
|
|
218
|
+
let lines = inputElement.value.split("\n");
|
|
219
|
+
let letterI = 0;
|
|
220
|
+
let currentLineI = lines.length - 1;
|
|
221
|
+
let newLine = "";
|
|
222
|
+
let numberIndents = 0;
|
|
223
|
+
|
|
224
|
+
// find the index of the line our cursor is currently on
|
|
225
|
+
for (let i = 0; i < lines.length; i++) {
|
|
226
|
+
letterI += lines[i].length + 1;
|
|
227
|
+
if(inputElement.selectionEnd < letterI) {
|
|
228
|
+
currentLineI = i;
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// count the number of indents the current line starts with (up to our cursor position in the line)
|
|
234
|
+
let cursorPosInLine = lines[currentLineI].length - (letterI - inputElement.selectionEnd) + 1;
|
|
235
|
+
for (let i = 0; i < cursorPosInLine; i += this.indentationNumChars) {
|
|
236
|
+
if (lines[currentLineI].substring(i, i+this.indentationNumChars) == this.indentation) {
|
|
237
|
+
numberIndents++;
|
|
238
|
+
} else {
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// determine the text before and after the cursor and chop the current line at the new line break
|
|
244
|
+
let textAfterCursor = "";
|
|
245
|
+
if (cursorPosInLine != lines[currentLineI].length) {
|
|
246
|
+
textAfterCursor = lines[currentLineI].substring(cursorPosInLine);
|
|
247
|
+
lines[currentLineI] = lines[currentLineI].substring(0, cursorPosInLine);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
let bracketThreeLinesTriggered = false;
|
|
251
|
+
let furtherIndentation = "";
|
|
252
|
+
if(this.bracketPairs != null) {
|
|
253
|
+
for(let openingBracket in this.bracketPairs) {
|
|
254
|
+
if(lines[currentLineI][lines[currentLineI].length-1] == openingBracket) {
|
|
255
|
+
let closingBracket = this.bracketPairs[openingBracket];
|
|
256
|
+
if(textAfterCursor.length > 0 && textAfterCursor[0] == closingBracket) {
|
|
257
|
+
// Create new line and then put textAfterCursor on yet another line:
|
|
258
|
+
// {
|
|
259
|
+
// |CARET|
|
|
260
|
+
// }
|
|
261
|
+
bracketThreeLinesTriggered = true;
|
|
262
|
+
for (let i = 0; i < numberIndents+1; i++) {
|
|
263
|
+
furtherIndentation += this.indentation;
|
|
264
|
+
}
|
|
265
|
+
} else {
|
|
266
|
+
// Just create new line:
|
|
267
|
+
// {
|
|
268
|
+
// |CARET|
|
|
269
|
+
numberIndents++;
|
|
270
|
+
}
|
|
271
|
+
break;
|
|
272
|
+
} else {
|
|
273
|
+
// Check whether brackets cause unindent
|
|
274
|
+
let closingBracket = this.bracketPairs[openingBracket];
|
|
275
|
+
if(textAfterCursor.length > 0 && textAfterCursor[0] == closingBracket) {
|
|
276
|
+
numberIndents--;
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// insert our indents and any text from the previous line that might have been after the line break
|
|
284
|
+
// negative indents shouldn't exist and would only break future calculations.
|
|
285
|
+
if(numberIndents < 0) {
|
|
286
|
+
numberIndents = 0;
|
|
287
|
+
}
|
|
288
|
+
for (let i = 0; i < numberIndents; i++) {
|
|
289
|
+
newLine += this.indentation;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// save the current cursor position
|
|
293
|
+
let selectionStartI = inputElement.selectionStart;
|
|
294
|
+
|
|
295
|
+
// automatedKeypresses property to prevent keypresses being captured
|
|
296
|
+
// by this plugin during automated input as some browsers
|
|
297
|
+
// (e.g. GNOME Web) do.
|
|
298
|
+
codeInput.pluginData.indent.automatedKeypresses = true;
|
|
299
|
+
if(bracketThreeLinesTriggered) {
|
|
300
|
+
document.execCommand("insertText", false, "\n" + furtherIndentation); // Write indented line
|
|
301
|
+
numberIndents += 1; // Reflects the new indent
|
|
302
|
+
}
|
|
303
|
+
document.execCommand("insertText", false, "\n" + newLine); // Write new line, including auto-indentation
|
|
304
|
+
codeInput.pluginData.indent.automatedKeypresses = false;
|
|
305
|
+
|
|
306
|
+
// move cursor to new position
|
|
307
|
+
inputElement.selectionStart = selectionStartI + numberIndents*this.indentationNumChars + 1; // count the indent level and the newline character
|
|
308
|
+
inputElement.selectionEnd = inputElement.selectionStart;
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
// Scroll down to cursor if necessary
|
|
312
|
+
let paddingTop = Number(getComputedStyle(inputElement).paddingTop.replace("px", ""));
|
|
313
|
+
let lineHeight = Number(getComputedStyle(inputElement).lineHeight.replace("px", ""));
|
|
314
|
+
let inputHeight = Number(getComputedStyle(codeInput).height.replace("px", ""));
|
|
315
|
+
if(currentLineI*lineHeight + lineHeight*2 + paddingTop >= inputElement.scrollTop + inputHeight) { // Cursor too far down
|
|
316
|
+
codeInput.scrollBy(0, Number(getComputedStyle(inputElement).lineHeight.replace("px", "")));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
codeInput.value = inputElement.value;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/* Deal with one 'tab' of spaces-based-indentation being deleted by each backspace, rather than one space */
|
|
323
|
+
checkBackspace(codeInput, event) {
|
|
324
|
+
if(codeInput.pluginData.indent.automatedKeypresses) return;
|
|
325
|
+
if(event.key != "Backspace" || this.indentationNumChars == 1) {
|
|
326
|
+
return; // Normal backspace when indentation of 1
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
let inputElement = codeInput.textareaElement;
|
|
330
|
+
|
|
331
|
+
if(inputElement.selectionStart == inputElement.selectionEnd && codeInput.value.substring(inputElement.selectionStart - this.indentationNumChars, inputElement.selectionStart) == this.indentation) {
|
|
332
|
+
// Indentation before cursor = delete it
|
|
333
|
+
inputElement.selectionStart -= this.indentationNumChars;
|
|
334
|
+
event.preventDefault();
|
|
335
|
+
document.execCommand("delete", false, "");
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/* Deal with the typing of closing brackets causing a decrease in indentation */
|
|
340
|
+
checkCloseBracket(codeInput, event) {
|
|
341
|
+
if(codeInput.textareaElement.selectionStart != codeInput.textareaElement.selectionEnd) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
for(let openingBracket in this.bracketPairs) {
|
|
346
|
+
let closingBracket = this.bracketPairs[openingBracket];
|
|
347
|
+
if(event.data == closingBracket) {
|
|
348
|
+
// Closing bracket unindents line
|
|
349
|
+
if(codeInput.value.substring(codeInput.textareaElement.selectionStart - this.indentationNumChars, codeInput.textareaElement.selectionStart) == this.indentation) {
|
|
350
|
+
// Indentation before cursor = delete it
|
|
351
|
+
codeInput.textareaElement.selectionStart -= this.indentationNumChars;
|
|
352
|
+
// document.execCommand("delete", false, "");
|
|
353
|
+
// event.preventDefault();
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
export default plugins.Indent;
|