@kerebron/extension-ui 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +40 -0
  3. package/assets/ui.css +32 -0
  4. package/esm/ExtensionUi.d.ts +6 -0
  5. package/esm/ExtensionUi.d.ts.map +1 -0
  6. package/esm/ExtensionUi.js +9 -0
  7. package/esm/ExtensionUi.js.map +1 -0
  8. package/esm/autocomplete/AutocompletePlugin.d.ts +53 -0
  9. package/esm/autocomplete/AutocompletePlugin.d.ts.map +1 -0
  10. package/esm/autocomplete/AutocompletePlugin.js +417 -0
  11. package/esm/autocomplete/AutocompletePlugin.js.map +1 -0
  12. package/esm/autocomplete/DefaultRenderer.d.ts +25 -0
  13. package/esm/autocomplete/DefaultRenderer.d.ts.map +1 -0
  14. package/esm/autocomplete/DefaultRenderer.js +162 -0
  15. package/esm/autocomplete/DefaultRenderer.js.map +1 -0
  16. package/esm/autocomplete/ExtensionAutocomplete.d.ts +11 -0
  17. package/esm/autocomplete/ExtensionAutocomplete.d.ts.map +1 -0
  18. package/esm/autocomplete/ExtensionAutocomplete.js +33 -0
  19. package/esm/autocomplete/ExtensionAutocomplete.js.map +1 -0
  20. package/esm/autocomplete/createDefaultMatcher.d.ts +11 -0
  21. package/esm/autocomplete/createDefaultMatcher.d.ts.map +1 -0
  22. package/esm/autocomplete/createDefaultMatcher.js +58 -0
  23. package/esm/autocomplete/createDefaultMatcher.js.map +1 -0
  24. package/esm/autocomplete/createPosMatcher.d.ts +3 -0
  25. package/esm/autocomplete/createPosMatcher.d.ts.map +1 -0
  26. package/esm/autocomplete/createPosMatcher.js +16 -0
  27. package/esm/autocomplete/createPosMatcher.js.map +1 -0
  28. package/esm/autocomplete/createRegexMatcher.d.ts +4 -0
  29. package/esm/autocomplete/createRegexMatcher.d.ts.map +1 -0
  30. package/esm/autocomplete/createRegexMatcher.js +50 -0
  31. package/esm/autocomplete/createRegexMatcher.js.map +1 -0
  32. package/esm/autocomplete/mod.d.ts +6 -0
  33. package/esm/autocomplete/mod.d.ts.map +1 -0
  34. package/esm/autocomplete/mod.js +6 -0
  35. package/esm/autocomplete/mod.js.map +1 -0
  36. package/esm/autocomplete/types.d.ts +49 -0
  37. package/esm/autocomplete/types.d.ts.map +1 -0
  38. package/esm/autocomplete/types.js +2 -0
  39. package/esm/autocomplete/types.js.map +1 -0
  40. package/esm/hover/ExtensionHover.d.ts +11 -0
  41. package/esm/hover/ExtensionHover.d.ts.map +1 -0
  42. package/esm/hover/ExtensionHover.js +33 -0
  43. package/esm/hover/ExtensionHover.js.map +1 -0
  44. package/esm/hover/HoverPlugin.d.ts +49 -0
  45. package/esm/hover/HoverPlugin.d.ts.map +1 -0
  46. package/esm/hover/HoverPlugin.js +368 -0
  47. package/esm/hover/HoverPlugin.js.map +1 -0
  48. package/esm/hover/MarkdownRenderer.d.ts +23 -0
  49. package/esm/hover/MarkdownRenderer.d.ts.map +1 -0
  50. package/esm/hover/MarkdownRenderer.js +54 -0
  51. package/esm/hover/MarkdownRenderer.js.map +1 -0
  52. package/esm/hover/mod.d.ts +3 -0
  53. package/esm/hover/mod.d.ts.map +1 -0
  54. package/esm/hover/mod.js +3 -0
  55. package/esm/hover/mod.js.map +1 -0
  56. package/esm/hover/types.d.ts +23 -0
  57. package/esm/hover/types.d.ts.map +1 -0
  58. package/esm/hover/types.js +2 -0
  59. package/esm/hover/types.js.map +1 -0
  60. package/esm/mod.d.ts +2 -0
  61. package/esm/mod.d.ts.map +1 -0
  62. package/esm/mod.js +2 -0
  63. package/esm/mod.js.map +1 -0
  64. package/esm/overlayer/mod.d.ts +13 -0
  65. package/esm/overlayer/mod.d.ts.map +1 -0
  66. package/esm/overlayer/mod.js +111 -0
  67. package/esm/overlayer/mod.js.map +1 -0
  68. package/esm/package.json +3 -0
  69. package/package.json +43 -0
  70. package/src/ExtensionUi.ts +10 -0
  71. package/src/autocomplete/AutocompletePlugin.ts +580 -0
  72. package/src/autocomplete/DefaultRenderer.ts +189 -0
  73. package/src/autocomplete/ExtensionAutocomplete.ts +49 -0
  74. package/src/autocomplete/createDefaultMatcher.ts +94 -0
  75. package/src/autocomplete/createPosMatcher.ts +21 -0
  76. package/src/autocomplete/createRegexMatcher.ts +70 -0
  77. package/src/autocomplete/mod.ts +5 -0
  78. package/src/autocomplete/types.ts +90 -0
  79. package/src/hover/ExtensionHover.ts +46 -0
  80. package/src/hover/HoverPlugin.ts +467 -0
  81. package/src/hover/MarkdownRenderer.ts +68 -0
  82. package/src/hover/mod.ts +2 -0
  83. package/src/hover/types.ts +26 -0
  84. package/src/mod.ts +1 -0
  85. package/src/overlayer/mod.ts +146 -0
