🎉 初始化项目
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
import { warn } from '../index.js';
|
||||
import { QuickReply } from './QuickReply.js';
|
||||
import { QuickReplySettings } from './QuickReplySettings.js';
|
||||
|
||||
export class AutoExecuteHandler {
|
||||
/** @type {QuickReplySettings} */ settings;
|
||||
|
||||
/** @type {Boolean[]}*/ preventAutoExecuteStack = [];
|
||||
|
||||
|
||||
|
||||
|
||||
constructor(/** @type {QuickReplySettings} */settings) {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
|
||||
checkExecute() {
|
||||
return this.settings.isEnabled && !this.preventAutoExecuteStack.slice(-1)[0];
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
async performAutoExecute(/** @type {QuickReply[]} */qrList) {
|
||||
for (const qr of qrList) {
|
||||
this.preventAutoExecuteStack.push(qr.preventAutoExecute);
|
||||
try {
|
||||
await qr.execute({ isAutoExecute:true });
|
||||
} catch (ex) {
|
||||
warn(ex);
|
||||
} finally {
|
||||
this.preventAutoExecuteStack.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
getCommands(eventName) {
|
||||
const getFromConfig = (config) => {
|
||||
// This safely handles cases where a link exists but the set hasn't been loaded (link.set is null)
|
||||
return config?.setList?.map(link => link.set ? link.set.qrList.filter(qr => qr[eventName]) : [])?.flat() ?? [];
|
||||
};
|
||||
return [
|
||||
...getFromConfig(this.settings.config),
|
||||
...getFromConfig(this.settings.chatConfig),
|
||||
...getFromConfig(this.settings.charConfig),
|
||||
];
|
||||
}
|
||||
|
||||
async handleStartup() {
|
||||
if (!this.checkExecute()) return;
|
||||
await this.performAutoExecute(this.getCommands('executeOnStartup'));
|
||||
}
|
||||
|
||||
async handleUser() {
|
||||
if (!this.checkExecute()) return;
|
||||
await this.performAutoExecute(this.getCommands('executeOnUser'));
|
||||
}
|
||||
|
||||
async handleAi() {
|
||||
if (!this.checkExecute()) return;
|
||||
await this.performAutoExecute(this.getCommands('executeOnAi'));
|
||||
}
|
||||
|
||||
async handleChatChanged() {
|
||||
if (!this.checkExecute()) return;
|
||||
await this.performAutoExecute(this.getCommands('executeOnChatChange'));
|
||||
}
|
||||
|
||||
async handleGroupMemberDraft() {
|
||||
if (!this.checkExecute()) return;
|
||||
await this.performAutoExecute(this.getCommands('executeOnGroupMemberDraft'));
|
||||
}
|
||||
|
||||
async handleNewChat() {
|
||||
if (!this.checkExecute()) return;
|
||||
await this.performAutoExecute(this.getCommands('executeOnNewChat'));
|
||||
}
|
||||
|
||||
async handleBeforeGeneration() {
|
||||
if (!this.checkExecute()) return;
|
||||
await this.performAutoExecute(this.getCommands('executeBeforeGeneration'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any[]} entries Set of activated entries
|
||||
*/
|
||||
async handleWIActivation(entries) {
|
||||
if (!this.checkExecute() || !Array.isArray(entries) || entries.length === 0) return;
|
||||
const automationIds = entries.map(entry => entry.automationId).filter(Boolean);
|
||||
if (automationIds.length === 0) return;
|
||||
|
||||
const getFromConfig = (config) => {
|
||||
return config?.setList
|
||||
?.map(link => link.set ? link.set.qrList.filter(qr => qr.automationId && automationIds.includes(qr.automationId)) : [])
|
||||
?.flat() ?? [];
|
||||
};
|
||||
|
||||
const qrList = [
|
||||
...getFromConfig(this.settings.config),
|
||||
...getFromConfig(this.settings.chatConfig),
|
||||
...getFromConfig(this.settings.charConfig),
|
||||
];
|
||||
|
||||
await this.performAutoExecute(qrList);
|
||||
}
|
||||
}
|
||||
1934
web-app/public/scripts/extensions/quick-reply/src/QuickReply.js
Normal file
1934
web-app/public/scripts/extensions/quick-reply/src/QuickReply.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,127 @@
|
||||
import { getSortableDelay } from '../../../utils.js';
|
||||
import { QuickReplySetLink } from './QuickReplySetLink.js';
|
||||
import { QuickReplySet } from './QuickReplySet.js';
|
||||
|
||||
export class QuickReplyConfig {
|
||||
/**@type {QuickReplySetLink[]}*/ setList = [];
|
||||
/**@type {'global'|'chat'|'character'}*/ scope;
|
||||
|
||||
/**@type {Function}*/ onUpdate;
|
||||
/**@type {Function}*/ onRequestEditSet;
|
||||
|
||||
/**@type {HTMLElement}*/ dom;
|
||||
/**@type {HTMLElement}*/ setListDom;
|
||||
|
||||
|
||||
|
||||
|
||||
static from(props) {
|
||||
props.setList = props.setList?.map(it=>QuickReplySetLink.from(it))?.filter(it=>it.set) ?? [];
|
||||
const instance = Object.assign(new this(), props);
|
||||
instance.init();
|
||||
return instance;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
init() {
|
||||
this.setList.forEach(it=>this.hookQuickReplyLink(it));
|
||||
}
|
||||
|
||||
|
||||
hasSet(qrs) {
|
||||
return this.setList.find(it=>it.set == qrs) != null;
|
||||
}
|
||||
addSet(qrs, isVisible = true) {
|
||||
if (!this.hasSet(qrs)) {
|
||||
const qrl = new QuickReplySetLink();
|
||||
qrl.set = qrs;
|
||||
qrl.isVisible = isVisible;
|
||||
this.hookQuickReplyLink(qrl);
|
||||
this.setList.push(qrl);
|
||||
this.setListDom.append(qrl.renderSettings(this.setList.length - 1));
|
||||
this.update();
|
||||
}
|
||||
}
|
||||
removeSet(qrs) {
|
||||
const idx = this.setList.findIndex(it=>it.set == qrs);
|
||||
if (idx > -1) {
|
||||
this.setList.splice(idx, 1);
|
||||
this.update();
|
||||
this.updateSetListDom();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
renderSettingsInto(/**@type {HTMLElement}*/root) {
|
||||
/**@type {HTMLElement}*/
|
||||
this.setListDom = root.querySelector('.qr--setList');
|
||||
root.querySelector('.qr--setListAdd').addEventListener('click', ()=>{
|
||||
const newSet = QuickReplySet.list.find(qr=>!this.setList.find(qrl=>qrl.set == qr));
|
||||
if (newSet) {
|
||||
this.addSet(newSet);
|
||||
} else {
|
||||
toastr.warning('All existing QR Sets have already been added.');
|
||||
}
|
||||
});
|
||||
this.updateSetListDom();
|
||||
}
|
||||
updateSetListDom() {
|
||||
this.setListDom.innerHTML = '';
|
||||
// @ts-ignore
|
||||
$(this.setListDom).sortable({
|
||||
delay: getSortableDelay(),
|
||||
stop: ()=>this.onSetListSort(),
|
||||
});
|
||||
this.setList.filter(it=>!it.set.isDeleted).forEach((qrl,idx)=>this.setListDom.append(qrl.renderSettings(idx)));
|
||||
}
|
||||
|
||||
|
||||
onSetListSort() {
|
||||
this.setList = Array.from(this.setListDom.children).map((it,idx)=>{
|
||||
const qrl = this.setList[Number(it.getAttribute('data-order'))];
|
||||
qrl.index = idx;
|
||||
it.setAttribute('data-order', String(idx));
|
||||
return qrl;
|
||||
});
|
||||
this.update();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @param {QuickReplySetLink} qrl
|
||||
*/
|
||||
hookQuickReplyLink(qrl) {
|
||||
qrl.onDelete = ()=>this.deleteQuickReplyLink(qrl);
|
||||
qrl.onUpdate = ()=>this.update();
|
||||
qrl.onRequestEditSet = ()=>this.requestEditSet(qrl.set);
|
||||
}
|
||||
|
||||
deleteQuickReplyLink(qrl) {
|
||||
this.setList.splice(this.setList.indexOf(qrl), 1);
|
||||
this.update();
|
||||
}
|
||||
|
||||
update() {
|
||||
if (this.onUpdate) {
|
||||
this.onUpdate(this);
|
||||
}
|
||||
}
|
||||
|
||||
requestEditSet(qrs) {
|
||||
if (this.onRequestEditSet) {
|
||||
this.onRequestEditSet(qrs);
|
||||
}
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
setList: this.setList,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { QuickReplySet } from './QuickReplySet.js';
|
||||
|
||||
export class QuickReplyContextLink {
|
||||
static from(props) {
|
||||
props.set = QuickReplySet.get(props.set);
|
||||
const x = Object.assign(new this(), props);
|
||||
return x;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**@type {QuickReplySet}*/ set;
|
||||
/**@type {Boolean}*/ isChained = false;
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
set: this.set?.name,
|
||||
isChained: this.isChained,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,412 @@
|
||||
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 {
|
||||
/**@type {QuickReplySet[]}*/ static list = [];
|
||||
|
||||
/**
|
||||
* @param {Partial<QuickReplySet>} props
|
||||
* @returns {QuickReplySet}
|
||||
*/
|
||||
static from(props) {
|
||||
props.qrList = []; //props.qrList?.map(it=>QuickReply.from(it));
|
||||
const instance = Object.assign(new this(), props);
|
||||
// instance.init();
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name - name of the QuickReplySet
|
||||
*/
|
||||
static get(name) {
|
||||
return this.list.find(it=>it.name == name);
|
||||
}
|
||||
|
||||
/**@type {string}*/ name;
|
||||
/**@type {'global'|'chat'|'character'}*/ scope = 'global';
|
||||
/**@type {boolean}*/ disableSend = false;
|
||||
/**@type {boolean}*/ placeBeforeInput = false;
|
||||
/**@type {boolean}*/ injectInput = false;
|
||||
/**@type {string}*/ color = 'transparent';
|
||||
/**@type {boolean}*/ onlyBorderColor = false;
|
||||
/**@type {QuickReply[]}*/ qrList = [];
|
||||
/**@type {number}*/ idIndex = 0;
|
||||
/**@type {boolean}*/ isDeleted = false;
|
||||
/**@type {function}*/ save;
|
||||
/**@type {HTMLElement}*/ dom;
|
||||
/**@type {HTMLElement}*/ 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;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param {QuickReply} qr
|
||||
* @param {number} idx
|
||||
*/
|
||||
renderSettingsItem(qr, idx) {
|
||||
this.settingsDom.append(qr.renderSettings(idx));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {QuickReply} qr
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {QuickReply} qr The QR to execute.
|
||||
* @param {object} options
|
||||
* @param {string} [options.message] (null) altered message to be used
|
||||
* @param {boolean} [options.isAutoExecute] (false) whether the execution is triggered by auto execute
|
||||
* @param {boolean} [options.isEditor] (false) whether the execution is triggered by the QR editor
|
||||
* @param {boolean} [options.isRun] (false) whether the execution is triggered by /run or /: (window.executeQuickReplyByName)
|
||||
* @param {SlashCommandScope} [options.scope] (null) scope to be used when running the command
|
||||
* @param {import('../../../slash-commands.js').ExecuteSlashCommandsOptions} [options.executionOptions] ({}) further execution options
|
||||
* @returns
|
||||
*/
|
||||
async executeWithOptions(qr, options = {}) {
|
||||
options = Object.assign({
|
||||
message:null,
|
||||
isAutoExecute:false,
|
||||
isEditor:false,
|
||||
isRun:false,
|
||||
scope:null,
|
||||
executionOptions:{},
|
||||
}, options);
|
||||
const execOptions = options.executionOptions;
|
||||
/**@type {HTMLTextAreaElement}*/
|
||||
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) {
|
||||
// @ts-ignore
|
||||
document.querySelector('#send_but').click();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {QuickReply} qr
|
||||
* @param {string} [message] - optional altered message to be used
|
||||
* @param {SlashCommandScope} [scope] - optional scope to be used when running the command
|
||||
*/
|
||||
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 {
|
||||
// not JSON data
|
||||
}
|
||||
if (data) {
|
||||
// JSON data
|
||||
if (data.label === undefined || data.message === undefined) {
|
||||
// not a QR
|
||||
toastr.error('Not a QR.');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// no JSON, use plaintext as QR message
|
||||
data = { message: qrJson };
|
||||
}
|
||||
} else {
|
||||
data = {};
|
||||
}
|
||||
const newQr = this.addQuickReply(data);
|
||||
return newQr;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {QuickReply} qr
|
||||
*/
|
||||
hookQuickReply(qr) {
|
||||
// @ts-ignore
|
||||
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()=>{
|
||||
/**@type {HTMLSelectElement} */
|
||||
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') {
|
||||
// @ts-ignore
|
||||
(dlg.dom ?? dlg.dlg).classList.remove('qr--isCopy');
|
||||
return;
|
||||
}
|
||||
});
|
||||
sel.addEventListener('keydown', (evt)=>{
|
||||
if (evt.key == 'Shift') {
|
||||
// @ts-ignore
|
||||
(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();
|
||||
});
|
||||
// @ts-ignore
|
||||
(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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import { QuickReplySet } from './QuickReplySet.js';
|
||||
|
||||
export class QuickReplySetLink {
|
||||
static from(props) {
|
||||
props.set = QuickReplySet.get(props.set);
|
||||
/**@type {QuickReplySetLink}*/
|
||||
const instance = Object.assign(new this(), props);
|
||||
return instance;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**@type {QuickReplySet}*/ set;
|
||||
/**@type {Boolean}*/ isVisible = true;
|
||||
|
||||
/**@type {Number}*/ index;
|
||||
|
||||
/**@type {Function}*/ onUpdate;
|
||||
/**@type {Function}*/ onRequestEditSet;
|
||||
/**@type {Function}*/ onDelete;
|
||||
|
||||
/**@type {HTMLElement}*/ settingsDom;
|
||||
|
||||
|
||||
|
||||
|
||||
renderSettings(idx) {
|
||||
this.index = idx;
|
||||
const item = document.createElement('div'); {
|
||||
this.settingsDom = item;
|
||||
item.classList.add('qr--item');
|
||||
item.setAttribute('data-order', String(this.index));
|
||||
const drag = document.createElement('div'); {
|
||||
drag.classList.add('drag-handle');
|
||||
drag.classList.add('ui-sortable-handle');
|
||||
drag.textContent = '☰';
|
||||
item.append(drag);
|
||||
}
|
||||
const set = document.createElement('select'); {
|
||||
set.classList.add('qr--set');
|
||||
// fix for jQuery sortable breaking childrens' touch events
|
||||
set.addEventListener('touchstart', (evt)=>evt.stopPropagation());
|
||||
set.addEventListener('change', ()=>{
|
||||
this.set = QuickReplySet.get(set.value);
|
||||
this.update();
|
||||
});
|
||||
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 == this.set;
|
||||
set.append(opt);
|
||||
}
|
||||
});
|
||||
item.append(set);
|
||||
}
|
||||
const visible = document.createElement('label'); {
|
||||
visible.classList.add('qr--visible');
|
||||
visible.title = 'Show buttons';
|
||||
const cb = document.createElement('input'); {
|
||||
cb.type = 'checkbox';
|
||||
cb.checked = this.isVisible;
|
||||
cb.addEventListener('click', ()=>{
|
||||
this.isVisible = cb.checked;
|
||||
this.update();
|
||||
});
|
||||
visible.append(cb);
|
||||
}
|
||||
visible.append('Buttons');
|
||||
item.append(visible);
|
||||
}
|
||||
const edit = document.createElement('div'); {
|
||||
edit.classList.add('menu_button');
|
||||
edit.classList.add('menu_button_icon');
|
||||
edit.classList.add('fa-solid');
|
||||
edit.classList.add('fa-pencil');
|
||||
edit.title = 'Edit quick reply set';
|
||||
edit.addEventListener('click', ()=>this.requestEditSet());
|
||||
item.append(edit);
|
||||
}
|
||||
const del = document.createElement('div'); {
|
||||
del.classList.add('qr--del');
|
||||
del.classList.add('menu_button');
|
||||
del.classList.add('menu_button_icon');
|
||||
del.classList.add('fa-solid');
|
||||
del.classList.add('fa-trash-can');
|
||||
del.title = 'Remove quick reply set';
|
||||
del.addEventListener('click', ()=>this.delete());
|
||||
item.append(del);
|
||||
}
|
||||
}
|
||||
return this.settingsDom;
|
||||
}
|
||||
unrenderSettings() {
|
||||
this.settingsDom?.remove();
|
||||
this.settingsDom = null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
update() {
|
||||
if (this.onUpdate) {
|
||||
this.onUpdate(this);
|
||||
}
|
||||
}
|
||||
requestEditSet() {
|
||||
if (this.onRequestEditSet) {
|
||||
this.onRequestEditSet(this.set);
|
||||
}
|
||||
}
|
||||
delete() {
|
||||
this.unrenderSettings();
|
||||
if (this.onDelete) {
|
||||
this.onDelete();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
set: this.set.name,
|
||||
isVisible: this.isVisible,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import { chat_metadata, saveSettingsDebounced } from '../../../../script.js';
|
||||
import { extension_settings, saveMetadataDebounced } from '../../../extensions.js';
|
||||
import { QuickReplyConfig } from './QuickReplyConfig.js';
|
||||
|
||||
export class QuickReplySettings {
|
||||
static from(props) {
|
||||
props.config = QuickReplyConfig.from(props.config);
|
||||
props.characterConfigs = props.characterConfigs ?? {};
|
||||
for (const key of Object.keys(props.characterConfigs)) {
|
||||
props.characterConfigs[key] = QuickReplyConfig.from(props.characterConfigs[key]);
|
||||
}
|
||||
const instance = Object.assign(new this(), props);
|
||||
instance.init();
|
||||
return instance;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**@type {Boolean}*/ isEnabled = false;
|
||||
/**@type {Boolean}*/ isCombined = false;
|
||||
/**@type {Boolean}*/ isPopout = false;
|
||||
/**@type {Boolean}*/ showPopoutButton = true;
|
||||
/**@type {QuickReplyConfig}*/ config;
|
||||
/**@type {{[key:string]: QuickReplyConfig}}*/ characterConfigs = {};
|
||||
/**@type {QuickReplyConfig}*/ _chatConfig;
|
||||
/**@type {QuickReplyConfig}*/ _charConfig;
|
||||
get chatConfig() {
|
||||
return this._chatConfig;
|
||||
}
|
||||
set chatConfig(value) {
|
||||
if (this._chatConfig != value) {
|
||||
this.unhookConfig(this._chatConfig);
|
||||
this._chatConfig = value;
|
||||
this.hookConfig(this._chatConfig);
|
||||
}
|
||||
}
|
||||
get charConfig() {
|
||||
return this._charConfig;
|
||||
}
|
||||
set charConfig(value) {
|
||||
if (this._charConfig != value) {
|
||||
this.unhookConfig(this._charConfig);
|
||||
this._charConfig = value;
|
||||
this.hookConfig(this._charConfig);
|
||||
}
|
||||
}
|
||||
|
||||
/**@type {Function}*/ onSave;
|
||||
/**@type {Function}*/ onRequestEditSet;
|
||||
|
||||
|
||||
|
||||
|
||||
init() {
|
||||
this.hookConfig(this.config);
|
||||
this.hookConfig(this.chatConfig);
|
||||
this.hookConfig(this.charConfig);
|
||||
}
|
||||
|
||||
hookConfig(config) {
|
||||
if (config) {
|
||||
config.onUpdate = ()=>this.save();
|
||||
config.onRequestEditSet = (qrs)=>this.requestEditSet(qrs);
|
||||
}
|
||||
}
|
||||
unhookConfig(config) {
|
||||
if (config) {
|
||||
config.onUpdate = null;
|
||||
config.onRequestEditSet = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
save() {
|
||||
extension_settings.quickReplyV2 = this.toJSON();
|
||||
saveSettingsDebounced();
|
||||
if (this.chatConfig) {
|
||||
chat_metadata.quickReply = this.chatConfig.toJSON();
|
||||
saveMetadataDebounced();
|
||||
}
|
||||
if (this.onSave) {
|
||||
this.onSave();
|
||||
}
|
||||
}
|
||||
|
||||
requestEditSet(qrs) {
|
||||
if (this.onRequestEditSet) {
|
||||
this.onRequestEditSet(qrs);
|
||||
}
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
const characterConfigs = {};
|
||||
for (const key of Object.keys(this.characterConfigs)) {
|
||||
if (this.characterConfigs[key]?.setList?.length === 0) {
|
||||
continue;
|
||||
}
|
||||
characterConfigs[key] = this.characterConfigs[key].toJSON();
|
||||
}
|
||||
return {
|
||||
isEnabled: this.isEnabled,
|
||||
isCombined: this.isCombined,
|
||||
isPopout: this.isPopout,
|
||||
showPopoutButton: this.showPopoutButton,
|
||||
config: this.config,
|
||||
characterConfigs,
|
||||
};
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
163
web-app/public/scripts/extensions/quick-reply/src/ui/ButtonUi.js
Normal file
163
web-app/public/scripts/extensions/quick-reply/src/ui/ButtonUi.js
Normal file
@@ -0,0 +1,163 @@
|
||||
import { animation_duration } from '../../../../../script.js';
|
||||
import { dragElement } from '../../../../RossAscends-mods.js';
|
||||
import { loadMovingUIState } from '../../../../power-user.js';
|
||||
import { QuickReplySettings } from '../QuickReplySettings.js';
|
||||
|
||||
export class ButtonUi {
|
||||
/** @type {QuickReplySettings} */ settings;
|
||||
|
||||
/**@type {HTMLElement}*/ dom;
|
||||
/**@type {HTMLElement}*/ popoutDom;
|
||||
|
||||
|
||||
|
||||
|
||||
constructor(/**@type {QuickReplySettings}*/settings) {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
render() {
|
||||
if (this.settings.isPopout) {
|
||||
return this.renderPopout();
|
||||
}
|
||||
return this.renderBar();
|
||||
}
|
||||
unrender() {
|
||||
this.dom?.remove();
|
||||
this.dom = null;
|
||||
this.popoutDom?.remove();
|
||||
this.popoutDom = null;
|
||||
}
|
||||
|
||||
show() {
|
||||
if (!this.settings.isEnabled) return;
|
||||
if (this.settings.isPopout) {
|
||||
document.body.append(this.render());
|
||||
loadMovingUIState();
|
||||
$(this.render()).fadeIn(animation_duration);
|
||||
dragElement($(this.render()));
|
||||
} else {
|
||||
const sendForm = document.querySelector('#send_form');
|
||||
if (sendForm.children.length > 0) {
|
||||
sendForm.children[0].insertAdjacentElement('beforebegin', this.render());
|
||||
} else {
|
||||
sendForm.append(this.render());
|
||||
}
|
||||
}
|
||||
}
|
||||
hide() {
|
||||
this.unrender();
|
||||
}
|
||||
refresh() {
|
||||
this.hide();
|
||||
this.show();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
renderBar() {
|
||||
if (!this.dom) {
|
||||
let buttonHolder;
|
||||
const root = document.createElement('div'); {
|
||||
this.dom = root;
|
||||
buttonHolder = root;
|
||||
root.id = 'qr--bar';
|
||||
root.classList.add('flex-container');
|
||||
root.classList.add('flexGap5');
|
||||
if (this.settings.showPopoutButton) {
|
||||
root.classList.add('popoutVisible');
|
||||
const popout = document.createElement('div'); {
|
||||
popout.id = 'qr--popoutTrigger';
|
||||
popout.classList.add('menu_button');
|
||||
popout.classList.add('fa-solid');
|
||||
popout.classList.add('fa-window-restore');
|
||||
popout.addEventListener('click', ()=>{
|
||||
this.settings.isPopout = true;
|
||||
this.refresh();
|
||||
this.settings.save();
|
||||
});
|
||||
root.append(popout);
|
||||
}
|
||||
}
|
||||
if (this.settings.isCombined) {
|
||||
const buttons = document.createElement('div'); {
|
||||
buttonHolder = buttons;
|
||||
buttons.classList.add('qr--buttons');
|
||||
root.append(buttons);
|
||||
}
|
||||
}
|
||||
[...this.settings.config.setList, ...(this.settings.chatConfig?.setList ?? []), ...(this.settings.charConfig?.setList ?? [])]
|
||||
.filter(link=>link.isVisible)
|
||||
.forEach(link=>buttonHolder.append(link.set.render()))
|
||||
;
|
||||
}
|
||||
}
|
||||
return this.dom;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
renderPopout() {
|
||||
if (!this.popoutDom) {
|
||||
let buttonHolder;
|
||||
const root = document.createElement('div'); {
|
||||
this.popoutDom = root;
|
||||
root.id = 'qr--popout';
|
||||
root.classList.add('qr--popout');
|
||||
root.classList.add('draggable');
|
||||
const head = document.createElement('div'); {
|
||||
head.classList.add('qr--header');
|
||||
root.append(head);
|
||||
const controls = document.createElement('div'); {
|
||||
controls.classList.add('qr--controls');
|
||||
controls.classList.add('panelControlBar');
|
||||
controls.classList.add('flex-container');
|
||||
const drag = document.createElement('div'); {
|
||||
drag.id = 'qr--popoutheader';
|
||||
drag.classList.add('fa-solid');
|
||||
drag.classList.add('fa-grip');
|
||||
drag.classList.add('drag-grabber');
|
||||
drag.classList.add('hoverglow');
|
||||
controls.append(drag);
|
||||
}
|
||||
const close = document.createElement('div'); {
|
||||
close.classList.add('qr--close');
|
||||
close.classList.add('fa-solid');
|
||||
close.classList.add('fa-circle-xmark');
|
||||
close.classList.add('hoverglow');
|
||||
close.addEventListener('click', ()=>{
|
||||
this.settings.isPopout = false;
|
||||
this.refresh();
|
||||
this.settings.save();
|
||||
});
|
||||
controls.append(close);
|
||||
}
|
||||
head.append(controls);
|
||||
}
|
||||
}
|
||||
const body = document.createElement('div'); {
|
||||
buttonHolder = body;
|
||||
body.classList.add('qr--body');
|
||||
if (this.settings.isCombined) {
|
||||
const buttons = document.createElement('div'); {
|
||||
buttonHolder = buttons;
|
||||
buttons.classList.add('qr--buttons');
|
||||
body.append(buttons);
|
||||
}
|
||||
}
|
||||
[...this.settings.config.setList, ...(this.settings.chatConfig?.setList ?? []), ...(this.settings.charConfig?.setList ?? [])]
|
||||
.filter(link=>link.isVisible)
|
||||
.forEach(link=>buttonHolder.append(link.set.render()))
|
||||
;
|
||||
root.append(body);
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.popoutDom;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,548 @@
|
||||
import { Popup } from '../../../../popup.js';
|
||||
import { getSortableDelay } from '../../../../utils.js';
|
||||
import { log, warn } from '../../index.js';
|
||||
import { QuickReply } from '../QuickReply.js';
|
||||
import { QuickReplySet } from '../QuickReplySet.js';
|
||||
import { QuickReplySettings } from '../QuickReplySettings.js';
|
||||
|
||||
export class SettingsUi {
|
||||
/** @type {QuickReplySettings} */ settings;
|
||||
|
||||
/** @type {HTMLElement} */ template;
|
||||
/** @type {HTMLElement} */ dom;
|
||||
|
||||
/**@type {HTMLInputElement}*/ isEnabled;
|
||||
/**@type {HTMLInputElement}*/ isCombined;
|
||||
/**@type {HTMLInputElement}*/ showPopoutButton;
|
||||
|
||||
/**@type {HTMLElement}*/ globalSetList;
|
||||
|
||||
/**@type {HTMLElement}*/ chatSetList;
|
||||
/**@type {HTMLElement}*/ characterSetList;
|
||||
|
||||
/**@type {QuickReplySet}*/ currentQrSet;
|
||||
/**@type {HTMLInputElement}*/ disableSend;
|
||||
/**@type {HTMLInputElement}*/ placeBeforeInput;
|
||||
/**@type {HTMLInputElement}*/ injectInput;
|
||||
/**@type {HTMLInputElement}*/ color;
|
||||
/**@type {HTMLInputElement}*/ onlyBorderColor;
|
||||
/**@type {HTMLSelectElement}*/ currentSet;
|
||||
|
||||
|
||||
|
||||
|
||||
constructor(/**@type {QuickReplySettings}*/settings) {
|
||||
this.settings = settings;
|
||||
settings.onRequestEditSet = (qrs) => this.selectQrSet(qrs);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
rerender() {
|
||||
if (!this.dom) return;
|
||||
const content = this.dom.querySelector('.inline-drawer-content');
|
||||
content.innerHTML = '';
|
||||
// @ts-ignore
|
||||
Array.from(this.template.querySelector('.inline-drawer-content').cloneNode(true).children).forEach(el=>{
|
||||
content.append(el);
|
||||
});
|
||||
this.prepareDom();
|
||||
}
|
||||
unrender() {
|
||||
this.dom?.remove();
|
||||
this.dom = null;
|
||||
}
|
||||
async render() {
|
||||
if (!this.dom) {
|
||||
const response = await fetch('/scripts/extensions/quick-reply/html/settings.html', { cache: 'no-store' });
|
||||
if (response.ok) {
|
||||
this.template = document.createRange().createContextualFragment(await response.text()).querySelector('#qr--settings');
|
||||
// @ts-ignore
|
||||
this.dom = this.template.cloneNode(true);
|
||||
this.prepareDom();
|
||||
} else {
|
||||
warn('failed to fetch settings template');
|
||||
}
|
||||
}
|
||||
return this.dom;
|
||||
}
|
||||
|
||||
|
||||
prepareGeneralSettings() {
|
||||
// general settings
|
||||
this.isEnabled = this.dom.querySelector('#qr--isEnabled');
|
||||
this.isEnabled.checked = this.settings.isEnabled;
|
||||
this.isEnabled.addEventListener('click', ()=>this.onIsEnabled());
|
||||
|
||||
this.isCombined = this.dom.querySelector('#qr--isCombined');
|
||||
this.isCombined.checked = this.settings.isCombined;
|
||||
this.isCombined.addEventListener('click', ()=>this.onIsCombined());
|
||||
|
||||
this.showPopoutButton = this.dom.querySelector('#qr--showPopoutButton');
|
||||
this.showPopoutButton.checked = this.settings.showPopoutButton;
|
||||
this.showPopoutButton.addEventListener('click', ()=>this.onShowPopoutButton());
|
||||
}
|
||||
|
||||
prepareGlobalSetList() {
|
||||
const dom = this.template.querySelector('#qr--global');
|
||||
const clone = dom.cloneNode(true);
|
||||
// @ts-ignore
|
||||
this.settings.config.renderSettingsInto(clone);
|
||||
this.dom.querySelector('#qr--global').replaceWith(clone);
|
||||
}
|
||||
prepareChatSetList() {
|
||||
const dom = this.template.querySelector('#qr--chat');
|
||||
const clone = dom.cloneNode(true);
|
||||
if (this.settings.chatConfig) {
|
||||
// @ts-ignore
|
||||
this.settings.chatConfig.renderSettingsInto(clone);
|
||||
} else {
|
||||
const info = document.createElement('div'); {
|
||||
info.textContent = 'No active chat.';
|
||||
// @ts-ignore
|
||||
clone.append(info);
|
||||
}
|
||||
}
|
||||
this.dom.querySelector('#qr--chat').replaceWith(clone);
|
||||
}
|
||||
prepareCharacterSetList() {
|
||||
const dom = this.template.querySelector('#qr--character');
|
||||
const clone = /** @type {HTMLElement} */ (dom.cloneNode(true));
|
||||
|
||||
if (!this.settings.charConfig) {
|
||||
const setListContainer = /** @type {HTMLElement} */ (clone.querySelector('.qr--setList'));
|
||||
setListContainer.innerHTML = '';
|
||||
const info = document.createElement('div');
|
||||
info.textContent = 'No character is currently loaded.';
|
||||
setListContainer.append(info);
|
||||
} else {
|
||||
// Let the config object handle its own rendering. It will render an empty list if there are no sets,
|
||||
// but the "add" button will always be functional.
|
||||
this.settings.charConfig.renderSettingsInto(clone);
|
||||
}
|
||||
|
||||
// Replace the old DOM element with our newly prepared clone.
|
||||
this.dom.querySelector('#qr--character').replaceWith(clone);
|
||||
}
|
||||
|
||||
prepareQrEditor() {
|
||||
// qr editor
|
||||
this.dom.querySelector('#qr--set-rename').addEventListener('click', async () => this.renameQrSet());
|
||||
this.dom.querySelector('#qr--set-new').addEventListener('click', async()=>this.addQrSet());
|
||||
/**@type {HTMLInputElement}*/
|
||||
const importFile = this.dom.querySelector('#qr--set-importFile');
|
||||
importFile.addEventListener('change', async()=>{
|
||||
await this.importQrSet(importFile.files);
|
||||
importFile.value = null;
|
||||
});
|
||||
this.dom.querySelector('#qr--set-import').addEventListener('click', ()=>importFile.click());
|
||||
this.dom.querySelector('#qr--set-export').addEventListener('click', async () => this.exportQrSet());
|
||||
this.dom.querySelector('#qr--set-duplicate').addEventListener('click', async () => this.duplicateQrSet());
|
||||
this.dom.querySelector('#qr--set-delete').addEventListener('click', async()=>this.deleteQrSet());
|
||||
this.dom.querySelector('#qr--set-add').addEventListener('click', async()=>{
|
||||
this.currentQrSet.addQuickReply();
|
||||
});
|
||||
this.dom.querySelector('#qr--set-paste').addEventListener('click', async()=>{
|
||||
const text = await navigator.clipboard.readText();
|
||||
this.currentQrSet.addQuickReplyFromText(text);
|
||||
});
|
||||
this.dom.querySelector('#qr--set-importQr').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.currentQrSet.addQuickReply(JSON.parse(text));
|
||||
}
|
||||
}
|
||||
});
|
||||
inp.click();
|
||||
}
|
||||
});
|
||||
this.qrList = this.dom.querySelector('#qr--set-qrList');
|
||||
this.currentSet = this.dom.querySelector('#qr--set');
|
||||
this.currentSet.addEventListener('change', ()=>this.onQrSetChange());
|
||||
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;
|
||||
this.currentSet.append(opt);
|
||||
}
|
||||
});
|
||||
this.disableSend = this.dom.querySelector('#qr--disableSend');
|
||||
this.disableSend.addEventListener('click', ()=>{
|
||||
const qrs = this.currentQrSet;
|
||||
qrs.disableSend = this.disableSend.checked;
|
||||
qrs.save();
|
||||
});
|
||||
this.placeBeforeInput = this.dom.querySelector('#qr--placeBeforeInput');
|
||||
this.placeBeforeInput.addEventListener('click', ()=>{
|
||||
const qrs = this.currentQrSet;
|
||||
qrs.placeBeforeInput = this.placeBeforeInput.checked;
|
||||
qrs.save();
|
||||
});
|
||||
this.injectInput = this.dom.querySelector('#qr--injectInput');
|
||||
this.injectInput.addEventListener('click', ()=>{
|
||||
const qrs = this.currentQrSet;
|
||||
qrs.injectInput = this.injectInput.checked;
|
||||
qrs.save();
|
||||
});
|
||||
let initialColorChange = true;
|
||||
this.color = this.dom.querySelector('#qr--color');
|
||||
// @ts-ignore
|
||||
this.color.color = this.currentQrSet?.color ?? 'transparent';
|
||||
this.color.addEventListener('change', (evt)=>{
|
||||
if (!this.dom.closest('body')) return;
|
||||
const qrs = this.currentQrSet;
|
||||
if (initialColorChange) {
|
||||
initialColorChange = false;
|
||||
// @ts-ignore
|
||||
this.color.color = qrs.color;
|
||||
return;
|
||||
}
|
||||
// @ts-ignore
|
||||
qrs.color = evt.detail.rgb;
|
||||
qrs.save();
|
||||
this.currentQrSet.updateColor();
|
||||
});
|
||||
// @ts-ignore
|
||||
this.dom.querySelector('#qr--colorClear').addEventListener('click', (evt)=>{
|
||||
const qrs = this.currentQrSet;
|
||||
// @ts-ignore
|
||||
this.color.color = 'transparent';
|
||||
qrs.save();
|
||||
this.currentQrSet.updateColor();
|
||||
});
|
||||
this.onlyBorderColor = this.dom.querySelector('#qr--onlyBorderColor');
|
||||
this.onlyBorderColor.addEventListener('click', ()=>{
|
||||
const qrs = this.currentQrSet;
|
||||
qrs.onlyBorderColor = this.onlyBorderColor.checked;
|
||||
qrs.save();
|
||||
this.currentQrSet.updateColor();
|
||||
});
|
||||
this.onQrSetChange();
|
||||
}
|
||||
onQrSetChange() {
|
||||
this.currentQrSet = QuickReplySet.get(this.currentSet.value) ?? new QuickReplySet();
|
||||
this.disableSend.checked = this.currentQrSet.disableSend;
|
||||
this.placeBeforeInput.checked = this.currentQrSet.placeBeforeInput;
|
||||
this.injectInput.checked = this.currentQrSet.injectInput;
|
||||
// @ts-ignore
|
||||
this.color.color = this.currentQrSet.color ?? 'transparent';
|
||||
this.onlyBorderColor.checked = this.currentQrSet.onlyBorderColor;
|
||||
this.qrList.innerHTML = '';
|
||||
const qrsDom = this.currentQrSet.renderSettings();
|
||||
this.qrList.append(qrsDom);
|
||||
// @ts-ignore
|
||||
$(qrsDom).sortable({
|
||||
delay: getSortableDelay(),
|
||||
handle: '.drag-handle',
|
||||
stop: ()=>this.onQrListSort(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
prepareDom() {
|
||||
this.prepareGeneralSettings();
|
||||
this.prepareGlobalSetList();
|
||||
this.prepareChatSetList();
|
||||
this.prepareCharacterSetList();
|
||||
this.prepareQrEditor();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
async onIsEnabled() {
|
||||
this.settings.isEnabled = this.isEnabled.checked;
|
||||
this.settings.save();
|
||||
}
|
||||
|
||||
async onIsCombined() {
|
||||
this.settings.isCombined = this.isCombined.checked;
|
||||
this.settings.save();
|
||||
}
|
||||
|
||||
async onShowPopoutButton() {
|
||||
this.settings.showPopoutButton = this.showPopoutButton.checked;
|
||||
this.settings.save();
|
||||
}
|
||||
|
||||
async onGlobalSetListSort() {
|
||||
this.settings.config.setList = Array.from(this.globalSetList.children).map((it,idx)=>{
|
||||
const set = this.settings.config.setList[Number(it.getAttribute('data-order'))];
|
||||
it.setAttribute('data-order', String(idx));
|
||||
return set;
|
||||
});
|
||||
this.settings.save();
|
||||
}
|
||||
|
||||
async onChatSetListSort() {
|
||||
this.settings.chatConfig.setList = Array.from(this.chatSetList.children).map((it,idx)=>{
|
||||
const set = this.settings.chatConfig.setList[Number(it.getAttribute('data-order'))];
|
||||
it.setAttribute('data-order', String(idx));
|
||||
return set;
|
||||
});
|
||||
this.settings.save();
|
||||
}
|
||||
|
||||
updateOrder(list) {
|
||||
Array.from(list.children).forEach((it,idx)=>{
|
||||
it.setAttribute('data-order', idx);
|
||||
});
|
||||
}
|
||||
|
||||
async onQrListSort() {
|
||||
this.currentQrSet.qrList = Array.from(this.qrList.querySelectorAll('.qr--set-item')).map((it,idx)=>{
|
||||
const qr = this.currentQrSet.qrList.find(qr=>qr.id == Number(it.getAttribute('data-id')));
|
||||
it.setAttribute('data-order', String(idx));
|
||||
return qr;
|
||||
});
|
||||
this.currentQrSet.save();
|
||||
}
|
||||
|
||||
async deleteQrSet() {
|
||||
const confirmed = await Popup.show.confirm('Delete Quick Reply Set', `Are you sure you want to delete the Quick Reply Set "${this.currentQrSet.name}"?<br>This cannot be undone.`);
|
||||
if (confirmed) {
|
||||
await this.doDeleteQrSet(this.currentQrSet);
|
||||
this.rerender();
|
||||
}
|
||||
}
|
||||
async doDeleteQrSet(qrs) {
|
||||
await qrs.delete();
|
||||
//TODO (HACK) should just bubble up from QuickReplySet.delete() but that would require proper or at least more comples onDelete listeners
|
||||
for (let i = this.settings.config.setList.length - 1; i >= 0; i--) {
|
||||
if (this.settings.config.setList[i].set == qrs) {
|
||||
this.settings.config.setList.splice(i, 1);
|
||||
}
|
||||
}
|
||||
if (this.settings.chatConfig) {
|
||||
for (let i = this.settings.chatConfig.setList.length - 1; i >= 0; i--) {
|
||||
if (this.settings.chatConfig.setList[i].set == qrs) {
|
||||
this.settings.chatConfig.setList.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.settings.charConfig) {
|
||||
for (let i = this.settings.charConfig.setList.length - 1; i >= 0; i--) {
|
||||
if (this.settings.charConfig.setList[i].set == qrs) {
|
||||
this.settings.charConfig.setList.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.settings.save();
|
||||
}
|
||||
|
||||
async renameQrSet() {
|
||||
const newName = await Popup.show.input('Rename Quick Reply Set', 'Enter a new name:', this.currentQrSet.name);
|
||||
if (newName && newName.length > 0) {
|
||||
const existingSet = QuickReplySet.get(newName);
|
||||
if (existingSet) {
|
||||
toastr.error(`A Quick Reply Set named "${newName}" already exists.`);
|
||||
return;
|
||||
}
|
||||
const oldName = this.currentQrSet.name;
|
||||
this.currentQrSet.name = newName;
|
||||
await this.currentQrSet.save();
|
||||
|
||||
// Update it in both set lists
|
||||
this.settings.config.setList.forEach(set => {
|
||||
if (set.set.name === oldName) {
|
||||
set.set.name = newName;
|
||||
}
|
||||
});
|
||||
this.settings.chatConfig?.setList.forEach(set => {
|
||||
if (set.set.name === oldName) {
|
||||
set.set.name = newName;
|
||||
}
|
||||
});
|
||||
this.settings.charConfig?.setList.forEach(set => {
|
||||
if (set.set.name === oldName) {
|
||||
set.set.name = newName;
|
||||
}
|
||||
});
|
||||
this.settings.save();
|
||||
|
||||
// Update the option in the current selected QR dropdown. All others will be refreshed via the prepare calls below.
|
||||
/** @type {HTMLOptionElement} */
|
||||
const option = this.currentSet.querySelector(`#qr--set option[value="${oldName}"]`);
|
||||
option.value = newName;
|
||||
option.textContent = newName;
|
||||
|
||||
this.currentSet.value = newName;
|
||||
this.onQrSetChange();
|
||||
this.prepareGlobalSetList();
|
||||
this.prepareChatSetList();
|
||||
this.prepareCharacterSetList();
|
||||
|
||||
console.info(`Quick Reply Set renamed from ""${oldName}" to "${newName}".`);
|
||||
}
|
||||
}
|
||||
|
||||
async addQrSet() {
|
||||
const name = await Popup.show.input('Create a new Quick Reply Set', 'Enter a name for the new Quick Reply Set:');
|
||||
if (name && name.length > 0) {
|
||||
const oldQrs = QuickReplySet.get(name);
|
||||
if (oldQrs) {
|
||||
const replace = Popup.show.confirm('Replace existing World Info', `A Quick Reply Set named "${name}" already exists.<br>Do you want to overwrite the existing Quick Reply Set?<br>The existing set will be deleted. This cannot be undone.`);
|
||||
if (replace) {
|
||||
const idx = QuickReplySet.list.indexOf(oldQrs);
|
||||
await this.doDeleteQrSet(oldQrs);
|
||||
const qrs = new QuickReplySet();
|
||||
qrs.name = name;
|
||||
qrs.addQuickReply();
|
||||
QuickReplySet.list.splice(idx, 0, qrs);
|
||||
this.rerender();
|
||||
this.currentSet.value = name;
|
||||
this.onQrSetChange();
|
||||
this.prepareGlobalSetList();
|
||||
this.prepareChatSetList();
|
||||
this.prepareCharacterSetList();
|
||||
}
|
||||
} else {
|
||||
const qrs = new QuickReplySet();
|
||||
qrs.name = name;
|
||||
qrs.addQuickReply();
|
||||
const idx = QuickReplySet.list.findIndex(it=>it.name.toLowerCase().localeCompare(name.toLowerCase()) == 1);
|
||||
if (idx > -1) {
|
||||
QuickReplySet.list.splice(idx, 0, qrs);
|
||||
} else {
|
||||
QuickReplySet.list.push(qrs);
|
||||
}
|
||||
const opt = document.createElement('option'); {
|
||||
opt.value = qrs.name;
|
||||
opt.textContent = qrs.name;
|
||||
if (idx > -1) {
|
||||
this.currentSet.children[idx].insertAdjacentElement('beforebegin', opt);
|
||||
} else {
|
||||
this.currentSet.append(opt);
|
||||
}
|
||||
}
|
||||
this.currentSet.value = name;
|
||||
this.onQrSetChange();
|
||||
this.prepareGlobalSetList();
|
||||
this.prepareChatSetList();
|
||||
this.prepareCharacterSetList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async importQrSet(/**@type {FileList}*/files) {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
await this.importSingleQrSet(files.item(i));
|
||||
}
|
||||
}
|
||||
async importSingleQrSet(/**@type {File}*/file) {
|
||||
log('FILE', file);
|
||||
try {
|
||||
const text = await file.text();
|
||||
const props = JSON.parse(text);
|
||||
if (!Number.isInteger(props.version) || typeof props.name != 'string') {
|
||||
toastr.error(`The file "${file.name}" does not appear to be a valid quick reply set.`);
|
||||
warn(`The file "${file.name}" does not appear to be a valid quick reply set.`);
|
||||
} else {
|
||||
/**@type {QuickReplySet}*/
|
||||
const qrs = QuickReplySet.from(JSON.parse(JSON.stringify(props)));
|
||||
qrs.qrList = props.qrList.map(it=>QuickReply.from(it));
|
||||
qrs.init();
|
||||
const oldQrs = QuickReplySet.get(props.name);
|
||||
if (oldQrs) {
|
||||
const replace = Popup.show.confirm('Replace existing World Info', `A Quick Reply Set named "${name}" already exists.<br>Do you want to overwrite the existing Quick Reply Set?<br>The existing set will be deleted. This cannot be undone.`);
|
||||
if (replace) {
|
||||
const idx = QuickReplySet.list.indexOf(oldQrs);
|
||||
await this.doDeleteQrSet(oldQrs);
|
||||
QuickReplySet.list.splice(idx, 0, qrs);
|
||||
await qrs.save();
|
||||
this.rerender();
|
||||
this.currentSet.value = qrs.name;
|
||||
this.onQrSetChange();
|
||||
this.prepareGlobalSetList();
|
||||
this.prepareChatSetList();
|
||||
this.prepareCharacterSetList();
|
||||
}
|
||||
} else {
|
||||
const idx = QuickReplySet.list.findIndex(it=>it.name.toLowerCase().localeCompare(qrs.name.toLowerCase()) == 1);
|
||||
if (idx > -1) {
|
||||
QuickReplySet.list.splice(idx, 0, qrs);
|
||||
} else {
|
||||
QuickReplySet.list.push(qrs);
|
||||
}
|
||||
await qrs.save();
|
||||
const opt = document.createElement('option'); {
|
||||
opt.value = qrs.name;
|
||||
opt.textContent = qrs.name;
|
||||
if (idx > -1) {
|
||||
this.currentSet.children[idx].insertAdjacentElement('beforebegin', opt);
|
||||
} else {
|
||||
this.currentSet.append(opt);
|
||||
}
|
||||
}
|
||||
this.currentSet.value = qrs.name;
|
||||
this.onQrSetChange();
|
||||
this.prepareGlobalSetList();
|
||||
this.prepareChatSetList();
|
||||
this.prepareCharacterSetList();
|
||||
}
|
||||
}
|
||||
} catch (ex) {
|
||||
warn(ex);
|
||||
toastr.error(`Failed to import "${file.name}":\n\n${ex.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
exportQrSet() {
|
||||
const blob = new Blob([JSON.stringify(this.currentQrSet)], { type:'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a'); {
|
||||
a.href = url;
|
||||
a.download = `${this.currentQrSet.name}.json`;
|
||||
a.click();
|
||||
}
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
async duplicateQrSet() {
|
||||
const newName = await Popup.show.input('Duplicate Quick Reply Set', 'Enter a name for the new Quick Reply Set:', `${this.currentQrSet.name} (Copy)`);
|
||||
if (newName && newName.length > 0) {
|
||||
const existingSet = QuickReplySet.get(newName);
|
||||
if (existingSet) {
|
||||
toastr.error(`A Quick Reply Set named "${newName}" already exists.`);
|
||||
return;
|
||||
}
|
||||
const newQrSet = QuickReplySet.from(this.currentQrSet.toJSON());
|
||||
newQrSet.name = newName;
|
||||
newQrSet.qrList = this.currentQrSet.qrList.map(qr => QuickReply.from(qr.toJSON()));
|
||||
newQrSet.init();
|
||||
const idx = QuickReplySet.list.findIndex(it => it.name.toLowerCase().localeCompare(newName.toLowerCase()) == 1);
|
||||
if (idx > -1) {
|
||||
QuickReplySet.list.splice(idx, 0, newQrSet);
|
||||
} else {
|
||||
QuickReplySet.list.push(newQrSet);
|
||||
}
|
||||
const opt = document.createElement('option'); {
|
||||
opt.value = newQrSet.name;
|
||||
opt.textContent = newQrSet.name;
|
||||
if (idx > -1) {
|
||||
this.currentSet.children[idx].insertAdjacentElement('beforebegin', opt);
|
||||
} else {
|
||||
this.currentSet.append(opt);
|
||||
}
|
||||
}
|
||||
this.currentSet.value = newName;
|
||||
this.onQrSetChange();
|
||||
this.prepareGlobalSetList();
|
||||
this.prepareChatSetList();
|
||||
this.prepareCharacterSetList();
|
||||
}
|
||||
}
|
||||
|
||||
selectQrSet(qrs) {
|
||||
this.currentSet.value = qrs.name;
|
||||
this.onQrSetChange();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import { QuickReply } from '../../QuickReply.js';
|
||||
import { QuickReplySet } from '../../QuickReplySet.js';
|
||||
import { MenuHeader } from './MenuHeader.js';
|
||||
import { MenuItem } from './MenuItem.js';
|
||||
|
||||
export class ContextMenu {
|
||||
/**@type {MenuItem[]}*/ itemList = [];
|
||||
/**@type {Boolean}*/ isActive = false;
|
||||
|
||||
/**@type {HTMLElement}*/ root;
|
||||
/**@type {HTMLElement}*/ menu;
|
||||
|
||||
|
||||
|
||||
|
||||
constructor(/**@type {QuickReply}*/qr) {
|
||||
// this.itemList = items;
|
||||
this.itemList = this.build(qr).children;
|
||||
this.itemList.forEach(item => {
|
||||
item.onExpand = () => {
|
||||
this.itemList.filter(it => it !== item)
|
||||
.forEach(it => it.collapse());
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {QuickReply} qr
|
||||
* @param {String} chainedMessage
|
||||
* @param {QuickReplySet[]} hierarchy
|
||||
* @param {String[]} labelHierarchy
|
||||
*/
|
||||
build(qr, chainedMessage = null, hierarchy = [], labelHierarchy = []) {
|
||||
const tree = {
|
||||
icon: qr.icon,
|
||||
showLabel: qr.showLabel,
|
||||
label: qr.label,
|
||||
title: qr.title,
|
||||
message: (chainedMessage && qr.message ? `${chainedMessage} | ` : '') + qr.message,
|
||||
children: [],
|
||||
};
|
||||
qr.contextList.forEach((cl) => {
|
||||
if (!cl.set) return;
|
||||
if (!hierarchy.includes(cl.set)) {
|
||||
const nextHierarchy = [...hierarchy, cl.set];
|
||||
const nextLabelHierarchy = [...labelHierarchy, tree.label];
|
||||
tree.children.push(new MenuHeader(cl.set.name));
|
||||
|
||||
// If the Quick Reply's own set is added as a context menu,
|
||||
// show only the sub-QRs that are Invisible but have an icon
|
||||
// intent: allow a QR set to be assigned to one of its own QR buttons for a "burger" menu
|
||||
// with "UI" QRs either in the bar or in the menu, and "library function" QRs still hidden.
|
||||
// - QRs already visible on the bar are filtered out,
|
||||
// - hidden QRs without an icon are filtered out,
|
||||
// - hidden QRs **with an icon** are shown in the menu
|
||||
// so everybody is happy
|
||||
const qrsOwnSetAddedAsContextMenu = cl.set.qrList.includes(qr);
|
||||
const visible = (subQr) => {
|
||||
return qrsOwnSetAddedAsContextMenu
|
||||
? subQr.isHidden && !!subQr.icon // yes .isHidden gets inverted here
|
||||
: !subQr.isHidden;
|
||||
};
|
||||
|
||||
cl.set.qrList.filter(visible).forEach(subQr => {
|
||||
const subTree = this.build(subQr, cl.isChained ? tree.message : null, nextHierarchy, nextLabelHierarchy);
|
||||
tree.children.push(new MenuItem(
|
||||
subTree.icon,
|
||||
subTree.showLabel,
|
||||
subTree.label,
|
||||
subTree.title,
|
||||
subTree.message,
|
||||
(evt) => {
|
||||
evt.stopPropagation();
|
||||
const finalQr = Object.assign(new QuickReply(), subQr);
|
||||
finalQr.message = subTree.message.replace(/%%parent(-\d+)?%%/g, (_, index) => {
|
||||
return nextLabelHierarchy.slice(parseInt(index ?? '-1'))[0];
|
||||
});
|
||||
cl.set.execute(finalQr);
|
||||
},
|
||||
subTree.children,
|
||||
));
|
||||
});
|
||||
}
|
||||
});
|
||||
return tree;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.root) {
|
||||
const blocker = document.createElement('div'); {
|
||||
this.root = blocker;
|
||||
blocker.classList.add('ctx-blocker');
|
||||
blocker.addEventListener('click', () => this.hide());
|
||||
const menu = document.createElement('ul'); {
|
||||
this.menu = menu;
|
||||
menu.classList.add('list-group');
|
||||
menu.classList.add('ctx-menu');
|
||||
this.itemList.forEach(it => menu.append(it.render()));
|
||||
blocker.append(menu);
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.root;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
show({ clientX, clientY }) {
|
||||
if (this.isActive) return;
|
||||
this.isActive = true;
|
||||
this.render();
|
||||
this.menu.style.bottom = `${window.innerHeight - clientY}px`;
|
||||
this.menu.style.left = `${clientX}px`;
|
||||
document.body.append(this.root);
|
||||
}
|
||||
hide() {
|
||||
if (this.root) {
|
||||
this.root.remove();
|
||||
}
|
||||
this.isActive = false;
|
||||
}
|
||||
toggle(/**@type {PointerEvent}*/evt) {
|
||||
if (this.isActive) {
|
||||
this.hide();
|
||||
} else {
|
||||
this.show(evt);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { MenuItem } from './MenuItem.js';
|
||||
|
||||
export class MenuHeader extends MenuItem {
|
||||
constructor(/**@type {String}*/label) {
|
||||
super(null, null, label, null, null, null, []);
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
if (!this.root) {
|
||||
const item = document.createElement('li'); {
|
||||
this.root = item;
|
||||
item.classList.add('list-group-item');
|
||||
item.classList.add('ctx-header');
|
||||
item.append(this.label);
|
||||
}
|
||||
}
|
||||
return this.root;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { SubMenu } from './SubMenu.js';
|
||||
|
||||
export class MenuItem {
|
||||
/**@type {string}*/ icon;
|
||||
/**@type {boolean}*/ showLabel;
|
||||
/**@type {string}*/ label;
|
||||
/**@type {string}*/ title;
|
||||
/**@type {object}*/ value;
|
||||
/**@type {function}*/ callback;
|
||||
/**@type {MenuItem[]}*/ childList = [];
|
||||
/**@type {SubMenu}*/ subMenu;
|
||||
|
||||
/**@type {HTMLElement}*/ root;
|
||||
|
||||
/**@type {function}*/ onExpand;
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {?string} icon
|
||||
* @param {?boolean} showLabel
|
||||
* @param {string} label
|
||||
* @param {?string} title Tooltip
|
||||
* @param {object} value
|
||||
* @param {function} callback
|
||||
* @param {MenuItem[]} children
|
||||
*/
|
||||
constructor(icon, showLabel, label, title, value, callback, children = []) {
|
||||
this.icon = icon;
|
||||
this.showLabel = showLabel;
|
||||
this.label = label;
|
||||
this.title = title;
|
||||
this.value = value;
|
||||
this.callback = callback;
|
||||
this.childList = children;
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
if (!this.root) {
|
||||
const item = document.createElement('li'); {
|
||||
this.root = item;
|
||||
item.classList.add('list-group-item');
|
||||
item.classList.add('ctx-item');
|
||||
|
||||
// if a title/tooltip is set, add it, otherwise use the QR content
|
||||
// same as for the main QR list
|
||||
item.title = this.title || this.value;
|
||||
|
||||
if (this.callback) {
|
||||
item.addEventListener('click', (evt) => this.callback(evt, this));
|
||||
}
|
||||
const icon = document.createElement('div'); {
|
||||
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);
|
||||
item.append(icon);
|
||||
}
|
||||
const lbl = document.createElement('div'); {
|
||||
lbl.classList.add('qr--button-label');
|
||||
if (this.icon && !this.showLabel) lbl.classList.add('qr--hidden');
|
||||
lbl.textContent = this.label;
|
||||
item.append(lbl);
|
||||
}
|
||||
if (this.childList.length > 0) {
|
||||
item.classList.add('ctx-has-children');
|
||||
const sub = new SubMenu(this.childList);
|
||||
this.subMenu = sub;
|
||||
const trigger = document.createElement('div'); {
|
||||
trigger.classList.add('ctx-expander');
|
||||
trigger.textContent = '⋮';
|
||||
trigger.addEventListener('click', (evt) => {
|
||||
evt.stopPropagation();
|
||||
this.toggle();
|
||||
});
|
||||
item.append(trigger);
|
||||
}
|
||||
item.addEventListener('mouseover', () => sub.show(item));
|
||||
item.addEventListener('mouseleave', () => sub.hide());
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.root;
|
||||
}
|
||||
|
||||
|
||||
expand() {
|
||||
this.subMenu?.show(this.root);
|
||||
if (this.onExpand) {
|
||||
this.onExpand();
|
||||
}
|
||||
}
|
||||
collapse() {
|
||||
this.subMenu?.hide();
|
||||
}
|
||||
toggle() {
|
||||
if (this.subMenu.isActive) {
|
||||
this.expand();
|
||||
} else {
|
||||
this.collapse();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* @typedef {import('./MenuItem.js').MenuItem} MenuItem
|
||||
*/
|
||||
|
||||
export class SubMenu {
|
||||
/**@type {MenuItem[]}*/ itemList = [];
|
||||
/**@type {Boolean}*/ isActive = false;
|
||||
|
||||
/**@type {HTMLElement}*/ root;
|
||||
|
||||
|
||||
|
||||
|
||||
constructor(/**@type {MenuItem[]}*/items) {
|
||||
this.itemList = items;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.root) {
|
||||
const menu = document.createElement('ul'); {
|
||||
this.root = menu;
|
||||
menu.classList.add('list-group');
|
||||
menu.classList.add('ctx-menu');
|
||||
menu.classList.add('ctx-sub-menu');
|
||||
this.itemList.forEach(it => menu.append(it.render()));
|
||||
}
|
||||
}
|
||||
return this.root;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
show(/**@type {HTMLElement}*/parent) {
|
||||
if (this.isActive) return;
|
||||
this.isActive = true;
|
||||
this.render();
|
||||
parent.append(this.root);
|
||||
requestAnimationFrame(() => {
|
||||
const rect = this.root.getBoundingClientRect();
|
||||
console.log(window.innerHeight, rect);
|
||||
if (rect.bottom > window.innerHeight - 5) {
|
||||
this.root.style.top = `${window.innerHeight - 5 - rect.bottom}px`;
|
||||
}
|
||||
if (rect.right > window.innerWidth - 5) {
|
||||
this.root.style.left = 'unset';
|
||||
this.root.style.right = '100%';
|
||||
}
|
||||
});
|
||||
}
|
||||
hide() {
|
||||
if (this.root) {
|
||||
this.root.remove();
|
||||
this.root.style.top = '';
|
||||
this.root.style.left = '';
|
||||
}
|
||||
this.isActive = false;
|
||||
}
|
||||
toggle(/**@type {HTMLElement}*/parent) {
|
||||
if (this.isActive) {
|
||||
this.hide();
|
||||
} else {
|
||||
this.show(parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user