mirror of
https://github.com/open-webui/open-webui.git
synced 2026-09-20 00:11:27 +00:00
127 lines
3.3 KiB
TypeScript
127 lines
3.3 KiB
TypeScript
import { mount, unmount } from 'svelte';
|
||
import { createClassComponent } from 'svelte/legacy';
|
||
|
||
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;
|
||
let refEl: HTMLDivElement | null = null; // dummy reference
|
||
|
||
return {
|
||
onStart: (props: any) => {
|
||
container = document.createElement('div');
|
||
container.className = 'suggestion-list-container';
|
||
document.body.appendChild(container);
|
||
|
||
// mount Svelte component
|
||
component = createClassComponent({
|
||
component: Component,
|
||
target: container,
|
||
props: {
|
||
char: props?.text,
|
||
query: props?.query,
|
||
command: (item) => {
|
||
props.command({ id: item.id, label: item.label });
|
||
},
|
||
...ComponentProps
|
||
},
|
||
context: new Map<string, any>([['i18n', ComponentProps?.i18n]])
|
||
});
|
||
|
||
// Create a tiny reference element so outside taps are truly "outside"
|
||
refEl = document.createElement('div');
|
||
Object.assign(refEl.style, {
|
||
position: 'fixed',
|
||
left: '0px',
|
||
top: '0px',
|
||
width: '0px',
|
||
height: '0px'
|
||
});
|
||
document.body.appendChild(refEl);
|
||
|
||
popup = tippy(refEl, {
|
||
getReferenceClientRect: props.clientRect as any,
|
||
appendTo: () => document.body,
|
||
content: container,
|
||
interactive: true,
|
||
trigger: 'manual',
|
||
theme: 'transparent',
|
||
placement: 'top-start',
|
||
offset: [-10, -2],
|
||
arrow: false,
|
||
popperOptions: {
|
||
strategy: 'fixed',
|
||
modifiers: [
|
||
{
|
||
name: 'preventOverflow',
|
||
options: {
|
||
boundary: 'viewport', // keep within the viewport
|
||
altAxis: true, // also prevent overflow on the cross axis (X)
|
||
tether: true,
|
||
padding: 8
|
||
}
|
||
},
|
||
{
|
||
name: 'flip',
|
||
options: {
|
||
boundary: 'viewport',
|
||
fallbackPlacements: ['top-end', 'bottom-start', 'bottom-end']
|
||
}
|
||
},
|
||
// Ensure transforms don’t cause layout widening in some browsers
|
||
{ name: 'computeStyles', options: { adaptive: true } }
|
||
]
|
||
},
|
||
// Helps avoid accidental focus/hover “linking” from far away elements
|
||
interactiveBorder: 8
|
||
});
|
||
popup?.show();
|
||
},
|
||
|
||
onUpdate: (props: any) => {
|
||
if (!component) return;
|
||
|
||
component.$set({
|
||
query: props.query,
|
||
command: (item) => {
|
||
props.command({ id: item.id, label: item.label });
|
||
}
|
||
});
|
||
|
||
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;
|
||
|
||
try {
|
||
component.$destroy();
|
||
} catch (e) {
|
||
console.error('Error unmounting component:', e);
|
||
}
|
||
|
||
component = null;
|
||
|
||
if (container?.parentNode) container.parentNode.removeChild(container);
|
||
container = null;
|
||
|
||
if (refEl?.parentNode) refEl.parentNode.removeChild(refEl);
|
||
refEl = null;
|
||
}
|
||
};
|
||
};
|
||
}
|