@webcoder49/code-input 2.5.1 → 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.
Files changed (93) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +9 -126
  3. package/code-input.css +70 -33
  4. package/code-input.d.ts +135 -59
  5. package/code-input.js +201 -110
  6. package/code-input.min.css +1 -1
  7. package/code-input.min.js +12 -1
  8. package/docs/LICENSE +3 -0
  9. package/docs/LICENSE.CC-BY-SA-4.0 +116 -0
  10. package/docs/LICENSE.CC0-1.0 +30 -0
  11. package/docs/README.md +5 -0
  12. package/docs/_index.md +308 -0
  13. package/docs/i18n/_index.md +52 -0
  14. package/docs/interface/_index.md +3 -0
  15. package/docs/interface/css/_index.md +12 -0
  16. package/docs/interface/forms/_index.md +17 -0
  17. package/docs/interface/js/_index.md +11 -0
  18. package/docs/modules-and-frameworks/_index.md +3 -0
  19. package/docs/modules-and-frameworks/custom/_index.md +9 -0
  20. package/docs/modules-and-frameworks/hljs/_index.md +13 -0
  21. package/docs/modules-and-frameworks/hljs/esm/_index.md +71 -0
  22. package/docs/modules-and-frameworks/hljs/nuxt/_index.md +250 -0
  23. package/docs/modules-and-frameworks/hljs/nuxt/nuxt-demo-screenshot.png +0 -0
  24. package/docs/modules-and-frameworks/hljs/vue/_index.md +233 -0
  25. package/docs/modules-and-frameworks/hljs/vue/vue-demo-screenshot.png +0 -0
  26. package/docs/modules-and-frameworks/prism/_index.md +14 -0
  27. package/docs/plugins/_index.md +676 -0
  28. package/docs/plugins/new/_index.md +52 -0
  29. package/docs/theory/_index.md +9 -0
  30. package/esm/README.md +23 -0
  31. package/esm/code-input.d.mts +154 -0
  32. package/esm/code-input.mjs +997 -0
  33. package/esm/plugins/auto-close-brackets.d.mts +15 -0
  34. package/esm/plugins/auto-close-brackets.mjs +84 -0
  35. package/esm/plugins/autocomplete.d.mts +14 -0
  36. package/esm/plugins/autocomplete.mjs +93 -0
  37. package/esm/plugins/autodetect.d.mts +11 -0
  38. package/esm/plugins/autodetect.mjs +35 -0
  39. package/esm/plugins/find-and-replace.d.mts +43 -0
  40. package/esm/plugins/find-and-replace.mjs +777 -0
  41. package/esm/plugins/go-to-line.d.mts +29 -0
  42. package/esm/plugins/go-to-line.mjs +217 -0
  43. package/esm/plugins/indent.d.mts +22 -0
  44. package/esm/plugins/indent.mjs +359 -0
  45. package/esm/plugins/select-token-callbacks.d.mts +51 -0
  46. package/esm/plugins/select-token-callbacks.mjs +296 -0
  47. package/esm/plugins/special-chars.d.mts +25 -0
  48. package/esm/plugins/special-chars.mjs +207 -0
  49. package/esm/plugins/test.d.mts +16 -0
  50. package/esm/plugins/test.mjs +56 -0
  51. package/esm/templates/hljs.d.mts +16 -0
  52. package/esm/templates/hljs.mjs +28 -0
  53. package/esm/templates/prism.d.mts +16 -0
  54. package/esm/templates/prism.mjs +25 -0
  55. package/package.json +83 -7
  56. package/plugins/README.md +2 -0
  57. package/plugins/auto-close-brackets.js +2 -0
  58. package/plugins/auto-close-brackets.min.js +1 -1
  59. package/plugins/autocomplete.js +6 -6
  60. package/plugins/autocomplete.min.js +1 -1
  61. package/plugins/autodetect.js +4 -2
  62. package/plugins/autodetect.min.js +1 -1
  63. package/plugins/find-and-replace.css +0 -4
  64. package/plugins/find-and-replace.js +28 -8
  65. package/plugins/find-and-replace.min.css +1 -1
  66. package/plugins/find-and-replace.min.js +1 -1
  67. package/plugins/go-to-line.css +10 -5
  68. package/plugins/go-to-line.js +39 -6
  69. package/plugins/go-to-line.min.css +1 -1
  70. package/plugins/go-to-line.min.js +1 -1
  71. package/plugins/indent.js +4 -2
  72. package/plugins/indent.min.js +1 -1
  73. package/plugins/prism-line-numbers.css +14 -5
  74. package/plugins/prism-line-numbers.min.css +1 -1
  75. package/plugins/select-token-callbacks.js +3 -1
  76. package/plugins/select-token-callbacks.min.js +1 -1
  77. package/plugins/special-chars.css +13 -1
  78. package/plugins/special-chars.js +14 -4
  79. package/plugins/special-chars.min.css +1 -1
  80. package/plugins/special-chars.min.js +1 -1
  81. package/plugins/test.js +22 -7
  82. package/plugins/test.min.js +1 -1
  83. package/.github/workflows/minify.yml +0 -22
  84. package/.github/workflows/npm-publish.yml +0 -21
  85. package/CODE_OF_CONDUCT.md +0 -130
  86. package/CONTRIBUTING.md +0 -35
  87. package/tests/hljs.html +0 -55
  88. package/tests/i18n.html +0 -197
  89. package/tests/prism-match-braces-compatibility.js +0 -215
  90. package/tests/prism-match-braces-compatibility.min.js +0 -1
  91. package/tests/prism.html +0 -54
  92. package/tests/tester.js +0 -600
  93. package/tests/tester.min.js +0 -21
