mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-02-22 06:57:41 +01:00
implement QR basics
This commit is contained in:
parent
e19bf1afdd
commit
69d6b9379a
53
public/scripts/extensions/quick-reply/html/qrOptions.html
Normal file
53
public/scripts/extensions/quick-reply/html/qrOptions.html
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<div id="qr--qrOptions">
|
||||||
|
<h3>Context Menu</h3>
|
||||||
|
<div id="qr--ctxEditor">
|
||||||
|
<template id="qr--ctxItem">
|
||||||
|
<div class="qr--ctxItem" data-order="0">
|
||||||
|
<div class="drag-handle ui-sortable-handle">☰</div>
|
||||||
|
<select class="qr--set"></select>
|
||||||
|
<label class="qr--isChainedLabel checkbox_label" title="When enabled, the current Quick Reply will be sent together with (before) the clicked QR from the context menu.">
|
||||||
|
Chaining:
|
||||||
|
<input type="checkbox" class="qr--isChained">
|
||||||
|
</label>
|
||||||
|
<div class="qr--delete menu_button menu_button_icon fa-solid fa-trash-can" title="Remove entry"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="qr--ctxEditorActions">
|
||||||
|
<span id="qr--ctxAdd" class="menu_button menu_button_icon fa-solid fa-plus" title="Add quick reply set to context menu"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<h3>Auto-Execute</h3>
|
||||||
|
<div class="flex-container flexFlowColumn">
|
||||||
|
<label class="checkbox_label">
|
||||||
|
<input type="checkbox" id="qr--isHidden" >
|
||||||
|
<span><i class="fa-solid fa-fw fa-eye-slash"></i> Invisible (auto-execute only)</span>
|
||||||
|
</label>
|
||||||
|
<label class="checkbox_label">
|
||||||
|
<input type="checkbox" id="qr--executeOnStartup" >
|
||||||
|
<span><i class="fa-solid fa-fw fa-rocket"></i> Execute on app startup</span>
|
||||||
|
</label>
|
||||||
|
<label class="checkbox_label">
|
||||||
|
<input type="checkbox" id="qr--executeOnUser" >
|
||||||
|
<span><i class="fa-solid fa-fw fa-user"></i> Execute on user message</span>
|
||||||
|
</label>
|
||||||
|
<label class="checkbox_label">
|
||||||
|
<input type="checkbox" id="qr--executeOnAi" >
|
||||||
|
<span><i class="fa-solid fa-fw fa-robot"></i> Execute on AI message</span>
|
||||||
|
</label>
|
||||||
|
<label class="checkbox_label">
|
||||||
|
<input type="checkbox" id="qr--executeOnChatChange" >
|
||||||
|
<span><i class="fa-solid fa-fw fa-message"></i> Execute on opening chat</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<h3>UI Options</h3>
|
||||||
|
<div class="flex-container flexFlowColumn">
|
||||||
|
<label>
|
||||||
|
Title (tooltip, leave empty to show the message or /command)
|
||||||
|
<input type="text" class="text_pole" id="qr--title">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
61
public/scripts/extensions/quick-reply/html/settings.html
Normal file
61
public/scripts/extensions/quick-reply/html/settings.html
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<div id="qr--settings">
|
||||||
|
<div class="inline-drawer">
|
||||||
|
<div class="inline-drawer-toggle inline-drawer-header">
|
||||||
|
<strong>Quick Reply</strong>
|
||||||
|
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
|
||||||
|
</div>
|
||||||
|
<div class="inline-drawer-content">
|
||||||
|
<label class="flex-container">
|
||||||
|
<input type="checkbox" id="qr--isEnabled"> Enable Quick Replies
|
||||||
|
</label>
|
||||||
|
<label class="flex-container">
|
||||||
|
<input type="checkbox" id="qr--isCombined"> Combine buttons from all active sets
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div class="qr--head">
|
||||||
|
<div class="qr--title">Global Quick Reply Sets</div>
|
||||||
|
<div class="qr--actions">
|
||||||
|
<div class="qr--add menu_button menu_button_icon fa-solid fa-plus" id="qr--global-setListAdd" title="Add quick reply set"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="qr--global-setList" class="qr--setList"></div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div class="qr--head">
|
||||||
|
<div class="qr--title">Chat Quick Reply Sets</div>
|
||||||
|
<div class="qr--actions">
|
||||||
|
<div class="qr--add menu_button menu_button_icon fa-solid fa-plus" id="qr--chat-setListAdd" title="Add quick reply set"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="qr--chat-setList" class="qr--setList"></div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div class="qr--head">
|
||||||
|
<div class="qr--title">Edit Quick Replies</div>
|
||||||
|
<div class="qr--actions">
|
||||||
|
<select id="qr--set" class="text_pole"></select>
|
||||||
|
<div class="qr--add menu_button menu_button_icon fa-solid fa-plus" id="qr--set-new" title="Create new quick reply set"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="qr--set-settings">
|
||||||
|
<label class="flex-container">
|
||||||
|
<input type="checkbox" id="qr--disableSend"> <span>Disable send (insert into input field)</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex-container">
|
||||||
|
<input type="checkbox" id="qr--placeBeforeInput"> <span>Place quick reply before input</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex-container" id="qr--injectInputContainer">
|
||||||
|
<input type="checkbox" id="qr--injectInput"> <span>Inject user input automatically <small>(if disabled, use <code>{{input}}</code> macro for manual injection)</small></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div id="qr--set-qrList" class="qr--qrList"></div>
|
||||||
|
<div class="qr--set-qrListActions">
|
||||||
|
<div class="qr--add menu_button menu_button_icon fa-solid fa-plus" id="qr--set-add" title="Add quick reply"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
202
public/scripts/extensions/quick-reply/index.js
Normal file
202
public/scripts/extensions/quick-reply/index.js
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
import { chat_metadata, eventSource, event_types, getRequestHeaders } from '../../../script.js';
|
||||||
|
import { extension_settings } from '../../extensions.js';
|
||||||
|
import { QuickReply } from './src/QuickReply.js';
|
||||||
|
import { QuickReplyConfig } from './src/QuickReplyConfig.js';
|
||||||
|
import { QuickReplyContextLink } from './src/QuickReplyContextLink.js';
|
||||||
|
import { QuickReplySet } from './src/QuickReplySet.js';
|
||||||
|
import { QuickReplySettings } from './src/QuickReplySettings.js';
|
||||||
|
import { ButtonUi } from './src/ui/ButtonUi.js';
|
||||||
|
import { SettingsUi } from './src/ui/SettingsUi.js';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//TODO popout QR button bar (allow separate popouts for each QR set?)
|
||||||
|
//TODO context menus
|
||||||
|
//TODO move advanced QR options into own UI class
|
||||||
|
//TODO slash commands
|
||||||
|
//TODO create new QR set
|
||||||
|
//TODO delete QR set
|
||||||
|
//TODO easy way to CRUD QRs and sets
|
||||||
|
//TODO easy way to set global and chat sets
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const _VERBOSE = true;
|
||||||
|
export const log = (...msg) => _VERBOSE ? console.log('[QR2]', ...msg) : null;
|
||||||
|
export const warn = (...msg) => _VERBOSE ? console.warn('[QR2]', ...msg) : null;
|
||||||
|
|
||||||
|
|
||||||
|
const defaultConfig = {
|
||||||
|
setList: [{
|
||||||
|
set: 'Default',
|
||||||
|
isVisible: true,
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultSettings = {
|
||||||
|
isEnabled: true,
|
||||||
|
isCombined: false,
|
||||||
|
config: defaultConfig,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/** @type {QuickReplySettings}*/
|
||||||
|
let settings;
|
||||||
|
/** @type {SettingsUi} */
|
||||||
|
let manager;
|
||||||
|
/** @type {ButtonUi} */
|
||||||
|
let buttons;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const loadSets = async () => {
|
||||||
|
const response = await fetch('/api/settings/get', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getRequestHeaders(),
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const setList = (await response.json()).quickReplyPresets ?? [];
|
||||||
|
for (const set of setList) {
|
||||||
|
if (set.version == 2) {
|
||||||
|
QuickReplySet.list.push(QuickReplySet.from(set));
|
||||||
|
} else {
|
||||||
|
const qrs = new QuickReplySet();
|
||||||
|
qrs.name = set.name;
|
||||||
|
qrs.disableSend = set.quickActionEnabled ?? false;
|
||||||
|
qrs.placeBeforeInput = set.placeBeforeInputEnabled ?? false;
|
||||||
|
qrs.injectInput = set.AutoInputInject ?? false;
|
||||||
|
qrs.qrList = set.quickReplySlots.map((slot,idx)=>{
|
||||||
|
const qr = new QuickReply();
|
||||||
|
qr.id = idx + 1;
|
||||||
|
qr.label = slot.label;
|
||||||
|
qr.title = slot.title;
|
||||||
|
qr.message = slot.mes;
|
||||||
|
qr.isHidden = slot.hidden ?? false;
|
||||||
|
qr.executeOnStartup = slot.autoExecute_appStartup ?? false;
|
||||||
|
qr.executeOnUser = slot.autoExecute_userMessage ?? false;
|
||||||
|
qr.executeOnAi = slot.autoExecute_botMessage ?? false;
|
||||||
|
qr.executeOnChatChange = slot.autoExecute_chatLoad ?? false;
|
||||||
|
qr.contextList = (slot.contextMenu ?? []).map(it=>(QuickReplyContextLink.from({
|
||||||
|
set: it.preset,
|
||||||
|
isChained: it.chain,
|
||||||
|
})));
|
||||||
|
return qr;
|
||||||
|
});
|
||||||
|
QuickReplySet.list.push(qrs);
|
||||||
|
await qrs.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log('sets: ', QuickReplySet.list);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadSettings = async () => {
|
||||||
|
//TODO migrate old settings
|
||||||
|
if (!extension_settings.quickReplyV2) {
|
||||||
|
extension_settings.quickReplyV2 = defaultSettings;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
settings = QuickReplySettings.from(extension_settings.quickReplyV2);
|
||||||
|
} catch {
|
||||||
|
settings = QuickReplySettings.from(defaultSettings);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const init = async () => {
|
||||||
|
await loadSets();
|
||||||
|
await loadSettings();
|
||||||
|
log('settings: ', settings);
|
||||||
|
|
||||||
|
manager = new SettingsUi(settings);
|
||||||
|
document.querySelector('#extensions_settings2').append(await manager.render());
|
||||||
|
|
||||||
|
buttons = new ButtonUi(settings);
|
||||||
|
buttons.show();
|
||||||
|
settings.onSave = ()=>buttons.refresh();
|
||||||
|
|
||||||
|
window['executeQuickReplyByName'] = async(name) => {
|
||||||
|
let qr = [...settings.config.setList, ...(settings.chatConfig?.setList ?? [])]
|
||||||
|
.map(it=>it.set.qrList)
|
||||||
|
.flat()
|
||||||
|
.find(it=>it.label == name)
|
||||||
|
;
|
||||||
|
if (!qr) {
|
||||||
|
let [setName, ...qrName] = name.split('.');
|
||||||
|
name = qrName.join('.');
|
||||||
|
let qrs = QuickReplySet.get(setName);
|
||||||
|
if (qrs) {
|
||||||
|
qr = qrs.qrList.find(it=>it.label == name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (qr && qr.onExecute) {
|
||||||
|
return await qr.onExecute();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (settings.isEnabled) {
|
||||||
|
const qrList = [
|
||||||
|
...settings.config.setList.map(link=>link.set.qrList.filter(qr=>qr.executeOnStartup)).flat(),
|
||||||
|
...(settings.chatConfig?.setList?.map(link=>link.set.qrList.filter(qr=>qr.executeOnStartup))?.flat() ?? []),
|
||||||
|
];
|
||||||
|
for (const qr of qrList) {
|
||||||
|
await qr.onExecute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
eventSource.on(event_types.APP_READY, init);
|
||||||
|
|
||||||
|
const onChatChanged = async (chatIdx) => {
|
||||||
|
log('CHAT_CHANGED', chatIdx);
|
||||||
|
if (chatIdx) {
|
||||||
|
settings.chatConfig = QuickReplyConfig.from(chat_metadata.quickReply ?? {});
|
||||||
|
} else {
|
||||||
|
settings.chatConfig = null;
|
||||||
|
}
|
||||||
|
manager.rerender();
|
||||||
|
buttons.refresh();
|
||||||
|
|
||||||
|
if (settings.isEnabled) {
|
||||||
|
const qrList = [
|
||||||
|
...settings.config.setList.map(link=>link.set.qrList.filter(qr=>qr.executeOnChatChange)).flat(),
|
||||||
|
...(settings.chatConfig?.setList?.map(link=>link.set.qrList.filter(qr=>qr.executeOnChatChange))?.flat() ?? []),
|
||||||
|
];
|
||||||
|
for (const qr of qrList) {
|
||||||
|
await qr.onExecute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
|
||||||
|
|
||||||
|
const onUserMessage = async () => {
|
||||||
|
if (settings.isEnabled) {
|
||||||
|
const qrList = [
|
||||||
|
...settings.config.setList.map(link=>link.set.qrList.filter(qr=>qr.executeOnUser)).flat(),
|
||||||
|
...(settings.chatConfig?.setList?.map(link=>link.set.qrList.filter(qr=>qr.executeOnUser))?.flat() ?? []),
|
||||||
|
];
|
||||||
|
for (const qr of qrList) {
|
||||||
|
await qr.onExecute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
eventSource.on(event_types.USER_MESSAGE_RENDERED, onUserMessage);
|
||||||
|
|
||||||
|
const onAiMessage = async () => {
|
||||||
|
if (settings.isEnabled) {
|
||||||
|
const qrList = [
|
||||||
|
...settings.config.setList.map(link=>link.set.qrList.filter(qr=>qr.executeOnAi)).flat(),
|
||||||
|
...(settings.chatConfig?.setList?.map(link=>link.set.qrList.filter(qr=>qr.executeOnAi))?.flat() ?? []),
|
||||||
|
];
|
||||||
|
for (const qr of qrList) {
|
||||||
|
await qr.onExecute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
eventSource.on(event_types.CHARACTER_MESSAGE_RENDERED, onAiMessage);
|
338
public/scripts/extensions/quick-reply/src/QuickReply.js
Normal file
338
public/scripts/extensions/quick-reply/src/QuickReply.js
Normal file
@ -0,0 +1,338 @@
|
|||||||
|
import { callPopup } from '../../../../script.js';
|
||||||
|
import { getSortableDelay } from '../../../utils.js';
|
||||||
|
import { log, warn } from '../index.js';
|
||||||
|
import { QuickReplyContextLink } from './QuickReplyContextLink.js';
|
||||||
|
import { QuickReplySet } from './QuickReplySet.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}*/ label = '';
|
||||||
|
/**@type {String}*/ title = '';
|
||||||
|
/**@type {String}*/ message = '';
|
||||||
|
|
||||||
|
/**@type {QuickReplyContextLink[]}*/ contextList;
|
||||||
|
|
||||||
|
/**@type {Boolean}*/ isHidden = false;
|
||||||
|
/**@type {Boolean}*/ executeOnStartup = false;
|
||||||
|
/**@type {Boolean}*/ executeOnUser = false;
|
||||||
|
/**@type {Boolean}*/ executeOnAi = false;
|
||||||
|
/**@type {Boolean}*/ executeOnChatChange = false;
|
||||||
|
|
||||||
|
/**@type {Function}*/ onExecute;
|
||||||
|
/**@type {Function}*/ onDelete;
|
||||||
|
/**@type {Function}*/ onUpdate;
|
||||||
|
|
||||||
|
|
||||||
|
/**@type {HTMLElement}*/ dom;
|
||||||
|
/**@type {HTMLElement}*/ settingsDom;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
unrender() {
|
||||||
|
this.dom?.remove();
|
||||||
|
this.dom = null;
|
||||||
|
}
|
||||||
|
updateRender() {
|
||||||
|
if (!this.dom) return;
|
||||||
|
this.dom.title = this.title || this.message;
|
||||||
|
this.dom.textContent = this.label;
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
this.unrender();
|
||||||
|
if (!this.dom) {
|
||||||
|
const root = document.createElement('div'); {
|
||||||
|
this.dom = root;
|
||||||
|
root.classList.add('qr--button');
|
||||||
|
root.title = this.title || this.message;
|
||||||
|
root.textContent = this.label;
|
||||||
|
root.addEventListener('click', ()=>{
|
||||||
|
if (this.message?.length > 0 && this.onExecute) {
|
||||||
|
this.onExecute(this);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.dom;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {any} idx
|
||||||
|
*/
|
||||||
|
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 drag = document.createElement('div'); {
|
||||||
|
drag.classList.add('drag-handle');
|
||||||
|
drag.classList.add('ui-sortable-handle');
|
||||||
|
drag.textContent = '☰';
|
||||||
|
item.append(drag);
|
||||||
|
}
|
||||||
|
const lblContainer = document.createElement('div'); {
|
||||||
|
lblContainer.classList.add('qr--set-itemLabelContainer');
|
||||||
|
const lbl = document.createElement('input'); {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
item.append(lblContainer);
|
||||||
|
}
|
||||||
|
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-solid');
|
||||||
|
opt.textContent = '⁝';
|
||||||
|
opt.title = 'Additional options:\n - context menu\n - auto-execution\n - tooltip';
|
||||||
|
opt.addEventListener('click', ()=>this.showOptions());
|
||||||
|
optContainer.append(opt);
|
||||||
|
}
|
||||||
|
item.append(optContainer);
|
||||||
|
}
|
||||||
|
const expandContainer = document.createElement('div'); {
|
||||||
|
expandContainer.classList.add('qr--set-optionsContainer');
|
||||||
|
const expand = document.createElement('div'); {
|
||||||
|
expand.classList.add('qr--expand');
|
||||||
|
expand.classList.add('menu_button');
|
||||||
|
expand.classList.add('menu_button_icon');
|
||||||
|
expand.classList.add('editor_maximize');
|
||||||
|
expand.classList.add('fa-solid');
|
||||||
|
expand.classList.add('fa-maximize');
|
||||||
|
expand.title = 'Expand the editor';
|
||||||
|
expand.setAttribute('data-for', `qr--set--item${this.id}`);
|
||||||
|
expand.setAttribute('data-tab', 'true');
|
||||||
|
expandContainer.append(expand);
|
||||||
|
}
|
||||||
|
item.append(expandContainer);
|
||||||
|
}
|
||||||
|
const mes = document.createElement('textarea'); {
|
||||||
|
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));
|
||||||
|
item.append(mes);
|
||||||
|
}
|
||||||
|
const actions = document.createElement('div'); {
|
||||||
|
actions.classList.add('qr--actions');
|
||||||
|
const del = document.createElement('div'); {
|
||||||
|
del.classList.add('qr--action');
|
||||||
|
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';
|
||||||
|
del.addEventListener('click', ()=>this.delete());
|
||||||
|
actions.append(del);
|
||||||
|
}
|
||||||
|
item.append(actions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.settingsDom;
|
||||||
|
}
|
||||||
|
unrenderSettings() {
|
||||||
|
this.settingsDom?.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
delete() {
|
||||||
|
if (this.onDelete) {
|
||||||
|
this.unrender();
|
||||||
|
this.unrenderSettings();
|
||||||
|
this.onDelete(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param {string} value
|
||||||
|
*/
|
||||||
|
updateMessage(value) {
|
||||||
|
if (this.onUpdate) {
|
||||||
|
this.message = value;
|
||||||
|
this.updateRender();
|
||||||
|
this.onUpdate(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param {string} value
|
||||||
|
*/
|
||||||
|
updateLabel(value) {
|
||||||
|
if (this.onUpdate) {
|
||||||
|
this.label = value;
|
||||||
|
this.updateRender();
|
||||||
|
this.onUpdate(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateContext() {
|
||||||
|
if (this.onUpdate) {
|
||||||
|
this.updateRender();
|
||||||
|
this.onUpdate(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async showOptions() {
|
||||||
|
const response = await fetch('/scripts/extensions/quick-reply/html/qrOptions.html', { cache: 'no-store' });
|
||||||
|
if (response.ok) {
|
||||||
|
this.template = document.createRange().createContextualFragment(await response.text()).querySelector('#qr--qrOptions');
|
||||||
|
/**@type {HTMLElement} */
|
||||||
|
// @ts-ignore
|
||||||
|
const dom = this.template.cloneNode(true);
|
||||||
|
const popupResult = callPopup(dom, 'text', undefined, { okButton: 'OK', wide: false, large: false, rows: 1 });
|
||||||
|
/**@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].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 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();
|
||||||
|
});
|
||||||
|
|
||||||
|
// UI options
|
||||||
|
/**@type {HTMLInputElement}*/
|
||||||
|
const title = dom.querySelector('#qr--title');
|
||||||
|
title.value = this.title;
|
||||||
|
title.addEventListener('input', () => {
|
||||||
|
this.title = title.value.trim();
|
||||||
|
this.updateContext();
|
||||||
|
});
|
||||||
|
|
||||||
|
await popupResult;
|
||||||
|
} else {
|
||||||
|
warn('failed to fetch qrOptions template');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
label: this.label,
|
||||||
|
title: this.title,
|
||||||
|
message: this.message,
|
||||||
|
contextList: this.contextList,
|
||||||
|
isHidden: this.isHidden,
|
||||||
|
executeOnStartup: this.executeOnStartup,
|
||||||
|
executeOnUser: this.executeOnUser,
|
||||||
|
executeOnAi: this.executeOnAi,
|
||||||
|
executeOnChatChange: this.executeOnChatChange,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
import { QuickReplyLink } from './QuickReplyLink.js';
|
||||||
|
|
||||||
|
export class QuickReplyConfig {
|
||||||
|
/**@type {QuickReplyLink[]}*/ setList = [];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
static from(props) {
|
||||||
|
props.setList = props.setList?.map(it=>QuickReplyLink.from(it)) ?? [];
|
||||||
|
return Object.assign(new this(), props);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
return {
|
||||||
|
setList: this.setList,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
24
public/scripts/extensions/quick-reply/src/QuickReplyLink.js
Normal file
24
public/scripts/extensions/quick-reply/src/QuickReplyLink.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { QuickReplySet } from './QuickReplySet.js';
|
||||||
|
|
||||||
|
export class QuickReplyLink {
|
||||||
|
/**@type {QuickReplySet}*/ set;
|
||||||
|
/**@type {Boolean}*/ isVisible = true;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
static from(props) {
|
||||||
|
props.set = QuickReplySet.get(props.set);
|
||||||
|
return Object.assign(new this(), props);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
return {
|
||||||
|
set: this.set.name,
|
||||||
|
isVisible: this.isVisible,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
192
public/scripts/extensions/quick-reply/src/QuickReplySet.js
Normal file
192
public/scripts/extensions/quick-reply/src/QuickReplySet.js
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
import { getRequestHeaders } from '../../../../script.js';
|
||||||
|
import { executeSlashCommands } from '../../../slash-commands.js';
|
||||||
|
import { debounce } from '../../../utils.js';
|
||||||
|
import { warn } from '../index.js';
|
||||||
|
import { QuickReply } from './QuickReply.js';
|
||||||
|
|
||||||
|
export class QuickReplySet {
|
||||||
|
/**@type {QuickReplySet[]}*/ static list = [];
|
||||||
|
|
||||||
|
|
||||||
|
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 {Boolean}*/ disableSend = false;
|
||||||
|
/**@type {Boolean}*/ placeBeforeInput = false;
|
||||||
|
/**@type {Boolean}*/ injectInput = false;
|
||||||
|
|
||||||
|
/**@type {QuickReply[]}*/ qrList = [];
|
||||||
|
|
||||||
|
/**@type {Number}*/ idIndex = 0;
|
||||||
|
|
||||||
|
/**@type {Function}*/ save;
|
||||||
|
|
||||||
|
|
||||||
|
/**@type {HTMLElement}*/ dom;
|
||||||
|
/**@type {HTMLElement}*/ settingsDom;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.save = debounce(()=>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.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());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {QuickReply} qr
|
||||||
|
*/
|
||||||
|
async execute(qr) {
|
||||||
|
/**@type {HTMLTextAreaElement}*/
|
||||||
|
const ta = document.querySelector('#send_textarea');
|
||||||
|
let input = ta.value;
|
||||||
|
if (this.injectInput && input.length > 0) {
|
||||||
|
if (this.placeBeforeInput) {
|
||||||
|
input = `${qr.message} ${input}`;
|
||||||
|
} else {
|
||||||
|
input = `${input} ${qr.message}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
input = `${qr.message} `;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input[0] == '/' && !this.disableSend) {
|
||||||
|
const result = await executeSlashCommands(input);
|
||||||
|
return typeof result === 'object' ? result?.pipe : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
ta.value = input;
|
||||||
|
ta.focus();
|
||||||
|
|
||||||
|
if (!this.disableSend) {
|
||||||
|
// @ts-ignore
|
||||||
|
document.querySelector('#send_but').click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
addQuickReply() {
|
||||||
|
const id = Math.max(this.idIndex, this.qrList.reduce((max,qr)=>Math.max(max,qr.id),0)) + 1;
|
||||||
|
this.idIndex = id + 1;
|
||||||
|
const qr = QuickReply.from({ id });
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
hookQuickReply(qr) {
|
||||||
|
qr.onExecute = ()=>this.execute(qr);
|
||||||
|
qr.onDelete = ()=>this.removeQuickReply(qr);
|
||||||
|
qr.onUpdate = ()=>this.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
qrList: this.qrList,
|
||||||
|
idIndex: this.idIndex,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async performSave() {
|
||||||
|
const response = await fetch('/savequickreply', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getRequestHeaders(),
|
||||||
|
body: JSON.stringify(this),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
this.rerender();
|
||||||
|
} else {
|
||||||
|
warn(`Failed to save Quick Reply Set: ${this.name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
import { chat_metadata, saveChatDebounced, saveSettingsDebounced } from '../../../../script.js';
|
||||||
|
import { extension_settings } from '../../../extensions.js';
|
||||||
|
import { QuickReplyConfig } from './QuickReplyConfig.js';
|
||||||
|
|
||||||
|
export class QuickReplySettings {
|
||||||
|
static from(props) {
|
||||||
|
props.config = QuickReplyConfig.from(props.config);
|
||||||
|
return Object.assign(new this(), props);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**@type {Boolean}*/ isEnabled = false;
|
||||||
|
/**@type {Boolean}*/ isCombined = false;
|
||||||
|
/**@type {QuickReplyConfig}*/ config;
|
||||||
|
/**@type {QuickReplyConfig}*/ chatConfig;
|
||||||
|
|
||||||
|
/**@type {Function}*/ onSave;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
save() {
|
||||||
|
extension_settings.quickReplyV2 = this.toJSON();
|
||||||
|
saveSettingsDebounced();
|
||||||
|
if (this.chatConfig) {
|
||||||
|
chat_metadata.quickReply = this.chatConfig.toJSON();
|
||||||
|
saveChatDebounced();
|
||||||
|
}
|
||||||
|
if (this.onSave) {
|
||||||
|
this.onSave();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
return {
|
||||||
|
isEnabled: this.isEnabled,
|
||||||
|
config: this.config,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
63
public/scripts/extensions/quick-reply/src/ui/ButtonUi.js
Normal file
63
public/scripts/extensions/quick-reply/src/ui/ButtonUi.js
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { QuickReplySettings } from '../QuickReplySettings.js';
|
||||||
|
|
||||||
|
export class ButtonUi {
|
||||||
|
/**@type {QuickReplySettings}*/ settings;
|
||||||
|
|
||||||
|
/**@type {HTMLElement}*/ dom;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
constructor(/**@type {QuickReplySettings}*/settings) {
|
||||||
|
this.settings = settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
render() {
|
||||||
|
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.isCombined) {
|
||||||
|
const buttons = document.createElement('div'); {
|
||||||
|
buttonHolder = buttons;
|
||||||
|
buttons.classList.add('qr--buttons');
|
||||||
|
root.append(buttons);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
[...this.settings.config.setList, ...(this.settings.chatConfig?.setList ?? [])]
|
||||||
|
.filter(link=>link.isVisible)
|
||||||
|
.forEach(link=>buttonHolder.append(link.set.render()))
|
||||||
|
;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.dom;
|
||||||
|
}
|
||||||
|
unrender() {
|
||||||
|
this.dom?.remove();
|
||||||
|
this.dom = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
show() {
|
||||||
|
if (!this.settings.isEnabled) return;
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
310
public/scripts/extensions/quick-reply/src/ui/SettingsUi.js
Normal file
310
public/scripts/extensions/quick-reply/src/ui/SettingsUi.js
Normal file
@ -0,0 +1,310 @@
|
|||||||
|
import { getSortableDelay } from '../../../../utils.js';
|
||||||
|
import { warn } from '../../index.js';
|
||||||
|
import { QuickReplyLink } from '../QuickReplyLink.js';
|
||||||
|
import { QuickReplySet } from '../QuickReplySet.js';
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { QuickReplySettings } from '../QuickReplySettings.js';
|
||||||
|
|
||||||
|
export class SettingsUi {
|
||||||
|
/**@type {QuickReplySettings}*/ settings;
|
||||||
|
|
||||||
|
/**@type {HTMLElement}*/ template;
|
||||||
|
/**@type {HTMLElement}*/ dom;
|
||||||
|
|
||||||
|
/**@type {HTMLInputElement}*/ isEnabled;
|
||||||
|
/**@type {HTMLInputElement}*/ isCombined;
|
||||||
|
|
||||||
|
/**@type {HTMLElement}*/ globalSetList;
|
||||||
|
|
||||||
|
/**@type {HTMLElement}*/ chatSetList;
|
||||||
|
|
||||||
|
/**@type {QuickReplySet}*/ currentQrSet;
|
||||||
|
/**@type {HTMLInputElement}*/ disableSend;
|
||||||
|
/**@type {HTMLInputElement}*/ placeBeforeInput;
|
||||||
|
/**@type {HTMLInputElement}*/ injectInput;
|
||||||
|
/**@type {HTMLSelectElement}*/ currentSet;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
constructor(/**@type {QuickReplySettings}*/settings) {
|
||||||
|
this.settings = settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {QuickReplyLink} qrl
|
||||||
|
* @param {Number} idx
|
||||||
|
*/
|
||||||
|
renderQrLinkItem(qrl, idx, isGlobal) {
|
||||||
|
const item = document.createElement('div'); {
|
||||||
|
item.classList.add('qr--item');
|
||||||
|
item.setAttribute('data-order', String(idx));
|
||||||
|
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.addEventListener('change', ()=>{
|
||||||
|
qrl.set = QuickReplySet.get(set.value);
|
||||||
|
this.settings.save();
|
||||||
|
});
|
||||||
|
QuickReplySet.list.forEach(qrs=>{
|
||||||
|
const opt = document.createElement('option'); {
|
||||||
|
opt.value = qrs.name;
|
||||||
|
opt.textContent = qrs.name;
|
||||||
|
opt.selected = qrs == qrl.set;
|
||||||
|
set.append(opt);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
item.append(set);
|
||||||
|
}
|
||||||
|
const visible = document.createElement('label'); {
|
||||||
|
visible.classList.add('qr--visible');
|
||||||
|
const cb = document.createElement('input'); {
|
||||||
|
cb.type = 'checkbox';
|
||||||
|
cb.checked = qrl.isVisible;
|
||||||
|
cb.addEventListener('click', ()=>{
|
||||||
|
qrl.isVisible = cb.checked;
|
||||||
|
this.settings.save();
|
||||||
|
});
|
||||||
|
visible.append(cb);
|
||||||
|
}
|
||||||
|
visible.append('Show 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.currentSet.value = qrl.set.name;
|
||||||
|
this.onQrSetChange();
|
||||||
|
});
|
||||||
|
item.append(edit);
|
||||||
|
}
|
||||||
|
const del = document.createElement('div'); {
|
||||||
|
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', ()=>{
|
||||||
|
item.remove();
|
||||||
|
if (isGlobal) {
|
||||||
|
this.settings.config.setList.splice(this.settings.config.setList.indexOf(qrl), 1);
|
||||||
|
this.updateOrder(this.globalSetList);
|
||||||
|
} else {
|
||||||
|
this.settings.chatConfig.setList.splice(this.settings.chatConfig.setList.indexOf(qrl), 1);
|
||||||
|
this.updateOrder(this.chatSetList);
|
||||||
|
}
|
||||||
|
this.settings.save();
|
||||||
|
});
|
||||||
|
item.append(del);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
prepareGlobalSetList() {
|
||||||
|
// global set list
|
||||||
|
this.dom.querySelector('#qr--global-setListAdd').addEventListener('click', ()=>{
|
||||||
|
const qrl = new QuickReplyLink();
|
||||||
|
qrl.set = QuickReplySet.list[0];
|
||||||
|
this.settings.config.setList.push(qrl);
|
||||||
|
this.globalSetList.append(this.renderQrLinkItem(qrl, this.settings.config.setList.length - 1, true));
|
||||||
|
this.settings.save();
|
||||||
|
});
|
||||||
|
this.globalSetList = this.dom.querySelector('#qr--global-setList');
|
||||||
|
// @ts-ignore
|
||||||
|
$(this.globalSetList).sortable({
|
||||||
|
delay: getSortableDelay(),
|
||||||
|
stop: ()=>this.onGlobalSetListSort(),
|
||||||
|
});
|
||||||
|
this.settings.config.setList.forEach((qrl,idx)=>{
|
||||||
|
this.globalSetList.append(this.renderQrLinkItem(qrl, idx, true));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
prepareChatSetList() {
|
||||||
|
// chat set list
|
||||||
|
this.dom.querySelector('#qr--chat-setListAdd').addEventListener('click', ()=>{
|
||||||
|
if (!this.settings.chatConfig) {
|
||||||
|
toastr.warning('No active chat.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const qrl = new QuickReplyLink();
|
||||||
|
qrl.set = QuickReplySet.list[0];
|
||||||
|
this.settings.chatConfig.setList.push(qrl);
|
||||||
|
this.chatSetList.append(this.renderQrLinkItem(qrl, this.settings.chatConfig.setList.length - 1, false));
|
||||||
|
this.settings.save();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.chatSetList = this.dom.querySelector('#qr--chat-setList');
|
||||||
|
if (!this.settings.chatConfig) {
|
||||||
|
const info = document.createElement('small'); {
|
||||||
|
info.textContent = 'No active chat.';
|
||||||
|
this.chatSetList.append(info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
|
$(this.chatSetList).sortable({
|
||||||
|
delay: getSortableDelay(),
|
||||||
|
stop: ()=>this.onChatSetListSort(),
|
||||||
|
});
|
||||||
|
this.settings.chatConfig?.setList?.forEach((qrl,idx)=>{
|
||||||
|
this.chatSetList.append(this.renderQrLinkItem(qrl, idx, false));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
prepareQrEditor() {
|
||||||
|
// qr editor
|
||||||
|
this.dom.querySelector('#qr--set-add').addEventListener('click', async()=>{
|
||||||
|
this.currentQrSet.addQuickReply();
|
||||||
|
});
|
||||||
|
this.qrList = this.dom.querySelector('#qr--set-qrList');
|
||||||
|
this.currentSet = this.dom.querySelector('#qr--set');
|
||||||
|
this.currentSet.addEventListener('change', ()=>this.onQrSetChange());
|
||||||
|
QuickReplySet.list.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();
|
||||||
|
});
|
||||||
|
this.onQrSetChange();
|
||||||
|
}
|
||||||
|
onQrSetChange() {
|
||||||
|
this.currentQrSet = QuickReplySet.get(this.currentSet.value);
|
||||||
|
this.disableSend.checked = this.currentQrSet.disableSend;
|
||||||
|
this.placeBeforeInput.checked = this.currentQrSet.placeBeforeInput;
|
||||||
|
this.injectInput.checked = this.currentQrSet.injectInput;
|
||||||
|
this.qrList.innerHTML = '';
|
||||||
|
const qrsDom = this.currentQrSet.renderSettings();
|
||||||
|
this.qrList.append(qrsDom);
|
||||||
|
// @ts-ignore
|
||||||
|
$(qrsDom).sortable({
|
||||||
|
delay: getSortableDelay(),
|
||||||
|
stop: ()=>this.onQrListSort(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
prepareDom() {
|
||||||
|
this.prepareGeneralSettings();
|
||||||
|
this.prepareGlobalSetList();
|
||||||
|
this.prepareChatSetList();
|
||||||
|
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 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();
|
||||||
|
}
|
||||||
|
}
|
114
public/scripts/extensions/quick-reply/style.css
Normal file
114
public/scripts/extensions/quick-reply/style.css
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
#qr--bar {
|
||||||
|
outline: none;
|
||||||
|
margin: 0;
|
||||||
|
transition: 0.3s;
|
||||||
|
opacity: 0.7;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
order: 1;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
#qr--bar > .qr--buttons {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 5px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
#qr--bar > .qr--buttons > .qr--buttons {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
#qr--bar > .qr--buttons .qr--button {
|
||||||
|
color: var(--SmartThemeBodyColor);
|
||||||
|
background-color: var(--black50a);
|
||||||
|
border: 1px solid var(--SmartThemeBorderColor);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 3px 5px;
|
||||||
|
margin: 3px 0;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: 0.3s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
#qr--bar > .qr--buttons .qr--button:hover {
|
||||||
|
opacity: 1;
|
||||||
|
filter: brightness(1.2);
|
||||||
|
}
|
||||||
|
#qr--settings .qr--head {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 1em;
|
||||||
|
}
|
||||||
|
#qr--settings .qr--head > .qr--title {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
#qr--settings .qr--head > .qr--actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 0.5em;
|
||||||
|
}
|
||||||
|
#qr--settings .qr--setList > .qr--item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 0.5em;
|
||||||
|
align-items: baseline;
|
||||||
|
padding: 0 0.5em;
|
||||||
|
}
|
||||||
|
#qr--settings .qr--setList > .qr--item > .qr--visible {
|
||||||
|
flex: 1 1 200px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
#qr--settings #qr--set-settings #qr--injectInputContainer {
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
#qr--settings #qr--set-qrList .qr--set-qrListContents {
|
||||||
|
padding: 0 0.5em;
|
||||||
|
}
|
||||||
|
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 0.5em;
|
||||||
|
align-items: baseline;
|
||||||
|
padding: 0.25em 0;
|
||||||
|
}
|
||||||
|
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(1) {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(2) {
|
||||||
|
flex: 1 1 25%;
|
||||||
|
}
|
||||||
|
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(3) {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(4) {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(5) {
|
||||||
|
flex: 1 1 75%;
|
||||||
|
}
|
||||||
|
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(6) {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemLabel,
|
||||||
|
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--action {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemMessage {
|
||||||
|
font-size: smaller;
|
||||||
|
}
|
||||||
|
#qr--qrOptions > #qr--ctxEditor .qr--ctxItem {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 0.5em;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
124
public/scripts/extensions/quick-reply/style.less
Normal file
124
public/scripts/extensions/quick-reply/style.less
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
#qr--bar {
|
||||||
|
outline: none;
|
||||||
|
margin: 0;
|
||||||
|
transition: 0.3s;
|
||||||
|
opacity: 0.7;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
order: 1;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
> .qr--buttons {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 5px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
> .qr--buttons {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr--button {
|
||||||
|
color: var(--SmartThemeBodyColor);
|
||||||
|
background-color: var(--black50a);
|
||||||
|
border: 1px solid var(--SmartThemeBorderColor);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 3px 5px;
|
||||||
|
margin: 3px 0;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: 0.3s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
filter: brightness(1.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#qr--settings {
|
||||||
|
.qr--head {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 1em;
|
||||||
|
> .qr--title {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
> .qr--actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.qr--setList {
|
||||||
|
> .qr--item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 0.5em;
|
||||||
|
align-items: baseline;
|
||||||
|
padding: 0 0.5em;
|
||||||
|
> .qr--visible {
|
||||||
|
flex: 1 1 200px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#qr--set-settings {
|
||||||
|
#qr--injectInputContainer {
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#qr--set-qrList {
|
||||||
|
.qr--set-qrListContents > {
|
||||||
|
padding: 0 0.5em;
|
||||||
|
> .qr--set-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 0.5em;
|
||||||
|
align-items: baseline;
|
||||||
|
padding: 0.25em 0;
|
||||||
|
> :nth-child(1) { flex: 0 0 auto; }
|
||||||
|
> :nth-child(2) { flex: 1 1 25%; }
|
||||||
|
> :nth-child(3) { flex: 0 0 auto; }
|
||||||
|
> :nth-child(4) { flex: 0 0 auto; }
|
||||||
|
> :nth-child(5) { flex: 1 1 75%; }
|
||||||
|
> :nth-child(6) { flex: 0 0 auto; }
|
||||||
|
.qr--set-itemLabel, .qr--action {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.qr--set-itemMessage {
|
||||||
|
font-size: smaller;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#qr--qrOptions {
|
||||||
|
> #qr--ctxEditor {
|
||||||
|
.qr--ctxItem {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 0.5em;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user