migrate
This commit is contained in:
505
public/scripts/extensions/quick-reply/api/QuickReplyApi.js
Normal file
505
public/scripts/extensions/quick-reply/api/QuickReplyApi.js
Normal file
@@ -0,0 +1,505 @@
|
||||
import { QuickReply } from '../src/QuickReply.js';
|
||||
import { QuickReplyContextLink } from '../src/QuickReplyContextLink.js';
|
||||
import { QuickReplySet } from '../src/QuickReplySet.js';
|
||||
import { QuickReplySettings } from '../src/QuickReplySettings.js';
|
||||
import { SettingsUi } from '../src/ui/SettingsUi.js';
|
||||
import { onlyUnique } from '../../../utils.js';
|
||||
|
||||
export class QuickReplyApi {
|
||||
/** @type {QuickReplySettings} */ settings;
|
||||
/** @type {SettingsUi} */ settingsUi;
|
||||
|
||||
|
||||
|
||||
|
||||
constructor(/** @type {QuickReplySettings} */settings, /** @type {SettingsUi} */settingsUi) {
|
||||
this.settings = settings;
|
||||
this.settingsUi = settingsUi;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @param {QuickReply} qr
|
||||
* @returns {QuickReplySet}
|
||||
*/
|
||||
getSetByQr(qr) {
|
||||
return QuickReplySet.list.find(it=>it.qrList.includes(qr));
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and returns an existing Quick Reply Set by its name.
|
||||
*
|
||||
* @param {string} name name of the quick reply set
|
||||
* @returns the quick reply set, or undefined if not found
|
||||
*/
|
||||
getSetByName(name) {
|
||||
return QuickReplySet.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and returns an existing Quick Reply by its set's name and its label.
|
||||
*
|
||||
* @param {string} setName name of the quick reply set
|
||||
* @param {string|number} label label or numeric ID of the quick reply
|
||||
* @returns the quick reply, or undefined if not found
|
||||
*/
|
||||
getQrByLabel(setName, label) {
|
||||
const set = this.getSetByName(setName);
|
||||
if (!set) return;
|
||||
if (Number.isInteger(label)) return set.qrList.find(it=>it.id == label);
|
||||
return set.qrList.find(it=>it.label == label);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Executes a quick reply by its index and returns the result.
|
||||
*
|
||||
* @param {Number} idx the index (zero-based) of the quick reply to execute
|
||||
* @returns the return value of the quick reply, or undefined if not found
|
||||
*/
|
||||
async executeQuickReplyByIndex(idx) {
|
||||
const qr = [...this.settings.config.setList, ...(this.settings.chatConfig?.setList ?? [])]
|
||||
.map(it=>it.set.qrList)
|
||||
.flat()[idx]
|
||||
;
|
||||
if (qr) {
|
||||
return await qr.onExecute();
|
||||
} else {
|
||||
throw new Error(`No quick reply at index "${idx}"`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes an existing quick reply.
|
||||
*
|
||||
* @param {string} setName name of the existing quick reply set
|
||||
* @param {string|number} label label of the existing quick reply (text on the button) or its numeric ID
|
||||
* @param {object} [args] optional arguments
|
||||
* @param {import('../../../slash-commands.js').ExecuteSlashCommandsOptions} [options] optional execution options
|
||||
*/
|
||||
async executeQuickReply(setName, label, args = {}, options = {}) {
|
||||
const qr = this.getQrByLabel(setName, label);
|
||||
if (!qr) {
|
||||
throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`);
|
||||
}
|
||||
return await qr.execute(args, false, false, options);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Adds or removes a quick reply set to the list of globally active quick reply sets.
|
||||
*
|
||||
* @param {string} name the name of the set
|
||||
* @param {boolean} isVisible whether to show the set's buttons or not
|
||||
*/
|
||||
toggleGlobalSet(name, isVisible = true) {
|
||||
const set = this.getSetByName(name);
|
||||
if (!set) {
|
||||
throw new Error(`No quick reply set with name "${name}" found.`);
|
||||
}
|
||||
if (this.settings.config.hasSet(set)) {
|
||||
this.settings.config.removeSet(set);
|
||||
} else {
|
||||
this.settings.config.addSet(set, isVisible);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a quick reply set to the list of globally active quick reply sets.
|
||||
*
|
||||
* @param {string} name the name of the set
|
||||
* @param {boolean} isVisible whether to show the set's buttons or not
|
||||
*/
|
||||
addGlobalSet(name, isVisible = true) {
|
||||
const set = this.getSetByName(name);
|
||||
if (!set) {
|
||||
throw new Error(`No quick reply set with name "${name}" found.`);
|
||||
}
|
||||
this.settings.config.addSet(set, isVisible);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a quick reply set from the list of globally active quick reply sets.
|
||||
*
|
||||
* @param {string} name the name of the set
|
||||
*/
|
||||
removeGlobalSet(name) {
|
||||
const set = this.getSetByName(name);
|
||||
if (!set) {
|
||||
throw new Error(`No quick reply set with name "${name}" found.`);
|
||||
}
|
||||
this.settings.config.removeSet(set);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Adds or removes a quick reply set to the list of the current chat's active quick reply sets.
|
||||
*
|
||||
* @param {string} name the name of the set
|
||||
* @param {boolean} isVisible whether to show the set's buttons or not
|
||||
*/
|
||||
toggleChatSet(name, isVisible = true) {
|
||||
if (!this.settings.chatConfig) return;
|
||||
const set = this.getSetByName(name);
|
||||
if (!set) {
|
||||
throw new Error(`No quick reply set with name "${name}" found.`);
|
||||
}
|
||||
if (this.settings.chatConfig.hasSet(set)) {
|
||||
this.settings.chatConfig.removeSet(set);
|
||||
} else {
|
||||
this.settings.chatConfig.addSet(set, isVisible);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a quick reply set to the list of the current chat's active quick reply sets.
|
||||
*
|
||||
* @param {string} name the name of the set
|
||||
* @param {boolean} isVisible whether to show the set's buttons or not
|
||||
*/
|
||||
addChatSet(name, isVisible = true) {
|
||||
if (!this.settings.chatConfig) return;
|
||||
const set = this.getSetByName(name);
|
||||
if (!set) {
|
||||
throw new Error(`No quick reply set with name "${name}" found.`);
|
||||
}
|
||||
this.settings.chatConfig.addSet(set, isVisible);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a quick reply set from the list of the current chat's active quick reply sets.
|
||||
*
|
||||
* @param {string} name the name of the set
|
||||
*/
|
||||
removeChatSet(name) {
|
||||
if (!this.settings.chatConfig) return;
|
||||
const set = this.getSetByName(name);
|
||||
if (!set) {
|
||||
throw new Error(`No quick reply set with name "${name}" found.`);
|
||||
}
|
||||
this.settings.chatConfig.removeSet(set);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new quick reply in an existing quick reply set.
|
||||
*
|
||||
* @param {string} setName name of the quick reply set to insert the new quick reply into
|
||||
* @param {string} label label for the new quick reply (text on the button)
|
||||
* @param {object} [props]
|
||||
* @param {string} [props.icon] the icon to show on the QR button
|
||||
* @param {boolean} [props.showLabel] whether to show the label even when an icon is assigned
|
||||
* @param {string} [props.message] the message to be sent or slash command to be executed by the new quick reply
|
||||
* @param {string} [props.title] the title / tooltip to be shown on the quick reply button
|
||||
* @param {boolean} [props.isHidden] whether to hide or show the button
|
||||
* @param {boolean} [props.executeOnStartup] whether to execute the quick reply when ChuQuadrant starts
|
||||
* @param {boolean} [props.executeOnUser] whether to execute the quick reply after a user has sent a message
|
||||
* @param {boolean} [props.executeOnAi] whether to execute the quick reply after the AI has sent a message
|
||||
* @param {boolean} [props.executeOnChatChange] whether to execute the quick reply when a new chat is loaded
|
||||
* @param {boolean} [props.executeOnGroupMemberDraft] whether to execute the quick reply when a group member is selected
|
||||
* @param {boolean} [props.executeOnNewChat] whether to execute the quick reply when a new chat is created
|
||||
* @param {string} [props.automationId] when not empty, the quick reply will be executed when the WI with the given automation ID is activated
|
||||
* @returns {QuickReply} the new quick reply
|
||||
*/
|
||||
createQuickReply(setName, label, {
|
||||
icon,
|
||||
showLabel,
|
||||
message,
|
||||
title,
|
||||
isHidden,
|
||||
executeOnStartup,
|
||||
executeOnUser,
|
||||
executeOnAi,
|
||||
executeOnChatChange,
|
||||
executeOnGroupMemberDraft,
|
||||
executeOnNewChat,
|
||||
automationId,
|
||||
} = {}) {
|
||||
const set = this.getSetByName(setName);
|
||||
if (!set) {
|
||||
throw new Error(`No quick reply set with named "${setName}" found.`);
|
||||
}
|
||||
const qr = set.addQuickReply();
|
||||
qr.label = label ?? '';
|
||||
qr.icon = icon ?? '';
|
||||
qr.showLabel = showLabel ?? false;
|
||||
qr.message = message ?? '';
|
||||
qr.title = title ?? '';
|
||||
qr.isHidden = isHidden ?? false;
|
||||
qr.executeOnStartup = executeOnStartup ?? false;
|
||||
qr.executeOnUser = executeOnUser ?? false;
|
||||
qr.executeOnAi = executeOnAi ?? false;
|
||||
qr.executeOnChatChange = executeOnChatChange ?? false;
|
||||
qr.executeOnGroupMemberDraft = executeOnGroupMemberDraft ?? false;
|
||||
qr.executeOnNewChat = executeOnNewChat ?? false;
|
||||
qr.automationId = automationId ?? '';
|
||||
qr.onUpdate();
|
||||
return qr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing quick reply.
|
||||
*
|
||||
* @param {string} setName name of the existing quick reply set
|
||||
* @param {string|number} label label of the existing quick reply (text on the button) or its numeric ID
|
||||
* @param {object} [props]
|
||||
* @param {string} [props.icon] the icon to show on the QR button
|
||||
* @param {boolean} [props.showLabel] whether to show the label even when an icon is assigned
|
||||
* @param {string} [props.newLabel] new label for quick reply (text on the button)
|
||||
* @param {string} [props.message] the message to be sent or slash command to be executed by the quick reply
|
||||
* @param {string} [props.title] the title / tooltip to be shown on the quick reply button
|
||||
* @param {boolean} [props.isHidden] whether to hide or show the button
|
||||
* @param {boolean} [props.executeOnStartup] whether to execute the quick reply when ChuQuadrant starts
|
||||
* @param {boolean} [props.executeOnUser] whether to execute the quick reply after a user has sent a message
|
||||
* @param {boolean} [props.executeOnAi] whether to execute the quick reply after the AI has sent a message
|
||||
* @param {boolean} [props.executeOnChatChange] whether to execute the quick reply when a new chat is loaded
|
||||
* @param {boolean} [props.executeOnGroupMemberDraft] whether to execute the quick reply when a group member is selected
|
||||
* @param {boolean} [props.executeOnNewChat] whether to execute the quick reply when a new chat is created
|
||||
* @param {string} [props.automationId] when not empty, the quick reply will be executed when the WI with the given automation ID is activated
|
||||
* @returns {QuickReply} the altered quick reply
|
||||
*/
|
||||
updateQuickReply(setName, label, {
|
||||
icon,
|
||||
showLabel,
|
||||
newLabel,
|
||||
message,
|
||||
title,
|
||||
isHidden,
|
||||
executeOnStartup,
|
||||
executeOnUser,
|
||||
executeOnAi,
|
||||
executeOnChatChange,
|
||||
executeOnGroupMemberDraft,
|
||||
executeOnNewChat,
|
||||
automationId,
|
||||
} = {}) {
|
||||
const qr = this.getQrByLabel(setName, label);
|
||||
if (!qr) {
|
||||
throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`);
|
||||
}
|
||||
qr.updateIcon(icon ?? qr.icon);
|
||||
qr.updateShowLabel(showLabel ?? qr.showLabel);
|
||||
qr.updateLabel(newLabel ?? qr.label);
|
||||
qr.updateMessage(message ?? qr.message);
|
||||
qr.updateTitle(title ?? qr.title);
|
||||
qr.isHidden = isHidden ?? qr.isHidden;
|
||||
qr.executeOnStartup = executeOnStartup ?? qr.executeOnStartup;
|
||||
qr.executeOnUser = executeOnUser ?? qr.executeOnUser;
|
||||
qr.executeOnAi = executeOnAi ?? qr.executeOnAi;
|
||||
qr.executeOnChatChange = executeOnChatChange ?? qr.executeOnChatChange;
|
||||
qr.executeOnGroupMemberDraft = executeOnGroupMemberDraft ?? qr.executeOnGroupMemberDraft;
|
||||
qr.executeOnNewChat = executeOnNewChat ?? qr.executeOnNewChat;
|
||||
qr.automationId = automationId ?? qr.automationId;
|
||||
qr.onUpdate();
|
||||
return qr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an existing quick reply.
|
||||
*
|
||||
* @param {string} setName name of the existing quick reply set
|
||||
* @param {string|number} label label of the existing quick reply (text on the button) or its numeric ID
|
||||
*/
|
||||
deleteQuickReply(setName, label) {
|
||||
const qr = this.getQrByLabel(setName, label);
|
||||
if (!qr) {
|
||||
throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`);
|
||||
}
|
||||
qr.delete();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Adds an existing quick reply set as a context menu to an existing quick reply.
|
||||
*
|
||||
* @param {string} setName name of the existing quick reply set containing the quick reply
|
||||
* @param {string|number} label label of the existing quick reply or its numeric ID
|
||||
* @param {string} contextSetName name of the existing quick reply set to be used as a context menu
|
||||
* @param {boolean} isChained whether or not to chain the context menu quick replies
|
||||
*/
|
||||
createContextItem(setName, label, contextSetName, isChained = false) {
|
||||
const qr = this.getQrByLabel(setName, label);
|
||||
const set = this.getSetByName(contextSetName);
|
||||
if (!qr) {
|
||||
throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`);
|
||||
}
|
||||
if (!set) {
|
||||
throw new Error(`No quick reply set with name "${contextSetName}" found.`);
|
||||
}
|
||||
const cl = new QuickReplyContextLink();
|
||||
cl.set = set;
|
||||
cl.isChained = isChained;
|
||||
qr.addContextLink(cl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a quick reply set from a quick reply's context menu.
|
||||
*
|
||||
* @param {string} setName name of the existing quick reply set containing the quick reply
|
||||
* @param {string|number} label label of the existing quick reply or its numeric ID
|
||||
* @param {string} contextSetName name of the existing quick reply set to be used as a context menu
|
||||
*/
|
||||
deleteContextItem(setName, label, contextSetName) {
|
||||
const qr = this.getQrByLabel(setName, label);
|
||||
const set = this.getSetByName(contextSetName);
|
||||
if (!qr) {
|
||||
throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`);
|
||||
}
|
||||
if (!set) {
|
||||
throw new Error(`No quick reply set with name "${contextSetName}" found.`);
|
||||
}
|
||||
qr.removeContextLink(set.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all entries from a quick reply's context menu.
|
||||
*
|
||||
* @param {string} setName name of the existing quick reply set containing the quick reply
|
||||
* @param {string|number} label label of the existing quick reply or its numeric ID
|
||||
*/
|
||||
clearContextMenu(setName, label) {
|
||||
const qr = this.getQrByLabel(setName, label);
|
||||
if (!qr) {
|
||||
throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`);
|
||||
}
|
||||
qr.clearContextLinks();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a new quick reply set.
|
||||
*
|
||||
* @param {string} name name of the new quick reply set
|
||||
* @param {object} [props]
|
||||
* @param {boolean} [props.disableSend] whether or not to send the quick replies or put the message or slash command into the char input box
|
||||
* @param {boolean} [props.placeBeforeInput] whether or not to place the quick reply contents before the existing user input
|
||||
* @param {boolean} [props.injectInput] whether or not to automatically inject the user input at the end of the quick reply
|
||||
* @returns {Promise<QuickReplySet>} the new quick reply set
|
||||
*/
|
||||
async createSet(name, {
|
||||
disableSend,
|
||||
placeBeforeInput,
|
||||
injectInput,
|
||||
} = {}) {
|
||||
const set = new QuickReplySet();
|
||||
set.name = name;
|
||||
set.disableSend = disableSend ?? false;
|
||||
set.placeBeforeInput = placeBeforeInput ?? false;
|
||||
set.injectInput = injectInput ?? false;
|
||||
const oldSet = this.getSetByName(name);
|
||||
if (oldSet) {
|
||||
QuickReplySet.list.splice(QuickReplySet.list.indexOf(oldSet), 1, set);
|
||||
} else {
|
||||
const idx = QuickReplySet.list.findIndex(it=>it.name.localeCompare(name) == 1);
|
||||
if (idx > -1) {
|
||||
QuickReplySet.list.splice(idx, 0, set);
|
||||
} else {
|
||||
QuickReplySet.list.push(set);
|
||||
}
|
||||
}
|
||||
await set.save();
|
||||
this.settingsUi.rerender();
|
||||
return set;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing quick reply set.
|
||||
*
|
||||
* @param {string} name name of the existing quick reply set
|
||||
* @param {object} [props]
|
||||
* @param {boolean} [props.disableSend] whether or not to send the quick replies or put the message or slash command into the char input box
|
||||
* @param {boolean} [props.placeBeforeInput] whether or not to place the quick reply contents before the existing user input
|
||||
* @param {boolean} [props.injectInput] whether or not to automatically inject the user input at the end of the quick reply
|
||||
* @returns {Promise<QuickReplySet>} the altered quick reply set
|
||||
*/
|
||||
async updateSet(name, {
|
||||
disableSend,
|
||||
placeBeforeInput,
|
||||
injectInput,
|
||||
} = {}) {
|
||||
const set = this.getSetByName(name);
|
||||
if (!set) {
|
||||
throw new Error(`No quick reply set with name "${name}" found.`);
|
||||
}
|
||||
set.disableSend = disableSend ?? false;
|
||||
set.placeBeforeInput = placeBeforeInput ?? false;
|
||||
set.injectInput = injectInput ?? false;
|
||||
await set.save();
|
||||
this.settingsUi.rerender();
|
||||
return set;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an existing quick reply set.
|
||||
*
|
||||
* @param {string} name name of the existing quick reply set
|
||||
*/
|
||||
async deleteSet(name) {
|
||||
const set = this.getSetByName(name);
|
||||
if (!set) {
|
||||
throw new Error(`No quick reply set with name "${name}" found.`);
|
||||
}
|
||||
await set.delete();
|
||||
this.settingsUi.rerender();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets a list of all quick reply sets.
|
||||
*
|
||||
* @returns array with the names of all quick reply sets
|
||||
*/
|
||||
listSets() {
|
||||
return QuickReplySet.list.map(it=>it.name);
|
||||
}
|
||||
/**
|
||||
* Gets a list of all globally active quick reply sets.
|
||||
*
|
||||
* @returns array with the names of all quick reply sets
|
||||
*/
|
||||
listGlobalSets() {
|
||||
return this.settings.config.setList.map(it=>it.set.name);
|
||||
}
|
||||
/**
|
||||
* Gets a list of all quick reply sets activated by the current chat.
|
||||
*
|
||||
* @returns array with the names of all quick reply sets
|
||||
*/
|
||||
listChatSets() {
|
||||
return this.settings.chatConfig?.setList?.flatMap(it=>it.set.name) ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of all quick replies in the quick reply set.
|
||||
*
|
||||
* @param {string} setName name of the existing quick reply set
|
||||
* @returns array with the labels of this set's quick replies
|
||||
*/
|
||||
listQuickReplies(setName) {
|
||||
const set = this.getSetByName(setName);
|
||||
if (!set) {
|
||||
throw new Error(`No quick reply set with name "${name}" found.`);
|
||||
}
|
||||
return set.qrList.map(it=>it.label);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of all Automation IDs used by quick replies.
|
||||
*
|
||||
* @returns {String[]} array with all automation IDs used by quick replies
|
||||
*/
|
||||
listAutomationIds() {
|
||||
return this
|
||||
.listSets()
|
||||
.flatMap(it => ({ set: it, qrs: this.listQuickReplies(it) }))
|
||||
.map(it => it.qrs?.map(qr => this.getQrByLabel(it.set, qr)?.automationId))
|
||||
.flat()
|
||||
.filter(Boolean)
|
||||
.filter(onlyUnique)
|
||||
.map(String);
|
||||
}
|
||||
}
|
157
public/scripts/extensions/quick-reply/html/qrEditor.html
Normal file
157
public/scripts/extensions/quick-reply/html/qrEditor.html
Normal file
@@ -0,0 +1,157 @@
|
||||
<div id="qr--modalEditor">
|
||||
<div id="qr--main">
|
||||
<h3 data-i18n="Labels and Message">Labels and Message</h3>
|
||||
<div class="qr--labels">
|
||||
<label class="qr--fit">
|
||||
<span class="qr--labelText" data-i18n="Label">Icon</span>
|
||||
<small class="qr--labelHint"> </small>
|
||||
<div class="menu_button fa-fw" id="qr--modal-icon" title="Click to change icon"></div>
|
||||
</label>
|
||||
<div class="label">
|
||||
<span class="qr--labelText" data-i18n="Label">Label</span>
|
||||
<small class="qr--labelHint" data-i18n="(label of the button, if no icon is chosen) ">(label of the button, if no icon is chosen)</small>
|
||||
<div class="qr--inputGroup">
|
||||
<label class="checkbox_label" title="Show label even if an icon is assigned">
|
||||
<input type="checkbox" id="qr--modal-showLabel">
|
||||
Show
|
||||
</label>
|
||||
<input type="text" class="text_pole" id="qr--modal-label">
|
||||
<div class="menu_button fa-fw fa-solid fa-chevron-down" id="qr--modal-switcher" title="Switch to another QR"></div>
|
||||
</div>
|
||||
</div>
|
||||
<label>
|
||||
<span class="qr--labelText" data-i18n="Title">Title</span>
|
||||
<small class="qr--labelHint" data-i18n="(tooltip, leave empty to show message or /command)">(tooltip, leave empty to show message or /command)</small>
|
||||
<input type="text" class="text_pole" id="qr--modal-title">
|
||||
</label>
|
||||
</div>
|
||||
<div class="qr--modal-messageContainer">
|
||||
<label for="qr--modal-message" data-i18n="Message / Command:">
|
||||
Message / Command:
|
||||
</label>
|
||||
<div class="qr--modal-editorSettings">
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" id="qr--modal-wrap">
|
||||
<span data-i18n="Word wrap">Word wrap</span>
|
||||
</label>
|
||||
<label class="checkbox_label">
|
||||
<span data-i18n="Tab size:">Tab size:</span>
|
||||
<input type="number" min="1" max="9" id="qr--modal-tabSize" class="text_pole">
|
||||
</label>
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" id="qr--modal-executeShortcut">
|
||||
<span data-i18n="Ctrl+Enter to execute">Ctrl+Enter to execute</span>
|
||||
</label>
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" id="qr--modal-syntax">
|
||||
<span>Syntax highlight</span>
|
||||
</label>
|
||||
<small>Ctrl+Alt+Click (or F9) to set / remove breakpoints</small>
|
||||
<small>Ctrl+<span id="qr--modal-commentKey"></span> to toggle block comments</small>
|
||||
</div>
|
||||
<div id="qr--modal-messageHolder">
|
||||
<pre id="qr--modal-messageSyntax"><code id="qr--modal-messageSyntaxInner" class="hljs language-stscript"></code></pre>
|
||||
<textarea id="qr--modal-message" spellcheck="false"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div id="qr--resizeHandle"></div>
|
||||
|
||||
|
||||
|
||||
<div id="qr--qrOptions">
|
||||
<h3 data-i18n="Context Menu">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.">
|
||||
<span data-i18n="Chaining:">Chaining:</span>
|
||||
<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 data-i18n="Auto-Execute">Auto-Execute</h3>
|
||||
<div id="qr--autoExec" class="flex-container flexFlowColumn">
|
||||
<label class="checkbox_label" title="Prevent this quick reply from triggering other auto-executed quick replies while auto-executing (i.e., prevent recursive auto-execution)">
|
||||
<input type="checkbox" id="qr--preventAutoExecute" >
|
||||
<span><i class="fa-solid fa-fw fa-plane-slash"></i><span data-i18n="Don't trigger auto-execute">Don't trigger auto-execute</span></span>
|
||||
</label>
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" id="qr--isHidden" >
|
||||
<span><i class="fa-solid fa-fw fa-eye-slash"></i><span data-i18n="Invisible (auto-execute only)">Invisible (auto-execute only)</span></span>
|
||||
</label>
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" id="qr--executeOnStartup" >
|
||||
<span><i class="fa-solid fa-fw fa-rocket"></i><span data-i18n="Execute on startup">Execute on startup</span></span>
|
||||
</label>
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" id="qr--executeOnUser" >
|
||||
<span><i class="fa-solid fa-fw fa-user"></i><span data-i18n="Execute on user message">Execute on user message</span></span>
|
||||
</label>
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" id="qr--executeOnAi" >
|
||||
<span><i class="fa-solid fa-fw fa-robot"></i><span data-i18n="Execute on AI message">Execute on AI message</span></span>
|
||||
</label>
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" id="qr--executeOnChatChange" >
|
||||
<span><i class="fa-solid fa-fw fa-message"></i><span data-i18n="Execute on chat change">Execute on chat change</span></span>
|
||||
</label>
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" id="qr--executeOnNewChat">
|
||||
<span><i class="fa-solid fa-fw fa-comments"></i><span data-i18n="Execute on new chat">Execute on new chat</span></span>
|
||||
</label>
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" id="qr--executeOnGroupMemberDraft">
|
||||
<span><i class="fa-solid fa-fw fa-people-group"></i><span data-i18n="Execute on group member draft">Execute on group member draft</span></span>
|
||||
</label>
|
||||
<div class="flex-container alignItemsBaseline flexFlowColumn flexNoGap" title="Activate this quick reply when a World Info entry with the same Automation ID is triggered.">
|
||||
<small data-i18n="Automation ID:">Automation ID</small>
|
||||
<input type="text" id="qr--automationId" class="text_pole flex1" placeholder="( None )">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<h3 data-i18n="Testing">Testing</h3>
|
||||
<div id="qr--modal-executeButtons">
|
||||
<div id="qr--modal-execute" class="qr--modal-executeButton menu_button" title="Execute the quick reply now">
|
||||
<i class="fa-solid fa-play"></i>
|
||||
<span data-i18n="Execute">Execute</span>
|
||||
</div>
|
||||
<div id="qr--modal-pause" class="qr--modal-executeButton menu_button" title="Pause / continue execution">
|
||||
<span class="qr--modal-executeComboIcon">
|
||||
<i class="fa-solid fa-play"></i>
|
||||
<i class="fa-solid fa-pause"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div id="qr--modal-stop" class="qr--modal-executeButton menu_button" title="Abort execution">
|
||||
<i class="fa-solid fa-stop"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div id="qr--modal-executeProgress"></div>
|
||||
<div id="qr--modal-executeErrors"></div>
|
||||
<div id="qr--modal-executeResult"></div>
|
||||
|
||||
<div id="qr--modal-debugButtons">
|
||||
<div title="Resume" id="qr--modal-resume" class="qr--modal-debugButton menu_button"></div>
|
||||
<div title="Step Over" id="qr--modal-step" class="qr--modal-debugButton menu_button"></div>
|
||||
<div title="Step Into" id="qr--modal-stepInto" class="qr--modal-debugButton menu_button"></div>
|
||||
<div title="Step Out" id="qr--modal-stepOut" class="qr--modal-debugButton menu_button"></div>
|
||||
<div title="Minimize" id="qr--modal-minimize" class="qr--modal-debugButton menu_button fa-solid fa-minimize"></div>
|
||||
<div title="Maximize" id="qr--modal-maximize" class="qr--modal-debugButton menu_button fa-solid fa-maximize"></div>
|
||||
</div>
|
||||
<textarea rows="1" id="qr--modal-send_textarea" placeholder="Chat input for use with {{input}}" title="Chat input for use with {{input}}"></textarea>
|
||||
<div id="qr--modal-debugState"></div>
|
||||
</div>
|
||||
</div>
|
86
public/scripts/extensions/quick-reply/html/settings.html
Normal file
86
public/scripts/extensions/quick-reply/html/settings.html
Normal file
@@ -0,0 +1,86 @@
|
||||
<div id="qr--settings">
|
||||
<div class="inline-drawer">
|
||||
<div class="inline-drawer-toggle inline-drawer-header">
|
||||
<strong data-i18n="Quick Reply">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"><span data-i18n="Enable Quick Replies">Enable Quick Replies</span>
|
||||
</label>
|
||||
<label class="flex-container">
|
||||
<input type="checkbox" id="qr--isCombined"><span data-i18n="Combine Quick Replies">Combine Quick Replies</span>
|
||||
</label>
|
||||
<label class="flex-container">
|
||||
<input type="checkbox" id="qr--showPopoutButton"><span data-i18n="Show Popout Button">Show Popout Button (on Desktop)</span>
|
||||
</label>
|
||||
|
||||
<hr>
|
||||
|
||||
<div id="qr--global">
|
||||
<div class="qr--head">
|
||||
<div class="qr--title" data-i18n="Global Quick Reply Sets">Global Quick Reply Sets</div>
|
||||
<div class="qr--actions">
|
||||
<div class="qr--setListAdd 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>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div id="qr--chat">
|
||||
<div class="qr--head">
|
||||
<div class="qr--title" data-i18n="Chat Quick Reply Sets">Chat Quick Reply Sets</div>
|
||||
<div class="qr--actions">
|
||||
<div class="qr--setListAdd 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>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div id="qr--editor">
|
||||
<div class="qr--head">
|
||||
<div class="qr--title" data-i18n="Edit Quick Replies">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-pencil" id="qr--set-rename" title="Rename quick reply set"></div>
|
||||
<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 class="qr--add menu_button menu_button_icon fa-solid fa-file-import" id="qr--set-import" title="Import quick reply set"></div>
|
||||
<input type="file" id="qr--set-importFile" accept=".json" hidden>
|
||||
<div class="qr--add menu_button menu_button_icon fa-solid fa-file-export" id="qr--set-export" title="Export quick reply set"></div>
|
||||
<div class="qr-add menu_button menu_button_icon fa-solid fa-paste" id="qr--set-duplicate" title="Duplicate quick reply set"></div>
|
||||
<div class="qr--del menu_button menu_button_icon fa-solid fa-trash redWarningBG" id="qr--set-delete" title="Delete quick reply set"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="qr--set-settings">
|
||||
<label class="flex-container">
|
||||
<input type="checkbox" id="qr--disableSend"> <span data-i18n="Disable Send (Insert Into Input Field)">Disable send (insert into input field)</span>
|
||||
</label>
|
||||
<label class="flex-container">
|
||||
<input type="checkbox" id="qr--placeBeforeInput"> <span data-i18n="Place Quick Reply Before Input">Place quick reply before input</span>
|
||||
</label>
|
||||
<label class="flex-container" id="qr--injectInputContainer">
|
||||
<input type="checkbox" id="qr--injectInput"> <span><span data-i18n="Inject user input automatically">Inject user input automatically</span> <small><span data-i18n="(if disabled, use ">(if disabled, use</span><code>{{input}}</code> <span data-i18n="macro for manual injection)">macro for manual injection)</span></small></span>
|
||||
</label>
|
||||
<div class="flex-container alignItemsCenter">
|
||||
<toolcool-color-picker id="qr--color"></toolcool-color-picker>
|
||||
<div class="menu_button" id="qr--colorClear">Clear</div>
|
||||
<span data-i18n="Color">Color</span>
|
||||
</div>
|
||||
<label class="flex-container" id="qr--onlyBorderColorContainer">
|
||||
<input type="checkbox" id="qr--onlyBorderColor"> <span data-i18n="Only apply color as accent">Only apply color as accent</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 class="qr--paste menu_button menu_button_icon fa-solid fa-paste" id="qr--set-paste" title="Paste quick reply from clipboard"></div>
|
||||
<div class="qr--import menu_button menu_button_icon fa-solid fa-file-import" id="qr--set-importQr" title="Import quick reply from file"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
244
public/scripts/extensions/quick-reply/index.js
Normal file
244
public/scripts/extensions/quick-reply/index.js
Normal file
@@ -0,0 +1,244 @@
|
||||
import { chat, chat_metadata, eventSource, event_types, getRequestHeaders } from '../../../script.js';
|
||||
import { extension_settings } from '../../extensions.js';
|
||||
import { QuickReplyApi } from './api/QuickReplyApi.js';
|
||||
import { AutoExecuteHandler } from './src/AutoExecuteHandler.js';
|
||||
import { QuickReply } from './src/QuickReply.js';
|
||||
import { QuickReplyConfig } from './src/QuickReplyConfig.js';
|
||||
import { QuickReplySet } from './src/QuickReplySet.js';
|
||||
import { QuickReplySettings } from './src/QuickReplySettings.js';
|
||||
import { SlashCommandHandler } from './src/SlashCommandHandler.js';
|
||||
import { ButtonUi } from './src/ui/ButtonUi.js';
|
||||
import { SettingsUi } from './src/ui/SettingsUi.js';
|
||||
import { debounceAsync } from '../../utils.js';
|
||||
export { debounceAsync };
|
||||
|
||||
|
||||
|
||||
|
||||
const _VERBOSE = true;
|
||||
export const debug = (...msg) => _VERBOSE ? console.debug('[QR2]', ...msg) : null;
|
||||
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: false,
|
||||
isCombined: false,
|
||||
config: defaultConfig,
|
||||
};
|
||||
|
||||
|
||||
/** @type {Boolean}*/
|
||||
let isReady = false;
|
||||
/** @type {Function[]}*/
|
||||
let executeQueue = [];
|
||||
/** @type {QuickReplySettings}*/
|
||||
let settings;
|
||||
/** @type {SettingsUi} */
|
||||
let manager;
|
||||
/** @type {ButtonUi} */
|
||||
let buttons;
|
||||
/** @type {AutoExecuteHandler} */
|
||||
let autoExec;
|
||||
/** @type {QuickReplyApi} */
|
||||
export let quickReplyApi;
|
||||
|
||||
|
||||
|
||||
|
||||
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) {
|
||||
// migrate old QR set
|
||||
set.version = 2;
|
||||
set.disableSend = set.quickActionEnabled ?? false;
|
||||
set.placeBeforeInput = set.placeBeforeInputEnabled ?? false;
|
||||
set.injectInput = set.AutoInputInject ?? false;
|
||||
set.qrList = set.quickReplySlots.map((slot,idx)=>{
|
||||
const qr = {};
|
||||
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.executeOnGroupMemberDraft = slot.autoExecute_groupMemberDraft ?? false;
|
||||
qr.executeOnNewChat = slot.autoExecute_newChat ?? false;
|
||||
qr.automationId = slot.automationId ?? '';
|
||||
qr.contextList = (slot.contextMenu ?? []).map(it=>({
|
||||
set: it.preset,
|
||||
isChained: it.chain,
|
||||
}));
|
||||
return qr;
|
||||
});
|
||||
}
|
||||
if (set.version == 2) {
|
||||
QuickReplySet.list.push(QuickReplySet.from(JSON.parse(JSON.stringify(set))));
|
||||
}
|
||||
}
|
||||
// need to load QR lists after all sets are loaded to be able to resolve context menu entries
|
||||
setList.forEach((set, idx)=>{
|
||||
QuickReplySet.list[idx].qrList = set.qrList.map(it=>QuickReply.from(it));
|
||||
QuickReplySet.list[idx].init();
|
||||
});
|
||||
log('sets: ', QuickReplySet.list);
|
||||
}
|
||||
};
|
||||
|
||||
const loadSettings = async () => {
|
||||
if (!extension_settings.quickReplyV2) {
|
||||
if (!extension_settings.quickReply) {
|
||||
extension_settings.quickReplyV2 = defaultSettings;
|
||||
} else {
|
||||
extension_settings.quickReplyV2 = {
|
||||
isEnabled: extension_settings.quickReply.quickReplyEnabled ?? false,
|
||||
isCombined: false,
|
||||
isPopout: false,
|
||||
config: {
|
||||
setList: [{
|
||||
set: extension_settings.quickReply.selectedPreset ?? extension_settings.quickReply.name ?? 'Default',
|
||||
isVisible: true,
|
||||
}],
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
try {
|
||||
settings = QuickReplySettings.from(extension_settings.quickReplyV2);
|
||||
} catch (ex) {
|
||||
settings = QuickReplySettings.from(defaultSettings);
|
||||
}
|
||||
};
|
||||
|
||||
const executeIfReadyElseQueue = async (functionToCall, args) => {
|
||||
if (isReady) {
|
||||
log('calling', { functionToCall, args });
|
||||
await functionToCall(...args);
|
||||
} else {
|
||||
log('queueing', { functionToCall, args });
|
||||
executeQueue.push(async()=>await functionToCall(...args));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
const init = async () => {
|
||||
await loadSets();
|
||||
await loadSettings();
|
||||
log('settings: ', settings);
|
||||
|
||||
manager = new SettingsUi(settings);
|
||||
document.querySelector('#qr_container').append(await manager.render());
|
||||
|
||||
buttons = new ButtonUi(settings);
|
||||
buttons.show();
|
||||
settings.onSave = ()=>buttons.refresh();
|
||||
|
||||
window['executeQuickReplyByName'] = async(name, args = {}, options = {}) => {
|
||||
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('.');
|
||||
qrName = qrName.join('.');
|
||||
let qrs = QuickReplySet.get(setName);
|
||||
if (qrs) {
|
||||
qr = qrs.qrList.find(it=>it.label == qrName);
|
||||
}
|
||||
}
|
||||
if (qr && qr.onExecute) {
|
||||
return await qr.execute(args, false, true, options);
|
||||
} else {
|
||||
throw new Error(`No Quick Reply found for "${name}".`);
|
||||
}
|
||||
};
|
||||
|
||||
quickReplyApi = new QuickReplyApi(settings, manager);
|
||||
const slash = new SlashCommandHandler(quickReplyApi);
|
||||
slash.init();
|
||||
autoExec = new AutoExecuteHandler(settings);
|
||||
|
||||
eventSource.on(event_types.APP_READY, async()=>await finalizeInit());
|
||||
|
||||
window['quickReplyApi'] = quickReplyApi;
|
||||
};
|
||||
const finalizeInit = async () => {
|
||||
debug('executing startup');
|
||||
await autoExec.handleStartup();
|
||||
debug('/executing startup');
|
||||
|
||||
debug(`executing queue (${executeQueue.length} items)`);
|
||||
while (executeQueue.length > 0) {
|
||||
const func = executeQueue.shift();
|
||||
await func();
|
||||
}
|
||||
debug('/executing queue');
|
||||
isReady = true;
|
||||
debug('READY');
|
||||
};
|
||||
await 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();
|
||||
|
||||
await autoExec.handleChatChanged();
|
||||
};
|
||||
eventSource.on(event_types.CHAT_CHANGED, (...args)=>executeIfReadyElseQueue(onChatChanged, args));
|
||||
|
||||
const onUserMessage = async () => {
|
||||
await autoExec.handleUser();
|
||||
};
|
||||
eventSource.makeFirst(event_types.USER_MESSAGE_RENDERED, (...args)=>executeIfReadyElseQueue(onUserMessage, args));
|
||||
|
||||
const onAiMessage = async (messageId) => {
|
||||
if (['...'].includes(chat[messageId]?.mes)) {
|
||||
log('QR auto-execution suppressed for swiped message');
|
||||
return;
|
||||
}
|
||||
|
||||
await autoExec.handleAi();
|
||||
};
|
||||
eventSource.makeFirst(event_types.CHARACTER_MESSAGE_RENDERED, (...args)=>executeIfReadyElseQueue(onAiMessage, args));
|
||||
|
||||
const onGroupMemberDraft = async () => {
|
||||
await autoExec.handleGroupMemberDraft();
|
||||
};
|
||||
eventSource.on(event_types.GROUP_MEMBER_DRAFTED, (...args) => executeIfReadyElseQueue(onGroupMemberDraft, args));
|
||||
|
||||
const onWIActivation = async (entries) => {
|
||||
await autoExec.handleWIActivation(entries);
|
||||
};
|
||||
eventSource.on(event_types.WORLD_INFO_ACTIVATED, (...args) => executeIfReadyElseQueue(onWIActivation, args));
|
||||
|
||||
const onNewChat = async () => {
|
||||
await autoExec.handleNewChat();
|
||||
};
|
||||
eventSource.on(event_types.CHAT_CREATED, (...args) => executeIfReadyElseQueue(onNewChat, args));
|
11
public/scripts/extensions/quick-reply/manifest.json
Normal file
11
public/scripts/extensions/quick-reply/manifest.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"display_name": "Quick Replies",
|
||||
"loading_order": 12,
|
||||
"requires": [],
|
||||
"optional": [],
|
||||
"js": "index.js",
|
||||
"css": "style.css",
|
||||
"author": "RossAscends#1779",
|
||||
"version": "2.0.0",
|
||||
"homePage": "https://github.com/ChuQuadrant/ChuQuadrant"
|
||||
}
|
108
public/scripts/extensions/quick-reply/src/AutoExecuteHandler.js
Normal file
108
public/scripts/extensions/quick-reply/src/AutoExecuteHandler.js
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async handleStartup() {
|
||||
if (!this.checkExecute()) return;
|
||||
const qrList = [
|
||||
...this.settings.config.setList.map(link=>link.set.qrList.filter(qr=>qr.executeOnStartup)).flat(),
|
||||
...(this.settings.chatConfig?.setList?.map(link=>link.set.qrList.filter(qr=>qr.executeOnStartup))?.flat() ?? []),
|
||||
];
|
||||
await this.performAutoExecute(qrList);
|
||||
}
|
||||
|
||||
async handleUser() {
|
||||
if (!this.checkExecute()) return;
|
||||
const qrList = [
|
||||
...this.settings.config.setList.map(link=>link.set.qrList.filter(qr=>qr.executeOnUser)).flat(),
|
||||
...(this.settings.chatConfig?.setList?.map(link=>link.set.qrList.filter(qr=>qr.executeOnUser))?.flat() ?? []),
|
||||
];
|
||||
await this.performAutoExecute(qrList);
|
||||
}
|
||||
|
||||
async handleAi() {
|
||||
if (!this.checkExecute()) return;
|
||||
const qrList = [
|
||||
...this.settings.config.setList.map(link=>link.set.qrList.filter(qr=>qr.executeOnAi)).flat(),
|
||||
...(this.settings.chatConfig?.setList?.map(link=>link.set.qrList.filter(qr=>qr.executeOnAi))?.flat() ?? []),
|
||||
];
|
||||
await this.performAutoExecute(qrList);
|
||||
}
|
||||
|
||||
async handleChatChanged() {
|
||||
if (!this.checkExecute()) return;
|
||||
const qrList = [
|
||||
...this.settings.config.setList.map(link=>link.set.qrList.filter(qr=>qr.executeOnChatChange)).flat(),
|
||||
...(this.settings.chatConfig?.setList?.map(link=>link.set.qrList.filter(qr=>qr.executeOnChatChange))?.flat() ?? []),
|
||||
];
|
||||
await this.performAutoExecute(qrList);
|
||||
}
|
||||
|
||||
async handleGroupMemberDraft() {
|
||||
if (!this.checkExecute()) return;
|
||||
const qrList = [
|
||||
...this.settings.config.setList.map(link=>link.set.qrList.filter(qr=>qr.executeOnGroupMemberDraft)).flat(),
|
||||
...(this.settings.chatConfig?.setList?.map(link=>link.set.qrList.filter(qr=>qr.executeOnGroupMemberDraft))?.flat() ?? []),
|
||||
];
|
||||
await this.performAutoExecute(qrList);
|
||||
}
|
||||
|
||||
async handleNewChat() {
|
||||
if (!this.checkExecute()) return;
|
||||
const qrList = [
|
||||
...this.settings.config.setList.map(link=>link.set.qrList.filter(qr=>qr.executeOnNewChat)).flat(),
|
||||
...(this.settings.chatConfig?.setList?.map(link=>link.set.qrList.filter(qr=>qr.executeOnNewChat))?.flat() ?? []),
|
||||
];
|
||||
await this.performAutoExecute(qrList);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 qrList = [
|
||||
...this.settings.config.setList.map(link=>link.set.qrList.filter(qr=>qr.automationId && automationIds.includes(qr.automationId))).flat(),
|
||||
...(this.settings.chatConfig?.setList?.map(link=>link.set.qrList.filter(qr=>qr.automationId && automationIds.includes(qr.automationId)))?.flat() ?? []),
|
||||
];
|
||||
|
||||
await this.performAutoExecute(qrList);
|
||||
}
|
||||
}
|
1910
public/scripts/extensions/quick-reply/src/QuickReply.js
Normal file
1910
public/scripts/extensions/quick-reply/src/QuickReply.js
Normal file
File diff suppressed because it is too large
Load Diff
127
public/scripts/extensions/quick-reply/src/QuickReplyConfig.js
Normal file
127
public/scripts/extensions/quick-reply/src/QuickReplyConfig.js
Normal file
@@ -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 {Boolean}*/ isGlobal;
|
||||
|
||||
/**@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,
|
||||
};
|
||||
}
|
||||
}
|
403
public/scripts/extensions/quick-reply/src/QuickReplySet.js
Normal file
403
public/scripts/extensions/quick-reply/src/QuickReplySet.js
Normal file
@@ -0,0 +1,403 @@
|
||||
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 = [];
|
||||
|
||||
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 {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) {
|
||||
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') {
|
||||
(dlg.dom ?? dlg.dlg).classList.remove('qr--isCopy');
|
||||
return;
|
||||
}
|
||||
});
|
||||
sel.addEventListener('keydown', (evt)=>{
|
||||
if (evt.key == 'Shift') {
|
||||
(dlg.dom ?? dlg.dlg).classList.add('qr--isCopy');
|
||||
return;
|
||||
}
|
||||
if (!evt.ctrlKey && !evt.altKey && evt.key == 'Enter') {
|
||||
evt.preventDefault();
|
||||
if (evt.shiftKey) isCopy = true;
|
||||
dlg.completeAffirmative();
|
||||
}
|
||||
});
|
||||
dom.append(sel);
|
||||
}
|
||||
const hintP = document.createElement('p'); {
|
||||
const hint = document.createElement('small'); {
|
||||
hint.textContent = 'Type or arrows to select QR Set. Enter to transfer. Shift+Enter to copy.';
|
||||
hintP.append(hint);
|
||||
}
|
||||
dom.append(hintP);
|
||||
}
|
||||
}
|
||||
const dlg = new Popup(dom, POPUP_TYPE.CONFIRM, null, { okButton:'Transfer', cancelButton:'Cancel' });
|
||||
const copyBtn = document.createElement('div'); {
|
||||
copyBtn.classList.add('qr--copy');
|
||||
copyBtn.classList.add('menu_button');
|
||||
copyBtn.textContent = 'Copy';
|
||||
copyBtn.addEventListener('click', ()=>{
|
||||
isCopy = true;
|
||||
dlg.completeAffirmative();
|
||||
});
|
||||
(dlg.ok ?? dlg.okButton).insertAdjacentElement('afterend', copyBtn);
|
||||
}
|
||||
const prom = dlg.show();
|
||||
sel.focus();
|
||||
await prom;
|
||||
if (dlg.result == POPUP_RESULT.AFFIRMATIVE) {
|
||||
const qrs = QuickReplySet.list.find(it=>it.name == sel.value);
|
||||
qrs.addQuickReply(qr.toJSON());
|
||||
if (!isCopy) {
|
||||
qr.delete();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
removeQuickReply(qr) {
|
||||
this.qrList.splice(this.qrList.indexOf(qr), 1);
|
||||
this.save();
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
version: 2,
|
||||
name: this.name,
|
||||
disableSend: this.disableSend,
|
||||
placeBeforeInput: this.placeBeforeInput,
|
||||
injectInput: this.injectInput,
|
||||
color: this.color,
|
||||
onlyBorderColor: this.onlyBorderColor,
|
||||
qrList: this.qrList,
|
||||
idIndex: this.idIndex,
|
||||
};
|
||||
}
|
||||
|
||||
async performSave() {
|
||||
const response = await fetch('/api/quick-replies/save', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify(this),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
this.rerender();
|
||||
} else {
|
||||
warn(`Failed to save Quick Reply Set: ${this.name}`);
|
||||
console.error('QR could not be saved', response);
|
||||
}
|
||||
}
|
||||
|
||||
async delete() {
|
||||
const response = await fetch('/api/quick-replies/delete', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify(this),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
this.unrender();
|
||||
const idx = QuickReplySet.list.indexOf(this);
|
||||
if (idx > -1) {
|
||||
QuickReplySet.list.splice(idx, 1);
|
||||
this.isDeleted = true;
|
||||
} else {
|
||||
warn(`Deleted Quick Reply Set was not found in the list of sets: ${this.name}`);
|
||||
}
|
||||
} else {
|
||||
warn(`Failed to delete Quick Reply Set: ${this.name}`);
|
||||
}
|
||||
}
|
||||
}
|
129
public/scripts/extensions/quick-reply/src/QuickReplySetLink.js
Normal file
129
public/scripts/extensions/quick-reply/src/QuickReplySetLink.js
Normal file
@@ -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,87 @@
|
||||
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);
|
||||
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 {QuickReplyConfig}*/ _chatConfig;
|
||||
get chatConfig() {
|
||||
return this._chatConfig;
|
||||
}
|
||||
set chatConfig(value) {
|
||||
if (this._chatConfig != value) {
|
||||
this.unhookConfig(this._chatConfig);
|
||||
this._chatConfig = value;
|
||||
this.hookConfig(this._chatConfig);
|
||||
}
|
||||
}
|
||||
|
||||
/**@type {Function}*/ onSave;
|
||||
/**@type {Function}*/ onRequestEditSet;
|
||||
|
||||
|
||||
|
||||
|
||||
init() {
|
||||
this.hookConfig(this.config);
|
||||
this.hookConfig(this.chatConfig);
|
||||
}
|
||||
|
||||
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();
|
||||
saveChatDebounced();
|
||||
}
|
||||
if (this.onSave) {
|
||||
this.onSave();
|
||||
}
|
||||
}
|
||||
|
||||
requestEditSet(qrs) {
|
||||
if (this.onRequestEditSet) {
|
||||
this.onRequestEditSet(qrs);
|
||||
}
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
isEnabled: this.isEnabled,
|
||||
isCombined: this.isCombined,
|
||||
isPopout: this.isPopout,
|
||||
showPopoutButton: this.showPopoutButton,
|
||||
config: this.config,
|
||||
};
|
||||
}
|
||||
}
|
1013
public/scripts/extensions/quick-reply/src/SlashCommandHandler.js
Normal file
1013
public/scripts/extensions/quick-reply/src/SlashCommandHandler.js
Normal file
File diff suppressed because it is too large
Load Diff
163
public/scripts/extensions/quick-reply/src/ui/ButtonUi.js
Normal file
163
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 ?? [])]
|
||||
.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 ?? [])]
|
||||
.filter(link=>link.isVisible)
|
||||
.forEach(link=>buttonHolder.append(link.set.render()))
|
||||
;
|
||||
root.append(body);
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.popoutDom;
|
||||
}
|
||||
}
|
503
public/scripts/extensions/quick-reply/src/ui/SettingsUi.js
Normal file
503
public/scripts/extensions/quick-reply/src/ui/SettingsUi.js
Normal file
@@ -0,0 +1,503 @@
|
||||
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 {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);
|
||||
}
|
||||
|
||||
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');
|
||||
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;
|
||||
this.color.color = qrs.color;
|
||||
return;
|
||||
}
|
||||
qrs.color = evt.detail.rgb;
|
||||
qrs.save();
|
||||
this.currentQrSet.updateColor();
|
||||
});
|
||||
this.dom.querySelector('#qr--colorClear').addEventListener('click', (evt)=>{
|
||||
const qrs = this.currentQrSet;
|
||||
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;
|
||||
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.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
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.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();
|
||||
|
||||
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();
|
||||
}
|
||||
} 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
} 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();
|
||||
}
|
||||
}
|
||||
} 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(JSON.parse(JSON.stringify(this.currentQrSet)));
|
||||
newQrSet.name = newName;
|
||||
newQrSet.qrList = this.currentQrSet.qrList.map(qr => QuickReply.from(JSON.parse(JSON.stringify(qr))));
|
||||
newQrSet.addQuickReply();
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
selectQrSet(qrs) {
|
||||
this.currentSet.value = qrs.name;
|
||||
this.onQrSetChange();
|
||||
}
|
||||
}
|
130
public/scripts/extensions/quick-reply/src/ui/ctx/ContextMenu.js
Normal file
130
public/scripts/extensions/quick-reply/src/ui/ctx/ContextMenu.js
Normal file
@@ -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;
|
||||
}
|
||||
}
|
107
public/scripts/extensions/quick-reply/src/ui/ctx/MenuItem.js
Normal file
107
public/scripts/extensions/quick-reply/src/ui/ctx/MenuItem.js
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
66
public/scripts/extensions/quick-reply/src/ui/ctx/SubMenu.js
Normal file
66
public/scripts/extensions/quick-reply/src/ui/ctx/SubMenu.js
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
1090
public/scripts/extensions/quick-reply/style.css
Normal file
1090
public/scripts/extensions/quick-reply/style.css
Normal file
File diff suppressed because it is too large
Load Diff
1159
public/scripts/extensions/quick-reply/style.less
Normal file
1159
public/scripts/extensions/quick-reply/style.less
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user