Blame Raw
Cohee · e3f41666 · · 1923 lines (99.0 KB)
1 contributor
1import { hljs, morphdom } from '../../../../lib.js';
2import { POPUP_RESULT, POPUP_TYPE, Popup } from '../../../popup.js';
3import { setSlashCommandAutoComplete } from '../../../slash-commands.js';
4import { SlashCommandAbortController } from '../../../slash-commands/SlashCommandAbortController.js';
5import { SlashCommandBreakPoint } from '../../../slash-commands/SlashCommandBreakPoint.js';
6import { SlashCommandClosure } from '../../../slash-commands/SlashCommandClosure.js';
7import { SlashCommandClosureResult } from '../../../slash-commands/SlashCommandClosureResult.js';
8import { SlashCommandDebugController } from '../../../slash-commands/SlashCommandDebugController.js';
9import { SlashCommandExecutor } from '../../../slash-commands/SlashCommandExecutor.js';
10import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js';
11import { SlashCommandParserError } from '../../../slash-commands/SlashCommandParserError.js';
12import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';
13import { accountStorage } from '../../../util/AccountStorage.js';
14import { debounce, delay, getSortableDelay, showFontAwesomePicker } from '../../../utils.js';
15import { log, quickReplyApi, warn } from '../index.js';
16import { QuickReplyContextLink } from './QuickReplyContextLink.js';
17import { QuickReplySet } from './QuickReplySet.js';
18import { ContextMenu } from './ui/ctx/ContextMenu.js';
19
20export class QuickReply {
21 /**
22 * @param {{ id?: number; contextList?: any; }} props
23 */
24 static from(props) {
25 props.contextList = (props.contextList ?? []).map((/** @type {any} */ it) => QuickReplyContextLink.from(it));
26 return Object.assign(new this(), props);
27 }
28
29
30 /**@type {number}*/ id;
31 /**@type {string}*/ icon;
32 /**@type {string}*/ label = '';
33 /**@type {boolean}*/ showLabel = false;
34 /**@type {string}*/ title = '';
35 /**@type {string}*/ message = '';
36
37 /**@type {QuickReplyContextLink[]}*/ contextList;
38
39 /**@type {boolean}*/ preventAutoExecute = true;
40 /**@type {boolean}*/ isHidden = false;
41 /**@type {boolean}*/ executeOnStartup = false;
42 /**@type {boolean}*/ executeOnUser = false;
43 /**@type {boolean}*/ executeOnAi = false;
44 /**@type {boolean}*/ executeOnChatChange = false;
45 /**@type {boolean}*/ executeOnGroupMemberDraft = false;
46 /**@type {boolean}*/ executeOnNewChat = false;
47 /**@type {boolean}*/ executeBeforeGeneration = false;
48 /**@type {string}*/ automationId = '';
49
50 /**@type {function}*/ onExecute;
51 /** @type {(qr:QuickReply)=>AsyncGenerator<SlashCommandClosureResult|{closure:SlashCommandClosure, executor:SlashCommandExecutor|SlashCommandClosureResult}, SlashCommandClosureResult, boolean>} */ onDebug;
52 /**@type {function}*/ onDelete;
53 /**@type {function}*/ onUpdate;
54 /**@type {function}*/ onInsertBefore;
55 /**@type {function}*/ onTransfer;
56
57
58 /**@type {HTMLElement}*/ dom;
59 /**@type {HTMLElement}*/ domIcon;
60 /**@type {HTMLElement}*/ domLabel;
61 /**@type {HTMLElement}*/ settingsDom;
62 /**@type {HTMLElement}*/ settingsDomIcon;
63 /**@type {HTMLInputElement}*/ settingsDomLabel;
64 /**@type {HTMLTextAreaElement}*/ settingsDomMessage;
65
66 /**@type {Popup}*/ editorPopup;
67 /**@type {HTMLElement}*/ editorDom;
68
69 /**@type {HTMLTextAreaElement}*/ editorMessage;
70 /**@type {HTMLTextAreaElement}*/ editorMessageLabel;
71 /**@type {HTMLElement}*/ editorSyntax;
72 /**@type {HTMLElement}*/ editorExecuteBtn;
73 /**@type {HTMLElement}*/ editorExecuteBtnPause;
74 /**@type {HTMLElement}*/ editorExecuteBtnStop;
75 /**@type {HTMLElement}*/ editorExecuteProgress;
76 /**@type {HTMLElement}*/ editorExecuteErrors;
77 /**@type {HTMLElement}*/ editorExecuteResult;
78 /**@type {HTMLElement}*/ editorDebugState;
79 /**@type {Promise}*/ editorExecutePromise;
80 /**@type {boolean}*/ isExecuting;
81 /**@type {SlashCommandAbortController}*/ abortController;
82 /**@type {SlashCommandDebugController}*/ debugController;
83
84
85 get hasContext() {
86 return this.contextList && this.contextList.filter(it => it.set).length > 0;
87 }
88
89
90 unrender() {
91 this.dom?.remove();
92 this.dom = null;
93 }
94 updateRender() {
95 if (!this.dom) return;
96 this.dom.title = this.title || this.message;
97 if (this.icon) {
98 this.domIcon.classList.remove('qr--hidden');
99 if (this.showLabel) this.domLabel.classList.remove('qr--hidden');
100 else this.domLabel.classList.add('qr--hidden');
101 } else {
102 this.domIcon.classList.add('qr--hidden');
103 this.domLabel.classList.remove('qr--hidden');
104 }
105 this.domLabel.textContent = this.label;
106 this.dom.classList[this.hasContext ? 'add' : 'remove']('qr--hasCtx');
107 }
108 render() {
109 this.unrender();
110 if (!this.dom) {
111 const root = document.createElement('div'); {
112 this.dom = root;
113 root.classList.add('qr--button');
114 root.classList.add('menu_button');
115 if (this.hasContext) {
116 root.classList.add('qr--hasCtx');
117 }
118 root.title = this.title || this.message;
119 root.addEventListener('contextmenu', (evt) => {
120 log('contextmenu', this, this.hasContext);
121 if (this.hasContext) {
122 evt.preventDefault();
123 evt.stopPropagation();
124 const menu = new ContextMenu(this);
125 menu.show(evt);
126 }
127 });
128 root.addEventListener('click', (evt) => {
129 if (evt.ctrlKey) {
130 this.showEditor();
131 return;
132 }
133 this.execute();
134 });
135 const icon = document.createElement('div'); {
136 this.domIcon = icon;
137 icon.classList.add('qr--button-icon');
138 icon.classList.add('fa-solid');
139 if (!this.icon) icon.classList.add('qr--hidden');
140 else icon.classList.add(this.icon);
141 root.append(icon);
142 }
143 const lbl = document.createElement('div'); {
144 this.domLabel = lbl;
145 lbl.classList.add('qr--button-label');
146 if (this.icon && !this.showLabel) lbl.classList.add('qr--hidden');
147 lbl.textContent = this.label;
148 root.append(lbl);
149 }
150 const expander = document.createElement('div'); {
151 expander.classList.add('qr--button-expander');
152 expander.textContent = '⋮';
153 expander.title = 'Open context menu';
154 expander.addEventListener('click', (evt) => {
155 evt.stopPropagation();
156 evt.preventDefault();
157 const menu = new ContextMenu(this);
158 menu.show(evt);
159 });
160 root.append(expander);
161 }
162 }
163 }
164 return this.dom;
165 }
166
167
168 renderSettings(idx) {
169 if (!this.settingsDom) {
170 const item = document.createElement('div'); {
171 this.settingsDom = item;
172 item.classList.add('qr--set-item');
173 item.setAttribute('data-order', String(idx));
174 item.setAttribute('data-id', String(this.id));
175 const adder = document.createElement('div'); {
176 adder.classList.add('qr--set-itemAdder');
177 const actions = document.createElement('div'); {
178 actions.classList.add('qr--actions');
179 const addNew = document.createElement('div'); {
180 addNew.classList.add('qr--action');
181 addNew.classList.add('qr--add');
182 addNew.classList.add('menu_button');
183 addNew.classList.add('menu_button_icon');
184 addNew.classList.add('fa-solid');
185 addNew.classList.add('fa-plus');
186 addNew.title = 'Add quick reply';
187 addNew.addEventListener('click', () => this.onInsertBefore());
188 actions.append(addNew);
189 }
190 const paste = document.createElement('div'); {
191 paste.classList.add('qr--action');
192 paste.classList.add('qr--paste');
193 paste.classList.add('menu_button');
194 paste.classList.add('menu_button_icon');
195 paste.classList.add('fa-solid');
196 paste.classList.add('fa-paste');
197 paste.title = 'Add quick reply from clipboard';
198 paste.addEventListener('click', async () => {
199 const text = await navigator.clipboard.readText();
200 this.onInsertBefore(text);
201 });
202 actions.append(paste);
203 }
204 const importFile = document.createElement('div'); {
205 importFile.classList.add('qr--action');
206 importFile.classList.add('qr--importFile');
207 importFile.classList.add('menu_button');
208 importFile.classList.add('menu_button_icon');
209 importFile.classList.add('fa-solid');
210 importFile.classList.add('fa-file-import');
211 importFile.title = 'Add quick reply from JSON file';
212 importFile.addEventListener('click', async () => {
213 const inp = document.createElement('input'); {
214 inp.type = 'file';
215 inp.accept = '.json';
216 inp.addEventListener('change', async () => {
217 if (inp.files.length > 0) {
218 for (const file of inp.files) {
219 const text = await file.text();
220 this.onInsertBefore(text);
221 }
222 }
223 });
224 inp.click();
225 }
226 });
227 actions.append(importFile);
228 }
229 adder.append(actions);
230 }
231 item.append(adder);
232 }
233 const itemContent = document.createElement('div'); {
234 itemContent.classList.add('qr--content');
235 const drag = document.createElement('div'); {
236 drag.classList.add('drag-handle');
237 drag.classList.add('ui-sortable-handle');
238 drag.textContent = '☰';
239 itemContent.append(drag);
240 }
241 const lblContainer = document.createElement('div'); {
242 lblContainer.classList.add('qr--set-itemLabelContainer');
243 const icon = document.createElement('div'); {
244 this.settingsDomIcon = icon;
245 icon.title = 'Click to change icon';
246 icon.classList.add('qr--set-itemIcon');
247 icon.classList.add('menu_button');
248 icon.classList.add('fa-fw');
249 if (this.icon) {
250 icon.classList.add('fa-solid');
251 icon.classList.add(this.icon);
252 }
253 icon.addEventListener('click', async () => {
254 let value = await showFontAwesomePicker();
255 this.updateIcon(value);
256 });
257 lblContainer.append(icon);
258 }
259 const lbl = document.createElement('input'); {
260 this.settingsDomLabel = lbl;
261 lbl.classList.add('qr--set-itemLabel');
262 lbl.classList.add('text_pole');
263 lbl.value = this.label;
264 lbl.addEventListener('input', () => this.updateLabel(lbl.value));
265 lblContainer.append(lbl);
266 }
267 itemContent.append(lblContainer);
268 }
269 item.append(itemContent);
270 }
271 const optContainer = document.createElement('div'); {
272 optContainer.classList.add('qr--set-optionsContainer');
273 const opt = document.createElement('div'); {
274 opt.classList.add('qr--action');
275 opt.classList.add('menu_button');
276 opt.classList.add('fa-fw');
277 opt.classList.add('fa-solid');
278 opt.textContent = '⁝';
279 opt.title = 'Additional options:\n - large editor\n - context menu\n - auto-execution\n - tooltip';
280 opt.addEventListener('click', () => this.showEditor());
281 optContainer.append(opt);
282 }
283 itemContent.append(optContainer);
284 }
285 const mes = document.createElement('textarea'); {
286 this.settingsDomMessage = mes;
287 mes.id = `qr--set--item${this.id}`;
288 mes.classList.add('qr--set-itemMessage');
289 mes.value = this.message;
290 //HACK need to use jQuery to catch the triggered event from the expanded editor
291 $(mes).on('input', () => this.updateMessage(mes.value));
292 itemContent.append(mes);
293 }
294 const actions = document.createElement('div'); {
295 actions.classList.add('qr--actions');
296 const move = document.createElement('div'); {
297 move.classList.add('qr--action');
298 move.classList.add('menu_button');
299 move.classList.add('fa-fw');
300 move.classList.add('fa-solid');
301 move.classList.add('fa-truck-arrow-right');
302 move.title = 'Move quick reply to other set';
303 move.addEventListener('click', () => this.onTransfer(this));
304 actions.append(move);
305 }
306 const copy = document.createElement('div'); {
307 copy.classList.add('qr--action');
308 copy.classList.add('menu_button');
309 copy.classList.add('fa-fw');
310 copy.classList.add('fa-solid');
311 copy.classList.add('fa-copy');
312 copy.title = 'Copy quick reply to clipboard';
313 copy.addEventListener('click', async () => {
314 await navigator.clipboard.writeText(JSON.stringify(this));
315 copy.classList.add('qr--success');
316 await delay(3010);
317 copy.classList.remove('qr--success');
318 });
319 actions.append(copy);
320 }
321 const cut = document.createElement('div'); {
322 cut.classList.add('qr--action');
323 cut.classList.add('menu_button');
324 cut.classList.add('fa-fw');
325 cut.classList.add('fa-solid');
326 cut.classList.add('fa-cut');
327 cut.title = 'Cut quick reply to clipboard (copy and remove)';
328 cut.addEventListener('click', async () => {
329 await navigator.clipboard.writeText(JSON.stringify(this));
330 this.delete();
331 });
332 actions.append(cut);
333 }
334 const exp = document.createElement('div'); {
335 exp.classList.add('qr--action');
336 exp.classList.add('menu_button');
337 exp.classList.add('fa-fw');
338 exp.classList.add('fa-solid');
339 exp.classList.add('fa-file-export');
340 exp.title = 'Export quick reply as file';
341 exp.addEventListener('click', () => {
342 const blob = new Blob([JSON.stringify(this)], { type: 'text' });
343 const url = URL.createObjectURL(blob);
344 const a = document.createElement('a'); {
345 a.href = url;
346 a.download = `${this.label}.qr.json`;
347 a.click();
348 }
349 });
350 actions.append(exp);
351 }
352 const del = document.createElement('div'); {
353 del.classList.add('qr--action');
354 del.classList.add('menu_button');
355 del.classList.add('fa-fw');
356 del.classList.add('fa-solid');
357 del.classList.add('fa-trash-can');
358 del.classList.add('redWarningBG');
359 del.title = 'Remove Quick Reply\n---\nShift+Click to skip confirmation';
360 del.addEventListener('click', async (evt) => {
361 if (!evt.shiftKey) {
362 const result = await Popup.show.confirm(
363 'Remove Quick Reply',
364 'Are you sure you want to remove this Quick Reply?',
365 );
366 if (result != POPUP_RESULT.AFFIRMATIVE) {
367 return;
368 }
369 }
370 this.delete();
371 });
372 actions.append(del);
373 }
374 itemContent.append(actions);
375 }
376 }
377 }
378 return this.settingsDom;
379 }
380 unrenderSettings() {
381 this.settingsDom?.remove();
382 }
383
384 async showEditor() {
385 const response = await fetch('/scripts/extensions/quick-reply/html/qrEditor.html', { cache: 'no-store' });
386 if (response.ok) {
387 this.template = document.createRange().createContextualFragment(await response.text()).querySelector('#qr--modalEditor');
388 /**@type {HTMLElement} */
389 // @ts-ignore
390 const dom = this.template.cloneNode(true);
391 this.editorDom = dom;
392 this.editorPopup = new Popup(dom, POPUP_TYPE.TEXT, undefined, { okButton: 'OK', wide: true, large: true, rows: 1 });
393 const popupResult = this.editorPopup.show();
394
395 // basics
396 /**@type {HTMLElement}*/
397 const icon = dom.querySelector('#qr--modal-icon');
398 if (this.icon) {
399 icon.classList.add('fa-solid');
400 icon.classList.add(this.icon);
401 } else {
402 icon.textContent = '…';
403 }
404 icon.addEventListener('click', async () => {
405 let value = await showFontAwesomePicker();
406 if (value === null) return;
407 if (this.icon) icon.classList.remove(this.icon);
408 if (value == '') {
409 icon.classList.remove('fa-solid');
410 icon.textContent = '…';
411 } else {
412 icon.textContent = '';
413 icon.classList.add('fa-solid');
414 icon.classList.add(value);
415 }
416 this.updateIcon(value);
417 });
418 /**@type {HTMLInputElement}*/
419 const showLabel = dom.querySelector('#qr--modal-showLabel');
420 showLabel.checked = this.showLabel;
421 showLabel.addEventListener('click', () => {
422 this.updateShowLabel(showLabel.checked);
423 });
424 /**@type {HTMLInputElement}*/
425 const label = dom.querySelector('#qr--modal-label');
426 label.value = this.label;
427 label.addEventListener('input', () => {
428 this.updateLabel(label.value);
429 });
430 let switcherList;
431 // @ts-ignore
432 dom.querySelector('#qr--modal-switcher').addEventListener('click', (evt) => {
433 if (switcherList) {
434 switcherList.remove();
435 switcherList = null;
436 return;
437 }
438 const list = document.createElement('ul'); {
439 switcherList = list;
440 list.classList.add('qr--modal-switcherList');
441 const makeList = (qrs) => {
442 const setItem = document.createElement('li'); {
443 setItem.classList.add('qr--modal-switcherItem');
444 setItem.addEventListener('click', () => {
445 list.innerHTML = '';
446 for (const qrs of quickReplyApi.listSets()) {
447 const item = document.createElement('li'); {
448 item.classList.add('qr--modal-switcherItem');
449 item.addEventListener('click', () => {
450 list.innerHTML = '';
451 makeList(quickReplyApi.getSetByName(qrs));
452 });
453 const lbl = document.createElement('div'); {
454 lbl.classList.add('qr--label');
455 lbl.textContent = qrs;
456 item.append(lbl);
457 }
458 list.append(item);
459 }
460 }
461 });
462 const lbl = document.createElement('div'); {
463 lbl.classList.add('qr--label');
464 const icon = document.createElement('i'); {
465 icon.classList.add('fa-solid');
466 icon.classList.add('fa-arrow-alt-circle-right');
467 icon.classList.add('menu_button');
468 lbl.append(icon);
469 }
470 const text = document.createElement('span'); {
471 text.textContent = 'Switch QR Sets...';
472 lbl.append(text);
473 }
474 setItem.append(lbl);
475 }
476 list.append(setItem);
477 }
478 const addItem = document.createElement('li'); {
479 addItem.classList.add('qr--modal-switcherItem');
480 addItem.addEventListener('click', () => {
481 const qr = quickReplyApi.getSetByQr(this).addQuickReply();
482 this.editorPopup.completeAffirmative();
483 qr.showEditor();
484 });
485 const lbl = document.createElement('div'); {
486 lbl.classList.add('qr--label');
487 const icon = document.createElement('i'); {
488 icon.classList.add('fa-solid');
489 icon.classList.add('fa-plus');
490 icon.classList.add('menu_button');
491 lbl.append(icon);
492 }
493 const text = document.createElement('span'); {
494 text.textContent = 'Add QR';
495 lbl.append(text);
496 }
497 addItem.append(lbl);
498 }
499 list.append(addItem);
500 }
501 for (const qr of qrs.qrList.toSorted((a, b) => a.label.toLowerCase().localeCompare(b.label.toLowerCase()))) {
502 const item = document.createElement('li'); {
503 item.classList.add('qr--modal-switcherItem');
504 if (qr == this) item.classList.add('qr--current');
505 else item.addEventListener('click', () => {
506 this.editorPopup.completeAffirmative();
507 qr.showEditor();
508 });
509 const lbl = document.createElement('div'); {
510 lbl.classList.add('qr--label');
511 lbl.textContent = qr.label;
512 item.append(lbl);
513 }
514 const id = document.createElement('div'); {
515 id.classList.add('qr--id');
516 id.textContent = qr.id.toString();
517 item.append(id);
518 }
519 const mes = document.createElement('div'); {
520 mes.classList.add('qr--message');
521 mes.textContent = qr.message;
522 item.append(mes);
523 }
524 list.append(item);
525 }
526 }
527 };
528 makeList(quickReplyApi.getSetByQr(this));
529 }
530 label.parentElement.append(list);
531 });
532 /**@type {HTMLInputElement}*/
533 const title = dom.querySelector('#qr--modal-title');
534 title.value = this.title;
535 title.addEventListener('input', () => {
536 this.updateTitle(title.value);
537 });
538 /**@type {HTMLElement}*/
539 const messageSyntaxInner = dom.querySelector('#qr--modal-messageSyntaxInner');
540 this.editorSyntax = messageSyntaxInner;
541 /**@type {HTMLInputElement}*/
542 const wrap = dom.querySelector('#qr--modal-wrap');
543 wrap.checked = JSON.parse(accountStorage.getItem('qr--wrap') ?? 'false');
544 wrap.addEventListener('click', () => {
545 accountStorage.setItem('qr--wrap', JSON.stringify(wrap.checked));
546 updateWrap();
547 });
548 const updateWrap = () => {
549 if (wrap.checked) {
550 message.style.whiteSpace = 'pre-wrap';
551 messageSyntaxInner.style.whiteSpace = 'pre-wrap';
552 if (this.clone) {
553 this.clone.style.whiteSpace = 'pre-wrap';
554 }
555 } else {
556 message.style.whiteSpace = 'pre';
557 messageSyntaxInner.style.whiteSpace = 'pre';
558 if (this.clone) {
559 this.clone.style.whiteSpace = 'pre';
560 }
561 }
562 updateScrollDebounced();
563 };
564 const updateScroll = (evt) => {
565 let left = message.scrollLeft;
566 let top = message.scrollTop;
567 if (evt) {
568 evt.preventDefault();
569 left = message.scrollLeft + evt.deltaX;
570 top = message.scrollTop + evt.deltaY;
571 message.scrollTo({
572 behavior: 'instant',
573 left,
574 top,
575 });
576 }
577 messageSyntaxInner.scrollTo({
578 behavior: 'instant',
579 left,
580 top,
581 });
582 };
583 const updateScrollDebounced = updateScroll;
584 const updateSyntaxEnabled = () => {
585 if (syntax.checked) {
586 dom.querySelector('#qr--modal-messageHolder').classList.remove('qr--noSyntax');
587 } else {
588 dom.querySelector('#qr--modal-messageHolder').classList.add('qr--noSyntax');
589 }
590 };
591 /**@type {HTMLInputElement}*/
592 const tabSize = dom.querySelector('#qr--modal-tabSize');
593 tabSize.value = JSON.parse(accountStorage.getItem('qr--tabSize') ?? '4');
594 const updateTabSize = () => {
595 message.style.tabSize = tabSize.value;
596 messageSyntaxInner.style.tabSize = tabSize.value;
597 updateScrollDebounced();
598 };
599 tabSize.addEventListener('change', () => {
600 accountStorage.setItem('qr--tabSize', JSON.stringify(Number(tabSize.value)));
601 updateTabSize();
602 });
603 /**@type {HTMLInputElement}*/
604 const executeShortcut = dom.querySelector('#qr--modal-executeShortcut');
605 executeShortcut.checked = JSON.parse(accountStorage.getItem('qr--executeShortcut') ?? 'true');
606 executeShortcut.addEventListener('click', () => {
607 accountStorage.setItem('qr--executeShortcut', JSON.stringify(executeShortcut.checked));
608 });
609 /**@type {HTMLInputElement}*/
610 const syntax = dom.querySelector('#qr--modal-syntax');
611 syntax.checked = JSON.parse(accountStorage.getItem('qr--syntax') ?? 'true');
612 syntax.addEventListener('click', () => {
613 accountStorage.setItem('qr--syntax', JSON.stringify(syntax.checked));
614 updateSyntaxEnabled();
615 });
616 // @ts-ignore
617 if (navigator.keyboard) {
618 // @ts-ignore
619 navigator.keyboard.getLayoutMap().then(it => dom.querySelector('#qr--modal-commentKey').textContent = it.get('Backslash'));
620 } else {
621 dom.querySelector('#qr--modal-commentKey').closest('small').remove();
622 }
623 this.editorMessageLabel = dom.querySelector('label[for="qr--modal-message"]');
624 /**@type {HTMLTextAreaElement}*/
625 const message = dom.querySelector('#qr--modal-message');
626 this.editorMessage = message;
627 message.value = this.message;
628 const updateMessageDebounced = debounce((value) => this.updateMessage(value), 10);
629 message.addEventListener('input', () => {
630 updateMessageDebounced(message.value);
631 updateScrollDebounced();
632 }, { passive: true });
633 const getLineStart = () => {
634 const start = message.selectionStart;
635 let lineStart;
636 if (start == 0 || message.value[start - 1] == '\n') {
637 // cursor is already at beginning of line
638 // -> keep start
639 lineStart = start;
640 } else {
641 // cursor is at end of line or somewhere in the line
642 // -> find last newline before cursor and start after that
643 lineStart = message.value.lastIndexOf('\n', start - 1) + 1;
644 }
645 return lineStart;
646 };
647 message.addEventListener('keydown', async (evt) => {
648 if (this.isExecuting) return;
649 if (evt.key == 'Tab' && !evt.shiftKey && !evt.ctrlKey && !evt.altKey) {
650 // increase indent
651 evt.preventDefault();
652 const start = message.selectionStart;
653 const end = message.selectionEnd;
654 if (end - start > 0 && message.value.substring(start, end).includes('\n')) {
655 evt.stopImmediatePropagation();
656 evt.stopPropagation();
657 const lineStart = getLineStart();
658 message.selectionStart = lineStart;
659 const affectedLines = message.value.substring(lineStart, end).split('\n');
660 // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history
661 document.execCommand('insertText', false, `\t${affectedLines.join('\n\t')}`);
662 message.selectionStart = start + 1;
663 message.selectionEnd = end + affectedLines.length;
664 message.dispatchEvent(new Event('input', { bubbles: true }));
665 } else if (!(ac.isReplaceable && ac.isActive)) {
666 evt.stopImmediatePropagation();
667 evt.stopPropagation();
668 // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history
669 document.execCommand('insertText', false, '\t');
670 message.dispatchEvent(new Event('input', { bubbles: true }));
671 }
672 } else if (evt.key == 'Tab' && evt.shiftKey && !evt.ctrlKey && !evt.altKey) {
673 // decrease indent
674 evt.preventDefault();
675 evt.stopImmediatePropagation();
676 evt.stopPropagation();
677 const start = message.selectionStart;
678 const end = message.selectionEnd;
679 const lineStart = getLineStart();
680 message.selectionStart = lineStart;
681 const affectedLines = message.value.substring(lineStart, end).split('\n');
682 const newText = affectedLines.map(it => it.replace(/^\t/, '')).join('\n');
683 const delta = affectedLines.join('\n').length - newText.length;
684 // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history
685 if (delta > 0) {
686 if (newText == '') {
687 document.execCommand('delete', false);
688 } else {
689 document.execCommand('insertText', false, newText);
690 }
691 message.selectionStart = start - (affectedLines[0].startsWith('\t') ? 1 : 0);
692 message.selectionEnd = end - delta;
693 message.dispatchEvent(new Event('input', { bubbles: true }));
694 } else {
695 message.selectionStart = start;
696 }
697 } else if (evt.key == 'Enter' && !evt.ctrlKey && !evt.shiftKey && !evt.altKey && !(ac.isReplaceable && ac.isActive)) {
698 // new line, keep indent
699 const start = message.selectionStart;
700 let lineStart = getLineStart();
701 const indent = /^([^\S\n]*)/.exec(message.value.slice(lineStart))[1] ?? '';
702 if (indent.length) {
703 evt.stopImmediatePropagation();
704 evt.stopPropagation();
705 evt.preventDefault();
706 // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history
707 document.execCommand('insertText', false, `\n${indent}`);
708 message.selectionStart = start + 1 + indent.length;
709 message.selectionEnd = message.selectionStart;
710 message.dispatchEvent(new Event('input', { bubbles: true }));
711 }
712 } else if (evt.key == 'Enter' && evt.ctrlKey && !evt.shiftKey && !evt.altKey) {
713 if (executeShortcut.checked) {
714 // execute QR
715 evt.stopImmediatePropagation();
716 evt.stopPropagation();
717 evt.preventDefault();
718 const selectionStart = message.selectionStart;
719 const selectionEnd = message.selectionEnd;
720 message.blur();
721 await this.executeFromEditor();
722 if (document.activeElement != message) {
723 message.focus();
724 message.selectionStart = selectionStart;
725 message.selectionEnd = selectionEnd;
726 }
727 }
728 } else if (evt.key == 'F9' && !evt.ctrlKey && !evt.shiftKey && !evt.altKey) {
729 // toggle breakpoint
730 evt.stopImmediatePropagation();
731 evt.stopPropagation();
732 evt.preventDefault();
733 preBreakPointStart = message.selectionStart;
734 preBreakPointEnd = message.selectionEnd;
735 toggleBreakpoint();
736 } else if (evt.code == 'Backslash' && evt.ctrlKey && !evt.shiftKey && !evt.altKey) {
737 // toggle block comment
738 // (evt.code will use the same physical key on the keyboard across different keyboard layouts)
739 evt.stopImmediatePropagation();
740 evt.stopPropagation();
741 evt.preventDefault();
742 // check if we are inside a comment -> uncomment
743 const parser = new SlashCommandParser();
744 parser.parse(message.value, false);
745 const start = message.selectionStart;
746 const end = message.selectionEnd;
747 const comment = parser.commandIndex.findLast(it => it.name == '*' && (it.start <= start && it.end >= start || it.start <= end && it.end >= end));
748 if (comment) {
749 // uncomment
750 let content = message.value.slice(comment.start + 1, comment.end - 1);
751 let len = content.length;
752 content = content.replace(/^ /, '');
753 const offsetStart = len - content.length;
754 len = content.length;
755 content = content.replace(/ $/, '');
756 const offsetEnd = len - content.length;
757 message.selectionStart = comment.start - 1;
758 message.selectionEnd = comment.end + 1;
759 // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history
760 document.execCommand('insertText', false, content);
761 message.selectionStart = start - (start >= comment.start ? 2 + offsetStart : 0);
762 message.selectionEnd = end - 2 - offsetStart - (end >= comment.end ? 2 + offsetEnd : 0);
763 } else {
764 // comment
765 const lineStart = getLineStart();
766 const lineEnd = message.value.indexOf('\n', end);
767 message.selectionStart = lineStart;
768 message.selectionEnd = lineEnd;
769 // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history
770 document.execCommand('insertText', false, `/* ${message.value.slice(lineStart, lineEnd)} *|`);
771 message.selectionStart = start + 3;
772 message.selectionEnd = end + 3;
773 }
774 message.dispatchEvent(new Event('input', { bubbles: true }));
775 }
776 });
777 const ac = await setSlashCommandAutoComplete(message, true);
778 message.addEventListener('wheel', (evt) => {
779 updateScrollDebounced(evt);
780 });
781 // @ts-ignore
782 message.addEventListener('scroll', (evt) => {
783 updateScrollDebounced();
784 });
785 let preBreakPointStart;
786 let preBreakPointEnd;
787 /**
788 * @param {SlashCommandBreakPoint} bp
789 */
790 const removeBreakpoint = (bp) => {
791 // start at -1 because "/" is not included in start-end
792 let start = bp.start - 1;
793 // step left until forward slash "/"
794 while (message.value[start] != '/') start--;
795 // step left while whitespace (except newline) before start
796 while (/[^\S\n]/.test(message.value[start - 1])) start--;
797 // if newline before indent, include the newline for removal
798 if (message.value[start - 1] == '\n') start--;
799 let end = bp.end;
800 // step right while whitespace
801 while (/\s/.test(message.value[end])) end++;
802 // if pipe after whitepace, include pipe for removal
803 if (message.value[end] == '|') end++;
804 message.selectionStart = start;
805 message.selectionEnd = end;
806 // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history
807 document.execCommand('insertText', false, '');
808 message.dispatchEvent(new Event('input', { bubbles: true }));
809 let postStart = preBreakPointStart;
810 let postEnd = preBreakPointEnd;
811 // set caret back to where it was
812 if (preBreakPointStart <= start) {
813 // selection start was before breakpoint: do nothing
814 } else if (preBreakPointStart > start && preBreakPointEnd < end) {
815 // selection start was inside breakpoint: move to index before breakpoint
816 postStart = start;
817 } else if (preBreakPointStart >= end) {
818 // selection start was behind breakpoint: move back by length of removed string
819 postStart = preBreakPointStart - (end - start);
820 }
821 if (preBreakPointEnd <= start) {
822 // do nothing
823 } else if (preBreakPointEnd > start && preBreakPointEnd < end) {
824 // selection end was inside breakpoint: move to index before breakpoint
825 postEnd = start;
826 } else if (preBreakPointEnd >= end) {
827 // selection end was behind breakpoint: move back by length of removed string
828 postEnd = preBreakPointEnd - (end - start);
829 }
830 return { start: postStart, end: postEnd };
831 };
832 /**
833 * @param {SlashCommandExecutor} cmd
834 */
835 const addBreakpoint = (cmd) => {
836 // start at -1 because "/" is not included in start-end
837 let start = cmd.start - 1;
838 let indent = '';
839 // step left until forward slash "/"
840 while (message.value[start] != '/') start--;
841 // step left while whitespace (except newline) before start, collect the whitespace to help build indentation
842 while (/[^\S\n]/.test(message.value[start - 1])) {
843 start--;
844 indent += message.value[start];
845 }
846 // if newline before indent, include the newline
847 if (message.value[start - 1] == '\n') {
848 start--;
849 indent = `\n${indent}`;
850 }
851 const breakpointText = `${indent}/breakpoint |`;
852 message.selectionStart = start;
853 message.selectionEnd = start;
854 // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history
855 document.execCommand('insertText', false, breakpointText);
856 message.dispatchEvent(new Event('input', { bubbles: true }));
857 return breakpointText.length;
858 };
859 const toggleBreakpoint = () => {
860 const idx = message.selectionStart;
861 let postStart = preBreakPointStart;
862 let postEnd = preBreakPointEnd;
863 const parser = new SlashCommandParser();
864 parser.parse(message.value, false);
865 const cmdIdx = parser.commandIndex.findLastIndex(it => it.start <= idx);
866 if (cmdIdx > -1) {
867 const cmd = parser.commandIndex[cmdIdx];
868 if (cmd instanceof SlashCommandBreakPoint) {
869 const bp = cmd;
870 const { start, end } = removeBreakpoint(bp);
871 postStart = start;
872 postEnd = end;
873 } else if (parser.commandIndex[cmdIdx - 1] instanceof SlashCommandBreakPoint) {
874 const bp = parser.commandIndex[cmdIdx - 1];
875 const { start, end } = removeBreakpoint(bp);
876 postStart = start;
877 postEnd = end;
878 } else {
879 const len = addBreakpoint(cmd);
880 postStart += len;
881 postEnd += len;
882 }
883 message.selectionStart = postStart;
884 message.selectionEnd = postEnd;
885 }
886 };
887 message.addEventListener('pointerdown', (evt) => {
888 if (!evt.ctrlKey || !evt.altKey) return;
889 preBreakPointStart = message.selectionStart;
890 preBreakPointEnd = message.selectionEnd;
891 });
892 message.addEventListener('pointerup', async (evt) => {
893 if (!evt.ctrlKey || !evt.altKey || message.selectionStart != message.selectionEnd) return;
894 toggleBreakpoint();
895 });
896 /** @type {any} */
897 const resizeListener = debounce((evt) => {
898 updateScrollDebounced(evt);
899 if (document.activeElement == message) {
900 message.blur();
901 message.focus();
902 }
903 });
904 window.addEventListener('resize', resizeListener);
905 updateSyntaxEnabled();
906 const updateSyntax = () => {
907 if (messageSyntaxInner && syntax.checked) {
908 morphdom(
909 messageSyntaxInner,
910 `<div>${hljs.highlight(`${message.value}${message.value.slice(-1) == '\n' ? ' ' : ''}`, { language: 'stscript', ignoreIllegals: true })?.value}</div>`,
911 { childrenOnly: true },
912 );
913 updateScrollDebounced();
914 }
915 };
916 let lastSyntaxUpdate = 0;
917 const fpsTime = 1000 / 30;
918 let lastMessageValue = null;
919 let wasSyntax = null;
920 const updateSyntaxLoop = () => {
921 const now = Date.now();
922 // fps limit
923 if (now - lastSyntaxUpdate < fpsTime) return requestAnimationFrame(updateSyntaxLoop);
924 // elements don't exist (yet?)
925 if (!messageSyntaxInner || !message) return requestAnimationFrame(updateSyntaxLoop);
926 // elements no longer part of the document
927 if (!messageSyntaxInner.closest('body')) return;
928 // debugger is running
929 if (this.isExecuting) {
930 lastMessageValue = null;
931 return requestAnimationFrame(updateSyntaxLoop);
932 }
933 // value hasn't changed
934 if (wasSyntax == syntax.checked && lastMessageValue == message.value) return requestAnimationFrame(updateSyntaxLoop);
935 wasSyntax = syntax.checked;
936 lastSyntaxUpdate = now;
937 lastMessageValue = message.value;
938 updateSyntax();
939 requestAnimationFrame(updateSyntaxLoop);
940 };
941 requestAnimationFrame(() => updateSyntaxLoop());
942 message.style.setProperty('text-shadow', 'none', 'important');
943 updateWrap();
944 updateTabSize();
945
946 // context menu
947 /**@type {HTMLTemplateElement}*/
948 const tpl = dom.querySelector('#qr--ctxItem');
949 const linkList = dom.querySelector('#qr--ctxEditor');
950 const fillQrSetSelect = (/**@type {HTMLSelectElement}*/select, /**@type {QuickReplyContextLink}*/ link) => {
951 [{ name: 'Select a QR set' }, ...QuickReplySet.list.toSorted((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()))].forEach(qrs => {
952 const opt = document.createElement('option'); {
953 opt.value = qrs.name;
954 opt.textContent = qrs.name;
955 opt.selected = qrs.name == link.set?.name;
956 select.append(opt);
957 }
958 });
959 };
960 const addCtxItem = (/**@type {QuickReplyContextLink}*/link, /**@type {number}*/idx) => {
961 /**@type {HTMLElement} */
962 // @ts-ignore
963 const itemDom = tpl.content.querySelector('.qr--ctxItem').cloneNode(true); {
964 itemDom.setAttribute('data-order', String(idx));
965
966 /**@type {HTMLSelectElement} */
967 const select = itemDom.querySelector('.qr--set');
968 fillQrSetSelect(select, link);
969 select.addEventListener('change', () => {
970 link.set = QuickReplySet.get(select.value);
971 this.updateContext();
972 });
973
974 /**@type {HTMLInputElement} */
975 const chain = itemDom.querySelector('.qr--isChained');
976 chain.checked = link.isChained;
977 chain.addEventListener('click', () => {
978 link.isChained = chain.checked;
979 this.updateContext();
980 });
981
982 itemDom.querySelector('.qr--delete').addEventListener('click', () => {
983 itemDom.remove();
984 this.contextList.splice(this.contextList.indexOf(link), 1);
985 this.updateContext();
986 });
987
988 linkList.append(itemDom);
989 }
990 };
991 [...this.contextList].forEach((link, idx) => addCtxItem(link, idx));
992 dom.querySelector('#qr--ctxAdd').addEventListener('click', () => {
993 const link = new QuickReplyContextLink();
994 this.contextList.push(link);
995 addCtxItem(link, this.contextList.length - 1);
996 });
997 const onContextSort = () => {
998 this.contextList = Array.from(linkList.querySelectorAll('.qr--ctxItem')).map((it, idx) => {
999 const link = this.contextList[Number(it.getAttribute('data-order'))];
1000 it.setAttribute('data-order', String(idx));
1001 return link;
1002 });
1003 this.updateContext();
1004 };
1005 // @ts-ignore
1006 $(linkList).sortable({
1007 delay: getSortableDelay(),
1008 stop: () => onContextSort(),
1009 });
1010
1011 // auto-exec
1012 /**@type {HTMLInputElement}*/
1013 const preventAutoExecute = dom.querySelector('#qr--preventAutoExecute');
1014 preventAutoExecute.checked = this.preventAutoExecute;
1015 preventAutoExecute.addEventListener('click', () => {
1016 this.preventAutoExecute = preventAutoExecute.checked;
1017 this.updateContext();
1018 });
1019 /**@type {HTMLInputElement}*/
1020 const isHidden = dom.querySelector('#qr--isHidden');
1021 isHidden.checked = this.isHidden;
1022 isHidden.addEventListener('click', () => {
1023 this.isHidden = isHidden.checked;
1024 this.updateContext();
1025 });
1026 /**@type {HTMLInputElement}*/
1027 const executeOnStartup = dom.querySelector('#qr--executeOnStartup');
1028 executeOnStartup.checked = this.executeOnStartup;
1029 executeOnStartup.addEventListener('click', () => {
1030 this.executeOnStartup = executeOnStartup.checked;
1031 this.updateContext();
1032 });
1033 /**@type {HTMLInputElement}*/
1034 const executeOnUser = dom.querySelector('#qr--executeOnUser');
1035 executeOnUser.checked = this.executeOnUser;
1036 executeOnUser.addEventListener('click', () => {
1037 this.executeOnUser = executeOnUser.checked;
1038 this.updateContext();
1039 });
1040 /**@type {HTMLInputElement}*/
1041 const executeOnAi = dom.querySelector('#qr--executeOnAi');
1042 executeOnAi.checked = this.executeOnAi;
1043 executeOnAi.addEventListener('click', () => {
1044 this.executeOnAi = executeOnAi.checked;
1045 this.updateContext();
1046 });
1047 /**@type {HTMLInputElement}*/
1048 const executeOnChatChange = dom.querySelector('#qr--executeOnChatChange');
1049 executeOnChatChange.checked = this.executeOnChatChange;
1050 executeOnChatChange.addEventListener('click', () => {
1051 this.executeOnChatChange = executeOnChatChange.checked;
1052 this.updateContext();
1053 });
1054 /**@type {HTMLInputElement}*/
1055 const executeOnGroupMemberDraft = dom.querySelector('#qr--executeOnGroupMemberDraft');
1056 executeOnGroupMemberDraft.checked = this.executeOnGroupMemberDraft;
1057 executeOnGroupMemberDraft.addEventListener('click', () => {
1058 this.executeOnGroupMemberDraft = executeOnGroupMemberDraft.checked;
1059 this.updateContext();
1060 });
1061 /**@type {HTMLInputElement}*/
1062 const executeBeforeGeneration = dom.querySelector('#qr--executeBeforeGeneration');
1063 executeBeforeGeneration.checked = this.executeBeforeGeneration;
1064 executeBeforeGeneration.addEventListener('click', () => {
1065 this.executeBeforeGeneration = executeBeforeGeneration.checked;
1066 this.updateContext();
1067 });
1068 /**@type {HTMLInputElement}*/
1069 const executeOnNewChat = dom.querySelector('#qr--executeOnNewChat');
1070 executeOnNewChat.checked = this.executeOnNewChat;
1071 executeOnNewChat.addEventListener('click', () => {
1072 this.executeOnNewChat = executeOnNewChat.checked;
1073 this.updateContext();
1074 });
1075 /**@type {HTMLInputElement}*/
1076 const automationId = dom.querySelector('#qr--automationId');
1077 automationId.value = this.automationId;
1078 automationId.addEventListener('input', () => {
1079 this.automationId = automationId.value;
1080 this.updateContext();
1081 });
1082
1083 /**@type {HTMLElement}*/
1084 const executeProgress = dom.querySelector('#qr--modal-executeProgress');
1085 this.editorExecuteProgress = executeProgress;
1086 /**@type {HTMLElement}*/
1087 const executeErrors = dom.querySelector('#qr--modal-executeErrors');
1088 this.editorExecuteErrors = executeErrors;
1089 /**@type {HTMLElement}*/
1090 const executeResult = dom.querySelector('#qr--modal-executeResult');
1091 this.editorExecuteResult = executeResult;
1092 /**@type {HTMLElement}*/
1093 const debugState = dom.querySelector('#qr--modal-debugState');
1094 this.editorDebugState = debugState;
1095 /**@type {HTMLElement}*/
1096 const executeBtn = dom.querySelector('#qr--modal-execute');
1097 this.editorExecuteBtn = executeBtn;
1098 executeBtn.addEventListener('click', async () => {
1099 await this.executeFromEditor();
1100 });
1101 /**@type {HTMLElement}*/
1102 const executeBtnPause = dom.querySelector('#qr--modal-pause');
1103 this.editorExecuteBtnPause = executeBtnPause;
1104 executeBtnPause.addEventListener('click', async () => {
1105 if (this.abortController) {
1106 if (this.abortController.signal.paused) {
1107 this.abortController.continue('Continue button clicked');
1108 this.editorExecuteProgress.classList.remove('qr--paused');
1109 } else {
1110 this.abortController.pause('Pause button clicked');
1111 this.editorExecuteProgress.classList.add('qr--paused');
1112 }
1113 }
1114 });
1115 /**@type {HTMLElement}*/
1116 const executeBtnStop = dom.querySelector('#qr--modal-stop');
1117 this.editorExecuteBtnStop = executeBtnStop;
1118 executeBtnStop.addEventListener('click', async () => {
1119 this.abortController?.abort('Stop button clicked');
1120 });
1121
1122 /**@type {HTMLTextAreaElement} */
1123 const inputOg = document.querySelector('#send_textarea');
1124 const inputMirror = dom.querySelector('#qr--modal-send_textarea');
1125 // @ts-ignore
1126 inputMirror.value = inputOg.value;
1127 const inputOgMo = new MutationObserver(muts => {
1128 if (muts.find(it => [...it.removedNodes].includes(inputMirror) || [...it.removedNodes].find(n => n.contains(inputMirror)))) {
1129 inputOg.removeEventListener('input', inputOgListener);
1130 }
1131 });
1132 inputOgMo.observe(document.body, { childList: true });
1133 const inputOgListener = () => {
1134 // @ts-ignore
1135 inputMirror.value = inputOg.value;
1136 };
1137 inputOg.addEventListener('input', inputOgListener);
1138 inputMirror.addEventListener('input', () => {
1139 // @ts-ignore
1140 inputOg.value = inputMirror.value;
1141 });
1142
1143 /**@type {HTMLElement}*/
1144 const resumeBtn = dom.querySelector('#qr--modal-resume');
1145 resumeBtn.addEventListener('click', () => {
1146 this.debugController?.resume();
1147 });
1148 /**@type {HTMLElement}*/
1149 const stepBtn = dom.querySelector('#qr--modal-step');
1150 stepBtn.addEventListener('click', () => {
1151 this.debugController?.step();
1152 });
1153 /**@type {HTMLElement}*/
1154 const stepIntoBtn = dom.querySelector('#qr--modal-stepInto');
1155 stepIntoBtn.addEventListener('click', () => {
1156 this.debugController?.stepInto();
1157 });
1158 /**@type {HTMLElement}*/
1159 const stepOutBtn = dom.querySelector('#qr--modal-stepOut');
1160 stepOutBtn.addEventListener('click', () => {
1161 this.debugController?.stepOut();
1162 });
1163 /**@type {HTMLElement}*/
1164 const minimizeBtn = dom.querySelector('#qr--modal-minimize');
1165 minimizeBtn.addEventListener('click', () => {
1166 this.editorDom.classList.add('qr--minimized');
1167 });
1168 const maximizeBtn = dom.querySelector('#qr--modal-maximize');
1169 maximizeBtn.addEventListener('click', () => {
1170 this.editorDom.classList.remove('qr--minimized');
1171 });
1172 /**@type {boolean}*/
1173 let isResizing = false;
1174 let resizeStart;
1175 let wStart;
1176 /**@type {HTMLElement}*/
1177 const resizeHandle = dom.querySelector('#qr--resizeHandle');
1178 resizeHandle.addEventListener('pointerdown', (evt) => {
1179 if (isResizing) return;
1180 isResizing = true;
1181 evt.preventDefault();
1182 resizeStart = evt.x;
1183 // @ts-ignore
1184 wStart = dom.querySelector('#qr--qrOptions').offsetWidth;
1185 const dragListener = debounce((evt) => {
1186 const w = wStart + resizeStart - evt.x;
1187 // @ts-ignore
1188 dom.querySelector('#qr--qrOptions').style.setProperty('--width', `${w}px`);
1189 }, 5);
1190 window.addEventListener('pointerup', () => {
1191 // @ts-ignore
1192 window.removeEventListener('pointermove', dragListener);
1193 isResizing = false;
1194 }, { once: true });
1195 // @ts-ignore
1196 window.addEventListener('pointermove', dragListener);
1197 });
1198
1199 await popupResult;
1200
1201 window.removeEventListener('resize', resizeListener);
1202 } else {
1203 warn('failed to fetch qrEditor template');
1204 }
1205 }
1206
1207 getEditorPosition(start, end, message = null) {
1208 const inputRect = this.editorMessage.getBoundingClientRect();
1209 const style = window.getComputedStyle(this.editorMessage);
1210 if (!this.clone) {
1211 this.clone = document.createElement('div');
1212 for (const key of style) {
1213 this.clone.style[key] = style[key];
1214 }
1215 this.clone.style.position = 'fixed';
1216 this.clone.style.visibility = 'hidden';
1217 const mo = new MutationObserver(muts => {
1218 if (muts.find(it => [...it.removedNodes].includes(this.editorMessage) || [...it.removedNodes].find(n => n.contains(this.editorMessage)))) {
1219 this.clone?.remove();
1220 this.clone = null;
1221 }
1222 });
1223 mo.observe(document.body, { childList: true });
1224 }
1225 document.body.append(this.clone);
1226 this.clone.style.width = `${inputRect.width}px`;
1227 this.clone.style.height = `${inputRect.height}px`;
1228 this.clone.style.left = `${inputRect.left}px`;
1229 this.clone.style.top = `${inputRect.top}px`;
1230 this.clone.style.whiteSpace = style.whiteSpace;
1231 this.clone.style.tabSize = style.tabSize;
1232 const text = message ?? this.editorMessage.value;
1233 const before = text.slice(0, start);
1234 this.clone.textContent = before;
1235 const locator = document.createElement('span');
1236 locator.textContent = text.slice(start, end);
1237 this.clone.append(locator);
1238 this.clone.append(text.slice(end));
1239 this.clone.scrollTop = this.editorSyntax.scrollTop;
1240 this.clone.scrollLeft = this.editorSyntax.scrollLeft;
1241 const locatorRect = locator.getBoundingClientRect();
1242 const bodyRect = document.body.getBoundingClientRect();
1243 const location = {
1244 left: locatorRect.left - bodyRect.left,
1245 right: locatorRect.right - bodyRect.left,
1246 top: locatorRect.top - bodyRect.top,
1247 bottom: locatorRect.bottom - bodyRect.top,
1248 };
1249 // this.clone.remove();
1250 return location;
1251 }
1252 async executeFromEditor() {
1253 if (this.isExecuting) return;
1254 this.editorPopup.onClosing = () => false;
1255 const uuidCheck = /^[0-9a-z]{8}(-[0-9a-z]{4}){3}-[0-9a-z]{12}$/;
1256 const oText = this.message;
1257 this.isExecuting = true;
1258 this.editorDom.classList.add('qr--isExecuting');
1259 const noSyntax = this.editorDom.querySelector('#qr--modal-messageHolder').classList.contains('qr--noSyntax');
1260 if (noSyntax) {
1261 this.editorDom.querySelector('#qr--modal-messageHolder').classList.remove('qr--noSyntax');
1262 }
1263 this.editorExecuteBtn.classList.add('qr--busy');
1264 this.editorExecuteProgress.style.setProperty('--prog', '0');
1265 this.editorExecuteErrors.classList.remove('qr--hasErrors');
1266 this.editorExecuteResult.classList.remove('qr--hasResult');
1267 this.editorExecuteProgress.classList.remove('qr--error');
1268 this.editorExecuteProgress.classList.remove('qr--success');
1269 this.editorExecuteProgress.classList.remove('qr--paused');
1270 this.editorExecuteProgress.classList.remove('qr--aborted');
1271 this.editorExecuteErrors.innerHTML = '';
1272 this.editorExecuteResult.innerHTML = '';
1273 const syntax = this.editorDom.querySelector('#qr--modal-messageSyntaxInner');
1274 const updateScroll = (evt) => {
1275 let left = syntax.scrollLeft;
1276 let top = syntax.scrollTop;
1277 if (evt) {
1278 evt.preventDefault();
1279 left = syntax.scrollLeft + evt.deltaX;
1280 top = syntax.scrollTop + evt.deltaY;
1281 syntax.scrollTo({
1282 behavior: 'instant',
1283 left,
1284 top,
1285 });
1286 }
1287 this.editorMessage.scrollTo({
1288 behavior: 'instant',
1289 left,
1290 top,
1291 });
1292 };
1293 const updateScrollDebounced = updateScroll;
1294 syntax.addEventListener('wheel', (evt) => {
1295 updateScrollDebounced(evt);
1296 });
1297 // @ts-ignore
1298 syntax.addEventListener('scroll', (evt) => {
1299 updateScrollDebounced();
1300 });
1301 try {
1302 this.abortController = new SlashCommandAbortController();
1303 this.debugController = new SlashCommandDebugController();
1304 this.debugController.onBreakPoint = async (closure, executor) => {
1305 this.editorDom.classList.add('qr--isPaused');
1306 syntax.innerHTML = hljs.highlight(`${closure.fullText}${closure.fullText.slice(-1) == '\n' ? ' ' : ''}`, { language: 'stscript', ignoreIllegals: true })?.value;
1307 this.editorMessageLabel.innerHTML = '';
1308 if (uuidCheck.test(closure.source)) {
1309 const p0 = document.createElement('span'); {
1310 p0.textContent = 'anonymous: ';
1311 this.editorMessageLabel.append(p0);
1312 }
1313 const p1 = document.createElement('strong'); {
1314 p1.textContent = executor.source.slice(0, 5);
1315 this.editorMessageLabel.append(p1);
1316 }
1317 const p2 = document.createElement('span'); {
1318 p2.textContent = executor.source.slice(5, -5);
1319 this.editorMessageLabel.append(p2);
1320 }
1321 const p3 = document.createElement('strong'); {
1322 p3.textContent = executor.source.slice(-5);
1323 this.editorMessageLabel.append(p3);
1324 }
1325 } else {
1326 this.editorMessageLabel.textContent = executor.source;
1327 }
1328 const source = closure.source;
1329 this.editorDebugState.innerHTML = '';
1330 let ci = -1;
1331 const varNames = [];
1332 const macroNames = [];
1333 /**
1334 * @param {SlashCommandScope} scope
1335 */
1336 const buildVars = (scope, isCurrent = false) => {
1337 if (!isCurrent) {
1338 ci--;
1339 }
1340 const c = this.debugController.stack.slice(ci)[0];
1341 const wrap = document.createElement('div'); {
1342 wrap.classList.add('qr--scope');
1343 if (isCurrent) {
1344 const executor = this.debugController.cmdStack.slice(-1)[0];
1345 { // named args
1346 const namedTitle = document.createElement('div'); {
1347 namedTitle.classList.add('qr--title');
1348 namedTitle.textContent = `Named Args - /${executor.name}`;
1349 if (executor.command.name == 'run') {
1350 namedTitle.textContent += `${(executor.name == ':' ? '' : ' ')}${executor.unnamedArgumentList[0]?.value}`;
1351 }
1352 wrap.append(namedTitle);
1353 }
1354 const keys = new Set([...Object.keys(this.debugController.namedArguments ?? {}), ...(executor.namedArgumentList ?? []).map(it => it.name)]);
1355 for (const key of keys) {
1356 if (key[0] == '_') continue;
1357 const item = document.createElement('div'); {
1358 item.classList.add('qr--var');
1359 const k = document.createElement('div'); {
1360 k.classList.add('qr--key');
1361 k.textContent = key;
1362 item.append(k);
1363 }
1364 const vUnresolved = document.createElement('div'); {
1365 vUnresolved.classList.add('qr--val');
1366 vUnresolved.classList.add('qr--singleCol');
1367 const val = executor.namedArgumentList.find(it => it.name == key)?.value;
1368 if (val instanceof SlashCommandClosure) {
1369 vUnresolved.classList.add('qr--closure');
1370 vUnresolved.title = val.rawText;
1371 vUnresolved.textContent = val.toString();
1372 } else if (val === undefined) {
1373 vUnresolved.classList.add('qr--undefined');
1374 vUnresolved.textContent = 'undefined';
1375 } else {
1376 let jsonVal;
1377 try { jsonVal = JSON.parse(val); } catch { /* empty */ }
1378 if (jsonVal && typeof jsonVal == 'object') {
1379 vUnresolved.textContent = JSON.stringify(jsonVal, null, 2);
1380 } else {
1381 vUnresolved.textContent = val;
1382 vUnresolved.classList.add('qr--simple');
1383 }
1384 }
1385 item.append(vUnresolved);
1386 }
1387 const vResolved = document.createElement('div'); {
1388 vResolved.classList.add('qr--val');
1389 vResolved.classList.add('qr--singleCol');
1390 if (this.debugController.namedArguments === undefined) {
1391 vResolved.classList.add('qr--unresolved');
1392 } else {
1393 const val = this.debugController.namedArguments?.[key];
1394 if (val instanceof SlashCommandClosure) {
1395 vResolved.classList.add('qr--closure');
1396 vResolved.title = val.rawText;
1397 vResolved.textContent = val.toString();
1398 } else if (val === undefined) {
1399 vResolved.classList.add('qr--undefined');
1400 vResolved.textContent = 'undefined';
1401 } else {
1402 let jsonVal;
1403 try { jsonVal = JSON.parse(val); } catch { /* empty */ }
1404 if (jsonVal && typeof jsonVal == 'object') {
1405 vResolved.textContent = JSON.stringify(jsonVal, null, 2);
1406 } else {
1407 vResolved.textContent = val;
1408 vResolved.classList.add('qr--simple');
1409 }
1410 }
1411 }
1412 item.append(vResolved);
1413 }
1414 wrap.append(item);
1415 }
1416 }
1417 }
1418 { // unnamed args
1419 const unnamedTitle = document.createElement('div'); {
1420 unnamedTitle.classList.add('qr--title');
1421 unnamedTitle.textContent = `Unnamed Args - /${executor.name}`;
1422 if (executor.command.name == 'run') {
1423 unnamedTitle.textContent += `${(executor.name == ':' ? '' : ' ')}${executor.unnamedArgumentList[0]?.value}`;
1424 }
1425 wrap.append(unnamedTitle);
1426 }
1427 let i = 0;
1428 let unnamed = this.debugController.unnamedArguments ?? [];
1429 if (!Array.isArray(unnamed)) unnamed = [unnamed];
1430 // @ts-ignore
1431 while (unnamed.length < executor.unnamedArgumentList?.length ?? 0) unnamed.push(undefined);
1432 // @ts-ignore
1433 unnamed = unnamed.map((it, idx) => [executor.unnamedArgumentList?.[idx], it]);
1434 // @ts-ignore
1435 for (const arg of unnamed) {
1436 i++;
1437 const item = document.createElement('div'); {
1438 item.classList.add('qr--var');
1439 const k = document.createElement('div'); {
1440 k.classList.add('qr--key');
1441 k.textContent = i.toString();
1442 item.append(k);
1443 }
1444 const vUnresolved = document.createElement('div'); {
1445 vUnresolved.classList.add('qr--val');
1446 vUnresolved.classList.add('qr--singleCol');
1447 const val = arg[0]?.value;
1448 if (val instanceof SlashCommandClosure) {
1449 vUnresolved.classList.add('qr--closure');
1450 vUnresolved.title = val.rawText;
1451 vUnresolved.textContent = val.toString();
1452 } else if (val === undefined) {
1453 vUnresolved.classList.add('qr--undefined');
1454 vUnresolved.textContent = 'undefined';
1455 } else {
1456 let jsonVal;
1457 try { jsonVal = JSON.parse(val); } catch { /* empty */ }
1458 if (jsonVal && typeof jsonVal == 'object') {
1459 vUnresolved.textContent = JSON.stringify(jsonVal, null, 2);
1460 } else {
1461 vUnresolved.textContent = val;
1462 vUnresolved.classList.add('qr--simple');
1463 }
1464 }
1465 item.append(vUnresolved);
1466 }
1467 const vResolved = document.createElement('div'); {
1468 vResolved.classList.add('qr--val');
1469 vResolved.classList.add('qr--singleCol');
1470 if (this.debugController.unnamedArguments === undefined) {
1471 vResolved.classList.add('qr--unresolved');
1472 } else if ((Array.isArray(this.debugController.unnamedArguments) ? this.debugController.unnamedArguments : [this.debugController.unnamedArguments]).length < i) {
1473 // do nothing
1474 } else {
1475 const val = arg[1];
1476 if (val instanceof SlashCommandClosure) {
1477 vResolved.classList.add('qr--closure');
1478 vResolved.title = val.rawText;
1479 vResolved.textContent = val.toString();
1480 } else if (val === undefined) {
1481 vResolved.classList.add('qr--undefined');
1482 vResolved.textContent = 'undefined';
1483 } else {
1484 let jsonVal;
1485 try { jsonVal = JSON.parse(val); } catch { /* empty */ }
1486 if (jsonVal && typeof jsonVal == 'object') {
1487 vResolved.textContent = JSON.stringify(jsonVal, null, 2);
1488 } else {
1489 vResolved.textContent = val;
1490 vResolved.classList.add('qr--simple');
1491 }
1492 }
1493 }
1494 item.append(vResolved);
1495 }
1496 wrap.append(item);
1497 }
1498 }
1499 }
1500 }
1501 // current scope
1502 const title = document.createElement('div'); {
1503 title.classList.add('qr--title');
1504 title.textContent = isCurrent ? 'Current Scope' : 'Parent Scope';
1505 if (c.source == source) {
1506 let hi;
1507 title.addEventListener('pointerenter', () => {
1508 const loc = this.getEditorPosition(Math.max(0, c.executorList[0].start - 1), c.executorList.slice(-1)[0].end, c.fullText);
1509 const layer = syntax.getBoundingClientRect();
1510 hi = document.createElement('div');
1511 hi.classList.add('qr--highlight-secondary');
1512 hi.style.left = `${loc.left - layer.left}px`;
1513 hi.style.width = `${loc.right - loc.left}px`;
1514 hi.style.top = `${loc.top - layer.top + syntax.scrollTop}px`;
1515 hi.style.height = `${loc.bottom - loc.top}px`;
1516 syntax.append(hi);
1517 });
1518 title.addEventListener('pointerleave', () => hi?.remove());
1519 }
1520 wrap.append(title);
1521 }
1522 for (const key of Object.keys(scope.variables)) {
1523 const isHidden = varNames.includes(key);
1524 if (!isHidden) varNames.push(key);
1525 const item = document.createElement('div'); {
1526 item.classList.add('qr--var');
1527 if (isHidden) item.classList.add('qr--isHidden');
1528 const k = document.createElement('div'); {
1529 k.classList.add('qr--key');
1530 k.textContent = key;
1531 item.append(k);
1532 }
1533 const v = document.createElement('div'); {
1534 v.classList.add('qr--val');
1535 const val = scope.variables[key];
1536 if (val instanceof SlashCommandClosure) {
1537 v.classList.add('qr--closure');
1538 v.title = val.rawText;
1539 v.textContent = val.toString();
1540 } else if (val === undefined) {
1541 v.classList.add('qr--undefined');
1542 v.textContent = 'undefined';
1543 } else {
1544 let jsonVal;
1545 try { jsonVal = JSON.parse(val); } catch { /* empty */ }
1546 if (jsonVal && typeof jsonVal == 'object') {
1547 v.textContent = JSON.stringify(jsonVal, null, 2);
1548 } else {
1549 v.textContent = val;
1550 v.classList.add('qr--simple');
1551 }
1552 }
1553 item.append(v);
1554 }
1555 wrap.append(item);
1556 }
1557 }
1558 for (const key of Object.keys(scope.macros)) {
1559 const isHidden = macroNames.includes(key);
1560 if (!isHidden) macroNames.push(key);
1561 const item = document.createElement('div'); {
1562 item.classList.add('qr--macro');
1563 if (isHidden) item.classList.add('qr--isHidden');
1564 const k = document.createElement('div'); {
1565 k.classList.add('qr--key');
1566 k.textContent = key;
1567 item.append(k);
1568 }
1569 const v = document.createElement('div'); {
1570 v.classList.add('qr--val');
1571 const val = scope.macros[key];
1572 if (val instanceof SlashCommandClosure) {
1573 v.classList.add('qr--closure');
1574 v.title = val.rawText;
1575 v.textContent = val.toString();
1576 } else if (val === undefined) {
1577 v.classList.add('qr--undefined');
1578 v.textContent = 'undefined';
1579 } else {
1580 let jsonVal;
1581 try { jsonVal = JSON.parse(val); } catch { /* empty */ }
1582 if (jsonVal && typeof jsonVal == 'object') {
1583 v.textContent = JSON.stringify(jsonVal, null, 2);
1584 } else {
1585 v.textContent = val;
1586 v.classList.add('qr--simple');
1587 }
1588 }
1589 item.append(v);
1590 }
1591 wrap.append(item);
1592 }
1593 }
1594 const pipeItem = document.createElement('div'); {
1595 pipeItem.classList.add('qr--pipe');
1596 const k = document.createElement('div'); {
1597 k.classList.add('qr--key');
1598 k.textContent = 'pipe';
1599 pipeItem.append(k);
1600 }
1601 const v = document.createElement('div'); {
1602 v.classList.add('qr--val');
1603 const val = scope.pipe;
1604 if (val instanceof SlashCommandClosure) {
1605 v.classList.add('qr--closure');
1606 v.title = val.rawText;
1607 v.textContent = val.toString();
1608 } else if (val === undefined) {
1609 v.classList.add('qr--undefined');
1610 v.textContent = 'undefined';
1611 } else {
1612 let jsonVal;
1613 try { jsonVal = JSON.parse(val); } catch { /* empty */ }
1614 if (jsonVal && typeof jsonVal == 'object') {
1615 v.textContent = JSON.stringify(jsonVal, null, 2);
1616 } else {
1617 v.textContent = val;
1618 v.classList.add('qr--simple');
1619 }
1620 }
1621 pipeItem.append(v);
1622 }
1623 wrap.append(pipeItem);
1624 }
1625 if (scope.parent) {
1626 wrap.append(buildVars(scope.parent));
1627 }
1628 }
1629 return wrap;
1630 };
1631 const buildStack = () => {
1632 const wrap = document.createElement('div'); {
1633 wrap.classList.add('qr--stack');
1634 const title = document.createElement('div'); {
1635 title.classList.add('qr--title');
1636 title.textContent = 'Call Stack';
1637 wrap.append(title);
1638 }
1639 let ei = -1;
1640 for (const executor of this.debugController.cmdStack.toReversed()) {
1641 ei++;
1642 const c = this.debugController.stack.toReversed()[ei];
1643 const item = document.createElement('div'); {
1644 item.classList.add('qr--item');
1645 if (executor.source == source) {
1646 let hi;
1647 item.addEventListener('pointerenter', () => {
1648 const loc = this.getEditorPosition(Math.max(0, executor.start - 1), executor.end, c.fullText);
1649 const layer = syntax.getBoundingClientRect();
1650 hi = document.createElement('div');
1651 hi.classList.add('qr--highlight-secondary');
1652 hi.style.left = `${loc.left - layer.left}px`;
1653 hi.style.width = `${loc.right - loc.left}px`;
1654 hi.style.top = `${loc.top - layer.top + syntax.scrollTop}px`;
1655 hi.style.height = `${loc.bottom - loc.top}px`;
1656 syntax.append(hi);
1657 });
1658 item.addEventListener('pointerleave', () => hi?.remove());
1659 }
1660 const cmd = document.createElement('div'); {
1661 cmd.classList.add('qr--cmd');
1662 cmd.textContent = `/${executor.name}`;
1663 if (executor.command.name == 'run') {
1664 cmd.textContent += `${(executor.name == ':' ? '' : ' ')}${executor.unnamedArgumentList[0]?.value}`;
1665 }
1666 item.append(cmd);
1667 }
1668 const src = document.createElement('div'); {
1669 src.classList.add('qr--source');
1670 const line = closure.fullText.slice(0, executor.start).split('\n').length;
1671 if (uuidCheck.test(executor.source)) {
1672 const p1 = document.createElement('span'); {
1673 p1.classList.add('qr--fixed');
1674 p1.textContent = executor.source.slice(0, 5);
1675 src.append(p1);
1676 }
1677 const p2 = document.createElement('span'); {
1678 p2.classList.add('qr--truncated');
1679 p2.textContent = '…';
1680 src.append(p2);
1681 }
1682 const p3 = document.createElement('span'); {
1683 p3.classList.add('qr--fixed');
1684 p3.textContent = `${executor.source.slice(-5)}:${line}`;
1685 src.append(p3);
1686 }
1687 src.title = `anonymous: ${executor.source}`;
1688 } else {
1689 src.textContent = `${executor.source}:${line}`;
1690 }
1691 item.append(src);
1692 }
1693 wrap.append(item);
1694 }
1695 }
1696 }
1697 return wrap;
1698 };
1699 this.editorDebugState.append(buildVars(closure.scope, true));
1700 this.editorDebugState.append(buildStack());
1701 this.editorDebugState.classList.add('qr--active');
1702 const loc = this.getEditorPosition(Math.max(0, executor.start - 1), executor.end, closure.fullText);
1703 const layer = syntax.getBoundingClientRect();
1704 const hi = document.createElement('div');
1705 hi.classList.add('qr--highlight');
1706 if (this.debugController.namedArguments === undefined) {
1707 hi.classList.add('qr--unresolved');
1708 }
1709 hi.style.left = `${loc.left - layer.left}px`;
1710 hi.style.width = `${loc.right - loc.left}px`;
1711 hi.style.top = `${loc.top - layer.top + syntax.scrollTop}px`;
1712 hi.style.height = `${loc.bottom - loc.top}px`;
1713 syntax.append(hi);
1714 const isStepping = await this.debugController.awaitContinue();
1715 hi.remove();
1716 this.editorDebugState.textContent = '';
1717 this.editorDebugState.classList.remove('qr--active');
1718 this.editorDom.classList.remove('qr--isPaused');
1719 return isStepping;
1720 };
1721 const result = await this.onDebug(this);
1722 if (this.abortController?.signal?.aborted) {
1723 this.editorExecuteProgress.classList.add('qr--aborted');
1724 } else {
1725 this.editorExecuteResult.textContent = result?.toString();
1726 this.editorExecuteResult.classList.add('qr--hasResult');
1727 this.editorExecuteProgress.classList.add('qr--success');
1728 }
1729 this.editorExecuteProgress.classList.remove('qr--paused');
1730 } catch (ex) {
1731 this.editorExecuteErrors.classList.add('qr--hasErrors');
1732 this.editorExecuteProgress.classList.add('qr--error');
1733 this.editorExecuteProgress.classList.remove('qr--paused');
1734 if (ex instanceof SlashCommandParserError) {
1735 this.editorExecuteErrors.innerHTML = `
1736 <div>${ex.message}</div>
1737 <div>Line: ${ex.line} Column: ${ex.column}</div>
1738 <pre style="text-align:left;">${ex.hint}</pre>
1739 `;
1740 } else {
1741 this.editorExecuteErrors.innerHTML = `
1742 <div>${ex.message}</div>
1743 `;
1744 }
1745 }
1746 if (noSyntax) {
1747 this.editorDom.querySelector('#qr--modal-messageHolder').classList.add('qr--noSyntax');
1748 }
1749 this.editorMessageLabel.innerHTML = '';
1750 this.editorMessageLabel.textContent = 'Message / Command: ';
1751 this.editorMessage.value = oText;
1752 this.editorMessage.dispatchEvent(new Event('input', { bubbles: true }));
1753 this.editorExecutePromise = null;
1754 this.editorExecuteBtn.classList.remove('qr--busy');
1755 this.editorDom.classList.remove('qr--isExecuting');
1756 this.isExecuting = false;
1757 this.editorPopup.onClosing = null;
1758 }
1759
1760 updateEditorProgress(done, total) {
1761 this.editorExecuteProgress.style.setProperty('--prog', `${done / total * 100}`);
1762 }
1763
1764
1765 delete() {
1766 if (this.onDelete) {
1767 this.unrender();
1768 this.unrenderSettings();
1769 this.onDelete(this);
1770 }
1771 }
1772
1773 /**
1774 * @param {string} value
1775 */
1776 updateMessage(value) {
1777 if (this.onUpdate) {
1778 if (this.settingsDomMessage && this.settingsDomMessage.value != value) {
1779 this.settingsDomMessage.value = value;
1780 }
1781 this.message = value;
1782 this.updateRender();
1783 this.onUpdate(this);
1784 }
1785 }
1786
1787 /**
1788 * @param {string} value
1789 */
1790 updateIcon(value) {
1791 if (this.onUpdate) {
1792 if (value === null) return;
1793 if (this.settingsDomIcon) {
1794 if (this.icon != value) {
1795 if (value == '') {
1796 if (this.icon) {
1797 this.settingsDomIcon.classList.remove(this.icon);
1798 }
1799 this.settingsDomIcon.textContent = '…';
1800 this.settingsDomIcon.classList.remove('fa-solid');
1801 } else {
1802 if (this.icon) {
1803 this.settingsDomIcon.classList.remove(this.icon);
1804 } else {
1805 this.settingsDomIcon.classList.add('fa-solid');
1806 }
1807 this.settingsDomIcon.classList.add(value);
1808 }
1809 }
1810 }
1811 this.icon = value;
1812 this.updateRender();
1813 this.onUpdate(this);
1814 }
1815 }
1816
1817 /**
1818 * @param {boolean} value
1819 */
1820 updateShowLabel(value) {
1821 if (this.onUpdate) {
1822 this.showLabel = value;
1823 this.updateRender();
1824 this.onUpdate(this);
1825 }
1826 }
1827
1828 /**
1829 * @param {string} value
1830 */
1831 updateLabel(value) {
1832 if (this.onUpdate) {
1833 if (this.settingsDomLabel && this.settingsDomLabel.value != value) {
1834 this.settingsDomLabel.value = value;
1835 }
1836 this.label = value;
1837 this.updateRender();
1838 this.onUpdate(this);
1839 }
1840 }
1841
1842 /**
1843 * @param {string} value
1844 */
1845 updateTitle(value) {
1846 if (this.onUpdate) {
1847 this.title = value;
1848 this.updateRender();
1849 this.onUpdate(this);
1850 }
1851 }
1852
1853 updateContext() {
1854 if (this.onUpdate) {
1855 this.updateRender();
1856 this.onUpdate(this);
1857 }
1858 }
1859 addContextLink(cl) {
1860 this.contextList.push(cl);
1861 this.updateContext();
1862 }
1863 removeContextLink(setName) {
1864 const idx = this.contextList.findIndex(it => it.set.name == setName);
1865 if (idx > -1) {
1866 this.contextList.splice(idx, 1);
1867 this.updateContext();
1868 }
1869 }
1870 clearContextLinks() {
1871 if (this.contextList.length) {
1872 this.contextList.splice(0, this.contextList.length);
1873 this.updateContext();
1874 }
1875 }
1876
1877
1878 async execute(args = {}, isEditor = false, isRun = false, options = {}) {
1879 if (this.message?.length > 0 && this.onExecute) {
1880 const scope = new SlashCommandScope();
1881 for (const key of Object.keys(args)) {
1882 if (key[0] == '_') continue;
1883 if (key == 'isAutoExecute') continue;
1884 scope.setMacro(`arg::${key}`, args[key]);
1885 }
1886 scope.setMacro('arg::*', '');
1887 if (isEditor) {
1888 this.abortController = new SlashCommandAbortController();
1889 }
1890 return await this.onExecute(this, {
1891 message: this.message,
1892 isAutoExecute: args.isAutoExecute ?? false,
1893 isEditor,
1894 isRun,
1895 scope,
1896 executionOptions: options,
1897 });
1898 }
1899 }
1900
1901
1902 toJSON() {
1903 return {
1904 id: this.id,
1905 icon: this.icon,
1906 showLabel: this.showLabel,
1907 label: this.label,
1908 title: this.title,
1909 message: this.message,
1910 contextList: this.contextList,
1911 preventAutoExecute: this.preventAutoExecute,
1912 isHidden: this.isHidden,
1913 executeOnStartup: this.executeOnStartup,
1914 executeOnUser: this.executeOnUser,
1915 executeOnAi: this.executeOnAi,
1916 executeOnChatChange: this.executeOnChatChange,
1917 executeOnGroupMemberDraft: this.executeOnGroupMemberDraft,
1918 executeOnNewChat: this.executeOnNewChat,
1919 executeBeforeGeneration: this.executeBeforeGeneration,
1920 automationId: this.automationId,
1921 };
1922 }
1923}