@liminis/editor 0.1.0-rc.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.
- package/LICENSE +34 -0
- package/README.md +376 -0
- package/dist/annotations/anchor-align.d.ts +50 -0
- package/dist/annotations/anchor-align.js +74 -0
- package/dist/annotations/anchor-model.d.ts +81 -0
- package/dist/annotations/anchor-model.js +80 -0
- package/dist/annotations/anchor-resolver.d.ts +92 -0
- package/dist/annotations/anchor-resolver.js +231 -0
- package/dist/annotations/block-structure.d.ts +65 -0
- package/dist/annotations/block-structure.js +113 -0
- package/dist/annotations/types.d.ts +159 -0
- package/dist/annotations/types.js +50 -0
- package/dist/annotations.d.ts +25 -0
- package/dist/annotations.js +27 -0
- package/dist/app/App.d.ts +64 -0
- package/dist/app/App.js +180 -0
- package/dist/app/editor/AmbientCorrectionPlugin.d.ts +18 -0
- package/dist/app/editor/AmbientCorrectionPlugin.js +185 -0
- package/dist/app/editor/AnchorScrollPlugin.d.ts +16 -0
- package/dist/app/editor/AnchorScrollPlugin.js +134 -0
- package/dist/app/editor/AnnotationMarkerPlugin.d.ts +24 -0
- package/dist/app/editor/AnnotationMarkerPlugin.js +224 -0
- package/dist/app/editor/AnnotationPlugin.d.ts +42 -0
- package/dist/app/editor/AnnotationPlugin.js +135 -0
- package/dist/app/editor/AnnotationSurface.d.ts +34 -0
- package/dist/app/editor/AnnotationSurface.js +115 -0
- package/dist/app/editor/AssetContext.d.ts +20 -0
- package/dist/app/editor/AssetContext.js +81 -0
- package/dist/app/editor/BlockClickPlugin.d.ts +8 -0
- package/dist/app/editor/BlockClickPlugin.js +126 -0
- package/dist/app/editor/CodeBlockPlugin.d.ts +1 -0
- package/dist/app/editor/CodeBlockPlugin.js +219 -0
- package/dist/app/editor/CorrectionPanelPlugin.d.ts +14 -0
- package/dist/app/editor/CorrectionPanelPlugin.js +333 -0
- package/dist/app/editor/DragHandlePlugin.d.ts +1 -0
- package/dist/app/editor/DragHandlePlugin.js +300 -0
- package/dist/app/editor/Editor.d.ts +84 -0
- package/dist/app/editor/Editor.js +539 -0
- package/dist/app/editor/FrontmatterPlugin.d.ts +19 -0
- package/dist/app/editor/FrontmatterPlugin.js +301 -0
- package/dist/app/editor/ImageModal.d.ts +13 -0
- package/dist/app/editor/ImageModal.js +267 -0
- package/dist/app/editor/ImagePlugin.d.ts +3 -0
- package/dist/app/editor/ImagePlugin.js +187 -0
- package/dist/app/editor/LinkClickPlugin.d.ts +23 -0
- package/dist/app/editor/LinkClickPlugin.js +90 -0
- package/dist/app/editor/MarkdownShortcutsPlugin.d.ts +49 -0
- package/dist/app/editor/MarkdownShortcutsPlugin.js +373 -0
- package/dist/app/editor/OrderedTaskListPlugin.d.ts +21 -0
- package/dist/app/editor/OrderedTaskListPlugin.js +78 -0
- package/dist/app/editor/SearchPlugin.d.ts +1 -0
- package/dist/app/editor/SearchPlugin.js +379 -0
- package/dist/app/editor/SelectionContextMenuPlugin.d.ts +29 -0
- package/dist/app/editor/SelectionContextMenuPlugin.js +160 -0
- package/dist/app/editor/SlashMenu.d.ts +11 -0
- package/dist/app/editor/SlashMenu.js +494 -0
- package/dist/app/editor/SlashMenuPlugin.d.ts +1 -0
- package/dist/app/editor/SlashMenuPlugin.js +168 -0
- package/dist/app/editor/TableActionsPlugin.d.ts +1 -0
- package/dist/app/editor/TableActionsPlugin.js +398 -0
- package/dist/app/editor/TogglePlugin.d.ts +1 -0
- package/dist/app/editor/TogglePlugin.js +159 -0
- package/dist/app/editor/Toolbar.d.ts +10 -0
- package/dist/app/editor/Toolbar.js +263 -0
- package/dist/app/editor/WikiLinkExistencePlugin.d.ts +17 -0
- package/dist/app/editor/WikiLinkExistencePlugin.js +104 -0
- package/dist/app/editor/WikiLinkFormatPlugin.d.ts +17 -0
- package/dist/app/editor/WikiLinkFormatPlugin.js +203 -0
- package/dist/app/editor/annotation-marks.d.ts +177 -0
- package/dist/app/editor/annotation-marks.js +750 -0
- package/dist/app/editor/annotationCommands.d.ts +13 -0
- package/dist/app/editor/annotationCommands.js +11 -0
- package/dist/app/editor/c4/C4InteractiveRenderer.d.ts +35 -0
- package/dist/app/editor/c4/C4InteractiveRenderer.js +299 -0
- package/dist/app/editor/c4/edge-clipping.d.ts +24 -0
- package/dist/app/editor/c4/edge-clipping.js +139 -0
- package/dist/app/editor/c4/hooks/useC4DiagramDrag.d.ts +38 -0
- package/dist/app/editor/c4/hooks/useC4DiagramDrag.js +112 -0
- package/dist/app/editor/c4/layout.d.ts +25 -0
- package/dist/app/editor/c4/layout.js +839 -0
- package/dist/app/editor/c4/parser.d.ts +19 -0
- package/dist/app/editor/c4/parser.js +410 -0
- package/dist/app/editor/c4/render-to-string.d.ts +24 -0
- package/dist/app/editor/c4/render-to-string.js +34 -0
- package/dist/app/editor/c4/renderer.d.ts +64 -0
- package/dist/app/editor/c4/renderer.js +569 -0
- package/dist/app/editor/c4/types.d.ts +203 -0
- package/dist/app/editor/c4/types.js +43 -0
- package/dist/app/editor/correction-yaml.d.ts +10 -0
- package/dist/app/editor/correction-yaml.js +40 -0
- package/dist/app/editor/editorNodes.d.ts +9 -0
- package/dist/app/editor/editorNodes.js +36 -0
- package/dist/app/editor/index.d.ts +1 -0
- package/dist/app/editor/index.js +1 -0
- package/dist/app/editor/mark-overlap-resolver.d.ts +6 -0
- package/dist/app/editor/mark-overlap-resolver.js +45 -0
- package/dist/app/editor/nodes/C4Component.d.ts +14 -0
- package/dist/app/editor/nodes/C4Component.js +284 -0
- package/dist/app/editor/nodes/C4Node.d.ts +32 -0
- package/dist/app/editor/nodes/C4Node.js +112 -0
- package/dist/app/editor/nodes/CalloutNode.d.ts +23 -0
- package/dist/app/editor/nodes/CalloutNode.js +94 -0
- package/dist/app/editor/nodes/CustomLinkNode.d.ts +57 -0
- package/dist/app/editor/nodes/CustomLinkNode.js +209 -0
- package/dist/app/editor/nodes/CustomListItemNode.d.ts +46 -0
- package/dist/app/editor/nodes/CustomListItemNode.js +120 -0
- package/dist/app/editor/nodes/CustomListNode.d.ts +27 -0
- package/dist/app/editor/nodes/CustomListNode.js +54 -0
- package/dist/app/editor/nodes/DefinitionListNode.d.ts +44 -0
- package/dist/app/editor/nodes/DefinitionListNode.js +151 -0
- package/dist/app/editor/nodes/DiagramContextMenu.d.ts +45 -0
- package/dist/app/editor/nodes/DiagramContextMenu.js +116 -0
- package/dist/app/editor/nodes/EquationComponent.d.ts +17 -0
- package/dist/app/editor/nodes/EquationComponent.js +130 -0
- package/dist/app/editor/nodes/EquationNode.d.ts +43 -0
- package/dist/app/editor/nodes/EquationNode.js +179 -0
- package/dist/app/editor/nodes/FootnoteNode.d.ts +40 -0
- package/dist/app/editor/nodes/FootnoteNode.js +151 -0
- package/dist/app/editor/nodes/FrontmatterNode.d.ts +24 -0
- package/dist/app/editor/nodes/FrontmatterNode.js +88 -0
- package/dist/app/editor/nodes/HorizontalRuleNode.d.ts +17 -0
- package/dist/app/editor/nodes/HorizontalRuleNode.js +57 -0
- package/dist/app/editor/nodes/HtmlNode.d.ts +32 -0
- package/dist/app/editor/nodes/HtmlNode.js +121 -0
- package/dist/app/editor/nodes/ImageComponent.d.ts +11 -0
- package/dist/app/editor/nodes/ImageComponent.js +228 -0
- package/dist/app/editor/nodes/ImageNode.d.ts +53 -0
- package/dist/app/editor/nodes/ImageNode.js +183 -0
- package/dist/app/editor/nodes/ListItemParagraphBreakNode.d.ts +42 -0
- package/dist/app/editor/nodes/ListItemParagraphBreakNode.js +95 -0
- package/dist/app/editor/nodes/MermaidComponent.d.ts +11 -0
- package/dist/app/editor/nodes/MermaidComponent.js +216 -0
- package/dist/app/editor/nodes/MermaidNode.d.ts +27 -0
- package/dist/app/editor/nodes/MermaidNode.js +96 -0
- package/dist/app/editor/nodes/ToggleNode.d.ts +56 -0
- package/dist/app/editor/nodes/ToggleNode.js +237 -0
- package/dist/app/editor/nodes/diagram-context-menu.d.ts +11 -0
- package/dist/app/editor/nodes/diagram-context-menu.js +70 -0
- package/dist/app/editor/nodes/diagram-utils.d.ts +13 -0
- package/dist/app/editor/nodes/diagram-utils.js +29 -0
- package/dist/app/editor/nodes/index.d.ts +30 -0
- package/dist/app/editor/nodes/index.js +17 -0
- package/dist/app/editor/utils.d.ts +2 -0
- package/dist/app/editor/utils.js +17 -0
- package/dist/app/mapper/index.d.ts +3 -0
- package/dist/app/mapper/index.js +2 -0
- package/dist/app/mapper/lexicalToMdast.d.ts +62 -0
- package/dist/app/mapper/lexicalToMdast.js +1937 -0
- package/dist/app/mapper/mdastToLexical.d.ts +31 -0
- package/dist/app/mapper/mdastToLexical.js +1075 -0
- package/dist/contract.d.ts +8 -0
- package/dist/contract.js +8 -0
- package/dist/headless.d.ts +40 -0
- package/dist/headless.js +45 -0
- package/dist/host/context.d.ts +20 -0
- package/dist/host/context.js +24 -0
- package/dist/host/defaults.d.ts +21 -0
- package/dist/host/defaults.js +47 -0
- package/dist/host/messages.d.ts +26 -0
- package/dist/host/messages.js +47 -0
- package/dist/host/types.d.ts +75 -0
- package/dist/host/types.js +13 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +51 -0
- package/dist/markdown/annotate-sentinels.d.ts +42 -0
- package/dist/markdown/annotate-sentinels.js +86 -0
- package/dist/markdown/parse.d.ts +56 -0
- package/dist/markdown/parse.js +516 -0
- package/dist/markdown/stringify.d.ts +7 -0
- package/dist/markdown/stringify.js +439 -0
- package/dist/markdown/vendor/mdast-util-wiki-link/LICENSE +21 -0
- package/dist/markdown/vendor/mdast-util-wiki-link/README.md +41 -0
- package/dist/markdown/vendor/mdast-util-wiki-link/from-markdown.d.ts +72 -0
- package/dist/markdown/vendor/mdast-util-wiki-link/from-markdown.js +127 -0
- package/dist/markdown/vendor/mdast-util-wiki-link/index.d.ts +8 -0
- package/dist/markdown/vendor/mdast-util-wiki-link/index.js +6 -0
- package/dist/markdown/vendor/mdast-util-wiki-link/to-markdown.d.ts +17 -0
- package/dist/markdown/vendor/mdast-util-wiki-link/to-markdown.js +33 -0
- package/dist/markdown.d.ts +28 -0
- package/dist/markdown.js +44 -0
- package/dist/mathjax-config.d.ts +89 -0
- package/dist/mathjax-config.js +133 -0
- package/dist/nodes.d.ts +34 -0
- package/dist/nodes.js +34 -0
- package/dist/stores/correctionStore.d.ts +15 -0
- package/dist/stores/correctionStore.js +12 -0
- package/dist/stores/editorStore.d.ts +16 -0
- package/dist/stores/editorStore.js +27 -0
- package/dist/styles.css +2477 -0
- package/dist/types.d.ts +296 -0
- package/dist/types.js +91 -0
- package/dist/utils/file-types.d.ts +10 -0
- package/dist/utils/file-types.js +46 -0
- package/docs/annotations.md +134 -0
- package/docs/decisions/README.md +72 -0
- package/docs/decisions/adr-002.md +27 -0
- package/docs/decisions/adr-007.md +27 -0
- package/docs/decisions/adr-010.md +26 -0
- package/docs/decisions/adr-024.md +124 -0
- package/docs/decisions/adr-024b.md +0 -0
- package/docs/decisions/adr-025.md +103 -0
- package/docs/decisions/adr-027.md +66 -0
- package/docs/decisions/adr-057.md +67 -0
- package/docs/decisions/adr-070.md +131 -0
- package/docs/decisions/adr-075.md +410 -0
- package/docs/decisions/adr-076.md +183 -0
- package/docs/decisions/adr-077.md +410 -0
- package/docs/decisions/adr-078.md +243 -0
- package/docs/decisions/adr-079.md +167 -0
- package/docs/decisions/adr-080.md +325 -0
- package/docs/decisions/adr-081.md +108 -0
- package/docs/decisions/adr-082.md +123 -0
- package/docs/decisions/adr-083.md +204 -0
- package/docs/decisions/adr-084.md +179 -0
- package/docs/editor-api.md +106 -0
- package/docs/markdown-pipeline.md +282 -0
- package/docs/provenance.md +106 -0
- package/docs/zusammen-editor-capability-map.md +232 -0
- package/package.json +175 -0
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
4
|
+
import { $createRangeSelection, $getRoot, $isTextNode, $setSelection } from 'lexical';
|
|
5
|
+
// Debounce delay in ms
|
|
6
|
+
const SEARCH_DEBOUNCE_MS = 150;
|
|
7
|
+
export function SearchPlugin() {
|
|
8
|
+
const [editor] = useLexicalComposerContext();
|
|
9
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
10
|
+
const [query, setQuery] = useState('');
|
|
11
|
+
const [debouncedQuery, setDebouncedQuery] = useState('');
|
|
12
|
+
const [matches, setMatches] = useState([]);
|
|
13
|
+
const [currentMatchIndex, setCurrentMatchIndex] = useState(0);
|
|
14
|
+
const inputRef = useRef(null);
|
|
15
|
+
const highlightLayerRef = useRef(null);
|
|
16
|
+
const debounceTimerRef = useRef(null);
|
|
17
|
+
const pendingMatchOffsetRef = useRef(null);
|
|
18
|
+
const lastContentRef = useRef('');
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
const handleExternalSearch = (event) => {
|
|
21
|
+
const customEvent = event;
|
|
22
|
+
const queryText = customEvent?.detail?.query ?? '';
|
|
23
|
+
const matchIndex = customEvent?.detail?.matchIndex;
|
|
24
|
+
pendingMatchOffsetRef.current = typeof matchIndex === 'number' ? matchIndex : null;
|
|
25
|
+
setIsOpen(true);
|
|
26
|
+
setQuery(queryText);
|
|
27
|
+
setDebouncedQuery(queryText);
|
|
28
|
+
setTimeout(() => {
|
|
29
|
+
inputRef.current?.focus();
|
|
30
|
+
inputRef.current?.select();
|
|
31
|
+
}, 0);
|
|
32
|
+
};
|
|
33
|
+
window.addEventListener('slashmd:search', handleExternalSearch);
|
|
34
|
+
return () => window.removeEventListener('slashmd:search', handleExternalSearch);
|
|
35
|
+
}, []);
|
|
36
|
+
// Debounce the query
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (debounceTimerRef.current !== null) {
|
|
39
|
+
clearTimeout(debounceTimerRef.current);
|
|
40
|
+
}
|
|
41
|
+
debounceTimerRef.current = window.setTimeout(() => {
|
|
42
|
+
setDebouncedQuery(query);
|
|
43
|
+
debounceTimerRef.current = null;
|
|
44
|
+
}, SEARCH_DEBOUNCE_MS);
|
|
45
|
+
return () => {
|
|
46
|
+
if (debounceTimerRef.current !== null) {
|
|
47
|
+
clearTimeout(debounceTimerRef.current);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}, [query]);
|
|
51
|
+
// Get all text nodes recursively - defined as a regular function to allow recursion
|
|
52
|
+
const getAllTextNodes = useCallback((startNode) => {
|
|
53
|
+
const textNodes = [];
|
|
54
|
+
// Inner recursive function
|
|
55
|
+
const collect = (node) => {
|
|
56
|
+
if ($isTextNode(node)) {
|
|
57
|
+
textNodes.push(node);
|
|
58
|
+
}
|
|
59
|
+
if ('getChildren' in node && typeof node.getChildren === 'function') {
|
|
60
|
+
const children = node.getChildren();
|
|
61
|
+
children.forEach((child) => {
|
|
62
|
+
collect(child);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
collect(startNode);
|
|
67
|
+
return textNodes;
|
|
68
|
+
}, []);
|
|
69
|
+
// Find all text matches in the editor
|
|
70
|
+
const findMatches = useCallback((searchQuery) => {
|
|
71
|
+
if (!searchQuery.trim())
|
|
72
|
+
return [];
|
|
73
|
+
const foundMatches = [];
|
|
74
|
+
const lowerQuery = searchQuery.toLowerCase();
|
|
75
|
+
editor.getEditorState().read(() => {
|
|
76
|
+
const root = $getRoot();
|
|
77
|
+
const textNodes = getAllTextNodes(root);
|
|
78
|
+
let runningOffset = 0;
|
|
79
|
+
textNodes.forEach((node) => {
|
|
80
|
+
const text = node.getTextContent().toLowerCase();
|
|
81
|
+
let startIndex = 0;
|
|
82
|
+
while (true) {
|
|
83
|
+
const matchIndex = text.indexOf(lowerQuery, startIndex);
|
|
84
|
+
if (matchIndex === -1)
|
|
85
|
+
break;
|
|
86
|
+
foundMatches.push({
|
|
87
|
+
node,
|
|
88
|
+
startOffset: matchIndex,
|
|
89
|
+
endOffset: matchIndex + searchQuery.length,
|
|
90
|
+
index: foundMatches.length,
|
|
91
|
+
globalOffset: runningOffset + matchIndex,
|
|
92
|
+
});
|
|
93
|
+
startIndex = matchIndex + 1;
|
|
94
|
+
}
|
|
95
|
+
runningOffset += node.getTextContent().length;
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
return foundMatches;
|
|
99
|
+
}, [editor, getAllTextNodes]);
|
|
100
|
+
// Run search when debounced query changes
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
const found = findMatches(debouncedQuery);
|
|
103
|
+
setMatches(found);
|
|
104
|
+
if (found.length === 0) {
|
|
105
|
+
setCurrentMatchIndex(-1);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (pendingMatchOffsetRef.current !== null) {
|
|
109
|
+
const targetOffset = pendingMatchOffsetRef.current;
|
|
110
|
+
const closest = found.reduce((best, match) => {
|
|
111
|
+
const distance = Math.abs(match.globalOffset - targetOffset);
|
|
112
|
+
if (!best || distance < best.distance) {
|
|
113
|
+
return { index: match.index, distance };
|
|
114
|
+
}
|
|
115
|
+
return best;
|
|
116
|
+
}, null);
|
|
117
|
+
setCurrentMatchIndex(closest ? closest.index : 0);
|
|
118
|
+
pendingMatchOffsetRef.current = null;
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
setCurrentMatchIndex(0);
|
|
122
|
+
}, [debouncedQuery, findMatches]);
|
|
123
|
+
// Find the actual text node in the DOM
|
|
124
|
+
const findTextNodeWithContent = useCallback((element, content) => {
|
|
125
|
+
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
|
|
126
|
+
let node;
|
|
127
|
+
while ((node = walker.nextNode())) {
|
|
128
|
+
if (node.textContent?.includes(content.substring(0, Math.min(content.length, 10)))) {
|
|
129
|
+
return node;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// Fallback: just return the first text node
|
|
133
|
+
const textWalker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
|
|
134
|
+
return textWalker.nextNode();
|
|
135
|
+
}, []);
|
|
136
|
+
// Create highlight overlay for matches
|
|
137
|
+
const updateHighlights = useCallback(() => {
|
|
138
|
+
// Remove existing highlights
|
|
139
|
+
if (highlightLayerRef.current) {
|
|
140
|
+
highlightLayerRef.current.remove();
|
|
141
|
+
highlightLayerRef.current = null;
|
|
142
|
+
}
|
|
143
|
+
if (matches.length === 0 || !debouncedQuery.trim())
|
|
144
|
+
return;
|
|
145
|
+
// Create highlight layer
|
|
146
|
+
const editorElement = editor.getRootElement();
|
|
147
|
+
if (!editorElement)
|
|
148
|
+
return;
|
|
149
|
+
const editorContainer = editorElement.closest('.editor-container');
|
|
150
|
+
if (!editorContainer)
|
|
151
|
+
return;
|
|
152
|
+
// Create highlight container
|
|
153
|
+
const highlightLayer = document.createElement('div');
|
|
154
|
+
highlightLayer.className = 'search-highlight-layer';
|
|
155
|
+
editorContainer.appendChild(highlightLayer);
|
|
156
|
+
highlightLayerRef.current = highlightLayer;
|
|
157
|
+
// Get container bounds for positioning (use container, not editor element)
|
|
158
|
+
const containerRect = editorContainer.getBoundingClientRect();
|
|
159
|
+
matches.forEach((match, idx) => {
|
|
160
|
+
editor.getEditorState().read(() => {
|
|
161
|
+
const key = match.node.getKey();
|
|
162
|
+
const domElement = editor.getElementByKey(key);
|
|
163
|
+
if (!domElement)
|
|
164
|
+
return;
|
|
165
|
+
// Find the text node within the DOM element
|
|
166
|
+
const textNode = findTextNodeWithContent(domElement, match.node.getTextContent());
|
|
167
|
+
if (!textNode)
|
|
168
|
+
return;
|
|
169
|
+
// Create a range for the match
|
|
170
|
+
const range = document.createRange();
|
|
171
|
+
try {
|
|
172
|
+
range.setStart(textNode, match.startOffset);
|
|
173
|
+
range.setEnd(textNode, match.endOffset);
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
// Get the bounding rectangles for the range
|
|
179
|
+
const rects = Array.from(range.getClientRects());
|
|
180
|
+
for (const rect of rects) {
|
|
181
|
+
const highlight = document.createElement('div');
|
|
182
|
+
highlight.className = `search-highlight ${idx === currentMatchIndex ? 'current' : ''}`;
|
|
183
|
+
// Position relative to the container, accounting for scroll
|
|
184
|
+
highlight.style.left = `${rect.left - containerRect.left + editorContainer.scrollLeft}px`;
|
|
185
|
+
highlight.style.top = `${rect.top - containerRect.top + editorContainer.scrollTop}px`;
|
|
186
|
+
highlight.style.width = `${rect.width}px`;
|
|
187
|
+
highlight.style.height = `${rect.height}px`;
|
|
188
|
+
highlightLayer.appendChild(highlight);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
}, [editor, matches, currentMatchIndex, debouncedQuery, findTextNodeWithContent]);
|
|
193
|
+
// Scroll to current match
|
|
194
|
+
// setSelection parameter controls whether to set Lexical selection (which steals focus)
|
|
195
|
+
// - true (default): Set selection AND scroll (for navigation via Enter/buttons)
|
|
196
|
+
// - false: Only scroll, no selection (for soft scroll during typing)
|
|
197
|
+
const scrollToMatch = useCallback((matchIndex, setSelection = true) => {
|
|
198
|
+
if (matches.length === 0 || matchIndex < 0 || matchIndex >= matches.length)
|
|
199
|
+
return;
|
|
200
|
+
const match = matches[matchIndex];
|
|
201
|
+
// Only set Lexical selection when explicitly navigating
|
|
202
|
+
if (setSelection) {
|
|
203
|
+
editor.update(() => {
|
|
204
|
+
const selection = $createRangeSelection();
|
|
205
|
+
selection.anchor.set(match.node.getKey(), match.startOffset, 'text');
|
|
206
|
+
selection.focus.set(match.node.getKey(), match.endOffset, 'text');
|
|
207
|
+
$setSelection(selection);
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
// Always scroll (no focus change)
|
|
211
|
+
editor.getEditorState().read(() => {
|
|
212
|
+
const key = match.node.getKey();
|
|
213
|
+
const domElement = editor.getElementByKey(key);
|
|
214
|
+
if (domElement) {
|
|
215
|
+
domElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
}, [editor, matches]);
|
|
219
|
+
// Navigate to next match
|
|
220
|
+
const goToNextMatch = useCallback(() => {
|
|
221
|
+
if (matches.length === 0)
|
|
222
|
+
return;
|
|
223
|
+
const nextIndex = (currentMatchIndex + 1) % matches.length;
|
|
224
|
+
setCurrentMatchIndex(nextIndex);
|
|
225
|
+
scrollToMatch(nextIndex, true); // Explicit selection for navigation
|
|
226
|
+
}, [matches, currentMatchIndex, scrollToMatch]);
|
|
227
|
+
// Navigate to previous match
|
|
228
|
+
const goToPrevMatch = useCallback(() => {
|
|
229
|
+
if (matches.length === 0)
|
|
230
|
+
return;
|
|
231
|
+
const prevIndex = currentMatchIndex <= 0 ? matches.length - 1 : currentMatchIndex - 1;
|
|
232
|
+
setCurrentMatchIndex(prevIndex);
|
|
233
|
+
scrollToMatch(prevIndex, true); // Explicit selection for navigation
|
|
234
|
+
}, [matches, currentMatchIndex, scrollToMatch]);
|
|
235
|
+
// Close search
|
|
236
|
+
const closeSearch = useCallback(() => {
|
|
237
|
+
// Set selection at current match before closing (if we have a valid match)
|
|
238
|
+
// This allows user to immediately edit at the found location after pressing Escape
|
|
239
|
+
scrollToMatch(currentMatchIndex, true);
|
|
240
|
+
// Save scroll position before any DOM changes
|
|
241
|
+
const scrollY = window.scrollY;
|
|
242
|
+
const scrollX = window.scrollX;
|
|
243
|
+
setIsOpen(false);
|
|
244
|
+
setQuery('');
|
|
245
|
+
setDebouncedQuery('');
|
|
246
|
+
setMatches([]);
|
|
247
|
+
setCurrentMatchIndex(-1);
|
|
248
|
+
// Remove highlights
|
|
249
|
+
if (highlightLayerRef.current) {
|
|
250
|
+
highlightLayerRef.current.remove();
|
|
251
|
+
highlightLayerRef.current = null;
|
|
252
|
+
}
|
|
253
|
+
// Return focus to editor without scrolling
|
|
254
|
+
const editorElement = editor.getRootElement();
|
|
255
|
+
if (editorElement) {
|
|
256
|
+
// Use preventScroll to avoid jumping
|
|
257
|
+
editorElement.focus({ preventScroll: true });
|
|
258
|
+
}
|
|
259
|
+
// Restore scroll position in case it shifted
|
|
260
|
+
requestAnimationFrame(() => {
|
|
261
|
+
window.scrollTo(scrollX, scrollY);
|
|
262
|
+
});
|
|
263
|
+
}, [editor, scrollToMatch, currentMatchIndex]);
|
|
264
|
+
// Update highlights when matches or current index changes
|
|
265
|
+
useEffect(() => {
|
|
266
|
+
if (isOpen) {
|
|
267
|
+
updateHighlights();
|
|
268
|
+
}
|
|
269
|
+
}, [isOpen, matches, currentMatchIndex, updateHighlights]);
|
|
270
|
+
// Update highlights on scroll
|
|
271
|
+
useEffect(() => {
|
|
272
|
+
if (!isOpen || matches.length === 0)
|
|
273
|
+
return;
|
|
274
|
+
const editorElement = editor.getRootElement();
|
|
275
|
+
if (!editorElement)
|
|
276
|
+
return;
|
|
277
|
+
const handleScroll = () => {
|
|
278
|
+
requestAnimationFrame(updateHighlights);
|
|
279
|
+
};
|
|
280
|
+
// Listen on window and editor container for scroll events
|
|
281
|
+
window.addEventListener('scroll', handleScroll, true);
|
|
282
|
+
return () => {
|
|
283
|
+
window.removeEventListener('scroll', handleScroll, true);
|
|
284
|
+
};
|
|
285
|
+
}, [isOpen, matches.length, editor, updateHighlights]);
|
|
286
|
+
// Cleanup highlights on unmount or when closing
|
|
287
|
+
useEffect(() => {
|
|
288
|
+
return () => {
|
|
289
|
+
if (highlightLayerRef.current) {
|
|
290
|
+
highlightLayerRef.current.remove();
|
|
291
|
+
highlightLayerRef.current = null;
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
}, []);
|
|
295
|
+
// Re-update highlights when editor content changes
|
|
296
|
+
useEffect(() => {
|
|
297
|
+
if (!isOpen)
|
|
298
|
+
return;
|
|
299
|
+
const unregister = editor.registerUpdateListener(({ editorState }) => {
|
|
300
|
+
if (!debouncedQuery)
|
|
301
|
+
return;
|
|
302
|
+
const textContent = editorState.read(() => $getRoot().getTextContent());
|
|
303
|
+
if (textContent === lastContentRef.current)
|
|
304
|
+
return;
|
|
305
|
+
lastContentRef.current = textContent;
|
|
306
|
+
const found = findMatches(debouncedQuery);
|
|
307
|
+
setMatches(found);
|
|
308
|
+
// Keep current index in bounds
|
|
309
|
+
setCurrentMatchIndex(prev => found.length === 0 ? -1 : Math.min(prev, found.length - 1));
|
|
310
|
+
});
|
|
311
|
+
return unregister;
|
|
312
|
+
}, [editor, isOpen, debouncedQuery, findMatches]);
|
|
313
|
+
// Handle keyboard shortcuts
|
|
314
|
+
useEffect(() => {
|
|
315
|
+
const handleKeyDown = (e) => {
|
|
316
|
+
// Cmd+F or Ctrl+F to open search
|
|
317
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'f') {
|
|
318
|
+
e.preventDefault();
|
|
319
|
+
e.stopPropagation();
|
|
320
|
+
setIsOpen(true);
|
|
321
|
+
// Focus input after render
|
|
322
|
+
setTimeout(() => {
|
|
323
|
+
inputRef.current?.focus();
|
|
324
|
+
inputRef.current?.select();
|
|
325
|
+
}, 0);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
// Only handle these if search is open
|
|
329
|
+
if (!isOpen)
|
|
330
|
+
return;
|
|
331
|
+
// Escape to close
|
|
332
|
+
if (e.key === 'Escape') {
|
|
333
|
+
e.preventDefault();
|
|
334
|
+
closeSearch();
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
// Enter to go to next, Shift+Enter to go to previous
|
|
338
|
+
if (e.key === 'Enter' && document.activeElement === inputRef.current) {
|
|
339
|
+
e.preventDefault();
|
|
340
|
+
if (e.shiftKey) {
|
|
341
|
+
goToPrevMatch();
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
goToNextMatch();
|
|
345
|
+
}
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
// Cmd+G or Ctrl+G for next match (even when not focused on input)
|
|
349
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'g') {
|
|
350
|
+
e.preventDefault();
|
|
351
|
+
if (e.shiftKey) {
|
|
352
|
+
goToPrevMatch();
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
goToNextMatch();
|
|
356
|
+
}
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
// Use capture phase to intercept before browser default
|
|
361
|
+
document.addEventListener('keydown', handleKeyDown, true);
|
|
362
|
+
return () => {
|
|
363
|
+
document.removeEventListener('keydown', handleKeyDown, true);
|
|
364
|
+
};
|
|
365
|
+
}, [isOpen, closeSearch, goToNextMatch, goToPrevMatch]);
|
|
366
|
+
// Scroll to first match when search results appear (soft scroll, no selection)
|
|
367
|
+
useEffect(() => {
|
|
368
|
+
if (!isOpen)
|
|
369
|
+
return;
|
|
370
|
+
if (matches.length > 0 && currentMatchIndex >= 0) {
|
|
371
|
+
scrollToMatch(currentMatchIndex, false); // Soft scroll only - don't steal focus from input
|
|
372
|
+
}
|
|
373
|
+
}, [isOpen, matches, currentMatchIndex, scrollToMatch]);
|
|
374
|
+
if (!isOpen)
|
|
375
|
+
return null;
|
|
376
|
+
return (_jsxs("div", { className: "search-bar", children: [_jsxs("div", { className: "search-input-wrapper", children: [_jsx("svg", { className: "search-icon", viewBox: "0 0 24 24", width: "16", height: "16", children: _jsx("path", { fill: "currentColor", d: "M15.5 14h-.79l-.28-.27A6.471 6.471 0 0 0 16 9.5 6.5 6.5 0 1 0 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z" }) }), _jsx("input", { ref: inputRef, type: "text", className: "search-input", value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Search...", autoFocus: true }), query && (_jsx("span", { className: "search-count", children: matches.length > 0
|
|
377
|
+
? `${currentMatchIndex + 1} of ${matches.length}`
|
|
378
|
+
: debouncedQuery !== query ? '...' : 'No results' }))] }), _jsxs("div", { className: "search-buttons", children: [_jsx("button", { type: "button", className: "search-nav-button", onClick: goToPrevMatch, disabled: matches.length === 0, title: "Previous match (Shift+Enter)", "aria-label": "Previous match", children: _jsx("svg", { viewBox: "0 0 24 24", width: "16", height: "16", children: _jsx("path", { fill: "currentColor", d: "M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z" }) }) }), _jsx("button", { type: "button", className: "search-nav-button", onClick: goToNextMatch, disabled: matches.length === 0, title: "Next match (Enter)", "aria-label": "Next match", children: _jsx("svg", { viewBox: "0 0 24 24", width: "16", height: "16", children: _jsx("path", { fill: "currentColor", d: "M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6z" }) }) }), _jsx("button", { type: "button", className: "search-close-button", onClick: closeSearch, title: "Close (Escape)", "aria-label": "Close search", children: _jsx("svg", { viewBox: "0 0 24 24", width: "16", height: "16", children: _jsx("path", { fill: "currentColor", d: "M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" }) }) })] })] }));
|
|
379
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lexical plugin that shows a context menu when right-clicking a text selection.
|
|
3
|
+
* Provides a "Chat about this..." action that passes the selected text and click
|
|
4
|
+
* position to the host via callback, plus one entry per configured
|
|
5
|
+
* `contextMenu`-surfaced annotation kind (ADR-077) — `correction` additionally
|
|
6
|
+
* opens the legacy inline correction panel.
|
|
7
|
+
*
|
|
8
|
+
* Follows the TableActionsPlugin pattern for attaching to the editor root element,
|
|
9
|
+
* and the DiagramContextMenu pattern for the overlay menu UI.
|
|
10
|
+
*/
|
|
11
|
+
export interface SelectionContextMenuEvent {
|
|
12
|
+
position: {
|
|
13
|
+
x: number;
|
|
14
|
+
y: number;
|
|
15
|
+
};
|
|
16
|
+
selectedText: string;
|
|
17
|
+
}
|
|
18
|
+
/** One context-menu-surfaced annotation kind's affordance, plain data only (FR-004/FR-010). */
|
|
19
|
+
export interface ContextMenuAnnotationAffordance {
|
|
20
|
+
kind: string;
|
|
21
|
+
label: string;
|
|
22
|
+
}
|
|
23
|
+
interface SelectionContextMenuPluginProps {
|
|
24
|
+
onSelectionContextMenu?: (event: SelectionContextMenuEvent) => void;
|
|
25
|
+
/** One entry per configured kind whose `createAffordance.surface` is `contextMenu` (ADR-077). */
|
|
26
|
+
annotationAffordances?: ContextMenuAnnotationAffordance[];
|
|
27
|
+
}
|
|
28
|
+
export declare function SelectionContextMenuPlugin({ onSelectionContextMenu, annotationAffordances, }: SelectionContextMenuPluginProps): import("react").JSX.Element;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Lexical plugin that shows a context menu when right-clicking a text selection.
|
|
4
|
+
* Provides a "Chat about this..." action that passes the selected text and click
|
|
5
|
+
* position to the host via callback, plus one entry per configured
|
|
6
|
+
* `contextMenu`-surfaced annotation kind (ADR-077) — `correction` additionally
|
|
7
|
+
* opens the legacy inline correction panel.
|
|
8
|
+
*
|
|
9
|
+
* Follows the TableActionsPlugin pattern for attaching to the editor root element,
|
|
10
|
+
* and the DiagramContextMenu pattern for the overlay menu UI.
|
|
11
|
+
*/
|
|
12
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
13
|
+
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
14
|
+
import { OPEN_ANNOTATION_COMPOSER_COMMAND } from './annotationCommands.js';
|
|
15
|
+
import { $getSelection, $isRangeSelection } from 'lexical';
|
|
16
|
+
import { useCorrectionStore } from '../../stores/correctionStore.js';
|
|
17
|
+
// --- Menu overlay component ---
|
|
18
|
+
const menuStyle = {
|
|
19
|
+
position: 'fixed',
|
|
20
|
+
zIndex: 10000,
|
|
21
|
+
borderRadius: '6px',
|
|
22
|
+
padding: '4px 0',
|
|
23
|
+
minWidth: '180px',
|
|
24
|
+
};
|
|
25
|
+
const itemStyle = {
|
|
26
|
+
display: 'block',
|
|
27
|
+
width: '100%',
|
|
28
|
+
padding: '6px 12px',
|
|
29
|
+
background: 'transparent',
|
|
30
|
+
border: 'none',
|
|
31
|
+
fontSize: '13px',
|
|
32
|
+
textAlign: 'left',
|
|
33
|
+
cursor: 'pointer',
|
|
34
|
+
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
35
|
+
};
|
|
36
|
+
function SelectionContextMenu({ visible, x, y, selectedText, onChatAboutThis, annotationAffordances, onCreateAnnotation, onClose, }) {
|
|
37
|
+
const menuRef = useRef(null);
|
|
38
|
+
const isDark = typeof document !== 'undefined' && document.documentElement.classList.contains('dark');
|
|
39
|
+
const bgColor = `var(--vscode-menu-background, ${isDark ? '#252526' : '#ffffff'})`;
|
|
40
|
+
const borderColor = `var(--vscode-menu-border, ${isDark ? '#454545' : '#d4d4d4'})`;
|
|
41
|
+
const textColor = `var(--vscode-menu-foreground, ${isDark ? '#cccccc' : '#333333'})`;
|
|
42
|
+
const hoverBg = `var(--vscode-menu-selectionBackground, ${isDark ? '#094771' : '#e8e8e8'})`;
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (!visible)
|
|
45
|
+
return;
|
|
46
|
+
const handleClick = (e) => {
|
|
47
|
+
if (menuRef.current && !menuRef.current.contains(e.target)) {
|
|
48
|
+
onClose();
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
const handleKey = (e) => {
|
|
52
|
+
if (e.key === 'Escape')
|
|
53
|
+
onClose();
|
|
54
|
+
};
|
|
55
|
+
document.addEventListener('mousedown', handleClick);
|
|
56
|
+
document.addEventListener('keydown', handleKey);
|
|
57
|
+
return () => {
|
|
58
|
+
document.removeEventListener('mousedown', handleClick);
|
|
59
|
+
document.removeEventListener('keydown', handleKey);
|
|
60
|
+
};
|
|
61
|
+
}, [visible, onClose]);
|
|
62
|
+
if (!visible)
|
|
63
|
+
return null;
|
|
64
|
+
const handleHover = (e, entering) => {
|
|
65
|
+
e.currentTarget.style.background = entering ? hoverBg : 'transparent';
|
|
66
|
+
};
|
|
67
|
+
const handleAnnotationClick = (kind) => {
|
|
68
|
+
// Every kind on this surface enters the same shared capture path
|
|
69
|
+
// (ADR-077). `correction` additionally opens the legacy, name-gated
|
|
70
|
+
// correction panel (Out-of-Scope: this generalizes only the dispatch, not
|
|
71
|
+
// the panel). The `correction` kind discards its transient mark, so
|
|
72
|
+
// nothing paints and the panel's visible behaviour is unchanged. A host
|
|
73
|
+
// that supplies no `onCreateAnnotation` (as liminis-app does not) has no
|
|
74
|
+
// handler registered for the command, so dispatch is simply a no-op there.
|
|
75
|
+
//
|
|
76
|
+
// CAUTION for future hosts: if a host ever configures a kind literally
|
|
77
|
+
// named `correction` *and* supplies `onCreateAnnotation`, both fire off
|
|
78
|
+
// this one click — the host's generic create flow and this hardcoded
|
|
79
|
+
// legacy panel, with no coordination between them. See the caveat on
|
|
80
|
+
// `AnnotationKindConfig` in `annotations/types.ts`.
|
|
81
|
+
onCreateAnnotation(kind);
|
|
82
|
+
if (kind === 'correction') {
|
|
83
|
+
useCorrectionStore.getState().open({ x, y }, selectedText);
|
|
84
|
+
}
|
|
85
|
+
onClose();
|
|
86
|
+
};
|
|
87
|
+
return (_jsxs("div", { ref: menuRef,
|
|
88
|
+
// Keep the document selection alive across a menu click. This overlay is
|
|
89
|
+
// a fixed-position sibling of the contenteditable, so pressing a button
|
|
90
|
+
// in it moves focus out of the editor on `mousedown` and collapses
|
|
91
|
+
// `window.getSelection()` — before any `onClick` handler runs. The
|
|
92
|
+
// annotation create path reads that native selection
|
|
93
|
+
// (`AnnotationPlugin`) and bails silently on a collapsed one, so a
|
|
94
|
+
// contextMenu-surfaced kind would capture no anchor while the correction
|
|
95
|
+
// panel still opened as if nothing had gone wrong (review finding,
|
|
96
|
+
// @handarbeit-pruefer). Preventing the default mousedown action stops
|
|
97
|
+
// the focus shift without affecting the subsequent click.
|
|
98
|
+
//
|
|
99
|
+
// Liminis doesn't feel this today only because it supplies no
|
|
100
|
+
// `onCreateAnnotation`, leaving the command unlistened — i.e. it would
|
|
101
|
+
// have bitten the first host to wire the callback up.
|
|
102
|
+
onMouseDown: (e) => e.preventDefault(), style: {
|
|
103
|
+
...menuStyle,
|
|
104
|
+
left: x,
|
|
105
|
+
top: y,
|
|
106
|
+
background: bgColor,
|
|
107
|
+
border: `1px solid ${borderColor}`,
|
|
108
|
+
boxShadow: isDark ? '0 4px 12px rgba(0,0,0,0.3)' : '0 4px 12px rgba(0,0,0,0.12)',
|
|
109
|
+
}, children: [onChatAboutThis && (_jsx("button", { onClick: () => { onChatAboutThis(); onClose(); }, style: { ...itemStyle, color: textColor }, onMouseEnter: (e) => handleHover(e, true), onMouseLeave: (e) => handleHover(e, false), children: "Chat about this\u2026" })), onChatAboutThis && annotationAffordances.length > 0 && (_jsx("hr", { style: { margin: '4px 0', border: 'none', borderTop: `1px solid ${borderColor}` } })), annotationAffordances.map(({ kind, label }) => (_jsx("button", { onClick: () => handleAnnotationClick(kind), style: { ...itemStyle, color: textColor }, onMouseEnter: (e) => handleHover(e, true), onMouseLeave: (e) => handleHover(e, false), children: label }, kind)))] }));
|
|
110
|
+
}
|
|
111
|
+
// --- Plugin ---
|
|
112
|
+
export function SelectionContextMenuPlugin({ onSelectionContextMenu, annotationAffordances = [], }) {
|
|
113
|
+
const [editor] = useLexicalComposerContext();
|
|
114
|
+
const [menuState, setMenuState] = useState({ visible: false, x: 0, y: 0, selectedText: '' });
|
|
115
|
+
const close = useCallback(() => {
|
|
116
|
+
setMenuState((prev) => ({ ...prev, visible: false }));
|
|
117
|
+
}, []);
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
const handleContextMenu = (e) => {
|
|
120
|
+
editor.getEditorState().read(() => {
|
|
121
|
+
const selection = $getSelection();
|
|
122
|
+
if (!$isRangeSelection(selection) || selection.isCollapsed())
|
|
123
|
+
return;
|
|
124
|
+
const selectedText = selection.getTextContent();
|
|
125
|
+
if (!selectedText.trim())
|
|
126
|
+
return;
|
|
127
|
+
e.preventDefault();
|
|
128
|
+
setMenuState({
|
|
129
|
+
visible: true,
|
|
130
|
+
x: e.clientX,
|
|
131
|
+
y: e.clientY,
|
|
132
|
+
selectedText,
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
};
|
|
136
|
+
const editorElement = editor.getRootElement();
|
|
137
|
+
if (editorElement) {
|
|
138
|
+
editorElement.addEventListener('contextmenu', handleContextMenu);
|
|
139
|
+
return () => editorElement.removeEventListener('contextmenu', handleContextMenu);
|
|
140
|
+
}
|
|
141
|
+
}, [editor]);
|
|
142
|
+
// Dispatching the command rather than calling the capture primitive
|
|
143
|
+
// directly is deliberate: `annotationCommands` declares the command and
|
|
144
|
+
// nothing else, so this statically-imported plugin can enter the annotation
|
|
145
|
+
// mechanism without pulling any annotation machinery into the default
|
|
146
|
+
// (annotations-disabled) import graph. The listener lives in the lazily
|
|
147
|
+
// loaded surface, and is simply absent when annotations are off.
|
|
148
|
+
const dispatchCreateAnnotation = useCallback((kind) => {
|
|
149
|
+
editor.dispatchCommand(OPEN_ANNOTATION_COMPOSER_COMMAND, { kind });
|
|
150
|
+
}, [editor]);
|
|
151
|
+
const handleChatAboutThis = useCallback(() => {
|
|
152
|
+
if (onSelectionContextMenu && menuState.selectedText) {
|
|
153
|
+
onSelectionContextMenu({
|
|
154
|
+
position: { x: menuState.x, y: menuState.y },
|
|
155
|
+
selectedText: menuState.selectedText,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}, [onSelectionContextMenu, menuState]);
|
|
159
|
+
return (_jsx(SelectionContextMenu, { visible: menuState.visible, x: menuState.x, y: menuState.y, selectedText: menuState.selectedText, onChatAboutThis: onSelectionContextMenu ? handleChatAboutThis : null, annotationAffordances: annotationAffordances, onCreateAnnotation: dispatchCreateAnnotation, onClose: close }));
|
|
160
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
interface SlashMenuProps {
|
|
2
|
+
isOpen: boolean;
|
|
3
|
+
position: {
|
|
4
|
+
top: number;
|
|
5
|
+
left: number;
|
|
6
|
+
} | null;
|
|
7
|
+
query: string;
|
|
8
|
+
onClose: () => void;
|
|
9
|
+
}
|
|
10
|
+
export declare function SlashMenu({ isOpen, position, query, onClose }: SlashMenuProps): import("react").JSX.Element | null;
|
|
11
|
+
export {};
|