File size: 3,329 Bytes
55bd140
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
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;
			}
		};
	};
}