more export/import options

- export QR as JSON
- copy QR to clipboard
- cut QR (copy to clipboard and delete)
- transfer QR to other QR Set
- paste QR from clipboard
- import QR from JSON file
- add/paste/import buttons between existing QRs
This commit is contained in:
LenAnderson 2024-07-10 17:34:48 -04:00
parent ffd44b622f
commit ba1761d90a
6 changed files with 483 additions and 52 deletions

View File

@ -64,6 +64,8 @@
<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>

View File

@ -9,7 +9,7 @@ import { SlashCommandExecutor } from '../../../slash-commands/SlashCommandExecut
import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js';
import { SlashCommandParserError } from '../../../slash-commands/SlashCommandParserError.js';
import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';
import { debounce, getSortableDelay, showFontAwesomePicker } from '../../../utils.js';
import { debounce, delay, getSortableDelay, showFontAwesomePicker } from '../../../utils.js';
import { log, warn } from '../index.js';
import { QuickReplyContextLink } from './QuickReplyContextLink.js';
import { QuickReplySet } from './QuickReplySet.js';
@ -49,6 +49,8 @@ export class QuickReply {
/**@type {(qr:QuickReply)=>AsyncGenerator<SlashCommandClosureResult|{closure:SlashCommandClosure, executor:SlashCommandExecutor|SlashCommandClosureResult}, SlashCommandClosureResult, boolean>}*/ onDebug;
/**@type {function}*/ onDelete;
/**@type {function}*/ onUpdate;
/**@type {function}*/ onInsertBefore;
/**@type {function}*/ onTransfer;
/**@type {HTMLElement}*/ dom;
@ -173,38 +175,100 @@ export class QuickReply {
item.classList.add('qr--set-item');
item.setAttribute('data-order', String(idx));
item.setAttribute('data-id', String(this.id));
const drag = document.createElement('div'); {
drag.classList.add('drag-handle');
drag.classList.add('ui-sortable-handle');
drag.textContent = '☰';
item.append(drag);
}
const lblContainer = document.createElement('div'); {
lblContainer.classList.add('qr--set-itemLabelContainer');
const icon = document.createElement('div'); {
this.settingsDomIcon = icon;
icon.title = 'Click to change icon';
icon.classList.add('qr--set-itemIcon');
icon.classList.add('menu_button');
if (this.icon) {
icon.classList.add('fa-solid');
icon.classList.add(this.icon);
const adder = document.createElement('div'); {
adder.classList.add('qr--set-itemAdder');
const actions = document.createElement('div'); {
actions.classList.add('qr--actions');
const addNew = document.createElement('div'); {
addNew.classList.add('qr--action');
addNew.classList.add('qr--add');
addNew.classList.add('menu_button');
addNew.classList.add('menu_button_icon');
addNew.classList.add('fa-solid');
addNew.classList.add('fa-plus');
addNew.title = 'Add quick reply';
addNew.addEventListener('click', ()=>this.onInsertBefore());
actions.append(addNew);
}
icon.addEventListener('click', async()=>{
let value = await showFontAwesomePicker();
this.updateIcon(value);
});
lblContainer.append(icon);
const paste = document.createElement('div'); {
paste.classList.add('qr--action');
paste.classList.add('qr--paste');
paste.classList.add('menu_button');
paste.classList.add('menu_button_icon');
paste.classList.add('fa-solid');
paste.classList.add('fa-paste');
paste.title = 'Add quick reply from clipboard';
paste.addEventListener('click', async()=>{
const text = await navigator.clipboard.readText();
this.onInsertBefore(text);
});
actions.append(paste);
}
const importFile = document.createElement('div'); {
importFile.classList.add('qr--action');
importFile.classList.add('qr--importFile');
importFile.classList.add('menu_button');
importFile.classList.add('menu_button_icon');
importFile.classList.add('fa-solid');
importFile.classList.add('fa-file-import');
importFile.title = 'Add quick reply from JSON file';
importFile.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.onInsertBefore(text);
}
}
});
inp.click();
}
});
actions.append(importFile);
}
adder.append(actions);
}
const lbl = document.createElement('input'); {
this.settingsDomLabel = lbl;
lbl.classList.add('qr--set-itemLabel');
lbl.classList.add('text_pole');
lbl.value = this.label;
lbl.addEventListener('input', ()=>this.updateLabel(lbl.value));
lblContainer.append(lbl);
item.append(adder);
}
const itemContent = document.createElement('div'); {
itemContent.classList.add('qr--content');
const drag = document.createElement('div'); {
drag.classList.add('drag-handle');
drag.classList.add('ui-sortable-handle');
drag.textContent = '☰';
itemContent.append(drag);
}
item.append(lblContainer);
const lblContainer = document.createElement('div'); {
lblContainer.classList.add('qr--set-itemLabelContainer');
const icon = document.createElement('div'); {
this.settingsDomIcon = icon;
icon.title = 'Click to change icon';
icon.classList.add('qr--set-itemIcon');
icon.classList.add('menu_button');
if (this.icon) {
icon.classList.add('fa-solid');
icon.classList.add(this.icon);
}
icon.addEventListener('click', async()=>{
let value = await showFontAwesomePicker();
this.updateIcon(value);
});
lblContainer.append(icon);
}
const lbl = document.createElement('input'); {
this.settingsDomLabel = lbl;
lbl.classList.add('qr--set-itemLabel');
lbl.classList.add('text_pole');
lbl.value = this.label;
lbl.addEventListener('input', ()=>this.updateLabel(lbl.value));
lblContainer.append(lbl);
}
itemContent.append(lblContainer);
}
item.append(itemContent);
}
const optContainer = document.createElement('div'); {
optContainer.classList.add('qr--set-optionsContainer');
@ -217,7 +281,7 @@ export class QuickReply {
opt.addEventListener('click', ()=>this.showEditor());
optContainer.append(opt);
}
item.append(optContainer);
itemContent.append(optContainer);
}
const mes = document.createElement('textarea'); {
this.settingsDomMessage = mes;
@ -226,10 +290,66 @@ export class QuickReply {
mes.value = this.message;
//HACK need to use jQuery to catch the triggered event from the expanded editor
$(mes).on('input', ()=>this.updateMessage(mes.value));
item.append(mes);
itemContent.append(mes);
}
const actions = document.createElement('div'); {
actions.classList.add('qr--actions');
const move = document.createElement('div'); {
move.classList.add('qr--action');
move.classList.add('menu_button');
move.classList.add('menu_button_icon');
move.classList.add('fa-solid');
move.classList.add('fa-truck-arrow-right');
move.title = 'Move quick reply to other set';
move.addEventListener('click', ()=>this.onTransfer(this));
actions.append(move);
}
const copy = document.createElement('div'); {
copy.classList.add('qr--action');
copy.classList.add('menu_button');
copy.classList.add('menu_button_icon');
copy.classList.add('fa-solid');
copy.classList.add('fa-copy');
copy.title = 'Copy quick reply to clipboard';
copy.addEventListener('click', async()=>{
await navigator.clipboard.writeText(JSON.stringify(this));
copy.classList.add('qr--success');
await delay(3010);
copy.classList.remove('qr--success');
});
actions.append(copy);
}
const cut = document.createElement('div'); {
cut.classList.add('qr--action');
cut.classList.add('menu_button');
cut.classList.add('menu_button_icon');
cut.classList.add('fa-solid');
cut.classList.add('fa-cut');
cut.title = 'Cut quick reply to clipboard (copy and remove)';
cut.addEventListener('click', async()=>{
await navigator.clipboard.writeText(JSON.stringify(this));
this.delete();
});
actions.append(cut);
}
const exp = document.createElement('div'); {
exp.classList.add('qr--action');
exp.classList.add('menu_button');
exp.classList.add('menu_button_icon');
exp.classList.add('fa-solid');
exp.classList.add('fa-file-export');
exp.title = 'Export quick reply as file';
exp.addEventListener('click', ()=>{
const blob = new Blob([JSON.stringify(this)], { type:'text' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a'); {
a.href = url;
a.download = `${this.label}.qr.json`;
a.click();
}
});
actions.append(exp);
}
const del = document.createElement('div'); {
del.classList.add('qr--action');
del.classList.add('menu_button');
@ -241,7 +361,7 @@ export class QuickReply {
del.addEventListener('click', ()=>this.delete());
actions.append(del);
}
item.append(actions);
itemContent.append(actions);
}
}
}
@ -1469,6 +1589,7 @@ export class QuickReply {
const scope = new SlashCommandScope();
for (const key of Object.keys(args)) {
if (key[0] == '_') continue;
if (key == 'isAutoExecute') continue;
scope.setMacro(`arg::${key}`, args[key]);
}
scope.setMacro('arg::*', '');

View File

@ -1,8 +1,9 @@
import { getRequestHeaders, substituteParams } from '../../../../script.js';
import { Popup, POPUP_RESULT, POPUP_TYPE } from '../../../popup.js';
import { executeSlashCommands, executeSlashCommandsOnChatInput, executeSlashCommandsWithOptions } from '../../../slash-commands.js';
import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js';
import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';
import { debounceAsync, warn } from '../index.js';
import { debounceAsync, log, warn } from '../index.js';
import { QuickReply } from './QuickReply.js';
export class QuickReplySet {
@ -94,6 +95,11 @@ export class QuickReplySet {
}
return this.settingsDom;
}
/**
*
* @param {QuickReply} qr
* @param {number} idx
*/
renderSettingsItem(qr, idx) {
this.settingsDom.append(qr.renderSettings(idx));
}
@ -198,10 +204,11 @@ export class QuickReplySet {
addQuickReply() {
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({ id });
const qr = QuickReply.from(data);
this.qrList.push(qr);
this.hookQuickReply(qr);
if (this.settingsDom) {
@ -223,6 +230,98 @@ export class QuickReplySet {
qr.onExecute = (_, options)=>this.executeWithOptions(qr, options);
qr.onDelete = ()=>this.removeQuickReply(qr);
qr.onUpdate = ()=>this.save();
qr.onInsertBefore = (qrJson)=>{
const data = JSON.parse(qrJson ?? '{}');
delete data.id;
log('onInsertBefore', data);
const newQr = this.addQuickReply(data);
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) {

View File

@ -117,6 +117,25 @@ export class SettingsUi {
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.addQuickReply(JSON.parse(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());

View File

@ -1,3 +1,20 @@
@keyframes qr--success {
0%,
100% {
color: var(--SmartThemeBodyColor);
}
25%,
75% {
color: #51a351;
}
}
.qr--success {
animation-name: qr--success;
animation-duration: 3s;
animation-timing-function: linear;
animation-delay: 0s;
animation-iteration-count: 1;
}
#qr--bar {
outline: none;
margin: 0;
@ -178,43 +195,75 @@
#qr--settings #qr--set-qrList .qr--set-qrListContents {
padding: 0 0.5em;
}
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item {
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder {
display: flex;
align-items: center;
opacity: 0;
transition: 100ms;
margin: -3px 0 -12px 0;
position: relative;
}
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder .qr--actions {
display: flex;
gap: 0.25em;
flex: 0 0 auto;
}
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder .qr--actions .qr--action {
margin: 0;
}
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder:before,
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder:after {
content: "";
display: block;
flex: 1 1 auto;
border: 1px solid;
margin: 0 1em;
height: 0;
}
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder:hover {
opacity: 1;
}
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content {
display: flex;
flex-direction: row;
gap: 0.5em;
align-items: baseline;
padding: 0.25em 0;
}
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(1) {
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > :nth-child(2) {
flex: 0 0 auto;
}
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(2) {
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > :nth-child(2) {
flex: 1 1 25%;
}
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(3) {
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > :nth-child(3) {
flex: 0 0 auto;
}
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(4) {
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > :nth-child(4) {
flex: 1 1 75%;
}
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(5) {
flex: 0 0 auto;
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > :nth-child(5) {
flex: 0 1 auto;
display: flex;
gap: 0.25em;
justify-content: flex-end;
flex-wrap: wrap;
}
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > .drag-handle {
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > .drag-handle {
padding: 0.75em;
}
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemLabelContainer {
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content .qr--set-itemLabelContainer {
display: flex;
align-items: center;
}
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemLabelContainer .qr--set-itemIcon:not(.fa-solid) {
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content .qr--set-itemLabelContainer .qr--set-itemIcon:not(.fa-solid) {
display: none;
}
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemLabel,
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--action {
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content .qr--set-itemLabel,
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content .qr--action {
margin: 0;
}
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemMessage {
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content .qr--set-itemMessage {
font-size: smaller;
}
#qr--settings .qr--set-qrListActions {
@ -737,3 +786,45 @@
.popup.qr--hide::backdrop {
opacity: 0 !important;
}
.popup:has(.qr--transferModal) .popup-button-ok {
display: flex;
align-items: center;
flex-direction: column;
white-space: pre;
font-weight: normal;
box-shadow: 0 0 0;
transition: 200ms;
}
.popup:has(.qr--transferModal) .popup-button-ok:after {
content: 'Transfer';
height: 0;
overflow: hidden;
font-weight: bold;
}
.popup:has(.qr--transferModal) .qr--copy {
display: flex;
align-items: center;
flex-direction: column;
white-space: pre;
font-weight: normal;
box-shadow: 0 0 0;
transition: 200ms;
}
.popup:has(.qr--transferModal) .qr--copy:after {
content: 'Copy';
height: 0;
overflow: hidden;
font-weight: bold;
}
.popup:has(.qr--transferModal):has(.qr--transferSelect:focus) .popup-button-ok {
font-weight: bold;
box-shadow: 0 0 10px;
}
.popup:has(.qr--transferModal):has(.qr--transferSelect:focus).qr--isCopy .popup-button-ok {
font-weight: normal;
box-shadow: 0 0 0;
}
.popup:has(.qr--transferModal):has(.qr--transferSelect:focus).qr--isCopy .qr--copy {
font-weight: bold;
box-shadow: 0 0 10px;
}

View File

@ -1,3 +1,18 @@
@keyframes qr--success {
0%, 100% {
color: var(--SmartThemeBodyColor);
}
25%, 75% {
color: rgb(81, 163, 81);
}
}
&.qr--success {
animation-name: qr--success;
animation-duration: 3s;
animation-timing-function: linear;
animation-delay: 0s;
animation-iteration-count: 1;
}
#qr--bar {
outline: none;
margin: 0;
@ -218,14 +233,41 @@
.qr--set-qrListContents> {
padding: 0 0.5em;
>.qr--set-item {
>.qr--set-item .qr--set-itemAdder {
display: flex;
align-items: center;
opacity: 0;
transition: 100ms;
margin: -3px 0 -12px 0;
position: relative;
.qr--actions {
display: flex;
gap: 0.25em;
flex: 0 0 auto;
.qr--action {
margin: 0;
}
}
&:before, &:after {
content: "";
display: block;
flex: 1 1 auto;
border: 1px solid;
margin: 0 1em;
height: 0;
}
&:hover {
opacity: 1;
}
}
>.qr--set-item .qr--content {
display: flex;
flex-direction: row;
gap: 0.5em;
align-items: baseline;
padding: 0.25em 0;
> :nth-child(1) {
> :nth-child(2) {
flex: 0 0 auto;
}
@ -242,7 +284,11 @@
}
> :nth-child(5) {
flex: 0 0 auto;
flex: 0 1 auto;
display: flex;
gap: 0.25em;
justify-content: flex-end;
flex-wrap: wrap;
}
>.drag-handle {
@ -266,6 +312,8 @@
font-size: smaller;
}
}
}
}
@ -827,3 +875,54 @@
.popup.qr--hide::backdrop {
opacity: 0 !important;
}
.popup:has(.qr--transferModal) {
.popup-button-ok {
&:after {
content: 'Transfer';
height: 0;
overflow: hidden;
font-weight: bold;
}
display: flex;
align-items: center;
flex-direction: column;
white-space: pre;
font-weight: normal;
box-shadow: 0 0 0;
transition: 200ms;
}
.qr--copy {
&:after {
content: 'Copy';
height: 0;
overflow: hidden;
font-weight: bold;
}
display: flex;
align-items: center;
flex-direction: column;
white-space: pre;
font-weight: normal;
box-shadow: 0 0 0;
transition: 200ms;
}
&:has(.qr--transferSelect:focus) {
.popup-button-ok {
font-weight: bold;
box-shadow: 0 0 10px;
}
&.qr--isCopy {
.popup-button-ok {
font-weight: normal;
box-shadow: 0 0 0;
}
.qr--copy {
font-weight: bold;
box-shadow: 0 0 10px;
}
}
}
}