refac/enh: commands ui
This commit is contained in:
@@ -22,7 +22,7 @@
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="flex gap-0.5 p-0.5 rounded-lg shadow-lg bg-white text-gray-800 dark:text-white dark:bg-gray-800 min-w-fit"
|
||||
class="flex gap-0.5 p-0.5 rounded-xl shadow-lg bg-white text-gray-800 dark:text-white dark:bg-gray-850 min-w-fit border border-gray-100 dark:border-gray-800"
|
||||
>
|
||||
<Tooltip placement="top" content={$i18n.t('H1')}>
|
||||
<button
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
<script lang="ts">
|
||||
export let query = '';
|
||||
|
||||
export let command: (payload: { id: string; label: string }) => void;
|
||||
export let selectedIndex = 0;
|
||||
|
||||
let ITEMS = [
|
||||
{ id: '1', label: 'alice' },
|
||||
{ id: '2', label: 'alex' },
|
||||
{ id: '3', label: 'bob' },
|
||||
{ id: '4', label: 'charlie' },
|
||||
{ id: '5', label: 'diana' },
|
||||
{ id: '6', label: 'eve' },
|
||||
{ id: '7', label: 'frank' },
|
||||
{ id: '8', label: 'grace' },
|
||||
{ id: '9', label: 'heidi' },
|
||||
{ id: '10', label: 'ivan' },
|
||||
{ id: '11', label: 'judy' },
|
||||
{ id: '12', label: 'mallory' },
|
||||
{ id: '13', label: 'oscar' },
|
||||
{ id: '14', label: 'peggy' },
|
||||
{ id: '15', label: 'trent' },
|
||||
{ id: '16', label: 'victor' },
|
||||
{ id: '17', label: 'walter' }
|
||||
];
|
||||
|
||||
let items = ITEMS;
|
||||
|
||||
$: items = ITEMS.filter((u) => u.label.toLowerCase().includes(query.toLowerCase())).slice(0, 5);
|
||||
|
||||
const select = (index: number) => {
|
||||
const item = items[index];
|
||||
if (item) command(item);
|
||||
};
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (!['ArrowUp', 'ArrowDown', 'Enter', 'Tab', 'Escape'].includes(event.key)) return false;
|
||||
|
||||
if (event.key === 'ArrowUp') {
|
||||
selectedIndex = (selectedIndex + items.length - 1) % items.length;
|
||||
return true;
|
||||
}
|
||||
if (event.key === 'ArrowDown') {
|
||||
selectedIndex = (selectedIndex + 1) % items.length;
|
||||
return true;
|
||||
}
|
||||
if (event.key === 'Enter' || event.key === 'Tab') {
|
||||
select(selectedIndex);
|
||||
return true;
|
||||
}
|
||||
if (event.key === 'Escape') {
|
||||
// tell tiptap we handled it (it will close)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// This method will be called from the suggestion renderer
|
||||
// @ts-ignore
|
||||
export function _onKeyDown(event: KeyboardEvent) {
|
||||
return onKeyDown(event);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="mention-list text-black dark:text-white rounded-2xl shadow-lg border border-gray-200 dark:border-gray-800 flex flex-col bg-white dark:bg-gray-850 overflow-y-auto scrollbar-thin max-h-60 w-52"
|
||||
id="suggestions-container"
|
||||
>
|
||||
{#if items.length === 0}
|
||||
<div class=" p-4 text-gray-400">No results</div>
|
||||
{:else}
|
||||
{#each items as item, i}
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => select(i)}
|
||||
class=" text-left w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition px-3 py-1 {i ===
|
||||
selectedIndex
|
||||
? 'bg-gray-50 dark:bg-gray-800 font-medium'
|
||||
: ''}"
|
||||
>
|
||||
@{item.label}
|
||||
</button>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Extension } from '@tiptap/core';
|
||||
import Suggestion from '@tiptap/suggestion';
|
||||
|
||||
export default Extension.create({
|
||||
name: 'commands',
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
suggestion: {
|
||||
char: '/',
|
||||
command: ({ editor, range, props }) => {
|
||||
props.command({ editor, range });
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
Suggestion({
|
||||
editor: this.editor,
|
||||
...this.options.suggestion
|
||||
})
|
||||
];
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
import tippy from 'tippy.js';
|
||||
|
||||
export function getSuggestionRenderer(Component: any, ComponentProps = {}) {
|
||||
return function suggestionRenderer() {
|
||||
let component = null;
|
||||
let container: HTMLDivElement | null = null;
|
||||
let popup: TippyInstance | null = null;
|
||||
|
||||
return {
|
||||
onStart: (props: any) => {
|
||||
container = document.createElement('div');
|
||||
container.className = 'suggestion-list-container';
|
||||
document.body.appendChild(container);
|
||||
|
||||
// mount Svelte component
|
||||
component = new Component({
|
||||
target: container,
|
||||
props: {
|
||||
char: props?.text,
|
||||
command: (item) => {
|
||||
props.command({ id: item.id, label: item.label });
|
||||
},
|
||||
...ComponentProps
|
||||
},
|
||||
context: new Map<string, any>([['i18n', ComponentProps?.i18n]])
|
||||
});
|
||||
|
||||
popup = tippy(document.body, {
|
||||
getReferenceClientRect: props.clientRect as any,
|
||||
appendTo: () => document.body,
|
||||
content: container, // ✅ real element, not Svelte internals
|
||||
interactive: true,
|
||||
trigger: 'manual',
|
||||
theme: 'transparent',
|
||||
placement: 'top-start',
|
||||
offset: [-10, -2],
|
||||
arrow: false
|
||||
});
|
||||
popup?.show();
|
||||
},
|
||||
|
||||
onUpdate: (props: any) => {
|
||||
if (!component) return;
|
||||
component.$set({ query: props.query });
|
||||
if (props.clientRect && popup) {
|
||||
popup.setProps({ getReferenceClientRect: props.clientRect as any });
|
||||
}
|
||||
},
|
||||
|
||||
onKeyDown: (props: any) => {
|
||||
// forward to the Svelte component’s handler
|
||||
// (expose this from component as `export function onKeyDown(evt)`)
|
||||
// @ts-ignore
|
||||
return component?._onKeyDown?.(props.event) ?? false;
|
||||
},
|
||||
|
||||
onExit: () => {
|
||||
popup?.destroy();
|
||||
popup = null;
|
||||
|
||||
component?.$destroy();
|
||||
component = null;
|
||||
|
||||
if (container?.parentNode) container.parentNode.removeChild(container);
|
||||
container = null;
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user