Spaces:
Paused
Paused
| import { hljs, morphdom } from '../../../../lib.js'; | |
| import { POPUP_RESULT, POPUP_TYPE, Popup } from '../../../popup.js'; | |
| import { setSlashCommandAutoComplete } from '../../../slash-commands.js'; | |
| import { SlashCommandAbortController } from '../../../slash-commands/SlashCommandAbortController.js'; | |
| import { SlashCommandBreakPoint } from '../../../slash-commands/SlashCommandBreakPoint.js'; | |
| import { SlashCommandClosure } from '../../../slash-commands/SlashCommandClosure.js'; | |
| import { SlashCommandClosureResult } from '../../../slash-commands/SlashCommandClosureResult.js'; | |
| import { SlashCommandDebugController } from '../../../slash-commands/SlashCommandDebugController.js'; | |
| import { SlashCommandExecutor } from '../../../slash-commands/SlashCommandExecutor.js'; | |
| import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js'; | |
| import { SlashCommandParserError } from '../../../slash-commands/SlashCommandParserError.js'; | |
| import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js'; | |
| import { accountStorage } from '../../../util/AccountStorage.js'; | |
| import { debounce, delay, getSortableDelay, showFontAwesomePicker } from '../../../utils.js'; | |
| import { log, quickReplyApi, warn } from '../index.js'; | |
| import { QuickReplyContextLink } from './QuickReplyContextLink.js'; | |
| import { QuickReplySet } from './QuickReplySet.js'; | |
| import { ContextMenu } from './ui/ctx/ContextMenu.js'; | |
| export class QuickReply { | |
| /** | |
| * @param {{ id?: number; contextList?: any; }} props | |
| */ | |
| static from(props) { | |
| props.contextList = (props.contextList ?? []).map((/** @type {any} */ it)=>QuickReplyContextLink.from(it)); | |
| return Object.assign(new this(), props); | |
| } | |
| /**@type {number}*/ id; | |
| /**@type {string}*/ icon; | |
| /**@type {string}*/ label = ''; | |
| /**@type {boolean}*/ showLabel = false; | |
| /**@type {string}*/ title = ''; | |
| /**@type {string}*/ message = ''; | |
| /**@type {QuickReplyContextLink[]}*/ contextList; | |
| /**@type {boolean}*/ preventAutoExecute = true; | |
| /**@type {boolean}*/ isHidden = false; | |
| /**@type {boolean}*/ executeOnStartup = false; | |
| /**@type {boolean}*/ executeOnUser = false; | |
| /**@type {boolean}*/ executeOnAi = false; | |
| /**@type {boolean}*/ executeOnChatChange = false; | |
| /**@type {boolean}*/ executeOnGroupMemberDraft = false; | |
| /**@type {boolean}*/ executeOnNewChat = false; | |
| /**@type {string}*/ automationId = ''; | |
| /**@type {function}*/ onExecute; | |
| /** @type {(qr:QuickReply)=>AsyncGenerator<SlashCommandClosureResult|{closure:SlashCommandClosure, executor:SlashCommandExecutor|SlashCommandClosureResult}, SlashCommandClosureResult, boolean>} */ onDebug; | |
| /**@type {function}*/ onDelete; | |
| /**@type {function}*/ onUpdate; | |
| /**@type {function}*/ onInsertBefore; | |
| /**@type {function}*/ onTransfer; | |
| /**@type {HTMLElement}*/ dom; | |
| /**@type {HTMLElement}*/ domIcon; | |
| /**@type {HTMLElement}*/ domLabel; | |
| /**@type {HTMLElement}*/ settingsDom; | |
| /**@type {HTMLElement}*/ settingsDomIcon; | |
| /**@type {HTMLInputElement}*/ settingsDomLabel; | |
| /**@type {HTMLTextAreaElement}*/ settingsDomMessage; | |
| /**@type {Popup}*/ editorPopup; | |
| /**@type {HTMLElement}*/ editorDom; | |
| /**@type {HTMLTextAreaElement}*/ editorMessage; | |
| /**@type {HTMLTextAreaElement}*/ editorMessageLabel; | |
| /**@type {HTMLElement}*/ editorSyntax; | |
| /**@type {HTMLElement}*/ editorExecuteBtn; | |
| /**@type {HTMLElement}*/ editorExecuteBtnPause; | |
| /**@type {HTMLElement}*/ editorExecuteBtnStop; | |
| /**@type {HTMLElement}*/ editorExecuteProgress; | |
| /**@type {HTMLElement}*/ editorExecuteErrors; | |
| /**@type {HTMLElement}*/ editorExecuteResult; | |
| /**@type {HTMLElement}*/ editorDebugState; | |
| /**@type {Promise}*/ editorExecutePromise; | |
| /**@type {boolean}*/ isExecuting; | |
| /**@type {SlashCommandAbortController}*/ abortController; | |
| /**@type {SlashCommandDebugController}*/ debugController; | |
| get hasContext() { | |
| return this.contextList && this.contextList.filter(it => it.set).length > 0; | |
| } | |
| unrender() { | |
| this.dom?.remove(); | |
| this.dom = null; | |
| } | |
| updateRender() { | |
| if (!this.dom) return; | |
| this.dom.title = this.title || this.message; | |
| if (this.icon) { | |
| this.domIcon.classList.remove('qr--hidden'); | |
| if (this.showLabel) this.domLabel.classList.remove('qr--hidden'); | |
| else this.domLabel.classList.add('qr--hidden'); | |
| } else { | |
| this.domIcon.classList.add('qr--hidden'); | |
| this.domLabel.classList.remove('qr--hidden'); | |
| } | |
| this.domLabel.textContent = this.label; | |
| this.dom.classList[this.hasContext ? 'add' : 'remove']('qr--hasCtx'); | |
| } | |
| render() { | |
| this.unrender(); | |
| if (!this.dom) { | |
| const root = document.createElement('div'); { | |
| this.dom = root; | |
| root.classList.add('qr--button'); | |
| root.classList.add('menu_button'); | |
| if (this.hasContext) { | |
| root.classList.add('qr--hasCtx'); | |
| } | |
| root.title = this.title || this.message; | |
| root.addEventListener('contextmenu', (evt) => { | |
| log('contextmenu', this, this.hasContext); | |
| if (this.hasContext) { | |
| evt.preventDefault(); | |
| evt.stopPropagation(); | |
| const menu = new ContextMenu(this); | |
| menu.show(evt); | |
| } | |
| }); | |
| root.addEventListener('click', (evt)=>{ | |
| if (evt.ctrlKey) { | |
| this.showEditor(); | |
| return; | |
| } | |
| this.execute(); | |
| }); | |
| const icon = document.createElement('div'); { | |
| this.domIcon = icon; | |
| icon.classList.add('qr--button-icon'); | |
| icon.classList.add('fa-solid'); | |
| if (!this.icon) icon.classList.add('qr--hidden'); | |
| else icon.classList.add(this.icon); | |
| root.append(icon); | |
| } | |
| const lbl = document.createElement('div'); { | |
| this.domLabel = lbl; | |
| lbl.classList.add('qr--button-label'); | |
| if (this.icon && !this.showLabel) lbl.classList.add('qr--hidden'); | |
| lbl.textContent = this.label; | |
| root.append(lbl); | |
| } | |
| const expander = document.createElement('div'); { | |
| expander.classList.add('qr--button-expander'); | |
| expander.textContent = '⋮'; | |
| expander.title = 'Open context menu'; | |
| expander.addEventListener('click', (evt) => { | |
| evt.stopPropagation(); | |
| evt.preventDefault(); | |
| const menu = new ContextMenu(this); | |
| menu.show(evt); | |
| }); | |
| root.append(expander); | |
| } | |
| } | |
| } | |
| return this.dom; | |
| } | |
| renderSettings(idx) { | |
| if (!this.settingsDom) { | |
| const item = document.createElement('div'); { | |
| this.settingsDom = item; | |
| item.classList.add('qr--set-item'); | |
| item.setAttribute('data-order', String(idx)); | |
| item.setAttribute('data-id', String(this.id)); | |
| const adder = document.createElement('div'); { | |
| adder.classList.add('qr--set-itemAdder'); | |
| const actions = document.createElement('div'); { | |
| actions.classList.add('qr--actions'); | |
| const addNew = document.createElement('div'); { | |
| addNew.classList.add('qr--action'); | |
| addNew.classList.add('qr--add'); | |
| addNew.classList.add('menu_button'); | |
| addNew.classList.add('menu_button_icon'); | |
| addNew.classList.add('fa-solid'); | |
| addNew.classList.add('fa-plus'); | |
| addNew.title = 'Add quick reply'; | |
| addNew.addEventListener('click', ()=>this.onInsertBefore()); | |
| actions.append(addNew); | |
| } | |
| const paste = document.createElement('div'); { | |
| paste.classList.add('qr--action'); | |
| paste.classList.add('qr--paste'); | |
| paste.classList.add('menu_button'); | |
| paste.classList.add('menu_button_icon'); | |
| paste.classList.add('fa-solid'); | |
| paste.classList.add('fa-paste'); | |
| paste.title = 'Add quick reply from clipboard'; | |
| paste.addEventListener('click', async()=>{ | |
| const text = await navigator.clipboard.readText(); | |
| this.onInsertBefore(text); | |
| }); | |
| actions.append(paste); | |
| } | |
| const importFile = document.createElement('div'); { | |
| importFile.classList.add('qr--action'); | |
| importFile.classList.add('qr--importFile'); | |
| importFile.classList.add('menu_button'); | |
| importFile.classList.add('menu_button_icon'); | |
| importFile.classList.add('fa-solid'); | |
| importFile.classList.add('fa-file-import'); | |
| importFile.title = 'Add quick reply from JSON file'; | |
| importFile.addEventListener('click', async()=>{ | |
| const inp = document.createElement('input'); { | |
| inp.type = 'file'; | |
| inp.accept = '.json'; | |
| inp.addEventListener('change', async()=>{ | |
| if (inp.files.length > 0) { | |
| for (const file of inp.files) { | |
| const text = await file.text(); | |
| this.onInsertBefore(text); | |
| } | |
| } | |
| }); | |
| inp.click(); | |
| } | |
| }); | |
| actions.append(importFile); | |
| } | |
| adder.append(actions); | |
| } | |
| item.append(adder); | |
| } | |
| const itemContent = document.createElement('div'); { | |
| itemContent.classList.add('qr--content'); | |
| const drag = document.createElement('div'); { | |
| drag.classList.add('drag-handle'); | |
| drag.classList.add('ui-sortable-handle'); | |
| drag.textContent = '☰'; | |
| itemContent.append(drag); | |
| } | |
| const lblContainer = document.createElement('div'); { | |
| lblContainer.classList.add('qr--set-itemLabelContainer'); | |
| const icon = document.createElement('div'); { | |
| this.settingsDomIcon = icon; | |
| icon.title = 'Click to change icon'; | |
| icon.classList.add('qr--set-itemIcon'); | |
| icon.classList.add('menu_button'); | |
| icon.classList.add('fa-fw'); | |
| if (this.icon) { | |
| icon.classList.add('fa-solid'); | |
| icon.classList.add(this.icon); | |
| } | |
| icon.addEventListener('click', async()=>{ | |
| let value = await showFontAwesomePicker(); | |
| this.updateIcon(value); | |
| }); | |
| lblContainer.append(icon); | |
| } | |
| const lbl = document.createElement('input'); { | |
| this.settingsDomLabel = lbl; | |
| lbl.classList.add('qr--set-itemLabel'); | |
| lbl.classList.add('text_pole'); | |
| lbl.value = this.label; | |
| lbl.addEventListener('input', ()=>this.updateLabel(lbl.value)); | |
| lblContainer.append(lbl); | |
| } | |
| itemContent.append(lblContainer); | |
| } | |
| item.append(itemContent); | |
| } | |
| const optContainer = document.createElement('div'); { | |
| optContainer.classList.add('qr--set-optionsContainer'); | |
| const opt = document.createElement('div'); { | |
| opt.classList.add('qr--action'); | |
| opt.classList.add('menu_button'); | |
| opt.classList.add('fa-fw'); | |
| opt.classList.add('fa-solid'); | |
| opt.textContent = '⁝'; | |
| opt.title = 'Additional options:\n - large editor\n - context menu\n - auto-execution\n - tooltip'; | |
| opt.addEventListener('click', ()=>this.showEditor()); | |
| optContainer.append(opt); | |
| } | |
| itemContent.append(optContainer); | |
| } | |
| const mes = document.createElement('textarea'); { | |
| this.settingsDomMessage = mes; | |
| mes.id = `qr--set--item${this.id}`; | |
| mes.classList.add('qr--set-itemMessage'); | |
| mes.value = this.message; | |
| //HACK need to use jQuery to catch the triggered event from the expanded editor | |
| $(mes).on('input', ()=>this.updateMessage(mes.value)); | |
| itemContent.append(mes); | |
| } | |
| const actions = document.createElement('div'); { | |
| actions.classList.add('qr--actions'); | |
| const move = document.createElement('div'); { | |
| move.classList.add('qr--action'); | |
| move.classList.add('menu_button'); | |
| move.classList.add('fa-fw'); | |
| move.classList.add('fa-solid'); | |
| move.classList.add('fa-truck-arrow-right'); | |
| move.title = 'Move quick reply to other set'; | |
| move.addEventListener('click', ()=>this.onTransfer(this)); | |
| actions.append(move); | |
| } | |
| const copy = document.createElement('div'); { | |
| copy.classList.add('qr--action'); | |
| copy.classList.add('menu_button'); | |
| copy.classList.add('fa-fw'); | |
| copy.classList.add('fa-solid'); | |
| copy.classList.add('fa-copy'); | |
| copy.title = 'Copy quick reply to clipboard'; | |
| copy.addEventListener('click', async()=>{ | |
| await navigator.clipboard.writeText(JSON.stringify(this)); | |
| copy.classList.add('qr--success'); | |
| await delay(3010); | |
| copy.classList.remove('qr--success'); | |
| }); | |
| actions.append(copy); | |
| } | |
| const cut = document.createElement('div'); { | |
| cut.classList.add('qr--action'); | |
| cut.classList.add('menu_button'); | |
| cut.classList.add('fa-fw'); | |
| cut.classList.add('fa-solid'); | |
| cut.classList.add('fa-cut'); | |
| cut.title = 'Cut quick reply to clipboard (copy and remove)'; | |
| cut.addEventListener('click', async()=>{ | |
| await navigator.clipboard.writeText(JSON.stringify(this)); | |
| this.delete(); | |
| }); | |
| actions.append(cut); | |
| } | |
| const exp = document.createElement('div'); { | |
| exp.classList.add('qr--action'); | |
| exp.classList.add('menu_button'); | |
| exp.classList.add('fa-fw'); | |
| exp.classList.add('fa-solid'); | |
| exp.classList.add('fa-file-export'); | |
| exp.title = 'Export quick reply as file'; | |
| exp.addEventListener('click', ()=>{ | |
| const blob = new Blob([JSON.stringify(this)], { type:'text' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); { | |
| a.href = url; | |
| a.download = `${this.label}.qr.json`; | |
| a.click(); | |
| } | |
| }); | |
| actions.append(exp); | |
| } | |
| const del = document.createElement('div'); { | |
| del.classList.add('qr--action'); | |
| del.classList.add('menu_button'); | |
| del.classList.add('fa-fw'); | |
| del.classList.add('fa-solid'); | |
| del.classList.add('fa-trash-can'); | |
| del.classList.add('redWarningBG'); | |
| del.title = 'Remove Quick Reply\n---\nShift+Click to skip confirmation'; | |
| del.addEventListener('click', async(evt)=>{ | |
| if (!evt.shiftKey) { | |
| const result = await Popup.show.confirm( | |
| 'Remove Quick Reply', | |
| 'Are you sure you want to remove this Quick Reply?', | |
| ); | |
| if (result != POPUP_RESULT.AFFIRMATIVE) { | |
| return; | |
| } | |
| } | |
| this.delete(); | |
| }); | |
| actions.append(del); | |
| } | |
| itemContent.append(actions); | |
| } | |
| } | |
| } | |
| return this.settingsDom; | |
| } | |
| unrenderSettings() { | |
| this.settingsDom?.remove(); | |
| } | |
| async showEditor() { | |
| const response = await fetch('/scripts/extensions/quick-reply/html/qrEditor.html', { cache: 'no-store' }); | |
| if (response.ok) { | |
| this.template = document.createRange().createContextualFragment(await response.text()).querySelector('#qr--modalEditor'); | |
| /**@type {HTMLElement} */ | |
| // @ts-ignore | |
| const dom = this.template.cloneNode(true); | |
| this.editorDom = dom; | |
| this.editorPopup = new Popup(dom, POPUP_TYPE.TEXT, undefined, { okButton: 'OK', wide: true, large: true, rows: 1 }); | |
| const popupResult = this.editorPopup.show(); | |
| // basics | |
| /**@type {HTMLElement}*/ | |
| const icon = dom.querySelector('#qr--modal-icon'); | |
| if (this.icon) { | |
| icon.classList.add('fa-solid'); | |
| icon.classList.add(this.icon); | |
| } | |
| else { | |
| icon.textContent = '…'; | |
| } | |
| icon.addEventListener('click', async()=>{ | |
| let value = await showFontAwesomePicker(); | |
| if (value === null) return; | |
| if (this.icon) icon.classList.remove(this.icon); | |
| if (value == '') { | |
| icon.classList.remove('fa-solid'); | |
| icon.textContent = '…'; | |
| } else { | |
| icon.textContent = ''; | |
| icon.classList.add('fa-solid'); | |
| icon.classList.add(value); | |
| } | |
| this.updateIcon(value); | |
| }); | |
| /**@type {HTMLInputElement}*/ | |
| const showLabel = dom.querySelector('#qr--modal-showLabel'); | |
| showLabel.checked = this.showLabel; | |
| showLabel.addEventListener('click', ()=>{ | |
| this.updateShowLabel(showLabel.checked); | |
| }); | |
| /**@type {HTMLInputElement}*/ | |
| const label = dom.querySelector('#qr--modal-label'); | |
| label.value = this.label; | |
| label.addEventListener('input', ()=>{ | |
| this.updateLabel(label.value); | |
| }); | |
| let switcherList; | |
| dom.querySelector('#qr--modal-switcher').addEventListener('click', (evt)=>{ | |
| if (switcherList) { | |
| switcherList.remove(); | |
| switcherList = null; | |
| return; | |
| } | |
| const list = document.createElement('ul'); { | |
| switcherList = list; | |
| list.classList.add('qr--modal-switcherList'); | |
| const makeList = (qrs)=>{ | |
| const setItem = document.createElement('li'); { | |
| setItem.classList.add('qr--modal-switcherItem'); | |
| setItem.addEventListener('click', ()=>{ | |
| list.innerHTML = ''; | |
| for (const qrs of quickReplyApi.listSets()) { | |
| const item = document.createElement('li'); { | |
| item.classList.add('qr--modal-switcherItem'); | |
| item.addEventListener('click', ()=>{ | |
| list.innerHTML = ''; | |
| makeList(quickReplyApi.getSetByName(qrs)); | |
| }); | |
| const lbl = document.createElement('div'); { | |
| lbl.classList.add('qr--label'); | |
| lbl.textContent = qrs; | |
| item.append(lbl); | |
| } | |
| list.append(item); | |
| } | |
| } | |
| }); | |
| const lbl = document.createElement('div'); { | |
| lbl.classList.add('qr--label'); | |
| const icon = document.createElement('i'); { | |
| icon.classList.add('fa-solid'); | |
| icon.classList.add('fa-arrow-alt-circle-right'); | |
| icon.classList.add('menu_button'); | |
| lbl.append(icon); | |
| } | |
| const text = document.createElement('span'); { | |
| text.textContent = 'Switch QR Sets...'; | |
| lbl.append(text); | |
| } | |
| setItem.append(lbl); | |
| } | |
| list.append(setItem); | |
| } | |
| const addItem = document.createElement('li'); { | |
| addItem.classList.add('qr--modal-switcherItem'); | |
| addItem.addEventListener('click', ()=>{ | |
| const qr = quickReplyApi.getSetByQr(this).addQuickReply(); | |
| this.editorPopup.completeAffirmative(); | |
| qr.showEditor(); | |
| }); | |
| const lbl = document.createElement('div'); { | |
| lbl.classList.add('qr--label'); | |
| const icon = document.createElement('i'); { | |
| icon.classList.add('fa-solid'); | |
| icon.classList.add('fa-plus'); | |
| icon.classList.add('menu_button'); | |
| lbl.append(icon); | |
| } | |
| const text = document.createElement('span'); { | |
| text.textContent = 'Add QR'; | |
| lbl.append(text); | |
| } | |
| addItem.append(lbl); | |
| } | |
| list.append(addItem); | |
| } | |
| for (const qr of qrs.qrList.toSorted((a,b)=>a.label.toLowerCase().localeCompare(b.label.toLowerCase()))) { | |
| const item = document.createElement('li'); { | |
| item.classList.add('qr--modal-switcherItem'); | |
| if (qr == this) item.classList.add('qr--current'); | |
| else item.addEventListener('click', ()=>{ | |
| this.editorPopup.completeAffirmative(); | |
| qr.showEditor(); | |
| }); | |
| const lbl = document.createElement('div'); { | |
| lbl.classList.add('qr--label'); | |
| lbl.textContent = qr.label; | |
| item.append(lbl); | |
| } | |
| const id = document.createElement('div'); { | |
| id.classList.add('qr--id'); | |
| id.textContent = qr.id.toString(); | |
| item.append(id); | |
| } | |
| const mes = document.createElement('div'); { | |
| mes.classList.add('qr--message'); | |
| mes.textContent = qr.message; | |
| item.append(mes); | |
| } | |
| list.append(item); | |
| } | |
| } | |
| }; | |
| makeList(quickReplyApi.getSetByQr(this)); | |
| } | |
| label.parentElement.append(list); | |
| }); | |
| /**@type {HTMLInputElement}*/ | |
| const title = dom.querySelector('#qr--modal-title'); | |
| title.value = this.title; | |
| title.addEventListener('input', () => { | |
| this.updateTitle(title.value); | |
| }); | |
| /**@type {HTMLElement}*/ | |
| const messageSyntaxInner = dom.querySelector('#qr--modal-messageSyntaxInner'); | |
| this.editorSyntax = messageSyntaxInner; | |
| /**@type {HTMLInputElement}*/ | |
| const wrap = dom.querySelector('#qr--modal-wrap'); | |
| wrap.checked = JSON.parse(accountStorage.getItem('qr--wrap') ?? 'false'); | |
| wrap.addEventListener('click', () => { | |
| accountStorage.setItem('qr--wrap', JSON.stringify(wrap.checked)); | |
| updateWrap(); | |
| }); | |
| const updateWrap = () => { | |
| if (wrap.checked) { | |
| message.style.whiteSpace = 'pre-wrap'; | |
| messageSyntaxInner.style.whiteSpace = 'pre-wrap'; | |
| if (this.clone) { | |
| this.clone.style.whiteSpace = 'pre-wrap'; | |
| } | |
| } else { | |
| message.style.whiteSpace = 'pre'; | |
| messageSyntaxInner.style.whiteSpace = 'pre'; | |
| if (this.clone) { | |
| this.clone.style.whiteSpace = 'pre'; | |
| } | |
| } | |
| updateScrollDebounced(); | |
| }; | |
| const updateScroll = (evt) => { | |
| let left = message.scrollLeft; | |
| let top = message.scrollTop; | |
| if (evt) { | |
| evt.preventDefault(); | |
| left = message.scrollLeft + evt.deltaX; | |
| top = message.scrollTop + evt.deltaY; | |
| message.scrollTo({ | |
| behavior: 'instant', | |
| left, | |
| top, | |
| }); | |
| } | |
| messageSyntaxInner.scrollTo({ | |
| behavior: 'instant', | |
| left, | |
| top, | |
| }); | |
| }; | |
| const updateScrollDebounced = updateScroll; | |
| const updateSyntaxEnabled = ()=>{ | |
| if (syntax.checked) { | |
| dom.querySelector('#qr--modal-messageHolder').classList.remove('qr--noSyntax'); | |
| } else { | |
| dom.querySelector('#qr--modal-messageHolder').classList.add('qr--noSyntax'); | |
| } | |
| }; | |
| /**@type {HTMLInputElement}*/ | |
| const tabSize = dom.querySelector('#qr--modal-tabSize'); | |
| tabSize.value = JSON.parse(accountStorage.getItem('qr--tabSize') ?? '4'); | |
| const updateTabSize = () => { | |
| message.style.tabSize = tabSize.value; | |
| messageSyntaxInner.style.tabSize = tabSize.value; | |
| updateScrollDebounced(); | |
| }; | |
| tabSize.addEventListener('change', () => { | |
| accountStorage.setItem('qr--tabSize', JSON.stringify(Number(tabSize.value))); | |
| updateTabSize(); | |
| }); | |
| /**@type {HTMLInputElement}*/ | |
| const executeShortcut = dom.querySelector('#qr--modal-executeShortcut'); | |
| executeShortcut.checked = JSON.parse(accountStorage.getItem('qr--executeShortcut') ?? 'true'); | |
| executeShortcut.addEventListener('click', () => { | |
| accountStorage.setItem('qr--executeShortcut', JSON.stringify(executeShortcut.checked)); | |
| }); | |
| /**@type {HTMLInputElement}*/ | |
| const syntax = dom.querySelector('#qr--modal-syntax'); | |
| syntax.checked = JSON.parse(accountStorage.getItem('qr--syntax') ?? 'true'); | |
| syntax.addEventListener('click', () => { | |
| accountStorage.setItem('qr--syntax', JSON.stringify(syntax.checked)); | |
| updateSyntaxEnabled(); | |
| }); | |
| if (navigator.keyboard) { | |
| navigator.keyboard.getLayoutMap().then(it=>dom.querySelector('#qr--modal-commentKey').textContent = it.get('Backslash')); | |
| } else { | |
| dom.querySelector('#qr--modal-commentKey').closest('small').remove(); | |
| } | |
| this.editorMessageLabel = dom.querySelector('label[for="qr--modal-message"]'); | |
| /**@type {HTMLTextAreaElement}*/ | |
| const message = dom.querySelector('#qr--modal-message'); | |
| this.editorMessage = message; | |
| message.value = this.message; | |
| const updateMessageDebounced = debounce((value)=>this.updateMessage(value), 10); | |
| message.addEventListener('input', () => { | |
| updateMessageDebounced(message.value); | |
| updateScrollDebounced(); | |
| }, { passive:true }); | |
| const getLineStart = ()=>{ | |
| const start = message.selectionStart; | |
| let lineStart; | |
| if (start == 0 || message.value[start - 1] == '\n') { | |
| // cursor is already at beginning of line | |
| // -> keep start | |
| lineStart = start; | |
| } else { | |
| // cursor is at end of line or somewhere in the line | |
| // -> find last newline before cursor and start after that | |
| lineStart = message.value.lastIndexOf('\n', start - 1) + 1; | |
| } | |
| return lineStart; | |
| }; | |
| message.addEventListener('keydown', async(evt) => { | |
| if (this.isExecuting) return; | |
| if (evt.key == 'Tab' && !evt.shiftKey && !evt.ctrlKey && !evt.altKey) { | |
| // increase indent | |
| evt.preventDefault(); | |
| const start = message.selectionStart; | |
| const end = message.selectionEnd; | |
| if (end - start > 0 && message.value.substring(start, end).includes('\n')) { | |
| evt.stopImmediatePropagation(); | |
| evt.stopPropagation(); | |
| const lineStart = getLineStart(); | |
| message.selectionStart = lineStart; | |
| const affectedLines = message.value.substring(lineStart, end).split('\n'); | |
| // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history | |
| document.execCommand('insertText', false, `\t${affectedLines.join('\n\t')}`); | |
| message.selectionStart = start + 1; | |
| message.selectionEnd = end + affectedLines.length; | |
| message.dispatchEvent(new Event('input', { bubbles:true })); | |
| } else if (!(ac.isReplaceable && ac.isActive)) { | |
| evt.stopImmediatePropagation(); | |
| evt.stopPropagation(); | |
| // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history | |
| document.execCommand('insertText', false, '\t'); | |
| message.dispatchEvent(new Event('input', { bubbles:true })); | |
| } | |
| } else if (evt.key == 'Tab' && evt.shiftKey && !evt.ctrlKey && !evt.altKey) { | |
| // decrease indent | |
| evt.preventDefault(); | |
| evt.stopImmediatePropagation(); | |
| evt.stopPropagation(); | |
| const start = message.selectionStart; | |
| const end = message.selectionEnd; | |
| const lineStart = getLineStart(); | |
| message.selectionStart = lineStart; | |
| const affectedLines = message.value.substring(lineStart, end).split('\n'); | |
| const newText = affectedLines.map(it=>it.replace(/^\t/, '')).join('\n'); | |
| const delta = affectedLines.join('\n').length - newText.length; | |
| // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history | |
| if (delta > 0) { | |
| if (newText == '') { | |
| document.execCommand('delete', false); | |
| } else { | |
| document.execCommand('insertText', false, newText); | |
| } | |
| message.selectionStart = start - (affectedLines[0].startsWith('\t') ? 1 : 0); | |
| message.selectionEnd = end - delta; | |
| message.dispatchEvent(new Event('input', { bubbles:true })); | |
| } else { | |
| message.selectionStart = start; | |
| } | |
| } else if (evt.key == 'Enter' && !evt.ctrlKey && !evt.shiftKey && !evt.altKey && !(ac.isReplaceable && ac.isActive)) { | |
| // new line, keep indent | |
| const start = message.selectionStart; | |
| let lineStart = getLineStart(); | |
| const indent = /^([^\S\n]*)/.exec(message.value.slice(lineStart))[1] ?? ''; | |
| if (indent.length) { | |
| evt.stopImmediatePropagation(); | |
| evt.stopPropagation(); | |
| evt.preventDefault(); | |
| // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history | |
| document.execCommand('insertText', false, `\n${indent}`); | |
| message.selectionStart = start + 1 + indent.length; | |
| message.selectionEnd = message.selectionStart; | |
| message.dispatchEvent(new Event('input', { bubbles:true })); | |
| } | |
| } else if (evt.key == 'Enter' && evt.ctrlKey && !evt.shiftKey && !evt.altKey) { | |
| if (executeShortcut.checked) { | |
| // execute QR | |
| evt.stopImmediatePropagation(); | |
| evt.stopPropagation(); | |
| evt.preventDefault(); | |
| const selectionStart = message.selectionStart; | |
| const selectionEnd = message.selectionEnd; | |
| message.blur(); | |
| await this.executeFromEditor(); | |
| if (document.activeElement != message) { | |
| message.focus(); | |
| message.selectionStart = selectionStart; | |
| message.selectionEnd = selectionEnd; | |
| } | |
| } | |
| } else if (evt.key == 'F9' && !evt.ctrlKey && !evt.shiftKey && !evt.altKey) { | |
| // toggle breakpoint | |
| evt.stopImmediatePropagation(); | |
| evt.stopPropagation(); | |
| evt.preventDefault(); | |
| preBreakPointStart = message.selectionStart; | |
| preBreakPointEnd = message.selectionEnd; | |
| toggleBreakpoint(); | |
| } else if (evt.code == 'Backslash' && evt.ctrlKey && !evt.shiftKey && !evt.altKey) { | |
| // toggle block comment | |
| // (evt.code will use the same physical key on the keyboard across different keyboard layouts) | |
| evt.stopImmediatePropagation(); | |
| evt.stopPropagation(); | |
| evt.preventDefault(); | |
| // check if we are inside a comment -> uncomment | |
| const parser = new SlashCommandParser(); | |
| parser.parse(message.value, false); | |
| const start = message.selectionStart; | |
| const end = message.selectionEnd; | |
| const comment = parser.commandIndex.findLast(it=>it.name == '*' && (it.start <= start && it.end >= start || it.start <= end && it.end >= end)); | |
| if (comment) { | |
| // uncomment | |
| let content = message.value.slice(comment.start + 1, comment.end - 1); | |
| let len = content.length; | |
| content = content.replace(/^ /, ''); | |
| const offsetStart = len - content.length; | |
| len = content.length; | |
| content = content.replace(/ $/, ''); | |
| const offsetEnd = len - content.length; | |
| message.selectionStart = comment.start - 1; | |
| message.selectionEnd = comment.end + 1; | |
| // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history | |
| document.execCommand('insertText', false, content); | |
| message.selectionStart = start - (start >= comment.start ? 2 + offsetStart : 0); | |
| message.selectionEnd = end - 2 - offsetStart - (end >= comment.end ? 2 + offsetEnd : 0); | |
| } else { | |
| // comment | |
| const lineStart = getLineStart(); | |
| const lineEnd = message.value.indexOf('\n', end); | |
| message.selectionStart = lineStart; | |
| message.selectionEnd = lineEnd; | |
| // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history | |
| document.execCommand('insertText', false, `/* ${message.value.slice(lineStart, lineEnd)} *|`); | |
| message.selectionStart = start + 3; | |
| message.selectionEnd = end + 3; | |
| } | |
| message.dispatchEvent(new Event('input', { bubbles:true })); | |
| } | |
| }); | |
| const ac = await setSlashCommandAutoComplete(message, true); | |
| message.addEventListener('wheel', (evt)=>{ | |
| updateScrollDebounced(evt); | |
| }); | |
| message.addEventListener('scroll', (evt)=>{ | |
| updateScrollDebounced(); | |
| }); | |
| let preBreakPointStart; | |
| let preBreakPointEnd; | |
| /** | |
| * @param {SlashCommandBreakPoint} bp | |
| */ | |
| const removeBreakpoint = (bp)=>{ | |
| // start at -1 because "/" is not included in start-end | |
| let start = bp.start - 1; | |
| // step left until forward slash "/" | |
| while (message.value[start] != '/') start--; | |
| // step left while whitespace (except newline) before start | |
| while (/[^\S\n]/.test(message.value[start - 1])) start--; | |
| // if newline before indent, include the newline for removal | |
| if (message.value[start - 1] == '\n') start--; | |
| let end = bp.end; | |
| // step right while whitespace | |
| while (/\s/.test(message.value[end])) end++; | |
| // if pipe after whitepace, include pipe for removal | |
| if (message.value[end] == '|') end++; | |
| message.selectionStart = start; | |
| message.selectionEnd = end; | |
| // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history | |
| document.execCommand('insertText', false, ''); | |
| message.dispatchEvent(new Event('input', { bubbles:true })); | |
| let postStart = preBreakPointStart; | |
| let postEnd = preBreakPointEnd; | |
| // set caret back to where it was | |
| if (preBreakPointStart <= start) { | |
| // selection start was before breakpoint: do nothing | |
| } else if (preBreakPointStart > start && preBreakPointEnd < end) { | |
| // selection start was inside breakpoint: move to index before breakpoint | |
| postStart = start; | |
| } else if (preBreakPointStart >= end) { | |
| // selection start was behind breakpoint: move back by length of removed string | |
| postStart = preBreakPointStart - (end - start); | |
| } | |
| if (preBreakPointEnd <= start) { | |
| // do nothing | |
| } else if (preBreakPointEnd > start && preBreakPointEnd < end) { | |
| // selection end was inside breakpoint: move to index before breakpoint | |
| postEnd = start; | |
| } else if (preBreakPointEnd >= end) { | |
| // selection end was behind breakpoint: move back by length of removed string | |
| postEnd = preBreakPointEnd - (end - start); | |
| } | |
| return { start:postStart, end:postEnd }; | |
| }; | |
| /** | |
| * @param {SlashCommandExecutor} cmd | |
| */ | |
| const addBreakpoint = (cmd)=>{ | |
| // start at -1 because "/" is not included in start-end | |
| let start = cmd.start - 1; | |
| let indent = ''; | |
| // step left until forward slash "/" | |
| while (message.value[start] != '/') start--; | |
| // step left while whitespace (except newline) before start, collect the whitespace to help build indentation | |
| while (/[^\S\n]/.test(message.value[start - 1])) { | |
| start--; | |
| indent += message.value[start]; | |
| } | |
| // if newline before indent, include the newline | |
| if (message.value[start - 1] == '\n') { | |
| start--; | |
| indent = `\n${indent}`; | |
| } | |
| const breakpointText = `${indent}/breakpoint |`; | |
| message.selectionStart = start; | |
| message.selectionEnd = start; | |
| // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history | |
| document.execCommand('insertText', false, breakpointText); | |
| message.dispatchEvent(new Event('input', { bubbles:true })); | |
| return breakpointText.length; | |
| }; | |
| const toggleBreakpoint = ()=>{ | |
| const idx = message.selectionStart; | |
| let postStart = preBreakPointStart; | |
| let postEnd = preBreakPointEnd; | |
| const parser = new SlashCommandParser(); | |
| parser.parse(message.value, false); | |
| const cmdIdx = parser.commandIndex.findLastIndex(it=>it.start <= idx); | |
| if (cmdIdx > -1) { | |
| const cmd = parser.commandIndex[cmdIdx]; | |
| if (cmd instanceof SlashCommandBreakPoint) { | |
| const bp = cmd; | |
| const { start, end } = removeBreakpoint(bp); | |
| postStart = start; | |
| postEnd = end; | |
| } else if (parser.commandIndex[cmdIdx - 1] instanceof SlashCommandBreakPoint) { | |
| const bp = parser.commandIndex[cmdIdx - 1]; | |
| const { start, end } = removeBreakpoint(bp); | |
| postStart = start; | |
| postEnd = end; | |
| } else { | |
| const len = addBreakpoint(cmd); | |
| postStart += len; | |
| postEnd += len; | |
| } | |
| message.selectionStart = postStart; | |
| message.selectionEnd = postEnd; | |
| } | |
| }; | |
| message.addEventListener('pointerdown', (evt)=>{ | |
| if (!evt.ctrlKey || !evt.altKey) return; | |
| preBreakPointStart = message.selectionStart; | |
| preBreakPointEnd = message.selectionEnd; | |
| }); | |
| message.addEventListener('pointerup', async(evt)=>{ | |
| if (!evt.ctrlKey || !evt.altKey || message.selectionStart != message.selectionEnd) return; | |
| toggleBreakpoint(); | |
| }); | |
| /** @type {any} */ | |
| const resizeListener = debounce((evt) => { | |
| updateScrollDebounced(evt); | |
| if (document.activeElement == message) { | |
| message.blur(); | |
| message.focus(); | |
| } | |
| }); | |
| window.addEventListener('resize', resizeListener); | |
| updateSyntaxEnabled(); | |
| const updateSyntax = ()=>{ | |
| if (messageSyntaxInner && syntax.checked) { | |
| morphdom( | |
| messageSyntaxInner, | |
| `<div>${hljs.highlight(`${message.value}${message.value.slice(-1) == '\n' ? ' ' : ''}`, { language:'stscript', ignoreIllegals:true })?.value}</div>`, | |
| { childrenOnly: true }, | |
| ); | |
| updateScrollDebounced(); | |
| } | |
| }; | |
| let lastSyntaxUpdate = 0; | |
| const fpsTime = 1000 / 30; | |
| let lastMessageValue = null; | |
| let wasSyntax = null; | |
| const updateSyntaxLoop = ()=>{ | |
| const now = Date.now(); | |
| // fps limit | |
| if (now - lastSyntaxUpdate < fpsTime) return requestAnimationFrame(updateSyntaxLoop); | |
| // elements don't exist (yet?) | |
| if (!messageSyntaxInner || !message) return requestAnimationFrame(updateSyntaxLoop); | |
| // elements no longer part of the document | |
| if (!messageSyntaxInner.closest('body')) return; | |
| // debugger is running | |
| if (this.isExecuting) { | |
| lastMessageValue = null; | |
| return requestAnimationFrame(updateSyntaxLoop); | |
| } | |
| // value hasn't changed | |
| if (wasSyntax == syntax.checked && lastMessageValue == message.value) return requestAnimationFrame(updateSyntaxLoop); | |
| wasSyntax = syntax.checked; | |
| lastSyntaxUpdate = now; | |
| lastMessageValue = message.value; | |
| updateSyntax(); | |
| requestAnimationFrame(updateSyntaxLoop); | |
| }; | |
| requestAnimationFrame(()=>updateSyntaxLoop()); | |
| message.style.setProperty('text-shadow', 'none', 'important'); | |
| updateWrap(); | |
| updateTabSize(); | |
| // context menu | |
| /**@type {HTMLTemplateElement}*/ | |
| const tpl = dom.querySelector('#qr--ctxItem'); | |
| const linkList = dom.querySelector('#qr--ctxEditor'); | |
| const fillQrSetSelect = (/**@type {HTMLSelectElement}*/select, /**@type {QuickReplyContextLink}*/ link) => { | |
| [{ name: 'Select a QR set' }, ...QuickReplySet.list.toSorted((a,b)=>a.name.toLowerCase().localeCompare(b.name.toLowerCase()))].forEach(qrs => { | |
| const opt = document.createElement('option'); { | |
| opt.value = qrs.name; | |
| opt.textContent = qrs.name; | |
| opt.selected = qrs.name == link.set?.name; | |
| select.append(opt); | |
| } | |
| }); | |
| }; | |
| const addCtxItem = (/**@type {QuickReplyContextLink}*/link, /**@type {number}*/idx) => { | |
| /**@type {HTMLElement} */ | |
| // @ts-ignore | |
| const itemDom = tpl.content.querySelector('.qr--ctxItem').cloneNode(true); { | |
| itemDom.setAttribute('data-order', String(idx)); | |
| /**@type {HTMLSelectElement} */ | |
| const select = itemDom.querySelector('.qr--set'); | |
| fillQrSetSelect(select, link); | |
| select.addEventListener('change', () => { | |
| link.set = QuickReplySet.get(select.value); | |
| this.updateContext(); | |
| }); | |
| /**@type {HTMLInputElement} */ | |
| const chain = itemDom.querySelector('.qr--isChained'); | |
| chain.checked = link.isChained; | |
| chain.addEventListener('click', () => { | |
| link.isChained = chain.checked; | |
| this.updateContext(); | |
| }); | |
| itemDom.querySelector('.qr--delete').addEventListener('click', () => { | |
| itemDom.remove(); | |
| this.contextList.splice(this.contextList.indexOf(link), 1); | |
| this.updateContext(); | |
| }); | |
| linkList.append(itemDom); | |
| } | |
| }; | |
| [...this.contextList].forEach((link, idx) => addCtxItem(link, idx)); | |
| dom.querySelector('#qr--ctxAdd').addEventListener('click', () => { | |
| const link = new QuickReplyContextLink(); | |
| this.contextList.push(link); | |
| addCtxItem(link, this.contextList.length - 1); | |
| }); | |
| const onContextSort = () => { | |
| this.contextList = Array.from(linkList.querySelectorAll('.qr--ctxItem')).map((it,idx) => { | |
| const link = this.contextList[Number(it.getAttribute('data-order'))]; | |
| it.setAttribute('data-order', String(idx)); | |
| return link; | |
| }); | |
| this.updateContext(); | |
| }; | |
| // @ts-ignore | |
| $(linkList).sortable({ | |
| delay: getSortableDelay(), | |
| stop: () => onContextSort(), | |
| }); | |
| // auto-exec | |
| /**@type {HTMLInputElement}*/ | |
| const preventAutoExecute = dom.querySelector('#qr--preventAutoExecute'); | |
| preventAutoExecute.checked = this.preventAutoExecute; | |
| preventAutoExecute.addEventListener('click', ()=>{ | |
| this.preventAutoExecute = preventAutoExecute.checked; | |
| this.updateContext(); | |
| }); | |
| /**@type {HTMLInputElement}*/ | |
| const isHidden = dom.querySelector('#qr--isHidden'); | |
| isHidden.checked = this.isHidden; | |
| isHidden.addEventListener('click', ()=>{ | |
| this.isHidden = isHidden.checked; | |
| this.updateContext(); | |
| }); | |
| /**@type {HTMLInputElement}*/ | |
| const executeOnStartup = dom.querySelector('#qr--executeOnStartup'); | |
| executeOnStartup.checked = this.executeOnStartup; | |
| executeOnStartup.addEventListener('click', ()=>{ | |
| this.executeOnStartup = executeOnStartup.checked; | |
| this.updateContext(); | |
| }); | |
| /**@type {HTMLInputElement}*/ | |
| const executeOnUser = dom.querySelector('#qr--executeOnUser'); | |
| executeOnUser.checked = this.executeOnUser; | |
| executeOnUser.addEventListener('click', ()=>{ | |
| this.executeOnUser = executeOnUser.checked; | |
| this.updateContext(); | |
| }); | |
| /**@type {HTMLInputElement}*/ | |
| const executeOnAi = dom.querySelector('#qr--executeOnAi'); | |
| executeOnAi.checked = this.executeOnAi; | |
| executeOnAi.addEventListener('click', ()=>{ | |
| this.executeOnAi = executeOnAi.checked; | |
| this.updateContext(); | |
| }); | |
| /**@type {HTMLInputElement}*/ | |
| const executeOnChatChange = dom.querySelector('#qr--executeOnChatChange'); | |
| executeOnChatChange.checked = this.executeOnChatChange; | |
| executeOnChatChange.addEventListener('click', ()=>{ | |
| this.executeOnChatChange = executeOnChatChange.checked; | |
| this.updateContext(); | |
| }); | |
| /**@type {HTMLInputElement}*/ | |
| const executeOnGroupMemberDraft = dom.querySelector('#qr--executeOnGroupMemberDraft'); | |
| executeOnGroupMemberDraft.checked = this.executeOnGroupMemberDraft; | |
| executeOnGroupMemberDraft.addEventListener('click', ()=>{ | |
| this.executeOnGroupMemberDraft = executeOnGroupMemberDraft.checked; | |
| this.updateContext(); | |
| }); | |
| /**@type {HTMLInputElement}*/ | |
| const executeOnNewChat = dom.querySelector('#qr--executeOnNewChat'); | |
| executeOnNewChat.checked = this.executeOnNewChat; | |
| executeOnNewChat.addEventListener('click', ()=>{ | |
| this.executeOnNewChat = executeOnNewChat.checked; | |
| this.updateContext(); | |
| }); | |
| /**@type {HTMLInputElement}*/ | |
| const automationId = dom.querySelector('#qr--automationId'); | |
| automationId.value = this.automationId; | |
| automationId.addEventListener('input', () => { | |
| this.automationId = automationId.value; | |
| this.updateContext(); | |
| }); | |
| /**@type {HTMLElement}*/ | |
| const executeProgress = dom.querySelector('#qr--modal-executeProgress'); | |
| this.editorExecuteProgress = executeProgress; | |
| /**@type {HTMLElement}*/ | |
| const executeErrors = dom.querySelector('#qr--modal-executeErrors'); | |
| this.editorExecuteErrors = executeErrors; | |
| /**@type {HTMLElement}*/ | |
| const executeResult = dom.querySelector('#qr--modal-executeResult'); | |
| this.editorExecuteResult = executeResult; | |
| /**@type {HTMLElement}*/ | |
| const debugState = dom.querySelector('#qr--modal-debugState'); | |
| this.editorDebugState = debugState; | |
| /**@type {HTMLElement}*/ | |
| const executeBtn = dom.querySelector('#qr--modal-execute'); | |
| this.editorExecuteBtn = executeBtn; | |
| executeBtn.addEventListener('click', async()=>{ | |
| await this.executeFromEditor(); | |
| }); | |
| /**@type {HTMLElement}*/ | |
| const executeBtnPause = dom.querySelector('#qr--modal-pause'); | |
| this.editorExecuteBtnPause = executeBtnPause; | |
| executeBtnPause.addEventListener('click', async()=>{ | |
| if (this.abortController) { | |
| if (this.abortController.signal.paused) { | |
| this.abortController.continue('Continue button clicked'); | |
| this.editorExecuteProgress.classList.remove('qr--paused'); | |
| } else { | |
| this.abortController.pause('Pause button clicked'); | |
| this.editorExecuteProgress.classList.add('qr--paused'); | |
| } | |
| } | |
| }); | |
| /**@type {HTMLElement}*/ | |
| const executeBtnStop = dom.querySelector('#qr--modal-stop'); | |
| this.editorExecuteBtnStop = executeBtnStop; | |
| executeBtnStop.addEventListener('click', async()=>{ | |
| this.abortController?.abort('Stop button clicked'); | |
| }); | |
| /**@type {HTMLTextAreaElement} */ | |
| const inputOg = document.querySelector('#send_textarea'); | |
| const inputMirror = dom.querySelector('#qr--modal-send_textarea'); | |
| inputMirror.value = inputOg.value; | |
| const inputOgMo = new MutationObserver(muts=>{ | |
| if (muts.find(it=>[...it.removedNodes].includes(inputMirror) || [...it.removedNodes].find(n=>n.contains(inputMirror)))) { | |
| inputOg.removeEventListener('input', inputOgListener); | |
| } | |
| }); | |
| inputOgMo.observe(document.body, { childList:true }); | |
| const inputOgListener = ()=>{ | |
| inputMirror.value = inputOg.value; | |
| }; | |
| inputOg.addEventListener('input', inputOgListener); | |
| inputMirror.addEventListener('input', ()=>{ | |
| inputOg.value = inputMirror.value; | |
| }); | |
| /**@type {HTMLElement}*/ | |
| const resumeBtn = dom.querySelector('#qr--modal-resume'); | |
| resumeBtn.addEventListener('click', ()=>{ | |
| this.debugController?.resume(); | |
| }); | |
| /**@type {HTMLElement}*/ | |
| const stepBtn = dom.querySelector('#qr--modal-step'); | |
| stepBtn.addEventListener('click', ()=>{ | |
| this.debugController?.step(); | |
| }); | |
| /**@type {HTMLElement}*/ | |
| const stepIntoBtn = dom.querySelector('#qr--modal-stepInto'); | |
| stepIntoBtn.addEventListener('click', ()=>{ | |
| this.debugController?.stepInto(); | |
| }); | |
| /**@type {HTMLElement}*/ | |
| const stepOutBtn = dom.querySelector('#qr--modal-stepOut'); | |
| stepOutBtn.addEventListener('click', ()=>{ | |
| this.debugController?.stepOut(); | |
| }); | |
| /**@type {HTMLElement}*/ | |
| const minimizeBtn = dom.querySelector('#qr--modal-minimize'); | |
| minimizeBtn.addEventListener('click', ()=>{ | |
| this.editorDom.classList.add('qr--minimized'); | |
| }); | |
| const maximizeBtn = dom.querySelector('#qr--modal-maximize'); | |
| maximizeBtn.addEventListener('click', ()=>{ | |
| this.editorDom.classList.remove('qr--minimized'); | |
| }); | |
| /**@type {boolean}*/ | |
| let isResizing = false; | |
| let resizeStart; | |
| let wStart; | |
| /**@type {HTMLElement}*/ | |
| const resizeHandle = dom.querySelector('#qr--resizeHandle'); | |
| resizeHandle.addEventListener('pointerdown', (evt)=>{ | |
| if (isResizing) return; | |
| isResizing = true; | |
| evt.preventDefault(); | |
| resizeStart = evt.x; | |
| wStart = dom.querySelector('#qr--qrOptions').offsetWidth; | |
| const dragListener = debounce((evt)=>{ | |
| const w = wStart + resizeStart - evt.x; | |
| dom.querySelector('#qr--qrOptions').style.setProperty('--width', `${w}px`); | |
| }, 5); | |
| window.addEventListener('pointerup', ()=>{ | |
| window.removeEventListener('pointermove', dragListener); | |
| isResizing = false; | |
| }, { once:true }); | |
| window.addEventListener('pointermove', dragListener); | |
| }); | |
| await popupResult; | |
| window.removeEventListener('resize', resizeListener); | |
| } else { | |
| warn('failed to fetch qrEditor template'); | |
| } | |
| } | |
| getEditorPosition(start, end, message = null) { | |
| const inputRect = this.editorMessage.getBoundingClientRect(); | |
| const style = window.getComputedStyle(this.editorMessage); | |
| if (!this.clone) { | |
| this.clone = document.createElement('div'); | |
| for (const key of style) { | |
| this.clone.style[key] = style[key]; | |
| } | |
| this.clone.style.position = 'fixed'; | |
| this.clone.style.visibility = 'hidden'; | |
| const mo = new MutationObserver(muts=>{ | |
| if (muts.find(it=>[...it.removedNodes].includes(this.editorMessage) || [...it.removedNodes].find(n=>n.contains(this.editorMessage)))) { | |
| this.clone?.remove(); | |
| this.clone = null; | |
| } | |
| }); | |
| mo.observe(document.body, { childList:true }); | |
| } | |
| document.body.append(this.clone); | |
| this.clone.style.width = `${inputRect.width}px`; | |
| this.clone.style.height = `${inputRect.height}px`; | |
| this.clone.style.left = `${inputRect.left}px`; | |
| this.clone.style.top = `${inputRect.top}px`; | |
| this.clone.style.whiteSpace = style.whiteSpace; | |
| this.clone.style.tabSize = style.tabSize; | |
| const text = message ?? this.editorMessage.value; | |
| const before = text.slice(0, start); | |
| this.clone.textContent = before; | |
| const locator = document.createElement('span'); | |
| locator.textContent = text.slice(start, end); | |
| this.clone.append(locator); | |
| this.clone.append(text.slice(end)); | |
| this.clone.scrollTop = this.editorSyntax.scrollTop; | |
| this.clone.scrollLeft = this.editorSyntax.scrollLeft; | |
| const locatorRect = locator.getBoundingClientRect(); | |
| const bodyRect = document.body.getBoundingClientRect(); | |
| const location = { | |
| left: locatorRect.left - bodyRect.left, | |
| right: locatorRect.right - bodyRect.left, | |
| top: locatorRect.top - bodyRect.top, | |
| bottom: locatorRect.bottom - bodyRect.top, | |
| }; | |
| // this.clone.remove(); | |
| return location; | |
| } | |
| async executeFromEditor() { | |
| if (this.isExecuting) return; | |
| this.editorPopup.onClosing = ()=>false; | |
| const uuidCheck = /^[0-9a-z]{8}(-[0-9a-z]{4}){3}-[0-9a-z]{12}$/; | |
| const oText = this.message; | |
| this.isExecuting = true; | |
| this.editorDom.classList.add('qr--isExecuting'); | |
| const noSyntax = this.editorDom.querySelector('#qr--modal-messageHolder').classList.contains('qr--noSyntax'); | |
| if (noSyntax) { | |
| this.editorDom.querySelector('#qr--modal-messageHolder').classList.remove('qr--noSyntax'); | |
| } | |
| this.editorExecuteBtn.classList.add('qr--busy'); | |
| this.editorExecuteProgress.style.setProperty('--prog', '0'); | |
| this.editorExecuteErrors.classList.remove('qr--hasErrors'); | |
| this.editorExecuteResult.classList.remove('qr--hasResult'); | |
| this.editorExecuteProgress.classList.remove('qr--error'); | |
| this.editorExecuteProgress.classList.remove('qr--success'); | |
| this.editorExecuteProgress.classList.remove('qr--paused'); | |
| this.editorExecuteProgress.classList.remove('qr--aborted'); | |
| this.editorExecuteErrors.innerHTML = ''; | |
| this.editorExecuteResult.innerHTML = ''; | |
| const syntax = this.editorDom.querySelector('#qr--modal-messageSyntaxInner'); | |
| const updateScroll = (evt) => { | |
| let left = syntax.scrollLeft; | |
| let top = syntax.scrollTop; | |
| if (evt) { | |
| evt.preventDefault(); | |
| left = syntax.scrollLeft + evt.deltaX; | |
| top = syntax.scrollTop + evt.deltaY; | |
| syntax.scrollTo({ | |
| behavior: 'instant', | |
| left, | |
| top, | |
| }); | |
| } | |
| this.editorMessage.scrollTo({ | |
| behavior: 'instant', | |
| left, | |
| top, | |
| }); | |
| }; | |
| const updateScrollDebounced = updateScroll; | |
| syntax.addEventListener('wheel', (evt)=>{ | |
| updateScrollDebounced(evt); | |
| }); | |
| syntax.addEventListener('scroll', (evt)=>{ | |
| updateScrollDebounced(); | |
| }); | |
| try { | |
| this.abortController = new SlashCommandAbortController(); | |
| this.debugController = new SlashCommandDebugController(); | |
| this.debugController.onBreakPoint = async(closure, executor)=>{ | |
| this.editorDom.classList.add('qr--isPaused'); | |
| syntax.innerHTML = hljs.highlight(`${closure.fullText}${closure.fullText.slice(-1) == '\n' ? ' ' : ''}`, { language:'stscript', ignoreIllegals:true })?.value; | |
| this.editorMessageLabel.innerHTML = ''; | |
| if (uuidCheck.test(closure.source)) { | |
| const p0 = document.createElement('span'); { | |
| p0.textContent = 'anonymous: '; | |
| this.editorMessageLabel.append(p0); | |
| } | |
| const p1 = document.createElement('strong'); { | |
| p1.textContent = executor.source.slice(0,5); | |
| this.editorMessageLabel.append(p1); | |
| } | |
| const p2 = document.createElement('span'); { | |
| p2.textContent = executor.source.slice(5, -5); | |
| this.editorMessageLabel.append(p2); | |
| } | |
| const p3 = document.createElement('strong'); { | |
| p3.textContent = executor.source.slice(-5); | |
| this.editorMessageLabel.append(p3); | |
| } | |
| } else { | |
| this.editorMessageLabel.textContent = executor.source; | |
| } | |
| const source = closure.source; | |
| this.editorDebugState.innerHTML = ''; | |
| let ci = -1; | |
| const varNames = []; | |
| const macroNames = []; | |
| /** | |
| * @param {SlashCommandScope} scope | |
| */ | |
| const buildVars = (scope, isCurrent = false)=>{ | |
| if (!isCurrent) { | |
| ci--; | |
| } | |
| const c = this.debugController.stack.slice(ci)[0]; | |
| const wrap = document.createElement('div'); { | |
| wrap.classList.add('qr--scope'); | |
| if (isCurrent) { | |
| const executor = this.debugController.cmdStack.slice(-1)[0]; | |
| { // named args | |
| const namedTitle = document.createElement('div'); { | |
| namedTitle.classList.add('qr--title'); | |
| namedTitle.textContent = `Named Args - /${executor.name}`; | |
| if (executor.command.name == 'run') { | |
| namedTitle.textContent += `${(executor.name == ':' ? '' : ' ')}${executor.unnamedArgumentList[0]?.value}`; | |
| } | |
| wrap.append(namedTitle); | |
| } | |
| const keys = new Set([...Object.keys(this.debugController.namedArguments ?? {}), ...(executor.namedArgumentList ?? []).map(it=>it.name)]); | |
| for (const key of keys) { | |
| if (key[0] == '_') continue; | |
| const item = document.createElement('div'); { | |
| item.classList.add('qr--var'); | |
| const k = document.createElement('div'); { | |
| k.classList.add('qr--key'); | |
| k.textContent = key; | |
| item.append(k); | |
| } | |
| const vUnresolved = document.createElement('div'); { | |
| vUnresolved.classList.add('qr--val'); | |
| vUnresolved.classList.add('qr--singleCol'); | |
| const val = executor.namedArgumentList.find(it=>it.name == key)?.value; | |
| if (val instanceof SlashCommandClosure) { | |
| vUnresolved.classList.add('qr--closure'); | |
| vUnresolved.title = val.rawText; | |
| vUnresolved.textContent = val.toString(); | |
| } else if (val === undefined) { | |
| vUnresolved.classList.add('qr--undefined'); | |
| vUnresolved.textContent = 'undefined'; | |
| } else { | |
| let jsonVal; | |
| try { jsonVal = JSON.parse(val); } catch { /* empty */ } | |
| if (jsonVal && typeof jsonVal == 'object') { | |
| vUnresolved.textContent = JSON.stringify(jsonVal, null, 2); | |
| } else { | |
| vUnresolved.textContent = val; | |
| vUnresolved.classList.add('qr--simple'); | |
| } | |
| } | |
| item.append(vUnresolved); | |
| } | |
| const vResolved = document.createElement('div'); { | |
| vResolved.classList.add('qr--val'); | |
| vResolved.classList.add('qr--singleCol'); | |
| if (this.debugController.namedArguments === undefined) { | |
| vResolved.classList.add('qr--unresolved'); | |
| } else { | |
| const val = this.debugController.namedArguments?.[key]; | |
| if (val instanceof SlashCommandClosure) { | |
| vResolved.classList.add('qr--closure'); | |
| vResolved.title = val.rawText; | |
| vResolved.textContent = val.toString(); | |
| } else if (val === undefined) { | |
| vResolved.classList.add('qr--undefined'); | |
| vResolved.textContent = 'undefined'; | |
| } else { | |
| let jsonVal; | |
| try { jsonVal = JSON.parse(val); } catch { /* empty */ } | |
| if (jsonVal && typeof jsonVal == 'object') { | |
| vResolved.textContent = JSON.stringify(jsonVal, null, 2); | |
| } else { | |
| vResolved.textContent = val; | |
| vResolved.classList.add('qr--simple'); | |
| } | |
| } | |
| } | |
| item.append(vResolved); | |
| } | |
| wrap.append(item); | |
| } | |
| } | |
| } | |
| { // unnamed args | |
| const unnamedTitle = document.createElement('div'); { | |
| unnamedTitle.classList.add('qr--title'); | |
| unnamedTitle.textContent = `Unnamed Args - /${executor.name}`; | |
| if (executor.command.name == 'run') { | |
| unnamedTitle.textContent += `${(executor.name == ':' ? '' : ' ')}${executor.unnamedArgumentList[0]?.value}`; | |
| } | |
| wrap.append(unnamedTitle); | |
| } | |
| let i = 0; | |
| let unnamed = this.debugController.unnamedArguments ?? []; | |
| if (!Array.isArray(unnamed)) unnamed = [unnamed]; | |
| while (unnamed.length < executor.unnamedArgumentList?.length ?? 0) unnamed.push(undefined); | |
| unnamed = unnamed.map((it,idx)=>[executor.unnamedArgumentList?.[idx], it]); | |
| for (const arg of unnamed) { | |
| i++; | |
| const item = document.createElement('div'); { | |
| item.classList.add('qr--var'); | |
| const k = document.createElement('div'); { | |
| k.classList.add('qr--key'); | |
| k.textContent = i.toString(); | |
| item.append(k); | |
| } | |
| const vUnresolved = document.createElement('div'); { | |
| vUnresolved.classList.add('qr--val'); | |
| vUnresolved.classList.add('qr--singleCol'); | |
| const val = arg[0]?.value; | |
| if (val instanceof SlashCommandClosure) { | |
| vUnresolved.classList.add('qr--closure'); | |
| vUnresolved.title = val.rawText; | |
| vUnresolved.textContent = val.toString(); | |
| } else if (val === undefined) { | |
| vUnresolved.classList.add('qr--undefined'); | |
| vUnresolved.textContent = 'undefined'; | |
| } else { | |
| let jsonVal; | |
| try { jsonVal = JSON.parse(val); } catch { /* empty */ } | |
| if (jsonVal && typeof jsonVal == 'object') { | |
| vUnresolved.textContent = JSON.stringify(jsonVal, null, 2); | |
| } else { | |
| vUnresolved.textContent = val; | |
| vUnresolved.classList.add('qr--simple'); | |
| } | |
| } | |
| item.append(vUnresolved); | |
| } | |
| const vResolved = document.createElement('div'); { | |
| vResolved.classList.add('qr--val'); | |
| vResolved.classList.add('qr--singleCol'); | |
| if (this.debugController.unnamedArguments === undefined) { | |
| vResolved.classList.add('qr--unresolved'); | |
| } else if ((Array.isArray(this.debugController.unnamedArguments) ? this.debugController.unnamedArguments : [this.debugController.unnamedArguments]).length < i) { | |
| // do nothing | |
| } else { | |
| const val = arg[1]; | |
| if (val instanceof SlashCommandClosure) { | |
| vResolved.classList.add('qr--closure'); | |
| vResolved.title = val.rawText; | |
| vResolved.textContent = val.toString(); | |
| } else if (val === undefined) { | |
| vResolved.classList.add('qr--undefined'); | |
| vResolved.textContent = 'undefined'; | |
| } else { | |
| let jsonVal; | |
| try { jsonVal = JSON.parse(val); } catch { /* empty */ } | |
| if (jsonVal && typeof jsonVal == 'object') { | |
| vResolved.textContent = JSON.stringify(jsonVal, null, 2); | |
| } else { | |
| vResolved.textContent = val; | |
| vResolved.classList.add('qr--simple'); | |
| } | |
| } | |
| } | |
| item.append(vResolved); | |
| } | |
| wrap.append(item); | |
| } | |
| } | |
| } | |
| } | |
| // current scope | |
| const title = document.createElement('div'); { | |
| title.classList.add('qr--title'); | |
| title.textContent = isCurrent ? 'Current Scope' : 'Parent Scope'; | |
| if (c.source == source) { | |
| let hi; | |
| title.addEventListener('pointerenter', ()=>{ | |
| const loc = this.getEditorPosition(Math.max(0, c.executorList[0].start - 1), c.executorList.slice(-1)[0].end, c.fullText); | |
| const layer = syntax.getBoundingClientRect(); | |
| hi = document.createElement('div'); | |
| hi.classList.add('qr--highlight-secondary'); | |
| hi.style.left = `${loc.left - layer.left}px`; | |
| hi.style.width = `${loc.right - loc.left}px`; | |
| hi.style.top = `${loc.top - layer.top + syntax.scrollTop}px`; | |
| hi.style.height = `${loc.bottom - loc.top}px`; | |
| syntax.append(hi); | |
| }); | |
| title.addEventListener('pointerleave', ()=>hi?.remove()); | |
| } | |
| wrap.append(title); | |
| } | |
| for (const key of Object.keys(scope.variables)) { | |
| const isHidden = varNames.includes(key); | |
| if (!isHidden) varNames.push(key); | |
| const item = document.createElement('div'); { | |
| item.classList.add('qr--var'); | |
| if (isHidden) item.classList.add('qr--isHidden'); | |
| const k = document.createElement('div'); { | |
| k.classList.add('qr--key'); | |
| k.textContent = key; | |
| item.append(k); | |
| } | |
| const v = document.createElement('div'); { | |
| v.classList.add('qr--val'); | |
| const val = scope.variables[key]; | |
| if (val instanceof SlashCommandClosure) { | |
| v.classList.add('qr--closure'); | |
| v.title = val.rawText; | |
| v.textContent = val.toString(); | |
| } else if (val === undefined) { | |
| v.classList.add('qr--undefined'); | |
| v.textContent = 'undefined'; | |
| } else { | |
| let jsonVal; | |
| try { jsonVal = JSON.parse(val); } catch { /* empty */ } | |
| if (jsonVal && typeof jsonVal == 'object') { | |
| v.textContent = JSON.stringify(jsonVal, null, 2); | |
| } else { | |
| v.textContent = val; | |
| v.classList.add('qr--simple'); | |
| } | |
| } | |
| item.append(v); | |
| } | |
| wrap.append(item); | |
| } | |
| } | |
| for (const key of Object.keys(scope.macros)) { | |
| const isHidden = macroNames.includes(key); | |
| if (!isHidden) macroNames.push(key); | |
| const item = document.createElement('div'); { | |
| item.classList.add('qr--macro'); | |
| if (isHidden) item.classList.add('qr--isHidden'); | |
| const k = document.createElement('div'); { | |
| k.classList.add('qr--key'); | |
| k.textContent = key; | |
| item.append(k); | |
| } | |
| const v = document.createElement('div'); { | |
| v.classList.add('qr--val'); | |
| const val = scope.macros[key]; | |
| if (val instanceof SlashCommandClosure) { | |
| v.classList.add('qr--closure'); | |
| v.title = val.rawText; | |
| v.textContent = val.toString(); | |
| } else if (val === undefined) { | |
| v.classList.add('qr--undefined'); | |
| v.textContent = 'undefined'; | |
| } else { | |
| let jsonVal; | |
| try { jsonVal = JSON.parse(val); } catch { /* empty */ } | |
| if (jsonVal && typeof jsonVal == 'object') { | |
| v.textContent = JSON.stringify(jsonVal, null, 2); | |
| } else { | |
| v.textContent = val; | |
| v.classList.add('qr--simple'); | |
| } | |
| } | |
| item.append(v); | |
| } | |
| wrap.append(item); | |
| } | |
| } | |
| const pipeItem = document.createElement('div'); { | |
| pipeItem.classList.add('qr--pipe'); | |
| const k = document.createElement('div'); { | |
| k.classList.add('qr--key'); | |
| k.textContent = 'pipe'; | |
| pipeItem.append(k); | |
| } | |
| const v = document.createElement('div'); { | |
| v.classList.add('qr--val'); | |
| const val = scope.pipe; | |
| if (val instanceof SlashCommandClosure) { | |
| v.classList.add('qr--closure'); | |
| v.title = val.rawText; | |
| v.textContent = val.toString(); | |
| } else if (val === undefined) { | |
| v.classList.add('qr--undefined'); | |
| v.textContent = 'undefined'; | |
| } else { | |
| let jsonVal; | |
| try { jsonVal = JSON.parse(val); } catch { /* empty */ } | |
| if (jsonVal && typeof jsonVal == 'object') { | |
| v.textContent = JSON.stringify(jsonVal, null, 2); | |
| } else { | |
| v.textContent = val; | |
| v.classList.add('qr--simple'); | |
| } | |
| } | |
| pipeItem.append(v); | |
| } | |
| wrap.append(pipeItem); | |
| } | |
| if (scope.parent) { | |
| wrap.append(buildVars(scope.parent)); | |
| } | |
| } | |
| return wrap; | |
| }; | |
| const buildStack = ()=>{ | |
| const wrap = document.createElement('div'); { | |
| wrap.classList.add('qr--stack'); | |
| const title = document.createElement('div'); { | |
| title.classList.add('qr--title'); | |
| title.textContent = 'Call Stack'; | |
| wrap.append(title); | |
| } | |
| let ei = -1; | |
| for (const executor of this.debugController.cmdStack.toReversed()) { | |
| ei++; | |
| const c = this.debugController.stack.toReversed()[ei]; | |
| const item = document.createElement('div'); { | |
| item.classList.add('qr--item'); | |
| if (executor.source == source) { | |
| let hi; | |
| item.addEventListener('pointerenter', ()=>{ | |
| const loc = this.getEditorPosition(Math.max(0, executor.start - 1), executor.end, c.fullText); | |
| const layer = syntax.getBoundingClientRect(); | |
| hi = document.createElement('div'); | |
| hi.classList.add('qr--highlight-secondary'); | |
| hi.style.left = `${loc.left - layer.left}px`; | |
| hi.style.width = `${loc.right - loc.left}px`; | |
| hi.style.top = `${loc.top - layer.top + syntax.scrollTop}px`; | |
| hi.style.height = `${loc.bottom - loc.top}px`; | |
| syntax.append(hi); | |
| }); | |
| item.addEventListener('pointerleave', ()=>hi?.remove()); | |
| } | |
| const cmd = document.createElement('div'); { | |
| cmd.classList.add('qr--cmd'); | |
| cmd.textContent = `/${executor.name}`; | |
| if (executor.command.name == 'run') { | |
| cmd.textContent += `${(executor.name == ':' ? '' : ' ')}${executor.unnamedArgumentList[0]?.value}`; | |
| } | |
| item.append(cmd); | |
| } | |
| const src = document.createElement('div'); { | |
| src.classList.add('qr--source'); | |
| const line = closure.fullText.slice(0, executor.start).split('\n').length; | |
| if (uuidCheck.test(executor.source)) { | |
| const p1 = document.createElement('span'); { | |
| p1.classList.add('qr--fixed'); | |
| p1.textContent = executor.source.slice(0,5); | |
| src.append(p1); | |
| } | |
| const p2 = document.createElement('span'); { | |
| p2.classList.add('qr--truncated'); | |
| p2.textContent = '…'; | |
| src.append(p2); | |
| } | |
| const p3 = document.createElement('span'); { | |
| p3.classList.add('qr--fixed'); | |
| p3.textContent = `${executor.source.slice(-5)}:${line}`; | |
| src.append(p3); | |
| } | |
| src.title = `anonymous: ${executor.source}`; | |
| } else { | |
| src.textContent = `${executor.source}:${line}`; | |
| } | |
| item.append(src); | |
| } | |
| wrap.append(item); | |
| } | |
| } | |
| } | |
| return wrap; | |
| }; | |
| this.editorDebugState.append(buildVars(closure.scope, true)); | |
| this.editorDebugState.append(buildStack()); | |
| this.editorDebugState.classList.add('qr--active'); | |
| const loc = this.getEditorPosition(Math.max(0, executor.start - 1), executor.end, closure.fullText); | |
| const layer = syntax.getBoundingClientRect(); | |
| const hi = document.createElement('div'); | |
| hi.classList.add('qr--highlight'); | |
| if (this.debugController.namedArguments === undefined) { | |
| hi.classList.add('qr--unresolved'); | |
| } | |
| hi.style.left = `${loc.left - layer.left}px`; | |
| hi.style.width = `${loc.right - loc.left}px`; | |
| hi.style.top = `${loc.top - layer.top + syntax.scrollTop}px`; | |
| hi.style.height = `${loc.bottom - loc.top}px`; | |
| syntax.append(hi); | |
| const isStepping = await this.debugController.awaitContinue(); | |
| hi.remove(); | |
| this.editorDebugState.textContent = ''; | |
| this.editorDebugState.classList.remove('qr--active'); | |
| this.editorDom.classList.remove('qr--isPaused'); | |
| return isStepping; | |
| }; | |
| const result = await this.onDebug(this); | |
| if (this.abortController?.signal?.aborted) { | |
| this.editorExecuteProgress.classList.add('qr--aborted'); | |
| } else { | |
| this.editorExecuteResult.textContent = result?.toString(); | |
| this.editorExecuteResult.classList.add('qr--hasResult'); | |
| this.editorExecuteProgress.classList.add('qr--success'); | |
| } | |
| this.editorExecuteProgress.classList.remove('qr--paused'); | |
| } catch (ex) { | |
| this.editorExecuteErrors.classList.add('qr--hasErrors'); | |
| this.editorExecuteProgress.classList.add('qr--error'); | |
| this.editorExecuteProgress.classList.remove('qr--paused'); | |
| if (ex instanceof SlashCommandParserError) { | |
| this.editorExecuteErrors.innerHTML = ` | |
| <div>${ex.message}</div> | |
| <div>Line: ${ex.line} Column: ${ex.column}</div> | |
| <pre style="text-align:left;">${ex.hint}</pre> | |
| `; | |
| } else { | |
| this.editorExecuteErrors.innerHTML = ` | |
| <div>${ex.message}</div> | |
| `; | |
| } | |
| } | |
| if (noSyntax) { | |
| this.editorDom.querySelector('#qr--modal-messageHolder').classList.add('qr--noSyntax'); | |
| } | |
| this.editorMessageLabel.innerHTML = ''; | |
| this.editorMessageLabel.textContent = 'Message / Command: '; | |
| this.editorMessage.value = oText; | |
| this.editorMessage.dispatchEvent(new Event('input', { bubbles:true })); | |
| this.editorExecutePromise = null; | |
| this.editorExecuteBtn.classList.remove('qr--busy'); | |
| this.editorDom.classList.remove('qr--isExecuting'); | |
| this.isExecuting = false; | |
| this.editorPopup.onClosing = null; | |
| } | |
| updateEditorProgress(done, total) { | |
| this.editorExecuteProgress.style.setProperty('--prog', `${done / total * 100}`); | |
| } | |
| delete() { | |
| if (this.onDelete) { | |
| this.unrender(); | |
| this.unrenderSettings(); | |
| this.onDelete(this); | |
| } | |
| } | |
| /** | |
| * @param {string} value | |
| */ | |
| updateMessage(value) { | |
| if (this.onUpdate) { | |
| if (this.settingsDomMessage && this.settingsDomMessage.value != value) { | |
| this.settingsDomMessage.value = value; | |
| } | |
| this.message = value; | |
| this.updateRender(); | |
| this.onUpdate(this); | |
| } | |
| } | |
| /** | |
| * @param {string} value | |
| */ | |
| updateIcon(value) { | |
| if (this.onUpdate) { | |
| if (value === null) return; | |
| if (this.settingsDomIcon) { | |
| if (this.icon != value) { | |
| if (value == '') { | |
| if (this.icon) { | |
| this.settingsDomIcon.classList.remove(this.icon); | |
| } | |
| this.settingsDomIcon.textContent = '…'; | |
| this.settingsDomIcon.classList.remove('fa-solid'); | |
| } else { | |
| if (this.icon) { | |
| this.settingsDomIcon.classList.remove(this.icon); | |
| } else { | |
| this.settingsDomIcon.classList.add('fa-solid'); | |
| } | |
| this.settingsDomIcon.classList.add(value); | |
| } | |
| } | |
| } | |
| this.icon = value; | |
| this.updateRender(); | |
| this.onUpdate(this); | |
| } | |
| } | |
| /** | |
| * @param {boolean} value | |
| */ | |
| updateShowLabel(value) { | |
| if (this.onUpdate) { | |
| this.showLabel = value; | |
| this.updateRender(); | |
| this.onUpdate(this); | |
| } | |
| } | |
| /** | |
| * @param {string} value | |
| */ | |
| updateLabel(value) { | |
| if (this.onUpdate) { | |
| if (this.settingsDomLabel && this.settingsDomLabel.value != value) { | |
| this.settingsDomLabel.value = value; | |
| } | |
| this.label = value; | |
| this.updateRender(); | |
| this.onUpdate(this); | |
| } | |
| } | |
| /** | |
| * @param {string} value | |
| */ | |
| updateTitle(value) { | |
| if (this.onUpdate) { | |
| this.title = value; | |
| this.updateRender(); | |
| this.onUpdate(this); | |
| } | |
| } | |
| updateContext() { | |
| if (this.onUpdate) { | |
| this.updateRender(); | |
| this.onUpdate(this); | |
| } | |
| } | |
| addContextLink(cl) { | |
| this.contextList.push(cl); | |
| this.updateContext(); | |
| } | |
| removeContextLink(setName) { | |
| const idx = this.contextList.findIndex(it=>it.set.name == setName); | |
| if (idx > -1) { | |
| this.contextList.splice(idx, 1); | |
| this.updateContext(); | |
| } | |
| } | |
| clearContextLinks() { | |
| if (this.contextList.length) { | |
| this.contextList.splice(0, this.contextList.length); | |
| this.updateContext(); | |
| } | |
| } | |
| async execute(args = {}, isEditor = false, isRun = false, options = {}) { | |
| if (this.message?.length > 0 && this.onExecute) { | |
| const scope = new SlashCommandScope(); | |
| for (const key of Object.keys(args)) { | |
| if (key[0] == '_') continue; | |
| if (key == 'isAutoExecute') continue; | |
| scope.setMacro(`arg::${key}`, args[key]); | |
| } | |
| scope.setMacro('arg::*', ''); | |
| if (isEditor) { | |
| this.abortController = new SlashCommandAbortController(); | |
| } | |
| return await this.onExecute(this, { | |
| message: this.message, | |
| isAutoExecute: args.isAutoExecute ?? false, | |
| isEditor, | |
| isRun, | |
| scope, | |
| executionOptions: options, | |
| }); | |
| } | |
| } | |
| toJSON() { | |
| return { | |
| id: this.id, | |
| icon: this.icon, | |
| showLabel: this.showLabel, | |
| label: this.label, | |
| title: this.title, | |
| message: this.message, | |
| contextList: this.contextList, | |
| preventAutoExecute: this.preventAutoExecute, | |
| isHidden: this.isHidden, | |
| executeOnStartup: this.executeOnStartup, | |
| executeOnUser: this.executeOnUser, | |
| executeOnAi: this.executeOnAi, | |
| executeOnChatChange: this.executeOnChatChange, | |
| executeOnGroupMemberDraft: this.executeOnGroupMemberDraft, | |
| executeOnNewChat: this.executeOnNewChat, | |
| automationId: this.automationId, | |
| }; | |
| } | |
| } | |