mirror of
				https://github.com/SillyTavern/SillyTavern.git
				synced 2025-06-05 21:59:27 +02:00 
			
		
		
		
	add qr context menus
This commit is contained in:
		| @@ -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() { | ||||
|   | ||||
							
								
								
									
										46
									
								
								public/scripts/extensions/quick-reply/src/ContextMenu.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								public/scripts/extensions/quick-reply/src/ContextMenu.js
									
									
									
									
									
										Normal 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(); | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										27
									
								
								public/scripts/extensions/quick-reply/src/MenuHeader.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								public/scripts/extensions/quick-reply/src/MenuHeader.js
									
									
									
									
									
										Normal 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; | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										43
									
								
								public/scripts/extensions/quick-reply/src/MenuItem.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								public/scripts/extensions/quick-reply/src/MenuItem.js
									
									
									
									
									
										Normal 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; | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										55
									
								
								public/scripts/extensions/quick-reply/src/SubMenu.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								public/scripts/extensions/quick-reply/src/SubMenu.js
									
									
									
									
									
										Normal 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; | ||||
| 	} | ||||
| } | ||||
| @@ -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%; | ||||
| } | ||||
		Reference in New Issue
	
	Block a user