@@ -0,0 +1,467 @@
1
+ import { Node as PmNode } from 'prosemirror-model';
2
+ import { Plugin, PluginKey, Selection, Transaction } from 'prosemirror-state';
3
+ import { Decoration, DecorationSet, EditorView } from 'prosemirror-view';
4
+
5
+ import { type CoreEditor, TextRange } from '@kerebron/editor';
6
+ import { debounce } from '@kerebron/editor/utilities';
7
+
8
+ import { HoverConfig, HoverMatch, HoverSource, HoverTrigger } from './types.js';
9
+ import { MarkdownRenderer } from './MarkdownRenderer.js';
10
+
11
+ export const HoverPluginKey = new PluginKey<HoverState>(
12
+ 'hover',
13
+ );
14
+
15
+ interface HoverMeta {
16
+ addHoverSource?: {
17
+ hoverSource: HoverSource;
18
+ };
19
+ trigger?: HoverTrigger;
20
+ setRequest?: HoverMatch;
21
+ setResponse?: {
22
+ id: number;
23
+ text: string;
24
+ };
25
+ clearRequest?: boolean;
26
+ }
27
+
28
+ class HoverState {
29
+ idGenerator = 1;
30
+
31
+ hoverSources: HoverSource[] = [];
32
+
33
+ request?: {
34
+ decorationId: string;
35
+ id: number;
36
+ range: TextRange;
37
+ // pos: number;
38
+ // inside: number;
39
+ // node: PmNode;
40
+ uri?: string;
41
+ source: HoverSource;
42
+ renderer: MarkdownRenderer;
43
+ };
44
+
45
+ response?: {
46
+ id: number;
47
+ text: string;
48
+ };
49
+
50
+ constructor(private editor: CoreEditor) {
51
+ this.onMouseMove = debounce(this.onMouseMove.bind(this), 200);
52
+ this.onMouseLeave = debounce(this.onMouseLeave.bind(this), 200);
53
+ this.matchSource = debounce(this.matchSource.bind(this), 200);
54
+ }
55
+
56
+ clearActive() {
57
+ if (this.request) {
58
+ this.request.renderer.destroy();
59
+ this.request = undefined;
60
+ }
61
+ this.response = undefined;
62
+ }
63
+
64
+ private matchSource(trigger: HoverTrigger): void {
65
+ const sources: HoverSource[] = this.hoverSources;
66
+ let matched: HoverMatch | undefined = undefined;
67
+ const parentNode = trigger.node;
68
+
69
+ // if (parentNode && !parentNode.isTextblock && !parentNode.isText) { //
70
+ // console.trace('!parentNode', trigger);
71
+ // return;
72
+ // }
73
+
74
+ for (const source of sources) {
75
+ if (!source.match) {
76
+ continue;
77
+ }
78
+
79
+ const match = source.match(trigger);
80
+ console.log('matchSource', match);
81
+
82
+ if (match) {
83
+ matched = match;
84
+
85
+ this.handleSource(match);
86
+ break;
87
+ }
88
+ }
89
+
90
+ if (!matched) {
91
+ this.dispatchMeta({ clearRequest: true });
92
+ }
93
+
94
+ console.groupEnd();
95
+
96
+ return undefined;
97
+ }
98
+
99
+ handleSource(match?: HoverMatch) {
100
+ if (match) {
101
+ this.dispatchMeta({
102
+ setRequest: {
103
+ source: match.source,
104
+ range: match.range,
105
+ text: match.text,
106
+ uri: match.uri,
107
+ },
108
+ });
109
+ } else {
110
+ this.clearRequest();
111
+ }
112
+ }
113
+
114
+ onMouseLeave(view: EditorView, event: Event | undefined) {
115
+ this.dispatchMeta({
116
+ clearRequest: true,
117
+ });
118
+ }
119
+
120
+ onMouseMove(view: EditorView, event: MouseEvent) {
121
+ const coords = {
122
+ left: event.clientX,
123
+ top: event.clientY,
124
+ };
125
+
126
+ const result = view.posAtCoords(coords);
127
+
128
+ if (!result) return;
129
+ if (result.inside === -1) return;
130
+
131
+ const pos = result.pos;
132
+ const node = view.state.doc.nodeAt(pos) || undefined;
133
+
134
+ if (!node) {
135
+ console.error('NO NODE at', pos);
136
+ }
137
+
138
+ // Handle nodeview for code_block
139
+ const dom = view.nodeDOM(pos);
140
+ if (dom instanceof HTMLElement) {
141
+ const editorElement = dom.querySelector('[data-uri]');
142
+ const uri =
143
+ (editorElement ? editorElement.getAttribute('data-uri') : undefined) ||
144
+ undefined;
145
+
146
+ if (uri && editorElement) {
147
+ const range = getNodeRange(event);
148
+ if (range) {
149
+ const offset = getOffsetWithinElement(editorElement, range);
150
+
151
+ result.inside = offset;
152
+ if (result.inside === -1) return;
153
+
154
+ this.dispatchMeta({
155
+ trigger: {
156
+ pos: result.pos,
157
+ inside: result.inside,
158
+ node,
159
+ uri,
160
+ },
161
+ });
162
+ }
163
+ return;
164
+ }
165
+ }
166
+
167
+ if (!node) {
168
+ return;
169
+ }
170
+
171
+ // avoid redundant updates
172
+ // if (this.request?.range.from === result.range.from && this.request?.range.to === result.inside) return;
173
+
174
+ this.dispatchMeta({
175
+ trigger: {
176
+ pos: result.pos,
177
+ inside: result.inside,
178
+ node,
179
+ uri: undefined,
180
+ },
181
+ });
182
+ }
183
+
184
+ handleCommands(pluginMeta: HoverMeta | undefined, transaction: Transaction) {
185
+ if (!pluginMeta) {
186
+ return false;
187
+ }
188
+
189
+ if (pluginMeta.addHoverSource) {
190
+ this.hoverSources.push(pluginMeta.addHoverSource.hoverSource);
191
+ return true;
192
+ }
193
+
194
+ if (pluginMeta.trigger) {
195
+ console.group('hover.trigger', pluginMeta.trigger);
196
+ this.matchSource(pluginMeta?.trigger);
197
+ console.groupEnd();
198
+ return true;
199
+ }
200
+
201
+ if (pluginMeta?.setRequest) {
202
+ console.group('setRequest');
203
+ const id = this.idGenerator++;
204
+ const decorationId = `id_${Math.floor(Math.random() * 0xffffffff)}`;
205
+
206
+ let renderer = this.request?.renderer;
207
+ if (this.request?.source !== pluginMeta.setRequest.source) {
208
+ if (renderer) {
209
+ renderer.destroy();
210
+ renderer = undefined;
211
+ }
212
+ }
213
+ if (!renderer) {
214
+ renderer = new MarkdownRenderer(this.editor);
215
+ renderer.addEventListener('close', () => {
216
+ this.dispatchMeta({
217
+ clearRequest: true,
218
+ });
219
+ });
220
+ }
221
+
222
+ renderer.setAnchorSelector(`[data-decoration-id="${decorationId}"]`, {
223
+ above: true,
224
+ });
225
+
226
+ this.request = {
227
+ id,
228
+ decorationId,
229
+ range: pluginMeta.setRequest.range,
230
+ uri: pluginMeta.setRequest.uri,
231
+ source: pluginMeta.setRequest.source,
232
+ renderer,
233
+ };
234
+
235
+ const request = this.request;
236
+
237
+ const go = async () => {
238
+ const item = await request.source.getItem(request.range);
239
+
240
+ this.dispatchMeta({
241
+ setResponse: {
242
+ id,
243
+ text: item.text,
244
+ },
245
+ });
246
+ };
247
+ go();
248
+
249
+ console.groupEnd();
250
+
251
+ return true;
252
+ }
253
+
254
+ if (pluginMeta?.setResponse && this.request) {
255
+ this.response = {
256
+ id: pluginMeta?.setResponse.id,
257
+ text: pluginMeta?.setResponse.text,
258
+ };
259
+ console.group('setResponse', this.response, this.request?.renderer);
260
+
261
+ this.request?.renderer.setResponse({ text: this.response.text });
262
+
263
+ console.groupEnd();
264
+ return true;
265
+ }
266
+
267
+ if (pluginMeta.clearRequest) {
268
+ console.trace('clearRequest');
269
+ this.clearRequest();
270
+ return true;
271
+ }
272
+
273
+ return false;
274
+ }
275
+
276
+ clearRequest() {
277
+ if (this.request) {
278
+ this.request.renderer.destroy();
279
+ this.request = undefined;
280
+ }
281
+ this.response = undefined;
282
+ }
283
+
284
+ dispatchMeta(meta: HoverMeta) {
285
+ const tr = this.editor.state.tr;
286
+ tr.setMeta(HoverPluginKey, meta);
287
+ this.editor.dispatchTransaction(tr);
288
+ }
289
+ }
290
+
291
+ function getNodeRange(
292
+ { clientX, clientY }: { clientX: number; clientY: number },
293
+ ) {
294
+ let range;
295
+
296
+ if (document.caretPositionFromPoint) {
297
+ const pos = document.caretPositionFromPoint(clientX, clientY);
298
+ if (!pos) return;
299
+ range = document.createRange();
300
+ range.setStart(pos.offsetNode, pos.offset);
301
+ } else if (document.caretRangeFromPoint) {
302
+ range = document.caretRangeFromPoint(clientX, clientY);
303
+ }
304
+
305
+ return range;
306
+ }
307
+
308
+ function getOffsetWithinElement(element: Node, range: Range): number {
309
+ const container = range.startContainer;
310
+ const offsetInNode = range.startOffset;
311
+
312
+ if (container === element) {
313
+ const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
314
+ const firstText = walker.nextNode();
315
+ if (!firstText) return offsetInNode;
316
+ let total = 0;
317
+ let node: Text | null = firstText as Text;
318
+ while (node) {
319
+ if (node === container) return total + offsetInNode;
320
+ total += node.nodeValue?.length ?? 0;
321
+ node = walker.nextNode() as Text | null;
322
+ }
323
+ return total;
324
+ }
325
+
326
+ const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
327
+ let total = 0;
328
+ let textNode = walker.nextNode() as Text | null;
329
+ while (textNode) {
330
+ if (textNode === container) {
331
+ return total + offsetInNode;
332
+ }
333
+ total += textNode.nodeValue?.length ?? 0;
334
+ textNode = walker.nextNode() as Text | null;
335
+ }
336
+ return -1;
337
+ }
338
+
339
+ // function matchSource(selection: Selection, sources: HoverSource[]) {
340
+ // let matched: MatchedSource | undefined = undefined;
341
+ // const parentNode = selection.$anchor.parent;
342
+
343
+ // for (const source of sources) {
344
+ // if (!source.match) {
345
+ // continue;
346
+ // }
347
+
348
+ // const trigger: HoverTrigger = {
349
+ // pos: 0,
350
+ // inside: 0,
351
+ // node: parentNode,
352
+ // uri: parentNode.attrs.uri
353
+ // };
354
+
355
+ // const match = source.match(trigger);
356
+ // if (match) {
357
+ // matched = match;
358
+ // return matched;
359
+ // }
360
+ // }
361
+ // return undefined;
362
+ // }
363
+
364
+ export class HoverPlugin<Item, TSelected> extends Plugin<HoverState> {
365
+ constructor(config: HoverConfig, editor: CoreEditor) {
366
+ super({
367
+ key: HoverPluginKey,
368
+ state: {
369
+ init() {
370
+ return new HoverState(editor);
371
+ },
372
+
373
+ apply(transaction, nextHoverState: HoverState, prevState, state) {
374
+ if (transaction.isGeneric) {
375
+ return nextHoverState;
376
+ }
377
+
378
+ const pluginMeta: HoverMeta | undefined = transaction.getMeta(
379
+ HoverPluginKey,
380
+ );
381
+
382
+ if (nextHoverState.handleCommands(pluginMeta, transaction)) {
383
+ return nextHoverState;
384
+ }
385
+
386
+ if (transaction.docChanged) {
387
+ nextHoverState.dispatchMeta({ clearRequest: true });
388
+ }
389
+
390
+ return nextHoverState;
391
+ },
392
+ },
393
+
394
+ view() {
395
+ return {
396
+ update: async (view, prevState) => {
397
+ return;
398
+ },
399
+
400
+ destroy: () => {
401
+ const pluginState = HoverPluginKey.getState(editor.state);
402
+ if (!pluginState) {
403
+ return;
404
+ }
405
+
406
+ if (pluginState.request) {
407
+ pluginState.request.renderer.destroy();
408
+ }
409
+ },
410
+ };
411
+ },
412
+
413
+ props: {
414
+ handleDOMEvents: {
415
+ mousemove: (view, event) => {
416
+ const pluginState = HoverPluginKey.getState(editor.state);
417
+ if (!pluginState) {
418
+ return;
419
+ }
420
+ pluginState.onMouseMove(view, event);
421
+ return;
422
+ },
423
+ mouseleave: (view, event) => {
424
+ const pluginState = HoverPluginKey.getState(editor.state);
425
+ if (!pluginState) {
426
+ return;
427
+ }
428
+ pluginState.onMouseLeave(view, event);
429
+ return;
430
+ },
431
+ },
432
+
433
+ decorations(state) {
434
+ const { request } = this.getState(state) || {};
435
+
436
+ if (!request) {
437
+ return null;
438
+ }
439
+
440
+ const attrs = {
441
+ class: config.decorationClass || 'kb-hover--decor',
442
+ 'data-decoration-id': request.decorationId,
443
+ };
444
+
445
+ if (request.range.from === request.range.to) {
446
+ return DecorationSet.create(state.doc, [
447
+ Decoration.widget(request.range.from, () => {
448
+ const widgetNode = document.createElement('span');
449
+ widgetNode.className = attrs.class;
450
+ for (const [k, v] of Object.entries(attrs)) {
451
+ widgetNode.setAttribute(k, v);
452
+ }
453
+ return widgetNode;
454
+ }, attrs),
455
+ ]);
456
+ } else {
457
+ return DecorationSet.create(state.doc, [
458
+ Decoration.inline(request.range.from, request.range.to, {
459
+ ...attrs,
460
+ }),
461
+ ]);
462
+ }
463
+ },
464
+ },
465
+ });
466
+ }
467
+ }
@@ -0,0 +1,68 @@
1
+ import { CoreEditor } from '@kerebron/editor';
2
+ import { anchorElement, type OverLayer } from '../overlayer/mod.js';
3
+
4
+ const CSS_PREFIX = 'kb-hover';
5
+
6
+ export interface AnchorOpts {
7
+ above?: boolean;
8
+ }
9
+
10
+ export class MarkdownRenderer extends EventTarget {
11
+ overlayer: OverLayer;
12
+ wrapper: HTMLElement | undefined;
13
+ anchor?: { selector: string; opts: AnchorOpts };
14
+ text: string | undefined;
15
+
16
+ constructor(private editor: CoreEditor) {
17
+ super();
18
+
19
+ this.overlayer = this.editor.ci.resolve('overlayer');
20
+ }
21
+
22
+ setResponse({ text }: { text: string | undefined }) {
23
+ this.text = text;
24
+ this.refresh();
25
+ }
26
+
27
+ refresh() {
28
+ if (!this.text) {
29
+ if (this.wrapper) {
30
+ this.wrapper.style.display = 'none';
31
+ }
32
+ return;
33
+ }
34
+
35
+ console.log('MarkdownRenderer this.wrapper', this.wrapper);
36
+
37
+ if (!this.wrapper) {
38
+ this.wrapper = this.overlayer.createElement('div');
39
+ this.wrapper.classList.add(CSS_PREFIX + '__wrapper');
40
+ }
41
+
42
+ this.wrapper.innerText = this.text;
43
+ this.wrapper.style.display = '';
44
+
45
+ if (this.anchor) {
46
+ console.log('anchorElement', this.anchor.selector);
47
+ anchorElement(this.wrapper, this.anchor.selector, {
48
+ container: this.editor.config.element,
49
+ above: this.anchor.opts.above,
50
+ });
51
+ }
52
+ }
53
+
54
+ destroy() {
55
+ if (this.wrapper) {
56
+ this.wrapper.parentNode?.removeChild(this.wrapper);
57
+ this.wrapper.dispatchEvent(new CustomEvent('removed'));
58
+ this.wrapper = undefined;
59
+ this.dispatchEvent(new Event('close'));
60
+ }
61
+ }
62
+
63
+ setAnchorSelector(selector: string, opts: AnchorOpts): void {
64
+ console.log('setAnchorSelector', selector);
65
+ this.anchor = { selector, opts };
66
+ this.refresh();
67
+ }
68
+ }
@@ -0,0 +1,2 @@
1
+ export * from './ExtensionHover.js';
2
+ export * from './types.js';
@@ -0,0 +1,26 @@
1
+ import { Node as PmNode } from 'prosemirror-model';
2
+ import { TextRange } from '@kerebron/editor';
3
+
4
+ export interface HoverConfig {
5
+ decorationTag?: string;
6
+ decorationClass?: string;
7
+ }
8
+
9
+ export interface HoverTrigger {
10
+ pos: number;
11
+ inside: number;
12
+ node?: PmNode;
13
+ uri?: string;
14
+ }
15
+
16
+ export interface HoverMatch {
17
+ source: HoverSource;
18
+ range: TextRange;
19
+ text?: string;
20
+ uri?: string;
21
+ }
22
+
23
+ export interface HoverSource<I = any> {
24
+ match: (trigger: HoverTrigger) => HoverMatch | undefined;
25
+ getItem: (range: TextRange) => I | Promise<I>;
26
+ }
package/src/mod.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './ExtensionUi.js';
@@ -0,0 +1,146 @@
1
+ export class OverLayer {
2
+ overLayerContainer: HTMLElement;
3
+
4
+ stack: HTMLElement[] = [];
5
+
6
+ constructor(container?: HTMLElement) {
7
+ if (!container) {
8
+ container = document.createElement('div');
9
+ document.body.appendChild(container);
10
+ container.classList.add('kb--top-layer');
11
+ }
12
+
13
+ this.overLayerContainer = container;
14
+ }
15
+
16
+ private removeStacked(start: number) {
17
+ for (let i = this.stack.length - 1; i >= start; i--) {
18
+ if (this.overLayerContainer.contains(this.stack[i])) {
19
+ this.overLayerContainer.removeChild(this.stack[i]);
20
+ }
21
+ }
22
+ this.stack.splice(start, this.stack.length - start);
23
+ }
24
+
25
+ createElement<T extends HTMLElement>(
26
+ type: string,
27
+ ancestor?: HTMLElement,
28
+ ): T {
29
+ const element = document.createElement(type);
30
+ this.overLayerContainer.appendChild(element);
31
+
32
+ const observer = new MutationObserver((mutations) => {
33
+ if (!element.isConnected) {
34
+ observer.disconnect();
35
+ const idx = this.stack.findIndex((item) => item === element);
36
+ if (idx > -1) {
37
+ this.removeStacked(idx);
38
+ }
39
+ element.dispatchEvent(new CustomEvent('removed'));
40
+ }
41
+ });
42
+
43
+ if (ancestor) {
44
+ const idx = this.stack.findIndex((item) => item === ancestor);
45
+ if (idx > -1) {
46
+ this.removeStacked(idx + 1);
47
+ }
48
+ } else {
49
+ this.removeStacked(0);
50
+ }
51
+
52
+ this.stack.push(element);
53
+
54
+ observer.observe(this.overLayerContainer, {
55
+ childList: true,
56
+ });
57
+
58
+ return element as T;
59
+ }
60
+ }
61
+
62
+ export interface AnchorParams {
63
+ container?: HTMLElement;
64
+ above?: boolean;
65
+ }
66
+
67
+ export function anchorElement(
68
+ popover: HTMLElement,
69
+ anchorSelector: string,
70
+ params?: AnchorParams,
71
+ ) {
72
+ let rafId: number | null = null;
73
+
74
+ popover.style.position = 'fixed';
75
+ popover.setAttribute('data-anchor-selector', anchorSelector);
76
+
77
+ function update() {
78
+ rafId = null;
79
+
80
+ const container: HTMLElement = params?.container || document.body;
81
+ const anchor = container.querySelector(anchorSelector);
82
+
83
+ if (anchor) {
84
+ const rect = anchor.getBoundingClientRect();
85
+
86
+ if (params?.above) {
87
+ popover.style.left = `${rect.left}px`;
88
+ popover.style.top = `${rect.top - popover.clientHeight}px`;
89
+ } else {
90
+ popover.style.left = `${rect.left}px`;
91
+ popover.style.top = `${rect.bottom}px`;
92
+ }
93
+
94
+ popover.style.display = '';
95
+ } else {
96
+ const ids = [];
97
+ const nodes = container.querySelectorAll('[data-decoration-id]');
98
+ nodes.forEach((n) => ids.push(n.getAttribute('data-decoration-id')));
99
+
100
+ // console.debug('!anchorSelector', anchorSelector, ids);
101
+ popover.style.display = 'none';
102
+ scheduleUpdate();
103
+ }
104
+ }
105
+
106
+ function scheduleUpdate() {
107
+ if (!rafId) {
108
+ rafId = requestAnimationFrame(update);
109
+ }
110
+ }
111
+
112
+ update();
113
+
114
+ // const resizeObserver = new ResizeObserver(scheduleUpdate);
115
+ // resizeObserver.observe(anchor);
116
+
117
+ // const moveObserver = new IntersectionObserver(scheduleUpdate);
118
+ // moveObserver.observe(anchor);
119
+
120
+ if (params?.container) {
121
+ const observer = new MutationObserver((mutations) => {
122
+ scheduleUpdate();
123
+ });
124
+
125
+ observer.observe(params?.container, {
126
+ childList: true,
127
+ characterData: true,
128
+ subtree: true,
129
+ });
130
+ }
131
+
132
+ globalThis.addEventListener('resize', scheduleUpdate, true);
133
+ document.addEventListener('scroll', scheduleUpdate, true);
134
+
135
+ popover.addEventListener('removed', () => {
136
+ // resizeObserver.disconnect();
137
+ // moveObserver.disconnect();
138
+ globalThis.removeEventListener('resize', scheduleUpdate, true);
139
+ document.removeEventListener('scroll', scheduleUpdate, true);
140
+
141
+ if (rafId) {
142
+ cancelAnimationFrame(rafId);
143
+ rafId = null;
144
+ }
145
+ });
146
+ }