@@ -0,0 +1,777 @@
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 Find-and-Replace (Ctrl+F for find, Ctrl+H for replace by default) functionality to the code editor.
7
+ * Files: find-and-replace.js / find-and-replace.css
8
+ */
9
+ "use strict";
10
+
11
+ plugins.FindAndReplace = class extends Plugin {
12
+ useCtrlF = false;
13
+ useCtrlH = false;
14
+
15
+ findMatchesOnValueChange = true; // Needed so the program can insert text to the find value and thus add it to Ctrl+Z without highlighting matches.
16
+
17
+ instructions = {
18
+ start: "Search for matches in your code.",
19
+ none: "No matches",
20
+ oneFound: "1 match found.",
21
+ matchIndex: (index, count) => `${index} of ${count} matches.`,
22
+ error: (message) => `Error: ${message}`,
23
+ infiniteLoopError: "Causes an infinite loop",
24
+ closeDialog: "Close Dialog and Return to Editor",
25
+ findPlaceholder: "Find",
26
+ findCaseSensitive: "Match Case Sensitive",
27
+ findRegExp: "Use JavaScript Regular Expression",
28
+ replaceTitle: "Replace",
29
+ replacePlaceholder: "Replace with",
30
+ findNext: "Find Next Occurrence",
31
+ findPrevious: "Find Previous Occurrence",
32
+ replaceActionShort: "Replace",
33
+ replaceAction: "Replace This Occurrence",
34
+ replaceAllActionShort: "Replace All",
35
+ replaceAllAction: "Replace All Occurrences"
36
+ };
37
+
38
+ /**
39
+ * Create a find-and-replace command plugin to pass into a template
40
+ * @param {boolean} useCtrlF Should Ctrl+F be overriden for find-and-replace find functionality? Either way, you can also trigger it yourself using (instance of this plugin)`.showPrompt(code-input element, false)`.
41
+ * @param {boolean} useCtrlH Should Ctrl+H be overriden for find-and-replace replace functionality? Either way, you can also trigger it yourself using (instance of this plugin)`.showPrompt(code-input element, true)`.
42
+ * @param {Object} instructionTranslations: user interface string keys mapped to translated versions for localisation. Look at the find-and-replace.js source code for the English text and available keys.
43
+ */
44
+ constructor(useCtrlF = true, useCtrlH = true, instructionTranslations = {}) {
45
+ super([]); // No observed attributes
46
+ this.useCtrlF = useCtrlF;
47
+ this.useCtrlH = useCtrlH;
48
+ this.addTranslations(this.instructions, instructionTranslations);
49
+ }
50
+
51
+ /* Add keystroke events */
52
+ afterElementsAdded(codeInput) {
53
+ const textarea = codeInput.textareaElement;
54
+ if(this.useCtrlF) {
55
+ textarea.addEventListener('keydown', (event) => { this.checkCtrlF(codeInput, event); });
56
+ }
57
+ if(this.useCtrlH) {
58
+ textarea.addEventListener('keydown', (event) => { this.checkCtrlH(codeInput, event); });
59
+ }
60
+ }
61
+
62
+ /* After highlight, retry match highlighting */
63
+ afterHighlight(codeInput) {
64
+ if(codeInput.pluginData.findAndReplace != undefined && codeInput.pluginData.findAndReplace.dialog != undefined) {
65
+ if(!codeInput.pluginData.findAndReplace.dialog.classList.contains("code-input_find-and-replace_hidden-dialog")) {
66
+ // Code updated and dialog open - re-highlight find matches
67
+ codeInput.pluginData.findAndReplace.dialog.findMatchState.rehighlightMatches();
68
+ this.updateMatchDescription(codeInput.pluginData.findAndReplace.dialog);
69
+
70
+ if(codeInput.pluginData.findAndReplace.dialog.findMatchState.numMatches == 0) {
71
+ // No more matches after editing
72
+ codeInput.pluginData.findAndReplace.dialog.findInput.classList.add('code-input_find-and-replace_error');
73
+ }
74
+ }
75
+ }
76
+ }
77
+
78
+ /* Get a Regular Expression to match a specific piece of text, by escaping the text: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions */
79
+ text2RegExp(string, caseSensitive, stringIsRegexp) {
80
+ // "i" flag means case-"i"nsensitive
81
+ return new RegExp(stringIsRegexp ? string : string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), caseSensitive ? "g" : "gi"); // $& means the whole matched string
82
+ }
83
+
84
+ /* Update the dialog description to show the find matches */
85
+ updateMatchDescription(dialog) {
86
+ // 1-indexed
87
+ if(dialog.findInput.value.length == 0) {
88
+ dialog.matchDescription.textContent = this.instructions.start;
89
+ } else if(dialog.findMatchState.numMatches <= 0) {
90
+ dialog.matchDescription.textContent = this.instructions.none;
91
+ } else if(dialog.findMatchState.numMatches == 1) {
92
+ dialog.matchDescription.textContent = this.instructions.oneFound;
93
+ } else {
94
+ dialog.matchDescription.textContent = this.instructions.matchIndex(dialog.findMatchState.focusedMatchID+1, dialog.findMatchState.numMatches);
95
+ }
96
+ }
97
+
98
+ /* Called with a find input keyup event to find all matches in the code and focus the next match if Enter is pressed */
99
+ updateFindMatches(dialog) {
100
+ // Update matches for find functionality; debounce it to prevent delay with single-character search
101
+ let oldValue = dialog.findInput.value;
102
+ setTimeout(() => {
103
+ if(oldValue == dialog.findInput.value) {
104
+ // Stopped typing
105
+ dialog.findMatchState.clearMatches();
106
+ if(oldValue.length > 0) {
107
+ try {
108
+ dialog.findMatchState.updateMatches(this.text2RegExp(dialog.findInput.value, dialog.findCaseSensitiveCheckbox.checked, dialog.findRegExpCheckbox.checked));
109
+ } catch (err) {
110
+ if(err instanceof SyntaxError) {
111
+ // Syntax error due to malformed RegExp
112
+ dialog.findInput.classList.add('code-input_find-and-replace_error');
113
+ // Only show last part of error message
114
+ let messageParts = err.message.split(": ");
115
+ dialog.matchDescription.textContent = this.instructions.error(messageParts[messageParts.length-1]); // Show only last part of error.
116
+ return;
117
+ } else {
118
+ throw err;
119
+ }
120
+ }
121
+
122
+ if(dialog.findMatchState.numMatches > 0) {
123
+ dialog.findInput.classList.remove('code-input_find-and-replace_error');
124
+ } else {
125
+ // No matches - error
126
+ dialog.findInput.classList.add('code-input_find-and-replace_error');
127
+ }
128
+ }
129
+ this.updateMatchDescription(dialog);
130
+ }
131
+ }, 100);
132
+ }
133
+
134
+ /* Deal with Enter being pressed in the find field */
135
+ checkFindPrompt(dialog, codeInput, event) {
136
+ if (event.key == 'Enter') {
137
+ // Find next match
138
+ dialog.findMatchState.nextMatch();
139
+ this.updateMatchDescription(dialog);
140
+ }
141
+ }
142
+
143
+ /* Deal with Enter being pressed in the replace field */
144
+ checkReplacePrompt(dialog, codeInput, event) {
145
+ if (event.key == 'Enter') {
146
+ // Replace focused match
147
+ dialog.findMatchState.replaceOnce(dialog.replaceInput.value);
148
+ dialog.replaceInput.focus();
149
+ this.updateMatchDescription(dialog);
150
+ }
151
+ }
152
+
153
+ /* Called with a dialog box keyup event to close and clear the dialog box */
154
+ cancelPrompt(dialog, codeInput, event) {
155
+ event.preventDefault();
156
+
157
+ // Add current value of find/replace to Ctrl+Z stack.
158
+ this.findMatchesOnValueChange = false;
159
+ dialog.findInput.focus();
160
+ dialog.findInput.selectionStart = 0;
161
+ dialog.findInput.selectionEnd = dialog.findInput.value.length;
162
+ document.execCommand("insertText", false, dialog.findInput.value);
163
+ this.findMatchesOnValueChange = true;
164
+
165
+ // Reset original selection in code-input
166
+ dialog.textarea.focus();
167
+ dialog.setAttribute("inert", true); // Hide from keyboard navigation when closed.
168
+ dialog.setAttribute("tabindex", -1); // Hide from keyboard navigation when closed.
169
+ dialog.setAttribute("aria-hidden", true); // Hide from screen reader when closed.
170
+
171
+ if(dialog.findMatchState.numMatches > 0) {
172
+ // Select focused match
173
+ codeInput.textareaElement.selectionStart = dialog.findMatchState.matchStartIndexes[dialog.findMatchState.focusedMatchID];
174
+ codeInput.textareaElement.selectionEnd = dialog.findMatchState.matchEndIndexes[dialog.findMatchState.focusedMatchID];
175
+ } else {
176
+ // Select text selected before
177
+ codeInput.textareaElement.selectionStart = dialog.selectionStart;
178
+ codeInput.textareaElement.selectionEnd = dialog.selectionEnd;
179
+ }
180
+
181
+ dialog.findMatchState.clearMatches();
182
+
183
+ // Remove dialog after animation
184
+ dialog.classList.add('code-input_find-and-replace_hidden-dialog');
185
+ }
186
+
187
+ /**
188
+ * Show a find-and-replace dialog.
189
+ * @param {codeInput.CodeInput} codeInputElement the `<code-input>` element.
190
+ * @param {boolean} replacePartExpanded whether the replace part of the find-and-replace dialog should be expanded
191
+ */
192
+ showPrompt(codeInputElement, replacePartExpanded) {
193
+ let dialog;
194
+ if(codeInputElement.pluginData.findAndReplace == undefined || codeInputElement.pluginData.findAndReplace.dialog == undefined) {
195
+ const textarea = codeInputElement.textareaElement;
196
+
197
+ dialog = document.createElement('div');
198
+ const findInput = document.createElement('input');
199
+ const findCaseSensitiveCheckbox = document.createElement('input');
200
+ const findRegExpCheckbox = document.createElement('input');
201
+ // TODO in next major version: use more semantic HTML element than code
202
+ const matchDescription = document.createElement('code');
203
+ matchDescription.setAttribute("aria-live", "assertive"); // Screen reader must read the number of matches found.
204
+
205
+ const replaceInput = document.createElement('input');
206
+ const replaceDropdown = document.createElement('details');
207
+ const replaceSummary = document.createElement('summary');
208
+
209
+ const buttonContainer = document.createElement('div');
210
+ const findNextButton = document.createElement('button');
211
+ const findPreviousButton = document.createElement('button');
212
+ const replaceButton = document.createElement('button');
213
+ const replaceAllButton = document.createElement('button');
214
+
215
+ // TODO: Make a button element (semantic HTML for accessibility) in next major version
216
+ const cancel = document.createElement('span');
217
+ cancel.setAttribute("role", "button");
218
+ cancel.setAttribute("aria-label", this.instructions.closeDialog);
219
+ cancel.setAttribute("tabindex", 0); // Visible to keyboard navigation
220
+ cancel.setAttribute("title", this.instructions.closeDialog);
221
+
222
+ buttonContainer.appendChild(findNextButton);
223
+ buttonContainer.appendChild(findPreviousButton);
224
+ buttonContainer.appendChild(replaceButton);
225
+ buttonContainer.appendChild(replaceAllButton);
226
+ buttonContainer.appendChild(cancel);
227
+ dialog.appendChild(buttonContainer);
228
+
229
+ dialog.appendChild(findInput);
230
+ dialog.appendChild(findRegExpCheckbox);
231
+ dialog.appendChild(findCaseSensitiveCheckbox);
232
+ dialog.appendChild(matchDescription);
233
+ replaceDropdown.appendChild(replaceSummary);
234
+ replaceDropdown.appendChild(replaceInput);
235
+
236
+ dialog.appendChild(replaceDropdown);
237
+
238
+ dialog.className = 'code-input_find-and-replace_dialog';
239
+ findInput.spellcheck = false;
240
+ findInput.placeholder = this.instructions.findPlaceholder;
241
+ findCaseSensitiveCheckbox.setAttribute("type", "checkbox");
242
+ findCaseSensitiveCheckbox.title = this.instructions.findCaseSensitive;
243
+ findCaseSensitiveCheckbox.classList.add("code-input_find-and-replace_case-sensitive-checkbox");
244
+ findRegExpCheckbox.setAttribute("type", "checkbox");
245
+ findRegExpCheckbox.title = this.instructions.findRegExp;
246
+ findRegExpCheckbox.classList.add("code-input_find-and-replace_reg-exp-checkbox");
247
+
248
+ matchDescription.textContent = this.instructions.start;
249
+ matchDescription.classList.add("code-input_find-and-replace_match-description");
250
+
251
+
252
+ replaceSummary.innerText = this.instructions.replaceTitle;
253
+ replaceInput.spellcheck = false;
254
+ replaceInput.placeholder = this.instructions.replacePlaceholder;
255
+ findNextButton.innerText = "↓";
256
+ findNextButton.title = this.instructions.findNext;
257
+ findNextButton.setAttribute("aria-label", this.instructions.findNext);
258
+ findPreviousButton.innerText = "↑";
259
+ findPreviousButton.title = this.instructions.findPrevious;
260
+ findNextButton.setAttribute("aria-label", this.instructions.findPrevious);
261
+ replaceButton.className = 'code-input_find-and-replace_button-hidden';
262
+ replaceButton.innerText = this.instructions.replaceActionShort;
263
+ replaceButton.title = this.instructions.replaceAction;
264
+ replaceButton.addEventListener("focus", () => {
265
+ // Show replace section
266
+ replaceDropdown.setAttribute("open", true);
267
+ });
268
+ replaceAllButton.className = 'code-input_find-and-replace_button-hidden';
269
+ replaceAllButton.innerText = this.instructions.replaceAllActionShort;
270
+ replaceAllButton.title = this.instructions.replaceAllAction;
271
+ replaceAllButton.addEventListener("focus", () => {
272
+ // Show replace section
273
+ replaceDropdown.setAttribute("open", true);
274
+ });
275
+
276
+ findNextButton.addEventListener("click", (event) => {
277
+ // Stop form submit
278
+ event.preventDefault();
279
+
280
+ dialog.findMatchState.nextMatch();
281
+ this.updateMatchDescription(dialog);
282
+ });
283
+ findPreviousButton.addEventListener("click", () => {
284
+ // Stop form submit
285
+ event.preventDefault();
286
+
287
+ dialog.findMatchState.previousMatch();
288
+ this.updateMatchDescription(dialog);
289
+ });
290
+ replaceButton.addEventListener("click", (event) => {
291
+ // Stop form submit
292
+ event.preventDefault();
293
+
294
+ dialog.findMatchState.replaceOnce(replaceInput.value);
295
+ dialog.focus();
296
+ });
297
+ replaceAllButton.addEventListener("click", (event) => {
298
+ // Stop form submit
299
+ event.preventDefault();
300
+
301
+ dialog.findMatchState.replaceAll(replaceInput.value);
302
+ replaceAllButton.focus();
303
+ });
304
+
305
+ replaceDropdown.addEventListener("toggle", () => {
306
+ // When replace dropdown opened show replace buttons
307
+ replaceButton.classList.toggle("code-input_find-and-replace_button-hidden");
308
+ replaceAllButton.classList.toggle("code-input_find-and-replace_button-hidden");
309
+ });
310
+
311
+ // To store the state of find-and-replace functionality
312
+ dialog.findMatchState = new plugins.FindAndReplace.FindMatchState(codeInputElement);
313
+
314
+ dialog.codeInput = codeInputElement;
315
+ dialog.textarea = textarea;
316
+ dialog.findInput = findInput;
317
+ dialog.findCaseSensitiveCheckbox = findCaseSensitiveCheckbox;
318
+ dialog.findRegExpCheckbox = findRegExpCheckbox;
319
+ dialog.matchDescription = matchDescription;
320
+ dialog.replaceInput = replaceInput;
321
+ dialog.replaceDropdown = replaceDropdown;
322
+
323
+ if(this.checkCtrlH) {
324
+ findInput.addEventListener('keydown', (event) => {
325
+ /* Open replace part on Ctrl+H */
326
+ if (event.ctrlKey && event.key == 'h') {
327
+ event.preventDefault();
328
+ replaceDropdown.setAttribute("open", true);
329
+ }
330
+ });
331
+ }
332
+
333
+ findInput.addEventListener('keypress', (event) => {
334
+ /* Stop enter from submitting form */
335
+ if (event.key == 'Enter') event.preventDefault();
336
+ });
337
+ replaceInput.addEventListener('keypress', (event) => {
338
+ /* Stop enter from submitting form */
339
+ if (event.key == 'Enter') event.preventDefault();
340
+ });
341
+ replaceInput.addEventListener('input', (event) => {
342
+ // Ctrl+Z can trigger this. If the dialog/replace dropdown aren't open, open them!
343
+ if(dialog.classList.contains("code-input_find-and-replace_hidden-dialog")) {
344
+ // Show prompt
345
+ this.showPrompt(dialog.codeInput, true);
346
+ } else if(!dialog.replaceDropdown.hasAttribute("open")) {
347
+ // Open dropdown
348
+ dialog.replaceDropdown.setAttribute("open", true);
349
+ }
350
+ });
351
+
352
+ dialog.addEventListener('keyup', (event) => {
353
+ /* Close prompt on Enter pressed */
354
+ if (event.key == 'Escape') this.cancelPrompt(dialog, codeInputElement, event);
355
+ });
356
+
357
+ findInput.addEventListener('keyup', (event) => { this.checkFindPrompt(dialog, codeInputElement, event); });
358
+ findInput.addEventListener('input', (event) => {
359
+ if(this.findMatchesOnValueChange) this.updateFindMatches(dialog);
360
+ // Ctrl+Z can trigger this. If the dialog isn't open, open it!
361
+ if(dialog.classList.contains("code-input_find-and-replace_hidden-dialog")) {
362
+ this.showPrompt(dialog.codeInput, false);
363
+ }
364
+ });
365
+ findCaseSensitiveCheckbox.addEventListener('click', (event) => { this.updateFindMatches(dialog); });
366
+ findRegExpCheckbox.addEventListener('click', (event) => { this.updateFindMatches(dialog); });
367
+
368
+ replaceInput.addEventListener('keyup', (event) => {
369
+ this.checkReplacePrompt(dialog, codeInputElement, event);
370
+ replaceInput.focus();
371
+ });
372
+ cancel.addEventListener('click', (event) => { this.cancelPrompt(dialog, codeInputElement, event); });
373
+ cancel.addEventListener('keypress', (event) => { if (event.key == "Space" || event.key == "Enter") this.cancelPrompt(dialog, codeInputElement, event); });
374
+
375
+ codeInputElement.dialogContainerElement.appendChild(dialog);
376
+ codeInputElement.pluginData.findAndReplace = {dialog: dialog};
377
+ findInput.focus();
378
+
379
+ if(replacePartExpanded) {
380
+ replaceDropdown.setAttribute("open", true);
381
+ }
382
+
383
+ // Save selection position
384
+ dialog.selectionStart = codeInputElement.textareaElement.selectionStart;
385
+ dialog.selectionEnd = codeInputElement.textareaElement.selectionEnd;
386
+
387
+ if(dialog.selectionStart < dialog.selectionEnd) {
388
+ // Copy selected text to Find input
389
+ let textToFind = codeInputElement.textareaElement.value.substring(dialog.selectionStart, dialog.selectionEnd);
390
+ dialog.findInput.focus();
391
+ dialog.findInput.selectionStart = 0;
392
+ dialog.findInput.selectionEnd = dialog.findInput.value.length;
393
+ document.execCommand("insertText", false, textToFind);
394
+ }
395
+ } else {
396
+ dialog = codeInputElement.pluginData.findAndReplace.dialog;
397
+ // Re-open dialog
398
+ dialog.classList.remove("code-input_find-and-replace_hidden-dialog");
399
+ dialog.removeAttribute("inert"); // Show to keyboard navigation when open.
400
+ dialog.setAttribute("tabindex", 0); // Show to keyboard navigation when open.
401
+ dialog.removeAttribute("aria-hidden"); // Show to screen reader when open.
402
+ dialog.findInput.focus();
403
+ if(replacePartExpanded) {
404
+ dialog.replaceDropdown.setAttribute("open", true);
405
+ } else {
406
+ dialog.replaceDropdown.removeAttribute("open");
407
+ }
408
+ }
409
+
410
+ // Save selection position
411
+ dialog.selectionStart = codeInputElement.textareaElement.selectionStart;
412
+ dialog.selectionEnd = codeInputElement.textareaElement.selectionEnd;
413
+
414
+ if(dialog.selectionStart < dialog.selectionEnd) {
415
+ // Copy selected text to Find input
416
+ let textToFind = codeInputElement.textareaElement.value.substring(dialog.selectionStart, dialog.selectionEnd);
417
+ dialog.findInput.focus();
418
+ dialog.findInput.selectionStart = 0;
419
+ dialog.findInput.selectionEnd = dialog.findInput.value.length;
420
+ document.execCommand("insertText", false, textToFind);
421
+ }
422
+
423
+ // Highlight matches
424
+ this.updateFindMatches(dialog);
425
+ }
426
+
427
+ /* Event handler for keydown event that makes Ctrl+F open find dialog */
428
+ checkCtrlF(codeInput, event) {
429
+ if (event.ctrlKey && event.key == 'f') {
430
+ event.preventDefault();
431
+ this.showPrompt(codeInput, false);
432
+ }
433
+ }
434
+
435
+ /* Event handler for keydown event that makes Ctrl+H open find+replace dialog */
436
+ checkCtrlH(codeInput, event) {
437
+ if (event.ctrlKey && event.key == 'h') {
438
+ event.preventDefault();
439
+ this.showPrompt(codeInput, true);
440
+ }
441
+ }
442
+ }
443
+
444
+ // Number of matches to highlight at once, for efficiency reasons
445
+ const CODE_INPUT_FIND_AND_REPLACE_MATCH_BLOCK_SIZE = 500;
446
+
447
+ /* This class stores the state of find and replace in a specific code-input element */
448
+ plugins.FindAndReplace.FindMatchState = class {
449
+ codeInput = null;
450
+ lastValue = null; // of codeInput, so know when to update matches
451
+ lastSearchRegexp = null; // to be able to update matches
452
+ numMatches = 0;
453
+ focusedMatchID = 0;
454
+ matchStartIndexes = []; // For each match in order
455
+ matchEndIndexes = []; // For each match in order
456
+ focusedMatchStartIndex = 0;
457
+
458
+ matchBlocksHighlighted = []; // Each block represents a CODE_INPUT_FIND_AND_REPLACE_MATCH_BLOCK_SIZE number of matches (up to it for the last), and is highlighted separately to prevent excessive delay.
459
+
460
+ constructor(codeInputElement) {
461
+ this.focusedMatchStartIndex = codeInputElement.textareaElement.selectionStart;
462
+ this.codeInput = codeInputElement;
463
+ }
464
+
465
+ /* Clear the find matches, often to prepare for new matches to be added. */
466
+ clearMatches() {
467
+ // Delete match information saved here
468
+ this.numMatches = 0;
469
+ this.matchStartIndexes = [];
470
+ this.matchEndIndexes = [];
471
+
472
+ // Remove generated spans to hold matches
473
+ let tempSpans = this.codeInput.codeElement.querySelectorAll(".code-input_find-and-replace_temporary-span");
474
+ for(let i = 0; i < tempSpans.length; i++) {
475
+ // Replace with textContent as Text node
476
+ tempSpans[i].parentElement.replaceChild(new Text(tempSpans[i].textContent), tempSpans[i]);
477
+ }
478
+
479
+ // Remove old matches
480
+ let oldMatches = this.codeInput.codeElement.querySelectorAll(".code-input_find-and-replace_find-match");
481
+ for(let i = 0; i < oldMatches.length; i++) {
482
+ oldMatches[i].removeAttribute("data-code-input_find-and-replace_match-id"); // No match ID
483
+ oldMatches[i].classList.remove("code-input_find-and-replace_find-match"); // No highlighting
484
+ oldMatches[i].classList.remove("code-input_find-and-replace_find-match-focused"); // No focused highlighting
485
+ }
486
+ }
487
+
488
+ /* Refresh the matches of find functionality with a new search term Regular Expression. */
489
+ updateMatches(searchRegexp) {
490
+ this.lastSearchRegexp = searchRegexp;
491
+ this.lastValue = this.codeInput.value;
492
+ // Add matches
493
+ let matchID = 0;
494
+ let match; // Search result
495
+ this.matchStartIndexes = [];
496
+ this.matchEndIndexes = [];
497
+ this.matchBlocksHighlighted = [];
498
+
499
+ // Make all match blocks be not highlighted except for currently focused
500
+ let focusedMatchBlock = Math.floor(this.focusedMatchID / CODE_INPUT_FIND_AND_REPLACE_MATCH_BLOCK_SIZE);
501
+ for(let i = 0; i < focusedMatchBlock; i++) {
502
+ this.matchBlocksHighlighted.push(false);
503
+ }
504
+ this.matchBlocksHighlighted.push(true);
505
+
506
+ while ((match = searchRegexp.exec(this.codeInput.value)) !== null) {
507
+ let matchText = match[0];
508
+ if(matchText.length == 0) {
509
+ throw SyntaxError(this.instructions.infiniteLoopError);
510
+ }
511
+
512
+ // Add next match block if needed
513
+ let currentMatchBlock = Math.floor(matchID / CODE_INPUT_FIND_AND_REPLACE_MATCH_BLOCK_SIZE);
514
+ if(this.matchBlocksHighlighted.length < currentMatchBlock) {
515
+ this.matchBlocksHighlighted.push(false);
516
+ }
517
+
518
+ if(this.matchBlocksHighlighted[currentMatchBlock]) {
519
+ this.highlightMatch(matchID, this.codeInput.codeElement, match.index, match.index + matchText.length);
520
+ }
521
+ this.matchStartIndexes.push(match.index);
522
+ this.matchEndIndexes.push(match.index + matchText.length);
523
+ matchID++;
524
+ }
525
+ this.numMatches = matchID;
526
+
527
+ if(this.numMatches > 0) {
528
+ this.focusMatch();
529
+ }
530
+ }
531
+
532
+ /* Highlight all currently found matches again if there are any matches */
533
+ rehighlightMatches() {
534
+ this.updateMatches(this.lastSearchRegexp);
535
+ this.focusMatch();
536
+ }
537
+
538
+ /* Replace one match with the replacementText given */
539
+ replaceOnce(replacementText) {
540
+ if(this.numMatches > 0 && replacementText != this.codeInput.value.substring(0, this.matchStartIndexes[this.focusedMatchID], this.matchEndIndexes[this.focusedMatchID])) {
541
+ // Go to next match
542
+ this.focusedMatchStartIndex += replacementText.length;
543
+
544
+ // Select the match
545
+ this.codeInput.handleEventsFromTextarea = false;
546
+ this.codeInput.textareaElement.focus();
547
+ this.codeInput.textareaElement.selectionStart = this.matchStartIndexes[this.focusedMatchID];
548
+ this.codeInput.textareaElement.selectionEnd = this.matchEndIndexes[this.focusedMatchID];
549
+
550
+ // Replace it with the replacement text
551
+ document.execCommand("insertText", false, replacementText);
552
+ this.codeInput.handleEventsFromTextarea = true;
553
+ }
554
+ }
555
+
556
+ /* Replace all matches with the replacementText given */
557
+ replaceAll(replacementText) {
558
+ const replacementNumChars = replacementText.length;
559
+ let numCharsAdded = 0; // So can adjust match positions
560
+
561
+ for(let i = 0; i < this.numMatches; i++) {
562
+ // Replace each match
563
+
564
+ // Select the match, taking into account characters added before
565
+ this.codeInput.handleEventsFromTextarea = false;
566
+ this.codeInput.textareaElement.focus();
567
+ this.codeInput.textareaElement.selectionStart = this.matchStartIndexes[i] + numCharsAdded;
568
+ this.codeInput.textareaElement.selectionEnd = this.matchEndIndexes[i] + numCharsAdded;
569
+
570
+ numCharsAdded += replacementNumChars - (this.matchEndIndexes[i] - this.matchStartIndexes[i]);
571
+
572
+ // Replace it with the replacement text
573
+ document.execCommand("insertText", false, replacementText);
574
+ this.codeInput.handleEventsFromTextarea = true;
575
+ }
576
+ }
577
+
578
+ /* Focus on the next match found in the find results */
579
+ nextMatch() {
580
+ this.focusMatch((this.focusedMatchID + 1) % this.numMatches);
581
+ }
582
+
583
+ /* Focus on the previous match found in the find results */
584
+ previousMatch() {
585
+ this.focusMatch((this.focusedMatchID + this.numMatches - 1) % this.numMatches);
586
+ }
587
+
588
+ /* Change the focused match to the match with ID matchID. */
589
+ focusMatch(matchID = undefined) {
590
+ if(matchID === undefined) {
591
+ // Focus on first match after focusedMatchStartIndex
592
+ matchID = 0;
593
+ while(matchID < this.matchStartIndexes.length && this.matchStartIndexes[matchID] < this.focusedMatchStartIndex) {
594
+ matchID++;
595
+ }
596
+ if(matchID >= this.matchStartIndexes.length) {
597
+ // After last match, move back to first match
598
+ matchID = 0;
599
+ }
600
+ }
601
+
602
+ // Save focusedMatchStartIndex so if code changed match stays at same place
603
+ this.focusedMatchStartIndex = this.matchStartIndexes[matchID];
604
+ this.focusedMatchID = matchID;
605
+
606
+ // Delete old focus
607
+ let oldFocused = this.codeInput.codeElement.querySelectorAll(".code-input_find-and-replace_find-match-focused");
608
+ for(let i = 0; i < oldFocused.length; i++) {
609
+ oldFocused[i].classList.remove("code-input_find-and-replace_find-match-focused");
610
+ }
611
+
612
+ // Highlight match block if needed
613
+ let highlightedMatchBlock = Math.floor(matchID / CODE_INPUT_FIND_AND_REPLACE_MATCH_BLOCK_SIZE);
614
+ if(!this.matchBlocksHighlighted[highlightedMatchBlock]) {
615
+ this.matchBlocksHighlighted[highlightedMatchBlock] = true;
616
+ for(let i = CODE_INPUT_FIND_AND_REPLACE_MATCH_BLOCK_SIZE*highlightedMatchBlock; i < CODE_INPUT_FIND_AND_REPLACE_MATCH_BLOCK_SIZE*(highlightedMatchBlock+1); i++) {
617
+ // Highlight match
618
+ this.highlightMatch(i, this.codeInput.codeElement, this.matchStartIndexes[i], this.matchEndIndexes[i])
619
+ }
620
+ }
621
+
622
+ // Add new focus
623
+ let newFocused = this.codeInput.codeElement.querySelectorAll(`.code-input_find-and-replace_find-match[data-code-input_find-and-replace_match-id="${matchID}"]`);
624
+ for(let i = 0; i < newFocused.length; i++) {
625
+ newFocused[i].classList.add("code-input_find-and-replace_find-match-focused");
626
+ }
627
+
628
+ if(newFocused.length > 0) {
629
+ this.codeInput.scrollTo(newFocused[0].offsetLeft - this.codeInput.offsetWidth / 2, newFocused[0].offsetTop - this.codeInput.offsetHeight / 2); // So focused match in centre of screen
630
+ }
631
+ }
632
+
633
+ /* Highlight a match from the find functionality given its start and end indexes in the text.
634
+ Start from the currentElement. Use the matchID in the class name
635
+ of the match so different matches can be identified.
636
+ */
637
+ highlightMatch(matchID, currentElement, startIndex, endIndex) {
638
+ const lines = currentElement.textContent.substring(startIndex, endIndex).split("\n");
639
+ let lineStartIndex = startIndex;
640
+ for(let i = 0; i < lines.length; i++) {
641
+ if(i == 0) {
642
+ this.highlightMatchNewlineOnlyAtStart(matchID, currentElement, lineStartIndex, lineStartIndex+lines[i].length);
643
+ } else {
644
+ // Include previous newline character too
645
+ this.highlightMatchNewlineOnlyAtStart(matchID, currentElement, lineStartIndex-1, lineStartIndex+lines[i].length);
646
+ }
647
+
648
+ lineStartIndex += lines[i].length + 1; // +1 for newline character
649
+ }
650
+ }
651
+
652
+ /* Same as highlightMatch, but assumes any newlines in the
653
+ match are at the startIndex (for simpler code). */
654
+ highlightMatchNewlineOnlyAtStart(matchID, currentElement, startIndex, endIndex) {
655
+ for(let i = 0; i < currentElement.childNodes.length; i++) {
656
+ let childElement = currentElement.childNodes[i];
657
+ let childText = childElement.textContent;
658
+
659
+ let noInnerElements = false;
660
+ if(childElement.nodeType == 3) {
661
+ // Text node
662
+ if(i + 1 < currentElement.childNodes.length && currentElement.childNodes[i+1].nodeType == 3) {
663
+ // Can merge with next text node
664
+ currentElement.childNodes[i+1].textContent = childElement.textContent + currentElement.childNodes[i+1].textContent; // Merge textContent with next node
665
+ currentElement.removeChild(childElement); // Delete this node
666
+ i--; // As an element removed
667
+ continue; // Move to next node
668
+ }
669
+ // Text node - replace with span
670
+ noInnerElements = true;
671
+
672
+ let replacementElement = document.createElement("span");
673
+ replacementElement.textContent = childText;
674
+ replacementElement.classList.add("code-input_find-and-replace_temporary-span"); // Can remove span later
675
+
676
+ currentElement.replaceChild(replacementElement, childElement);
677
+ childElement = replacementElement;
678
+ }
679
+
680
+ if(startIndex <= 0) {
681
+ // Started highlight
682
+ if(childText.length >= endIndex) {
683
+ // Match ends in childElement
684
+ if(noInnerElements) {
685
+ // Text node - highlight first part
686
+ let startSpan = document.createElement("span");
687
+ startSpan.classList.add("code-input_find-and-replace_find-match"); // Highlighted
688
+ startSpan.setAttribute("data-code-input_find-and-replace_match-id", matchID);
689
+ startSpan.classList.add("code-input_find-and-replace_temporary-span"); // Can remove span later
690
+ startSpan.textContent = childText.substring(0, endIndex);
691
+ if(startSpan.textContent[0] == "\n") {
692
+ // Newline at start - make clear
693
+ startSpan.classList.add("code-input_find-and-replace_start-newline");
694
+ }
695
+
696
+ let endText = childText.substring(endIndex);
697
+ childElement.textContent = endText;
698
+
699
+ childElement.insertAdjacentElement('beforebegin', startSpan);
700
+ i++; // An extra element has been added
701
+ return;
702
+ } else {
703
+ this.highlightMatchNewlineOnlyAtStart(matchID, childElement, 0, endIndex);
704
+ }
705
+
706
+ // Match ended - nothing to do after backtracking
707
+ return;
708
+ } else {
709
+ // Match goes through child element
710
+ childElement.classList.add("code-input_find-and-replace_find-match"); // Highlighted
711
+ childElement.setAttribute("data-code-input_find-and-replace_match-id", matchID);
712
+ if(childElement.textContent[0] == "\n") {
713
+ // Newline at start - make clear
714
+ childElement.classList.add("code-input_find-and-replace_start-newline");
715
+ }
716
+ }
717
+ } else if(childText.length > startIndex) {
718
+ // Match starts in childElement
719
+ if(noInnerElements) {
720
+ if(childText.length > endIndex) {
721
+ // Match starts and ends in childElement - highlight middle part
722
+ // Text node - highlight last part
723
+ let startSpan = document.createElement("span");
724
+ startSpan.classList.add("code-input_find-and-replace_temporary-span"); // Can remove span later
725
+ startSpan.textContent = childText.substring(0, startIndex);
726
+
727
+ let middleText = childText.substring(startIndex, endIndex);
728
+ childElement.textContent = middleText;
729
+ childElement.classList.add("code-input_find-and-replace_find-match"); // Highlighted
730
+ childElement.setAttribute("data-code-input_find-and-replace_match-id", matchID);
731
+ if(childElement.textContent[0] == "\n") {
732
+ // Newline at start - make clear
733
+ childElement.classList.add("code-input_find-and-replace_start-newline");
734
+ }
735
+
736
+ let endSpan = document.createElement("span");
737
+ endSpan.classList.add("code-input_find-and-replace_temporary-span"); // Can remove span later
738
+ endSpan.textContent = childText.substring(endIndex);
739
+
740
+ childElement.insertAdjacentElement('beforebegin', startSpan);
741
+ childElement.insertAdjacentElement('afterend', endSpan);
742
+ i++; // 2 extra elements have been added
743
+ } else {
744
+ // Text node - highlight last part
745
+ let startText = childText.substring(0, startIndex);
746
+ childElement.textContent = startText;
747
+
748
+ let endSpan = document.createElement("span");
749
+ endSpan.classList.add("code-input_find-and-replace_find-match"); // Highlighted
750
+ endSpan.setAttribute("data-code-input_find-and-replace_match-id", matchID);
751
+ endSpan.classList.add("code-input_find-and-replace_temporary-span"); // Can remove span later
752
+ endSpan.textContent = childText.substring(startIndex);
753
+ if(endSpan.textContent[0] == "\n") {
754
+ // Newline at start - make clear
755
+ endSpan.classList.add("code-input_find-and-replace_start-newline");
756
+ }
757
+
758
+ childElement.insertAdjacentElement('afterend', endSpan);
759
+ i++; // An extra element has been added
760
+ }
761
+ } else {
762
+ this.highlightMatchNewlineOnlyAtStart(matchID, childElement, startIndex, endIndex);
763
+ }
764
+
765
+ if(childText.length > endIndex) {
766
+ // Match completely in childElement - nothing to do after backtracking
767
+ return;
768
+ }
769
+ }
770
+
771
+ // Make indexes skip the element
772
+ startIndex -= childText.length;
773
+ endIndex -= childText.length;
774
+ }
775
+ }
776
+ }
777
+ export default plugins.FindAndReplace;