add copy/cut/paste to cxt menu and tweak position

This commit is contained in:
joshuaboud 2022-06-22 17:07:50 -03:00
parent 685cdeddc3
commit 1847ecc47b
No known key found for this signature in database
GPG Key ID: 17EFB59E2A8BF50E
3 changed files with 336 additions and 187 deletions

View File

@ -16,98 +16,138 @@ If not, see <https://www.gnu.org/licenses/>.
--> -->
<template> <template>
<div
v-if="show"
class="fixed inset-0 bg-transparent"
tabindex="-1"
@click="show = false"
@contextmenu.prevent="show = false"
@keypress="show = false"
></div>
<transition <transition
enter-active-class="origin-top-left transition ease-out duration-100" enter-active-class="transition ease-out duration-100"
enter-from-class="transform opacity-0 scale-95" enter-from-class="transform opacity-0 scale-95"
enter-to-class="transform opacity-100 scale-100" enter-to-class="transform opacity-100 scale-100"
leave-active-class="origin-top-left transition ease-in duration-75" leave-active-class="transition ease-in duration-75"
leave-from-class="transform opacity-100 scale-100" leave-from-class="transform opacity-100 scale-100"
leave-to-class="transform opacity-0 scale-95" leave-to-class="transform opacity-0 scale-95"
@enter="setPosition"
@after-leave="reset" @after-leave="reset"
> >
<div <div
v-if="show" v-if="show"
class="fixed inset-0 bg-transparent" class="fixed z-20 max-w-sm flex flex-col items-stretch bg-default shadow-lg divide-y divide-default"
tabindex="-1"
@click="show = false" @click="show = false"
@contextmenu.prevent="show = false" @contextmenu.prevent="show = false"
@keypress="show = false"
ref="contextMenuRef"
> >
<div <div class="flex items-stretch">
class="fixed z-20 max-w-sm flex flex-col items-stretch bg-default shadow-lg divide-y divide-default position-contextmenu"> <button
<div class="flex items-stretch"> :disabled="!pathHistory?.backAllowed()"
:class="{ 'grow flex items-center justify-center p-2': true, 'hover:bg-red-600/10': pathHistory?.backAllowed() }"
@click="$emit('browserAction', 'back')"
>
<ArrowLeftIcon class="size-icon icon-default" />
</button>
<button
:disabled="!pathHistory?.forwardAllowed()"
:class="{ 'grow flex items-center justify-center p-2': true, 'hover:bg-red-600/10': pathHistory?.forwardAllowed() }"
@click="$emit('browserAction', 'forward')"
>
<ArrowRightIcon class="size-icon icon-default" />
</button>
<button
class="grow hover:bg-red-600/10 flex items-center justify-center p-2"
@click="$emit('browserAction', 'up')"
>
<ArrowUpIcon class="size-icon icon-default" />
</button>
</div>
<div class="flex flex-col items-stretch">
<!-- Non-selection actions -->
<button
class="context-menu-button"
@click="$emit('browserAction', 'createFile', selection[0])"
>
<DocumentAddIcon class="size-icon icon-default" />
<span>New file</span>
</button>
<button
class="context-menu-button"
@click="$emit('browserAction', 'createDirectory', selection[0])"
>
<FolderAddIcon class="size-icon icon-default" />
<span>New directory</span>
</button>
<button
class="context-menu-button"
@click="$emit('browserAction', 'createLink', selection[0])"
>
<LinkIcon class="size-icon icon-default" />
<span>New link</span>
</button>
</div>
<template v-if="selection.length === 0">
<!-- Current directory actions -->
<div
v-if="clipboard.content.length"
class="flex flex-col items-stretch"
>
<button <button
:disabled="!pathHistory?.backAllowed()" class="context-menu-button"
:class="{ 'grow flex items-center justify-center p-2': true, 'hover:bg-red-600/10': pathHistory?.backAllowed() }" @click="$emit('directoryViewAction', 'paste', currentDirEntry)"
@click="$emit('browserAction', 'back')"
> >
<ArrowLeftIcon class="size-icon icon-default" /> <ClipboardIcon class="size-icon icon-default" />
</button> <span>
<button Paste {{ clipboard.content.length }}
:disabled="!pathHistory?.forwardAllowed()" item{{ clipboard.content.length > 1 ? 's' : '' }}
:class="{ 'grow flex items-center justify-center p-2': true, 'hover:bg-red-600/10': pathHistory?.forwardAllowed() }" here
@click="$emit('browserAction', 'forward')" </span>
>
<ArrowRightIcon class="size-icon icon-default" />
</button>
<button
class="grow hover:bg-red-600/10 flex items-center justify-center p-2"
@click="$emit('browserAction', 'up')"
>
<ArrowUpIcon class="size-icon icon-default" />
</button> </button>
</div> </div>
<div class="flex flex-col items-stretch"> <div class="flex flex-col items-stretch">
<!-- Non-selection actions -->
<button <button
class="context-menu-button" class="context-menu-button"
@click="$emit('browserAction', 'createFile', selection[0])" @click="$emit('browserAction', 'download', currentDirEntry)"
> >
<DocumentAddIcon class="size-icon icon-default" /> <FolderDownloadIcon class="size-icon icon-default" />
<span>New file</span> <span>Zip and download current directory</span>
</button>
<button
class="context-menu-button"
@click="$emit('browserAction', 'createDirectory', selection[0])"
>
<FolderAddIcon class="size-icon icon-default" />
<span>New directory</span>
</button>
<button
class="context-menu-button"
@click="$emit('browserAction', 'createLink', selection[0])"
>
<LinkIcon class="size-icon icon-default" />
<span>New link</span>
</button> </button>
</div> </div>
<template v-if="selection.length === 0"> <div class="flex flex-col items-stretch">
<!-- Current directory actions --> <button
<div class="flex flex-col items-stretch"> v-if="currentDirEntry?.path !== '/'"
<button class="context-menu-button"
class="context-menu-button" @click="$emit('browserAction', 'editPermissions', currentDirEntry)"
@click="$emit('browserAction', 'download', currentDirEntry)"
>
<FolderDownloadIcon class="size-icon icon-default" />
<span>Zip and download directory</span>
</button>
</div>
<div class="flex flex-col items-stretch">
<button
v-if="currentDirEntry?.path !== '/'"
class="context-menu-button"
@click="$emit('browserAction', 'editPermissions', currentDirEntry)"
>
<KeyIcon class="size-icon icon-default" />
<span>Edit permissions</span>
</button>
</div>
</template>
<template v-else-if="selection.length === 1">
<!-- Single entry selection actions -->
<div
v-if="selection[0]?.resolvedType === 'f'"
class="flex flex-col items-stretch"
> >
<!-- regular file actions --> <KeyIcon class="size-icon icon-default" />
<span>Edit permissions of current directory</span>
</button>
</div>
</template>
<template v-else-if="selection.length === 1">
<!-- Single entry selection actions -->
<template v-if="selection[0]?.resolvedType === 'f'">
<!-- regular file actions -->
<div class="flex flex-col items-stretch">
<button
class="context-menu-button"
@click="$emit('directoryViewAction', 'cut', selection[0])"
>
<ScissorsIcon class="size-icon icon-default" />
<span>Cut file</span>
</button>
<button
class="context-menu-button"
@click="$emit('directoryViewAction', 'copy', selection[0])"
>
<ClipboardCopyIcon class="size-icon icon-default" />
<span>Copy file</span>
</button>
</div>
<div class="flex flex-col items-stretch">
<button <button
class="context-menu-button" class="context-menu-button"
@click="$emit('browserAction', 'edit', selection[0])" @click="$emit('browserAction', 'edit', selection[0])"
@ -130,11 +170,37 @@ If not, see <https://www.gnu.org/licenses/>.
<span>Zip and download</span> <span>Zip and download</span>
</button> </button>
</div> </div>
<div </template>
v-else-if="selection[0]?.resolvedType === 'd'" <template v-else-if="selection[0]?.resolvedType === 'd'">
class="flex flex-col items-stretch" <!-- directory actions -->
> <div class="flex flex-col items-stretch">
<!-- directory actions --> <button
class="context-menu-button"
@click="$emit('directoryViewAction', 'cut', selection[0])"
>
<ScissorsIcon class="size-icon icon-default" />
<span>Cut directory</span>
</button>
<button
class="context-menu-button"
@click="$emit('directoryViewAction', 'copy', selection[0])"
>
<ClipboardCopyIcon class="size-icon icon-default" />
<span>Copy directory</span>
</button>
<button
v-if="clipboard.content.length"
class="context-menu-button"
@click="$emit('directoryViewAction', 'paste', selection[0])"
>
<ClipboardIcon class="size-icon icon-default" />
<span>
Paste {{ clipboard.content.length }}
item{{ clipboard.content.length > 1 ? 's' : '' }} into directory
</span>
</button>
</div>
<div class="flex flex-col items-stretch">
<button <button
class="context-menu-button" class="context-menu-button"
@click="$emit('browserAction', 'cd', selection[0])" @click="$emit('browserAction', 'cd', selection[0])"
@ -150,72 +216,88 @@ If not, see <https://www.gnu.org/licenses/>.
<span>Zip and download directory</span> <span>Zip and download directory</span>
</button> </button>
</div> </div>
<div
v-if="selection[0]?.type === 'l'"
class="flex flex-col items-stretch"
>
<!-- link actions -->
<button
class="context-menu-button"
@click="$emit('browserAction', 'editLink', selection[0])"
>
<LinkIcon class="size-icon icon-default" />
<span>Edit link target</span>
</button>
</div>
<div class="flex flex-col items-stretch">
<!-- general actions -->
<button
v-if="selection[0]?.path !== '/'"
class="context-menu-button"
@click="$emit('browserAction', 'editPermissions', selection[0])"
>
<KeyIcon class="size-icon icon-default" />
<span>Edit permissions</span>
</button>
<button
v-if="selection[0]?.path !== '/'"
class="context-menu-button"
@click="$emit('browserAction', 'rename', selection[0])"
>
<PencilIcon class="size-icon icon-default" />
<span>Rename</span>
</button>
<button
class="context-menu-button"
@click="$emit('browserAction', 'delete', selection[0])"
>
<TrashIcon class="size-icon icon-default" />
<span>Delete</span>
</button>
</div>
</template> </template>
<tempalte v-else> <div
<!-- Multi-entry selection actions --> v-if="selection[0]?.type === 'l'"
<div class="flex flex-col items-stretch"> class="flex flex-col items-stretch"
<button >
class="context-menu-button" <!-- link actions -->
@click="$emit('browserAction', 'download', [...selection])" <button
> class="context-menu-button"
<DownloadIcon class="size-icon-sm icon-default" /> @click="$emit('browserAction', 'editLink', selection[0])"
<span>Zip and download {{ selection.length }} items</span> >
</button> <LinkIcon class="size-icon icon-default" />
<button <span>Edit link target</span>
class="context-menu-button" </button>
@click="$emit('browserAction', 'delete', [...selection])" </div>
> <div class="flex flex-col items-stretch">
<TrashIcon class="size-icon-sm icon-default" /> <!-- general actions -->
<span>Delete {{ selection.length }} items</span> <button
</button> v-if="selection[0]?.path !== '/'"
</div> class="context-menu-button"
</tempalte> @click="$emit('browserAction', 'editPermissions', selection[0])"
</div> >
<KeyIcon class="size-icon icon-default" />
<span>Edit permissions</span>
</button>
<button
v-if="selection[0]?.path !== '/'"
class="context-menu-button"
@click="$emit('browserAction', 'rename', selection[0])"
>
<PencilIcon class="size-icon icon-default" />
<span>Rename</span>
</button>
<button
class="context-menu-button"
@click="$emit('browserAction', 'delete', selection[0])"
>
<TrashIcon class="size-icon icon-danger" />
<span>Delete</span>
</button>
</div>
</template>
<template v-else>
<!-- Multi-entry selection actions -->
<div class="flex flex-col items-stretch">
<button
class="context-menu-button"
@click="$emit('directoryViewAction', 'cut', [...selection])"
>
<ScissorsIcon class="size-icon icon-default" />
<span>Cut {{ selection.length }} items</span>
</button>
<button
class="context-menu-button"
@click="$emit('directoryViewAction', 'copy', [...selection])"
>
<ClipboardCopyIcon class="size-icon icon-default" />
<span>Copy {{ selection.length }} items</span>
</button>
</div>
<div class="flex flex-col items-stretch">
<button
class="context-menu-button"
@click="$emit('browserAction', 'download', [...selection])"
>
<DownloadIcon class="size-icon-sm icon-default" />
<span>Zip and download {{ selection.length }} items</span>
</button>
<button
class="context-menu-button"
@click="$emit('browserAction', 'delete', [...selection])"
>
<TrashIcon class="size-icon-sm icon-danger" />
<span>Delete {{ selection.length }} items</span>
</button>
</div>
</template>
</div> </div>
</transition> </transition>
</template> </template>
<script> <script>
import { inject, ref, computed } from 'vue' import { inject, ref, computed, nextTick } from 'vue'
import { import {
ArrowLeftIcon, ArrowLeftIcon,
ArrowRightIcon, ArrowRightIcon,
@ -231,8 +313,11 @@ import {
PencilIcon, PencilIcon,
KeyIcon, KeyIcon,
DownloadIcon, DownloadIcon,
ClipboardIcon,
ClipboardCopyIcon,
ScissorsIcon,
} from '@heroicons/vue/solid'; } from '@heroicons/vue/solid';
import { pathHistoryInjectionKey } from '../keys'; import { pathHistoryInjectionKey, clipboardInjectionKey } from '../keys';
export default { export default {
props: { props: {
@ -240,13 +325,18 @@ export default {
}, },
setup(props, { emit }) { setup(props, { emit }) {
const pathHistory = inject(pathHistoryInjectionKey); const pathHistory = inject(pathHistoryInjectionKey);
const clipboard = inject(clipboardInjectionKey);
const show = ref(); const show = ref();
const event = ref(); const event = ref();
const selection = ref(); const selection = ref();
const currentDirEntry = computed(() => ({ const currentDirEntry = computed(() => ({
...props.currentPath, ...props.currentPath,
name: `Current directory (${props.currentPath.path.split('/').pop()})` name: `Current directory (${props.currentPath.path.split('/').pop()})`,
type: 'd',
resolvedType: 'd',
resolvedPath: props.currentPath.path,
})); }));
const contextMenuRef = ref();
/** /**
* Open the context menu * Open the context menu
@ -260,6 +350,30 @@ export default {
show.value = true; show.value = true;
} }
const setPosition = (el, done) => {
// const { width: menuWidth, height: menuHeight } = el.getBoundingClientRect();
const menuWidth = el.scrollWidth;
const menuHeight = el.scrollHeight;
let x = event.value.clientX;
let y = event.value.clientY;
let origin;
if (y + menuHeight > window.innerHeight) {
y -= menuHeight;
origin = 'bottom';
} else {
origin = 'top';
}
if (x + menuWidth > window.innerWidth) {
x -= menuWidth;
origin += ' right';
} else {
origin += ' left';
}
el.style.left = `${x}px`;
el.style.top = `${y}px`;
el.style.transformOrigin = origin;
}
const reset = () => { const reset = () => {
event.value = null; event.value = null;
selection.value = []; selection.value = [];
@ -268,12 +382,15 @@ export default {
return { return {
// data // data
pathHistory, pathHistory,
clipboard,
show, show,
event, event,
selection, selection,
currentDirEntry, currentDirEntry,
contextMenuRef,
// methods // methods
open, open,
setPosition,
reset, reset,
} }
}, },
@ -292,22 +409,21 @@ export default {
PencilIcon, PencilIcon,
KeyIcon, KeyIcon,
DownloadIcon, DownloadIcon,
ClipboardIcon,
ClipboardCopyIcon,
ScissorsIcon,
}, },
emits: [ emits: [
'hide', 'hide',
'browserAction', 'browserAction',
'directoryViewAction',
] ]
} }
</script> </script>
<style scoped> <style scoped>
div.position-contextmenu {
top: v-bind("`${event?.clientY ?? 0}px`");
left: v-bind("`${event?.clientX ?? 0}px`");
}
button.context-menu-button { button.context-menu-button {
@apply text-default font-normal pl-1 pr-2 text-sm text-left flex items-center gap-1; @apply text-default font-normal pl-1 pr-2 py-1 text-sm text-left flex items-center gap-1;
} }
button.context-menu-button:hover { button.context-menu-button:hover {

View File

@ -26,7 +26,7 @@ If not, see <https://www.gnu.org/licenses/>.
class="h-full" class="h-full"
@selectRectangle="selectRectangle" @selectRectangle="selectRectangle"
@mouseup.exact="deselectAll" @mouseup.exact="deselectAll"
@contextmenu.prevent="$emit('browserAction', 'contextMenu', $event)" @contextmenu.prevent="contextMenuCallback"
> >
<Table <Table
:key="host + path" :key="host + path"
@ -36,6 +36,7 @@ If not, see <https://www.gnu.org/licenses/>.
stickyHeaders stickyHeaders
noShrink noShrink
noShrinkHeight="h-full" noShrinkHeight="h-full"
class="border-x-0"
> >
<template #thead> <template #thead>
<tr> <tr>
@ -249,6 +250,7 @@ import LoadingSpinner from './LoadingSpinner.vue';
import SortCallbackButton from './SortCallbackButton.vue'; import SortCallbackButton from './SortCallbackButton.vue';
import DirectoryEntryList from './DirectoryEntryList.vue'; import DirectoryEntryList from './DirectoryEntryList.vue';
import DragSelectArea from './DragSelectArea.vue'; import DragSelectArea from './DragSelectArea.vue';
import { commonPath } from '../functions/commonPath';
export default { export default {
props: { props: {
@ -331,7 +333,7 @@ export default {
selectedCount.value = gatherEntries().map(entry => entry.selected = true).length ?? 0; selectedCount.value = gatherEntries().map(entry => entry.selected = true).length ?? 0;
} }
const deselectAll = (event) => { const deselectAll = () => {
gatherEntries([], false).map(entry => entry.selected = false); gatherEntries([], false).map(entry => entry.selected = false);
selectedCount.value = 0; selectedCount.value = 0;
} }
@ -353,11 +355,64 @@ export default {
tallySelected(); tallySelected();
} }
const unCutEntries = () => gatherEntries([], false).map(entry => entry.cut = false);
const clipboardStore = (items, cut = false, append = false) => {
if (!append)
unCutEntries();
const newContent = items.map(entry => {
entry.cut = cut;
return {
uniqueId: entry.uniqueId,
host: entry.host,
path: entry.path,
name: entry.name,
cut,
clipboardRelativePath: entry.path.slice(props.path.length).replace(/^\//, '')
};
});
if (append)
clipboard.content = [...newContent, ...clipboard.content].filter((a, index, arr) => arr.findIndex(b => b.uniqueId === a.uniqueId) === index);
else
clipboard.content = newContent;
const message = append
? `Added ${newContent.length} items to clipboard.\n(${clipboard.content.length} items total)`
: `${cut ? 'Cut' : 'Copied'} ${newContent.length} items to clipboard.`;
notifications.value.constructNotification('Clipboard', message, 'info', 2000);
}
const paste = (destinations) => {
let destination;
if (destinations.length === 1) {
destination = destinations[0];
if (destination.resolvedType !== 'd') {
notifications.value.constructNotification("Paste Failed", 'Cannot paste to non-directory.', 'error');
return;
}
} else if (destinations.length === 0) {
destination = { host: props.host, path: props.path };
} else {
notifications.value.constructNotification("Paste Failed", 'Cannot paste to multiple directories.', 'error');
return;
}
console.log("paste", clipboard.content, destination);
const fullSources = [...clipboard.content];
const { common } = commonPath(fullSources.map(item => item.path));
const groupedByHost = fullSources.reduce((res, item) => {
if (!res[item.host])
res[item.host] = [];
res[item.host].push(item);
return res;
}, {});
for (const host of Object.keys(groupedByHost)) {
console.log(`rsync -avh ${groupedByHost[host].map(a => a.path).join(' ')} ${destination.host}:${destination.path}`);
}
}
/** /**
* @param {KeyboardEvent} event * @param {KeyboardEvent} event
*/ */
const keyHandler = (event) => { const keyHandler = (event) => {
const unCutEntries = () => gatherEntries([], false).map(entry => entry.cut = false);
const keypress = event.key.toLowerCase(); const keypress = event.key.toLowerCase();
const handleExact = (keypress) => { const handleExact = (keypress) => {
switch (keypress) { switch (keypress) {
@ -379,7 +434,6 @@ export default {
return; return;
} }
event.preventDefault(); event.preventDefault();
event.stopPropagation();
} }
const handleCtrl = (keypress) => { const handleCtrl = (keypress) => {
switch (keypress) { switch (keypress) {
@ -392,51 +446,15 @@ export default {
break; break;
case 'c': case 'c':
case 'x': case 'x':
const isCut = keypress === 'x'; clipboardStore(getSelected(), keypress === 'x', event.shiftKey);
if (!event.shiftKey)
unCutEntries();
const newContent = getSelected().map(entry => {
entry.cut = isCut;
return {
uniqueId: entry.uniqueId,
host: entry.host,
path: entry.path,
name: entry.name,
cut: isCut,
clipboardRelativePath: entry.path.slice(props.path.length).replace(/^\//, '')
};
});
if (event.shiftKey)
clipboard.content = [...newContent, ...clipboard.content].filter((a, index, arr) => arr.findIndex(b => b.uniqueId === a.uniqueId) === index);
else
clipboard.content = newContent;
const message = event.shiftKey
? `Added ${newContent.length} items to clipboard.\n(${clipboard.content.length} items total)`
: `Copied ${newContent.length} items to clipboard.`;
notifications.value.constructNotification('Clipboard', message, 'info', 2000);
break; break;
case 'v': case 'v':
const selected = getSelected(); paste(getSelected());
let destination;
if (selected.length === 1) {
destination = selected[0];
if (destination.resolvedType !== 'd') {
notifications.value.constructNotification("Paste Failed", 'Cannot paste to non-directory.', 'error');
break;
}
} else if (selected.length === 0) {
destination = { host: props.host, path: props.path };
} else {
notifications.value.constructNotification("Paste Failed", 'Cannot paste to multiple directories.', 'error');
break;
}
console.log("paste", clipboard.content, destination);
break; break;
default: default:
return; return;
} }
event.preventDefault(); event.preventDefault();
event.stopPropagation();
} }
const handleShift = (keypress) => { const handleShift = (keypress) => {
switch(keypress) { switch(keypress) {
@ -444,7 +462,6 @@ export default {
return; return;
} }
event.preventDefault(); event.preventDefault();
event.stopPropagation();
} }
const handleCtrlShift = (keypress) => { const handleCtrlShift = (keypress) => {
switch(keypress) { switch(keypress) {
@ -452,7 +469,6 @@ export default {
return; return;
} }
event.preventDefault(); event.preventDefault();
event.stopPropagation();
} }
const handleAny = (keypress) => { const handleAny = (keypress) => {
switch(keypress) { switch(keypress) {
@ -460,7 +476,6 @@ export default {
return; return;
} }
event.preventDefault(); event.preventDefault();
event.stopPropagation();
} }
if (event.ctrlKey && event.shiftKey) { if (event.ctrlKey && event.shiftKey) {
handleCtrlShift(keypress); handleCtrlShift(keypress);
@ -488,11 +503,27 @@ export default {
} }
} }
const contextMenuCallback = (event) => {
if (!(event.ctrlKey || event.shiftKey)) {
deselectAll();
}
emit('browserAction', 'contextMenu', event);
}
const handleAction = (action, ...args) => { const handleAction = (action, ...args) => {
switch (action) { switch (action) {
case 'toggleSelected': case 'toggleSelected':
toggleSelected(...args); toggleSelected(...args);
break; break;
case 'copy':
clipboardStore(args?.flat(1) ?? getSelected(), false, false);
break;
case 'cut':
clipboardStore(args?.flat(1) ?? getSelected(), true, false);
break;
case 'paste':
paste(args?.flat(1) ?? getSelected());
break;
default: default:
console.error('Unknown directoryViewAction:', action, args); console.error('Unknown directoryViewAction:', action, args);
break; break;
@ -539,6 +570,7 @@ export default {
selectAll, selectAll,
deselectAll, deselectAll,
selectRectangle, selectRectangle,
contextMenuCallback,
handleAction, handleAction,
tallySelected, tallySelected,
} }

View File

@ -192,6 +192,7 @@
<ContextMenu <ContextMenu
:currentPath="pathHistory.current() ?? { path: '/', host: 'localhost' }" :currentPath="pathHistory.current() ?? { path: '/', host: 'localhost' }"
@browserAction="handleAction" @browserAction="handleAction"
@directoryViewAction="(...args) => directoryViewRef?.handleAction(...args)"
ref="contextMenuRef" ref="contextMenuRef"
/> />
<ModalPrompt ref="modalPromptRef" /> <ModalPrompt ref="modalPromptRef" />