add qr context menus

This commit is contained in:
LenAnderson 2023-11-23 12:21:25 +00:00
parent b4afb10fab
commit cc426e9897
6 changed files with 268 additions and 3 deletions

View File

@ -2,6 +2,9 @@ import { saveSettingsDebounced, callPopup, getRequestHeaders, substituteParams }
import { getContext, extension_settings } from "../../extensions.js";
import { initScrollHeight, resetScrollHeight } from "../../utils.js";
import { executeSlashCommands, registerSlashCommand } from "../../slash-commands.js";
import { ContextMenu } from "./src/ContextMenu.js";
import { MenuItem } from "./src/MenuItem.js";
import { MenuHeader } from "./src/MenuHeader.js";
export { MODULE_NAME };
@ -99,7 +102,12 @@ function onQuickReplyInput(id) {
function onQuickReplyLabelInput(id) {
extension_settings.quickReply.quickReplySlots[id - 1].label = $(`#quickReply${id}Label`).val();
$(`#quickReply${id}`).text(String($(`#quickReply${id}Label`).val()));
let quickReplyLabel = extension_settings.quickReply.quickReplySlots[id - 1]?.label || '';
const parts = quickReplyLabel.split('...');
if (parts.length > 1) {
quickReplyLabel = `${parts.shift()}`;
}
$(`#quickReply${id}`).text(quickReplyLabel);
saveSettingsDebounced();
}
@ -129,13 +137,15 @@ async function onAutoInputInject() {
}
async function sendQuickReply(index) {
const existingText = $("#send_textarea").val();
const prompt = extension_settings.quickReply.quickReplySlots[index]?.mes || '';
await performQuickReply(prompt, index);
}
async function performQuickReply(prompt, index) {
if (!prompt) {
console.warn(`Quick reply slot ${index} is empty! Aborting.`);
return;
}
const existingText = $("#send_textarea").val();
let newText;
@ -170,6 +180,44 @@ async function sendQuickReply(index) {
}
function buildContextMenu(qr, chainMes=null, hierarchy=[]) {
const tree = {
label: qr.label,
mes: (chainMes&&qr.mes ? `${chainMes} | ` : '') + qr.mes,
children: [],
};
const parts = qr.label.split('...');
if (parts.length > 1) {
tree.label = parts.shift();
parts.forEach(subName=>{
let chain = false;
if (subName[0] == '!') {
chain = true;
subName = subName.substring(1);
}
const sub = presets.find(it=>it.name == subName);
if (sub) {
// prevent circular references
if (hierarchy.indexOf(sub.name) == -1) {
tree.children.push(new MenuHeader(sub.name));
sub.quickReplySlots.forEach(subQr=>{
const subInfo = buildContextMenu(subQr, chain?tree.mes:null, [...hierarchy, sub.name]);
tree.children.push(new MenuItem(
subInfo.label,
subInfo.mes,
(evt)=>{
evt.stopPropagation();
performQuickReply(subInfo.mes);
},
subInfo.children,
));
});
}
}
});
}
return tree;
}
function addQuickReplyBar() {
$('#quickReplyBar').remove();
let quickReplyButtonHtml = '';
@ -177,6 +225,10 @@ function addQuickReplyBar() {
for (let i = 0; i < extension_settings.quickReply.numberOfSlots; i++) {
let quickReplyMes = extension_settings.quickReply.quickReplySlots[i]?.mes || '';
let quickReplyLabel = extension_settings.quickReply.quickReplySlots[i]?.label || '';
const parts = quickReplyLabel.split('...');
if (parts.length > 1) {
quickReplyLabel = `${parts.shift()}`;
}
quickReplyButtonHtml += `<div title="${quickReplyMes}" class="quickReplyButton" data-index="${i}" id="quickReply${i + 1}">${quickReplyLabel}</div>`;
}
@ -194,6 +246,14 @@ function addQuickReplyBar() {
let index = $(this).data('index');
sendQuickReply(index);
});
$('.quickReplyButton').on('contextmenu', function (evt) {
evt.preventDefault();
let index = $(this).data('index');
const qr = extension_settings.quickReply.quickReplySlots[index];
const tree = buildContextMenu(qr);
const menu = new ContextMenu(tree.children);
menu.show(evt);
});
}
async function moduleWorker() {

View File

@ -0,0 +1,46 @@
import { MenuItem } from "./MenuItem.js";
export class ContextMenu {
/**@type {HTMLElement}*/ root;
/**@type {HTMLElement}*/ menu;
/**@type {MenuItem[]}*/ itemList = [];
constructor(/**@type {MenuItem[]}*/items) {
this.itemList = items;
}
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({screenX, screenY}) {
this.render();
this.menu.style.bottom = `${window.innerHeight - screenY}px`;
this.menu.style.left = `${screenX}px`;
document.body.append(this.root);
}
hide() {
this.root.remove();
}
}

View File

@ -0,0 +1,27 @@
import { SubMenu } from "./SubMenu.js";
export class MenuHeader {
/**@type {String}*/ label;
/**@type {HTMLElement}*/ root;
constructor(/**@type {String}*/label) {
this.label = label;
}
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;
}
}

View File

@ -0,0 +1,43 @@
import { SubMenu } from "./SubMenu.js";
export class MenuItem {
/**@type {String}*/ label;
/**@type {Object}*/ value;
/**@type {Function}*/ callback;
/**@type {MenuItem[]}*/ childList = [];
/**@type {HTMLElement}*/ root;
constructor(/**@type {String}*/label, /**@type {Object}*/value, /**@type {function}*/callback, /**@type {MenuItem[]}*/children=[]) {
this.label = label;
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');
item.title = this.value;
if (this.callback) {
item.addEventListener('click', (evt)=>this.callback(evt, this));
}
if (this.childList.length > 0) {
item.classList.add('ctx-has-children');
const sub = new SubMenu(this.childList);
item.addEventListener('pointerover', ()=>sub.show(item));
item.addEventListener('pointerleave', ()=>sub.hide());
}
item.append(this.label);
}
}
return this.root;
}
}

View File

@ -0,0 +1,55 @@
import { MenuItem } from "./MenuItem.js";
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() {
this.root.remove();
this.root.style.top = '';
this.root.style.left = '';
this.isActive = false;
}
}

View File

@ -44,4 +44,38 @@
opacity: 1;
filter: brightness(1.2);
cursor: pointer;
}
.ctx-blocker {
backdrop-filter: blur(1px);
background-color: rgba(0 0 0 / 10%);
bottom: 0;
left: 0;
position: fixed;
right: 0;
top: 0;
z-index: 999;
}
.ctx-menu {
position: fixed;
overflow: visible;
}
.list-group .list-group-item.ctx-header {
font-weight: bold;
cursor: default;
}
.ctx-item + .ctx-header {
border-top: 1px solid;
}
.ctx-item {
position: relative;
}
.ctx-item.ctx-has-children:after {
content: " >";
}
.ctx-sub-menu {
position: absolute;
top: 0;
left: 100%;
}