|
import { getRequestHeaders, substituteParams } from '../../../../script.js'; |
|
import { Popup, POPUP_RESULT, POPUP_TYPE } from '../../../popup.js'; |
|
import { executeSlashCommandsOnChatInput, executeSlashCommandsWithOptions } from '../../../slash-commands.js'; |
|
import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js'; |
|
import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js'; |
|
import { debounceAsync, warn } from '../index.js'; |
|
import { QuickReply } from './QuickReply.js'; |
|
|
|
export class QuickReplySet { |
|
static list = []; |
|
|
|
static from(props) { |
|
props.qrList = []; |
|
const instance = Object.assign(new this(), props); |
|
|
|
return instance; |
|
} |
|
|
|
|
|
|
|
|
|
static get(name) { |
|
return this.list.find(it=>it.name == name); |
|
} |
|
|
|
name; |
|
disableSend = false; |
|
placeBeforeInput = false; |
|
injectInput = false; |
|
color = 'transparent'; |
|
onlyBorderColor = false; |
|
qrList = []; |
|
idIndex = 0; |
|
isDeleted = false; |
|
save; |
|
dom; |
|
settingsDom; |
|
|
|
constructor() { |
|
this.save = debounceAsync(()=>this.performSave(), 200); |
|
} |
|
|
|
init() { |
|
this.qrList.forEach(qr=>this.hookQuickReply(qr)); |
|
} |
|
|
|
unrender() { |
|
this.dom?.remove(); |
|
this.dom = null; |
|
} |
|
render() { |
|
this.unrender(); |
|
if (!this.dom) { |
|
const root = document.createElement('div'); { |
|
this.dom = root; |
|
root.classList.add('qr--buttons'); |
|
this.updateColor(); |
|
this.qrList.filter(qr=>!qr.isHidden).forEach(qr=>{ |
|
root.append(qr.render()); |
|
}); |
|
} |
|
} |
|
return this.dom; |
|
} |
|
rerender() { |
|
if (!this.dom) return; |
|
this.dom.innerHTML = ''; |
|
this.qrList.filter(qr=>!qr.isHidden).forEach(qr=>{ |
|
this.dom.append(qr.render()); |
|
}); |
|
} |
|
updateColor() { |
|
if (!this.dom) return; |
|
if (this.color && this.color != 'transparent') { |
|
this.dom.style.setProperty('--qr--color', this.color); |
|
this.dom.classList.add('qr--color'); |
|
if (this.onlyBorderColor) { |
|
this.dom.classList.add('qr--borderColor'); |
|
} else { |
|
this.dom.classList.remove('qr--borderColor'); |
|
} |
|
} else { |
|
this.dom.style.setProperty('--qr--color', 'transparent'); |
|
this.dom.classList.remove('qr--color'); |
|
this.dom.classList.remove('qr--borderColor'); |
|
} |
|
} |
|
|
|
renderSettings() { |
|
if (!this.settingsDom) { |
|
this.settingsDom = document.createElement('div'); { |
|
this.settingsDom.classList.add('qr--set-qrListContents'); |
|
this.qrList.forEach((qr,idx)=>{ |
|
this.renderSettingsItem(qr, idx); |
|
}); |
|
} |
|
} |
|
return this.settingsDom; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
renderSettingsItem(qr, idx) { |
|
this.settingsDom.append(qr.renderSettings(idx)); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
async debug(qr) { |
|
const parser = new SlashCommandParser(); |
|
const closure = parser.parse(qr.message, true, [], qr.abortController, qr.debugController); |
|
closure.source = `${this.name}.${qr.label}`; |
|
closure.onProgress = (done, total) => qr.updateEditorProgress(done, total); |
|
closure.scope.setMacro('arg::*', ''); |
|
return (await closure.execute())?.pipe; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async executeWithOptions(qr, options = {}) { |
|
options = Object.assign({ |
|
message:null, |
|
isAutoExecute:false, |
|
isEditor:false, |
|
isRun:false, |
|
scope:null, |
|
executionOptions:{}, |
|
}, options); |
|
const execOptions = options.executionOptions; |
|
|
|
const ta = document.querySelector('#send_textarea'); |
|
const finalMessage = options.message ?? qr.message; |
|
let input = ta.value; |
|
if (!options.isAutoExecute && !options.isEditor && !options.isRun && this.injectInput && input.length > 0) { |
|
if (this.placeBeforeInput) { |
|
input = `${finalMessage} ${input}`; |
|
} else { |
|
input = `${input} ${finalMessage}`; |
|
} |
|
} else { |
|
input = `${finalMessage} `; |
|
} |
|
|
|
if (input[0] == '/' && !this.disableSend) { |
|
let result; |
|
if (options.isAutoExecute || options.isRun) { |
|
result = await executeSlashCommandsWithOptions(input, Object.assign(execOptions, { |
|
handleParserErrors: true, |
|
scope: options.scope, |
|
source: `${this.name}.${qr.label}`, |
|
})); |
|
} else if (options.isEditor) { |
|
result = await executeSlashCommandsWithOptions(input, Object.assign(execOptions, { |
|
handleParserErrors: false, |
|
scope: options.scope, |
|
abortController: qr.abortController, |
|
source: `${this.name}.${qr.label}`, |
|
onProgress: (done, total) => qr.updateEditorProgress(done, total), |
|
})); |
|
} else { |
|
result = await executeSlashCommandsOnChatInput(input, Object.assign(execOptions, { |
|
scope: options.scope, |
|
source: `${this.name}.${qr.label}`, |
|
})); |
|
} |
|
return typeof result === 'object' ? result?.pipe : ''; |
|
} |
|
|
|
ta.value = substituteParams(input); |
|
ta.focus(); |
|
|
|
if (!this.disableSend) { |
|
|
|
document.querySelector('#send_but').click(); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
async execute(qr, message = null, isAutoExecute = false, scope = null) { |
|
return this.executeWithOptions(qr, { |
|
message, |
|
isAutoExecute, |
|
scope, |
|
}); |
|
} |
|
|
|
addQuickReply(data = {}) { |
|
const id = Math.max(this.idIndex, this.qrList.reduce((max,qr)=>Math.max(max,qr.id),0)) + 1; |
|
data.id = |
|
this.idIndex = id + 1; |
|
const qr = QuickReply.from(data); |
|
this.qrList.push(qr); |
|
this.hookQuickReply(qr); |
|
if (this.settingsDom) { |
|
this.renderSettingsItem(qr, this.qrList.length - 1); |
|
} |
|
if (this.dom) { |
|
this.dom.append(qr.render()); |
|
} |
|
this.save(); |
|
return qr; |
|
} |
|
|
|
addQuickReplyFromText(qrJson) { |
|
let data; |
|
if (qrJson) { |
|
try { |
|
data = JSON.parse(qrJson ?? '{}'); |
|
delete data.id; |
|
} catch { |
|
|
|
} |
|
if (data) { |
|
|
|
if (data.label === undefined || data.message === undefined) { |
|
|
|
toastr.error('Not a QR.'); |
|
return; |
|
} |
|
} else { |
|
|
|
data = { message: qrJson }; |
|
} |
|
} else { |
|
data = {}; |
|
} |
|
const newQr = this.addQuickReply(data); |
|
return newQr; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
hookQuickReply(qr) { |
|
qr.onDebug = ()=>this.debug(qr); |
|
qr.onExecute = (_, options)=>this.executeWithOptions(qr, options); |
|
qr.onDelete = ()=>this.removeQuickReply(qr); |
|
qr.onUpdate = ()=>this.save(); |
|
qr.onInsertBefore = (qrJson)=>{ |
|
this.addQuickReplyFromText(qrJson); |
|
const newQr = this.qrList.pop(); |
|
this.qrList.splice(this.qrList.indexOf(qr), 0, newQr); |
|
if (qr.settingsDom) { |
|
qr.settingsDom.insertAdjacentElement('beforebegin', newQr.settingsDom); |
|
} |
|
this.save(); |
|
}; |
|
qr.onTransfer = async()=>{ |
|
|
|
let sel; |
|
let isCopy = false; |
|
const dom = document.createElement('div'); { |
|
dom.classList.add('qr--transferModal'); |
|
const title = document.createElement('h3'); { |
|
title.textContent = 'Transfer Quick Reply'; |
|
dom.append(title); |
|
} |
|
const subTitle = document.createElement('h4'); { |
|
const entryName = qr.label; |
|
const bookName = this.name; |
|
subTitle.textContent = `${bookName}: ${entryName}`; |
|
dom.append(subTitle); |
|
} |
|
sel = document.createElement('select'); { |
|
sel.classList.add('qr--transferSelect'); |
|
sel.setAttribute('autofocus', '1'); |
|
const noOpt = document.createElement('option'); { |
|
noOpt.value = ''; |
|
noOpt.textContent = '-- Select QR Set --'; |
|
sel.append(noOpt); |
|
} |
|
for (const qrs of QuickReplySet.list) { |
|
const opt = document.createElement('option'); { |
|
opt.value = qrs.name; |
|
opt.textContent = qrs.name; |
|
sel.append(opt); |
|
} |
|
} |
|
sel.addEventListener('keyup', (evt)=>{ |
|
if (evt.key == 'Shift') { |
|
(dlg.dom ?? dlg.dlg).classList.remove('qr--isCopy'); |
|
return; |
|
} |
|
}); |
|
sel.addEventListener('keydown', (evt)=>{ |
|
if (evt.key == 'Shift') { |
|
(dlg.dom ?? dlg.dlg).classList.add('qr--isCopy'); |
|
return; |
|
} |
|
if (!evt.ctrlKey && !evt.altKey && evt.key == 'Enter') { |
|
evt.preventDefault(); |
|
if (evt.shiftKey) isCopy = true; |
|
dlg.completeAffirmative(); |
|
} |
|
}); |
|
dom.append(sel); |
|
} |
|
const hintP = document.createElement('p'); { |
|
const hint = document.createElement('small'); { |
|
hint.textContent = 'Type or arrows to select QR Set. Enter to transfer. Shift+Enter to copy.'; |
|
hintP.append(hint); |
|
} |
|
dom.append(hintP); |
|
} |
|
} |
|
const dlg = new Popup(dom, POPUP_TYPE.CONFIRM, null, { okButton:'Transfer', cancelButton:'Cancel' }); |
|
const copyBtn = document.createElement('div'); { |
|
copyBtn.classList.add('qr--copy'); |
|
copyBtn.classList.add('menu_button'); |
|
copyBtn.textContent = 'Copy'; |
|
copyBtn.addEventListener('click', ()=>{ |
|
isCopy = true; |
|
dlg.completeAffirmative(); |
|
}); |
|
(dlg.ok ?? dlg.okButton).insertAdjacentElement('afterend', copyBtn); |
|
} |
|
const prom = dlg.show(); |
|
sel.focus(); |
|
await prom; |
|
if (dlg.result == POPUP_RESULT.AFFIRMATIVE) { |
|
const qrs = QuickReplySet.list.find(it=>it.name == sel.value); |
|
qrs.addQuickReply(qr.toJSON()); |
|
if (!isCopy) { |
|
qr.delete(); |
|
} |
|
} |
|
}; |
|
} |
|
|
|
removeQuickReply(qr) { |
|
this.qrList.splice(this.qrList.indexOf(qr), 1); |
|
this.save(); |
|
} |
|
|
|
toJSON() { |
|
return { |
|
version: 2, |
|
name: this.name, |
|
disableSend: this.disableSend, |
|
placeBeforeInput: this.placeBeforeInput, |
|
injectInput: this.injectInput, |
|
color: this.color, |
|
onlyBorderColor: this.onlyBorderColor, |
|
qrList: this.qrList, |
|
idIndex: this.idIndex, |
|
}; |
|
} |
|
|
|
async performSave() { |
|
const response = await fetch('/api/quick-replies/save', { |
|
method: 'POST', |
|
headers: getRequestHeaders(), |
|
body: JSON.stringify(this), |
|
}); |
|
|
|
if (response.ok) { |
|
this.rerender(); |
|
} else { |
|
warn(`Failed to save Quick Reply Set: ${this.name}`); |
|
console.error('QR could not be saved', response); |
|
} |
|
} |
|
|
|
async delete() { |
|
const response = await fetch('/api/quick-replies/delete', { |
|
method: 'POST', |
|
headers: getRequestHeaders(), |
|
body: JSON.stringify(this), |
|
}); |
|
|
|
if (response.ok) { |
|
this.unrender(); |
|
const idx = QuickReplySet.list.indexOf(this); |
|
if (idx > -1) { |
|
QuickReplySet.list.splice(idx, 1); |
|
this.isDeleted = true; |
|
} else { |
|
warn(`Deleted Quick Reply Set was not found in the list of sets: ${this.name}`); |
|
} |
|
} else { |
|
warn(`Failed to delete Quick Reply Set: ${this.name}`); |
|
} |
|
} |
|
} |
|
|