smartrte-react 0.3.5 → 1.0.0-beta.10
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/CHANGELOG.md +55 -0
- package/README.md +205 -14
- package/dist/adapters/domChecklistCommandBridge.js +1 -1
- package/dist/adapters/domCommandBridge.d.ts +2 -2
- package/dist/adapters/domCommandBridge.js +2 -2
- package/dist/adapters/domInlineAtomCommandBridge.d.ts +10 -9
- package/dist/adapters/domInlineAtomCommandBridge.js +154 -50
- package/dist/adapters/domInlineImageCommandBridge.d.ts +7 -7
- package/dist/adapters/domInlineImageCommandBridge.js +6 -72
- package/dist/adapters/domMoveCommandBridge.js +1 -1
- package/dist/adapters/domSelectionBridge.d.ts +3 -3
- package/dist/adapters/domSmartDocument.d.ts +4 -4
- package/dist/adapters/domSmartDocument.js +2 -2
- package/dist/adapters/domTableCommandBridge.d.ts +42 -16
- package/dist/adapters/domTableCommandBridge.js +203 -173
- package/dist/adapters/internalFlags.d.ts +0 -8
- package/dist/adapters/internalFlags.js +0 -24
- package/dist/adapters/legacyListShadowComparator.d.ts +1 -1
- package/dist/adapters/legacyListShadowComparator.js +1 -1
- package/dist/adapters/pdfPrint.d.ts +8 -0
- package/dist/adapters/pdfPrint.js +22 -0
- package/dist/adapters/shadowMode.d.ts +3 -3
- package/dist/adapters/shadowMode.js +1 -1
- package/dist/canonicalEditorRuntime.d.ts +65 -1
- package/dist/canonicalEditorRuntime.js +63 -5
- package/dist/capabilityPresets.d.ts +27 -0
- package/dist/capabilityPresets.js +21 -0
- package/dist/collabTransport.d.ts +66 -0
- package/dist/collabTransport.js +1 -0
- package/dist/commentProvider.d.ts +24 -0
- package/dist/commentProvider.js +1 -0
- package/dist/components/CanonicalAuthorityEditor.d.ts +56 -1
- package/dist/components/CanonicalAuthorityEditor.js +1304 -116
- package/dist/components/ClassicEditorAuthority.d.ts +93 -22
- package/dist/components/ClassicEditorAuthority.js +11 -43
- package/dist/components/ColorPickerPopover.d.ts +39 -0
- package/dist/components/ColorPickerPopover.js +287 -0
- package/dist/components/CommentMarkers.d.ts +25 -0
- package/dist/components/CommentMarkers.js +105 -0
- package/dist/components/CommentThreadPanel.d.ts +26 -0
- package/dist/components/CommentThreadPanel.js +47 -0
- package/dist/components/ContextMenu.d.ts +23 -0
- package/dist/components/ContextMenu.js +141 -0
- package/dist/components/FormulaLibraryPopover.d.ts +16 -0
- package/dist/components/FormulaLibraryPopover.js +107 -0
- package/dist/components/LinkEditorPopover.d.ts +3 -1
- package/dist/components/LinkEditorPopover.js +22 -6
- package/dist/components/MediaDetailsPopover.d.ts +35 -0
- package/dist/components/MediaDetailsPopover.js +136 -0
- package/dist/components/MediaManager.d.ts +4 -20
- package/dist/components/MediaManager.js +40 -11
- package/dist/components/MediaOverlay.d.ts +27 -0
- package/dist/components/MediaOverlay.js +228 -0
- package/dist/components/SpecialCharacterPopover.d.ts +18 -0
- package/dist/components/SpecialCharacterPopover.js +99 -0
- package/dist/components/StructuralSuggestionMarkers.d.ts +20 -0
- package/dist/components/StructuralSuggestionMarkers.js +56 -0
- package/dist/components/SuggestionPanel.d.ts +26 -0
- package/dist/components/SuggestionPanel.js +33 -0
- package/dist/components/TableBorderPopover.d.ts +59 -0
- package/dist/components/TableBorderPopover.js +149 -0
- package/dist/components/TableResizeHandles.d.ts +25 -0
- package/dist/components/TableResizeHandles.js +323 -0
- package/dist/components/TableSizePickerPopover.d.ts +16 -0
- package/dist/components/TableSizePickerPopover.js +126 -0
- package/dist/components/ToolbarPrimitives.d.ts +161 -0
- package/dist/components/ToolbarPrimitives.js +237 -0
- package/dist/components/VersionHistoryPanel.d.ts +9 -0
- package/dist/components/VersionHistoryPanel.js +157 -0
- package/dist/components/fixedPositioning.d.ts +25 -0
- package/dist/components/fixedPositioning.js +42 -0
- package/dist/formulaLibrary.d.ts +31 -0
- package/dist/formulaLibrary.js +90 -0
- package/dist/index.d.ts +25 -24
- package/dist/index.js +15 -11
- package/dist/mediaManagerAdapter.d.ts +12 -0
- package/dist/mediaManagerAdapter.js +34 -0
- package/dist/mediaProvider.d.ts +21 -1
- package/dist/specialCharacters.d.ts +22 -0
- package/dist/specialCharacters.js +86 -0
- package/dist/standalone/classic-editor-embed.js +19 -12
- package/dist/suggestionProvider.d.ts +17 -0
- package/dist/suggestionProvider.js +1 -0
- package/dist/theme.d.ts +1 -1
- package/dist/theme.js +219 -30
- package/dist/toolbarTools.d.ts +95 -0
- package/dist/toolbarTools.js +17 -0
- package/dist/versionProvider.d.ts +29 -0
- package/dist/versionProvider.js +1 -0
- package/package.json +28 -14
- package/dist/adapters/canonicalInlineCommandBridge.d.ts +0 -15
- package/dist/adapters/canonicalInlineCommandBridge.js +0 -269
- package/dist/adapters/canonicalListCommandBridge.d.ts +0 -26
- package/dist/adapters/canonicalListCommandBridge.js +0 -430
- package/dist/adapters/coreBold.d.ts +0 -5
- package/dist/adapters/coreBold.js +0 -3
- package/dist/adapters/documentFormats.d.ts +0 -26
- package/dist/adapters/documentFormats.js +0 -49
- package/dist/adapters/docxFormat.d.ts +0 -5
- package/dist/adapters/docxFormat.js +0 -272
- package/dist/adapters/domBlockCommandBridge.d.ts +0 -19
- package/dist/adapters/domBlockCommandBridge.js +0 -79
- package/dist/adapters/inlineMarkCoreExecution.d.ts +0 -9
- package/dist/adapters/inlineMarkCoreExecution.js +0 -25
- package/dist/adapters/pdfFormat.d.ts +0 -23
- package/dist/adapters/pdfFormat.js +0 -204
- package/dist/adapters/portableDocxAtoms.d.ts +0 -4
- package/dist/adapters/portableDocxAtoms.js +0 -58
- package/dist/adapters/styledDocxFormat.d.ts +0 -8
- package/dist/adapters/styledDocxFormat.js +0 -190
- package/dist/builtInFormatDefinitions.d.ts +0 -5
- package/dist/builtInFormatDefinitions.js +0 -82
- package/dist/canonicalAuthorityFlag.d.ts +0 -32
- package/dist/canonicalAuthorityFlag.js +0 -49
- package/dist/canonicalClipboardRuntime.d.ts +0 -15
- package/dist/canonicalClipboardRuntime.js +0 -76
- package/dist/components/ClassicEditor.d.ts +0 -76
- package/dist/components/ClassicEditor.js +0 -6729
- package/dist/editorController.d.ts +0 -78
- package/dist/editorController.js +0 -298
- package/dist/formatFidelity.d.ts +0 -14
- package/dist/formatFidelity.js +0 -107
- package/dist/formatRuntime.d.ts +0 -50
- package/dist/formatRuntime.js +0 -20
- package/dist/pluginRuntime.d.ts +0 -68
- package/dist/pluginRuntime.js +0 -78
- package/dist/standalone/editor.js +0 -1068
- package/dist/test-harness/atomShadowComparator.d.ts +0 -14
- package/dist/test-harness/atomShadowComparator.js +0 -63
- package/dist/test-harness/blockShadowComparator.d.ts +0 -30
- package/dist/test-harness/blockShadowComparator.js +0 -140
- package/dist/test-harness/clipboardShadowComparator.d.ts +0 -12
- package/dist/test-harness/clipboardShadowComparator.js +0 -46
- package/dist/test-harness/inlineShadowComparator.d.ts +0 -32
- package/dist/test-harness/inlineShadowComparator.js +0 -152
- package/dist/test-harness/legacyAtomEngine.d.ts +0 -10
- package/dist/test-harness/legacyAtomEngine.js +0 -15
- package/dist/test-harness/legacyBlockEngine.d.ts +0 -31
- package/dist/test-harness/legacyBlockEngine.js +0 -31
- package/dist/test-harness/legacyClipboardEngine.d.ts +0 -14
- package/dist/test-harness/legacyClipboardEngine.js +0 -67
- package/dist/test-harness/legacyInlineEngine.d.ts +0 -19
- package/dist/test-harness/legacyInlineEngine.js +0 -49
- package/dist/test-harness/legacyTableEngine.d.ts +0 -12
- package/dist/test-harness/legacyTableEngine.js +0 -25
- package/dist/test-harness/tableShadowComparator.d.ts +0 -29
- package/dist/test-harness/tableShadowComparator.js +0 -101
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useState } from "react";
|
|
3
|
+
import { resolveAnnotationRange } from "smartrte-core/foundation";
|
|
4
|
+
/**
|
|
5
|
+
* Live inline markers for comment threads, following `TableResizeHandles`'
|
|
6
|
+
* precedent (Phase 12a §1.2 finding): comments need many simultaneous
|
|
7
|
+
* persistent anchors, not the single-instance overlay pattern the other
|
|
8
|
+
* popovers use, so this measures real DOM rects from the live document and
|
|
9
|
+
* re-measures via ResizeObserver/MutationObserver/scroll, exactly like that
|
|
10
|
+
* component's column/row handles do for table boundaries.
|
|
11
|
+
*
|
|
12
|
+
* A thread's range is resolved fresh on every recompute via
|
|
13
|
+
* `resolveAnnotationRange` (not cached) - it is the authoritative "is this
|
|
14
|
+
* thread still anchored to real content" check, matching every other
|
|
15
|
+
* AnnotationRange consumer. A thread whose range no longer resolves (fully
|
|
16
|
+
* orphaned - the content it anchored to is gone) renders nothing here; it
|
|
17
|
+
* still exists in `threads` for a host's thread-list UI to surface.
|
|
18
|
+
*/
|
|
19
|
+
export function CommentMarkers({ positions, mapping, containerElement, threads, activeThreadId, onSelectThread }) {
|
|
20
|
+
const [rects, setRects] = useState([]);
|
|
21
|
+
const [badges, setBadges] = useState([]);
|
|
22
|
+
const recompute = useCallback(() => {
|
|
23
|
+
if (!threads.length) {
|
|
24
|
+
setRects([]);
|
|
25
|
+
setBadges([]);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const nextRects = [];
|
|
29
|
+
const nextBadges = [];
|
|
30
|
+
threads.forEach((thread) => {
|
|
31
|
+
const range = resolveAnnotationRange(thread.range, positions);
|
|
32
|
+
if (!range)
|
|
33
|
+
return;
|
|
34
|
+
const fromDom = mapping.posToDom(range.from);
|
|
35
|
+
const toDom = mapping.posToDom(range.to);
|
|
36
|
+
if (!fromDom || !toDom)
|
|
37
|
+
return;
|
|
38
|
+
const domRange = containerElement.ownerDocument.createRange();
|
|
39
|
+
try {
|
|
40
|
+
domRange.setStart(fromDom.node, fromDom.offset);
|
|
41
|
+
domRange.setEnd(toDom.node, toDom.offset);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const clientRects = [...domRange.getClientRects()];
|
|
47
|
+
clientRects.forEach((rect) => {
|
|
48
|
+
nextRects.push({ threadId: thread.id, resolved: thread.resolved, left: rect.left, top: rect.top, width: rect.width, height: rect.height });
|
|
49
|
+
});
|
|
50
|
+
const anchor = clientRects[clientRects.length - 1] || domRange.getBoundingClientRect();
|
|
51
|
+
nextBadges.push({ threadId: thread.id, resolved: thread.resolved, replyCount: thread.replies.length, left: anchor.right, top: anchor.top });
|
|
52
|
+
});
|
|
53
|
+
setRects(nextRects);
|
|
54
|
+
setBadges(nextBadges);
|
|
55
|
+
}, [positions, mapping, containerElement, threads]);
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
recompute();
|
|
58
|
+
const observer = new ResizeObserver(recompute);
|
|
59
|
+
observer.observe(containerElement);
|
|
60
|
+
const mutationObserver = new MutationObserver(recompute);
|
|
61
|
+
mutationObserver.observe(containerElement, { childList: true, subtree: true, characterData: true });
|
|
62
|
+
const ownerWindow = containerElement.ownerDocument.defaultView;
|
|
63
|
+
ownerWindow?.addEventListener("scroll", recompute, true);
|
|
64
|
+
ownerWindow?.addEventListener("resize", recompute);
|
|
65
|
+
return () => {
|
|
66
|
+
observer.disconnect();
|
|
67
|
+
mutationObserver.disconnect();
|
|
68
|
+
ownerWindow?.removeEventListener("scroll", recompute, true);
|
|
69
|
+
ownerWindow?.removeEventListener("resize", recompute);
|
|
70
|
+
};
|
|
71
|
+
}, [containerElement, recompute]);
|
|
72
|
+
return (_jsxs("div", { "data-srte-comment-markers": "true", style: { position: "fixed", inset: 0, pointerEvents: "none", zIndex: 55 }, children: [rects.map((rect, index) => (_jsx("div", { "data-srte-comment-highlight": rect.threadId, onPointerDown: () => onSelectThread(rect.threadId), style: {
|
|
73
|
+
position: "fixed",
|
|
74
|
+
left: rect.left,
|
|
75
|
+
top: rect.top,
|
|
76
|
+
width: rect.width,
|
|
77
|
+
height: rect.height,
|
|
78
|
+
background: rect.resolved
|
|
79
|
+
? "var(--srte-comment-highlight-resolved, rgba(148, 148, 148, 0.18))"
|
|
80
|
+
: activeThreadId === rect.threadId
|
|
81
|
+
? "var(--srte-comment-highlight-active, rgba(245, 180, 0, 0.55))"
|
|
82
|
+
: "var(--srte-comment-highlight, rgba(245, 180, 0, 0.28))",
|
|
83
|
+
cursor: "pointer",
|
|
84
|
+
pointerEvents: "auto",
|
|
85
|
+
} }, `${rect.threadId}-${index}`))), badges.map((badge) => (_jsx("button", { type: "button", "aria-label": badge.resolved ? "Open resolved comment thread" : "Open comment thread", "data-srte-comment-badge": badge.threadId, onClick: () => onSelectThread(badge.threadId), style: {
|
|
86
|
+
position: "fixed",
|
|
87
|
+
left: badge.left + 4,
|
|
88
|
+
top: badge.top - 4,
|
|
89
|
+
minWidth: 16,
|
|
90
|
+
height: 16,
|
|
91
|
+
padding: "0 4px",
|
|
92
|
+
borderRadius: 8,
|
|
93
|
+
border: "1px solid var(--srte-border, #d0d0d0)",
|
|
94
|
+
background: badge.resolved
|
|
95
|
+
? "var(--srte-comment-badge-resolved, #e3e3e3)"
|
|
96
|
+
: activeThreadId === badge.threadId
|
|
97
|
+
? "var(--srte-primary)"
|
|
98
|
+
: "var(--srte-comment-badge, #f5b400)",
|
|
99
|
+
color: badge.resolved ? "#555" : "#1a1300",
|
|
100
|
+
fontSize: 10,
|
|
101
|
+
lineHeight: "14px",
|
|
102
|
+
cursor: "pointer",
|
|
103
|
+
pointerEvents: "auto",
|
|
104
|
+
}, children: badge.replyCount }, badge.threadId)))] }));
|
|
105
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { AnnotationRange, CommentThread } from "smartrte-core/foundation";
|
|
2
|
+
export interface CommentThreadPanelProps {
|
|
3
|
+
open: boolean;
|
|
4
|
+
onClose: () => void;
|
|
5
|
+
threads: readonly CommentThread[];
|
|
6
|
+
activeThreadId: string | null;
|
|
7
|
+
onSelectThread: (id: string) => void;
|
|
8
|
+
/** Set when "Add comment" was just clicked against a valid, non-collapsed selection - renders a compose box for a brand-new thread. */
|
|
9
|
+
pendingRange: AnnotationRange | null;
|
|
10
|
+
onCreateThread: (text: string) => void;
|
|
11
|
+
onReply: (threadId: string, text: string) => void;
|
|
12
|
+
onResolve: (threadId: string, resolved: boolean) => void;
|
|
13
|
+
onDelete: (threadId: string) => void;
|
|
14
|
+
busyThreadId: string | null;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Docked side panel, not a blocking modal like `VersionHistoryPanel`/
|
|
18
|
+
* `MediaManager` - a comment thread is meant to be read and replied to
|
|
19
|
+
* while continuing to edit nearby, not a full-attention dialog. Threads
|
|
20
|
+
* whose range no longer resolves aren't distinguished here with a special
|
|
21
|
+
* flag (per `CommentThread`'s own doc comment, "orphaned" is derived, not
|
|
22
|
+
* stored) - a host wanting to visually flag them can cross-reference
|
|
23
|
+
* `CommentMarkers`' own resolution, which already only renders markers for
|
|
24
|
+
* threads that still resolve.
|
|
25
|
+
*/
|
|
26
|
+
export declare function CommentThreadPanel(props: CommentThreadPanelProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
const formatTimestamp = (createdAt) => new Date(createdAt).toLocaleString();
|
|
4
|
+
/**
|
|
5
|
+
* Docked side panel, not a blocking modal like `VersionHistoryPanel`/
|
|
6
|
+
* `MediaManager` - a comment thread is meant to be read and replied to
|
|
7
|
+
* while continuing to edit nearby, not a full-attention dialog. Threads
|
|
8
|
+
* whose range no longer resolves aren't distinguished here with a special
|
|
9
|
+
* flag (per `CommentThread`'s own doc comment, "orphaned" is derived, not
|
|
10
|
+
* stored) - a host wanting to visually flag them can cross-reference
|
|
11
|
+
* `CommentMarkers`' own resolution, which already only renders markers for
|
|
12
|
+
* threads that still resolve.
|
|
13
|
+
*/
|
|
14
|
+
export function CommentThreadPanel(props) {
|
|
15
|
+
const { open, onClose, threads, activeThreadId, onSelectThread, pendingRange, onCreateThread, onReply, onResolve, onDelete, busyThreadId } = props;
|
|
16
|
+
const [showResolved, setShowResolved] = useState(false);
|
|
17
|
+
const [draft, setDraft] = useState("");
|
|
18
|
+
const [replyDrafts, setReplyDrafts] = useState({});
|
|
19
|
+
if (!open)
|
|
20
|
+
return null;
|
|
21
|
+
const visible = threads.filter((thread) => showResolved || !thread.resolved);
|
|
22
|
+
const submitCompose = () => {
|
|
23
|
+
const text = draft.trim();
|
|
24
|
+
if (!text)
|
|
25
|
+
return;
|
|
26
|
+
onCreateThread(text);
|
|
27
|
+
setDraft("");
|
|
28
|
+
};
|
|
29
|
+
const submitReply = (threadId) => {
|
|
30
|
+
const text = (replyDrafts[threadId] || "").trim();
|
|
31
|
+
if (!text)
|
|
32
|
+
return;
|
|
33
|
+
onReply(threadId, text);
|
|
34
|
+
setReplyDrafts((current) => ({ ...current, [threadId]: "" }));
|
|
35
|
+
};
|
|
36
|
+
return (_jsxs("div", { role: "complementary", "aria-label": "Comments", "data-srte-comment-panel": "true", style: {
|
|
37
|
+
position: "fixed", top: 0, right: 0, bottom: 0, width: 320, maxWidth: "90vw",
|
|
38
|
+
background: "var(--srte-modal-bg)", color: "var(--srte-modal-text)",
|
|
39
|
+
borderLeft: "1px solid var(--srte-border-light)", boxShadow: "var(--srte-menu-shadow)",
|
|
40
|
+
display: "flex", flexDirection: "column", zIndex: 70,
|
|
41
|
+
}, children: [_jsxs("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", padding: "10px 14px", borderBottom: "1px solid var(--srte-border-light)" }, children: [_jsx("div", { style: { fontWeight: 600 }, children: "Comments" }), _jsx("button", { type: "button", "aria-label": "Close comments", onClick: onClose, children: "\u2715" })] }), _jsxs("label", { style: { display: "flex", alignItems: "center", gap: 6, padding: "8px 14px", fontSize: 13, color: "var(--srte-text-muted)", borderBottom: "1px solid var(--srte-border-light)" }, children: [_jsx("input", { type: "checkbox", checked: showResolved, onChange: (event) => setShowResolved(event.target.checked) }), "Show resolved"] }), pendingRange && (_jsxs("div", { "data-srte-comment-compose": "true", style: { padding: "10px 14px", borderBottom: "1px solid var(--srte-border-light)" }, children: [_jsx("textarea", { value: draft, onChange: (event) => setDraft(event.target.value), placeholder: "Add a comment\u2026", rows: 3, style: { width: "100%", padding: "6px 8px", border: "1px solid var(--srte-border)", borderRadius: 6, background: "var(--srte-input-bg)", color: "var(--srte-input-text)", resize: "vertical" } }), _jsx("div", { style: { display: "flex", justifyContent: "flex-end", gap: 8, marginTop: 6 }, children: _jsx("button", { type: "button", "data-srte-comment-compose-submit": "true", disabled: !draft.trim(), onClick: submitCompose, children: "Comment" }) })] })), _jsxs("div", { style: { flex: 1, overflowY: "auto", padding: "0 14px 14px" }, children: [visible.length === 0 && _jsx("div", { style: { color: "var(--srte-text-muted)", padding: "12px 0" }, children: "No comments yet." }), visible.map((thread) => (_jsxs("div", { "data-srte-comment-thread": thread.id, onClick: () => onSelectThread(thread.id), style: {
|
|
42
|
+
padding: "10px 0", borderBottom: "1px solid var(--srte-border-light)",
|
|
43
|
+
opacity: thread.resolved ? 0.6 : 1,
|
|
44
|
+
outline: activeThreadId === thread.id ? "2px solid var(--srte-primary)" : "none",
|
|
45
|
+
}, children: [thread.replies.map((reply) => (_jsxs("div", { style: { marginBottom: 6 }, children: [_jsx("div", { style: { fontSize: 12, fontWeight: 600 }, children: reply.authorId }), _jsx("div", { style: { fontSize: 13, whiteSpace: "pre-wrap" }, children: reply.text }), _jsx("div", { style: { fontSize: 11, color: "var(--srte-text-muted)" }, children: formatTimestamp(reply.createdAt) })] }, reply.id))), _jsxs("div", { style: { display: "flex", gap: 8, marginTop: 6 }, children: [_jsx("button", { type: "button", "data-srte-comment-resolve": thread.id, disabled: busyThreadId === thread.id, onClick: (event) => { event.stopPropagation(); onResolve(thread.id, !thread.resolved); }, children: thread.resolved ? "Reopen" : "Resolve" }), _jsx("button", { type: "button", "data-srte-comment-delete": thread.id, disabled: busyThreadId === thread.id, onClick: (event) => { event.stopPropagation(); onDelete(thread.id); }, style: { color: "var(--srte-danger)" }, children: "Delete" })] }), !thread.resolved && (_jsxs("div", { style: { display: "flex", gap: 6, marginTop: 8 }, onClick: (event) => event.stopPropagation(), children: [_jsx("input", { value: replyDrafts[thread.id] || "", onChange: (event) => setReplyDrafts((current) => ({ ...current, [thread.id]: event.target.value })), onKeyDown: (event) => { if (event.key === "Enter")
|
|
46
|
+
submitReply(thread.id); }, placeholder: "Reply\u2026", style: { flex: 1, padding: "5px 7px", border: "1px solid var(--srte-border)", borderRadius: 6, background: "var(--srte-input-bg)", color: "var(--srte-input-text)" } }), _jsx("button", { type: "button", "data-srte-comment-reply-submit": thread.id, disabled: !(replyDrafts[thread.id] || "").trim(), onClick: () => submitReply(thread.id), children: "Reply" })] }))] }, thread.id)))] })] }));
|
|
47
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface ContextMenuItem {
|
|
2
|
+
id: string;
|
|
3
|
+
label: string;
|
|
4
|
+
onSelect: () => void;
|
|
5
|
+
danger?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface ContextMenuProps {
|
|
8
|
+
x: number;
|
|
9
|
+
y: number;
|
|
10
|
+
items: readonly ContextMenuItem[];
|
|
11
|
+
onDismiss: () => void;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Phase 11.5 §2.3: a generic right-click menu, position-at-cursor and
|
|
15
|
+
* keyboard-dismissible following LinkEditorPopover/ColorPickerPopover's
|
|
16
|
+
* pattern - plus click-outside-to-close, which those two don't have but a
|
|
17
|
+
* native-context-menu-replacement genuinely needs (an accidental click
|
|
18
|
+
* elsewhere shouldn't leave it stuck open). Items are supplied by the
|
|
19
|
+
* caller (CanonicalAuthorityEditor's resolveContextMenuItems), which
|
|
20
|
+
* resolves the applicable plugin registry `contextMenu` contributions for
|
|
21
|
+
* the current scope - this component has no knowledge of commands/scopes.
|
|
22
|
+
*/
|
|
23
|
+
export declare function ContextMenu({ x, y, items, onDismiss }: ContextMenuProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
3
|
+
// A fixed cap, not just "whatever fits below the click point" - the menu's
|
|
4
|
+
// item count has grown considerably (marks, table, media, cell colour,
|
|
5
|
+
// link actions can all stack for one click), and a height that changes
|
|
6
|
+
// with click position made the scroll behavior feel inconsistent. Still
|
|
7
|
+
// clamped against the real available viewport space too (see placement
|
|
8
|
+
// below), so it never overflows a genuinely short viewport.
|
|
9
|
+
const MAX_MENU_HEIGHT = 400;
|
|
10
|
+
const itemStyle = (danger) => ({
|
|
11
|
+
display: "block",
|
|
12
|
+
width: "100%",
|
|
13
|
+
textAlign: "left",
|
|
14
|
+
padding: "8px 10px",
|
|
15
|
+
border: 0,
|
|
16
|
+
borderRadius: 6,
|
|
17
|
+
background: "transparent",
|
|
18
|
+
color: danger ? "var(--srte-danger)" : "var(--srte-menu-text)",
|
|
19
|
+
cursor: "pointer",
|
|
20
|
+
font: "inherit",
|
|
21
|
+
fontSize: 13,
|
|
22
|
+
fontWeight: 500,
|
|
23
|
+
});
|
|
24
|
+
/**
|
|
25
|
+
* Phase 11.5 §2.3: a generic right-click menu, position-at-cursor and
|
|
26
|
+
* keyboard-dismissible following LinkEditorPopover/ColorPickerPopover's
|
|
27
|
+
* pattern - plus click-outside-to-close, which those two don't have but a
|
|
28
|
+
* native-context-menu-replacement genuinely needs (an accidental click
|
|
29
|
+
* elsewhere shouldn't leave it stuck open). Items are supplied by the
|
|
30
|
+
* caller (CanonicalAuthorityEditor's resolveContextMenuItems), which
|
|
31
|
+
* resolves the applicable plugin registry `contextMenu` contributions for
|
|
32
|
+
* the current scope - this component has no knowledge of commands/scopes.
|
|
33
|
+
*/
|
|
34
|
+
export function ContextMenu({ x, y, items, onDismiss }) {
|
|
35
|
+
const rootRef = useRef(null);
|
|
36
|
+
// Null until measured: the menu's real width/height depends on its
|
|
37
|
+
// content (item label lengths), not a fixed guess, so the final position
|
|
38
|
+
// - including whether to flip left/up to stay in the viewport - can only
|
|
39
|
+
// be computed after a first, invisible render exposes the real
|
|
40
|
+
// getBoundingClientRect(). Fixes both the viewport-overflow bug (the
|
|
41
|
+
// previous clamp math assumed a hardcoded 220px width that real labels
|
|
42
|
+
// like "Insert column right" can exceed) and gives scroll a real
|
|
43
|
+
// available-height number to clamp against.
|
|
44
|
+
const [placement, setPlacement] = useState(null);
|
|
45
|
+
// CanonicalAuthorityEditor passes a fresh onDismiss closure on every
|
|
46
|
+
// render (it re-renders on every editor subscribe tick, i.e. on every
|
|
47
|
+
// document/selection change - possible even while this menu is open, if
|
|
48
|
+
// the triggering right-click itself moved the caret). Reading through a
|
|
49
|
+
// ref - rather than depending on `onDismiss` in the effect below - keeps
|
|
50
|
+
// the outside-click listener mounted exactly once for this menu's
|
|
51
|
+
// lifetime, so a re-render can't create a window where the listener has
|
|
52
|
+
// been torn down and not yet reattached.
|
|
53
|
+
const onDismissRef = useRef(onDismiss);
|
|
54
|
+
onDismissRef.current = onDismiss;
|
|
55
|
+
useLayoutEffect(() => {
|
|
56
|
+
const el = rootRef.current;
|
|
57
|
+
if (!el)
|
|
58
|
+
return;
|
|
59
|
+
const margin = 8;
|
|
60
|
+
const { width, height } = el.getBoundingClientRect();
|
|
61
|
+
const viewportWidth = window.innerWidth;
|
|
62
|
+
const viewportHeight = window.innerHeight;
|
|
63
|
+
// Open leftward (menu's right edge at the click point) instead of the
|
|
64
|
+
// default rightward open when the default would overflow the right
|
|
65
|
+
// edge - never silently overflow.
|
|
66
|
+
const overflowsRight = x + width > viewportWidth - margin;
|
|
67
|
+
const left = overflowsRight ? x - width : x;
|
|
68
|
+
const clampedLeft = Math.min(Math.max(margin, left), Math.max(margin, viewportWidth - width - margin));
|
|
69
|
+
// Same flip for the bottom edge.
|
|
70
|
+
const overflowsBottom = y + height > viewportHeight - margin;
|
|
71
|
+
const top = overflowsBottom ? y - height : y;
|
|
72
|
+
const clampedTop = Math.min(Math.max(margin, top), Math.max(margin, viewportHeight - margin));
|
|
73
|
+
// The smaller of the fixed cap and whatever space actually remains
|
|
74
|
+
// below wherever the menu opens - a menu taller than that scrolls
|
|
75
|
+
// internally instead of extending past the viewport OR growing
|
|
76
|
+
// without bound on a tall screen.
|
|
77
|
+
const maxHeight = Math.min(MAX_MENU_HEIGHT, viewportHeight - clampedTop - margin);
|
|
78
|
+
setPlacement({ left: clampedLeft, top: clampedTop, maxHeight });
|
|
79
|
+
}, [x, y, items.length]);
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (!placement)
|
|
82
|
+
return;
|
|
83
|
+
const buttons = rootRef.current?.querySelectorAll("[role=menuitem]");
|
|
84
|
+
buttons?.[0]?.focus();
|
|
85
|
+
}, [placement]);
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
const dismissIfOutside = (event) => {
|
|
88
|
+
if (rootRef.current && !rootRef.current.contains(event.target))
|
|
89
|
+
onDismissRef.current();
|
|
90
|
+
};
|
|
91
|
+
// Both pointerdown and mousedown: pointerdown is the primary signal,
|
|
92
|
+
// but not every input path is guaranteed to dispatch PointerEvents in
|
|
93
|
+
// every embedding context - mousedown is the same "was this outside"
|
|
94
|
+
// check as a defense-in-depth fallback, not a second independent path.
|
|
95
|
+
window.addEventListener("pointerdown", dismissIfOutside, true);
|
|
96
|
+
window.addEventListener("mousedown", dismissIfOutside, true);
|
|
97
|
+
return () => {
|
|
98
|
+
window.removeEventListener("pointerdown", dismissIfOutside, true);
|
|
99
|
+
window.removeEventListener("mousedown", dismissIfOutside, true);
|
|
100
|
+
};
|
|
101
|
+
}, []);
|
|
102
|
+
return (_jsxs("div", { ref: rootRef, "data-srte-context-menu": "true", role: "menu", "aria-label": "Editor actions", style: {
|
|
103
|
+
position: "fixed",
|
|
104
|
+
left: placement?.left ?? x,
|
|
105
|
+
top: placement?.top ?? y,
|
|
106
|
+
// Hidden (not unrendered) until the real size is measured and a
|
|
107
|
+
// final, in-viewport position is computed - unrendered would mean
|
|
108
|
+
// getBoundingClientRect() has nothing to measure; visible-but-
|
|
109
|
+
// unpositioned would flash at the wrong spot for a frame first.
|
|
110
|
+
visibility: placement ? "visible" : "hidden",
|
|
111
|
+
zIndex: 80,
|
|
112
|
+
minWidth: 200,
|
|
113
|
+
maxWidth: "calc(100vw - 16px)",
|
|
114
|
+
maxHeight: placement ? placement.maxHeight : "calc(100vh - 16px)",
|
|
115
|
+
overflowY: "auto",
|
|
116
|
+
boxSizing: "border-box",
|
|
117
|
+
background: "var(--srte-menu-bg)",
|
|
118
|
+
color: "var(--srte-menu-text)",
|
|
119
|
+
border: "1px solid var(--srte-border)",
|
|
120
|
+
borderRadius: 10,
|
|
121
|
+
boxShadow: "var(--srte-menu-shadow)",
|
|
122
|
+
padding: 6,
|
|
123
|
+
}, onKeyDown: (event) => {
|
|
124
|
+
if (event.key === "Escape") {
|
|
125
|
+
event.preventDefault();
|
|
126
|
+
onDismiss();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (event.key !== "ArrowDown" && event.key !== "ArrowUp")
|
|
130
|
+
return;
|
|
131
|
+
event.preventDefault();
|
|
132
|
+
const buttons = Array.from(rootRef.current?.querySelectorAll("[role=menuitem]") || []);
|
|
133
|
+
if (!buttons.length)
|
|
134
|
+
return;
|
|
135
|
+
const currentIndex = buttons.indexOf(document.activeElement);
|
|
136
|
+
const nextIndex = event.key === "ArrowDown"
|
|
137
|
+
? (currentIndex + 1) % buttons.length
|
|
138
|
+
: (currentIndex - 1 + buttons.length) % buttons.length;
|
|
139
|
+
buttons[nextIndex]?.focus();
|
|
140
|
+
}, children: [items.length === 0 && (_jsx("div", { style: { padding: "8px 10px", fontSize: 13, color: "var(--srte-menu-text)", opacity: 0.6 }, children: "No actions here" })), items.map((item) => (_jsx("button", { type: "button", role: "menuitem", "data-srte-context-menu-item": item.id, onClick: () => { item.onSelect(); onDismiss(); }, style: itemStyle(item.danger), children: item.label }, item.id)))] }));
|
|
141
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import "katex/contrib/mhchem";
|
|
2
|
+
import { type FormulaLibraryEntry } from "../formulaLibrary.js";
|
|
3
|
+
export interface FormulaLibraryPopoverProps {
|
|
4
|
+
x: number;
|
|
5
|
+
y: number;
|
|
6
|
+
onInsert: (entry: FormulaLibraryEntry) => void;
|
|
7
|
+
onCancel: () => void;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Formula library popover ("Small" scope, 55 entries across 6 domains -
|
|
11
|
+
* see formulaLibrary.ts's own doc comment). Browsable by domain tab and
|
|
12
|
+
* searchable by name; each entry renders a real KaTeX preview (the same
|
|
13
|
+
* renderer the document itself uses) so the user sees exactly what they're
|
|
14
|
+
* about to insert, not a name they have to guess the shape of.
|
|
15
|
+
*/
|
|
16
|
+
export declare function FormulaLibraryPopover({ x, y, onInsert, onCancel }: FormulaLibraryPopoverProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import katex from "katex";
|
|
4
|
+
// Registers \ce{...} for this popover's own chemistry-entry previews - see
|
|
5
|
+
// the matching import's comment in core's surface/renderer.ts for why this
|
|
6
|
+
// is needed once per katex module instance.
|
|
7
|
+
import "katex/contrib/mhchem";
|
|
8
|
+
import { FORMULA_DOMAINS, FORMULA_LIBRARY } from "../formulaLibrary.js";
|
|
9
|
+
const renderPreview = (latex) => {
|
|
10
|
+
try {
|
|
11
|
+
return katex.renderToString(latex, { trust: false, strict: "error", throwOnError: true });
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return latex;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Formula library popover ("Small" scope, 55 entries across 6 domains -
|
|
19
|
+
* see formulaLibrary.ts's own doc comment). Browsable by domain tab and
|
|
20
|
+
* searchable by name; each entry renders a real KaTeX preview (the same
|
|
21
|
+
* renderer the document itself uses) so the user sees exactly what they're
|
|
22
|
+
* about to insert, not a name they have to guess the shape of.
|
|
23
|
+
*/
|
|
24
|
+
export function FormulaLibraryPopover({ x, y, onInsert, onCancel }) {
|
|
25
|
+
const [domain, setDomain] = useState("all");
|
|
26
|
+
const [query, setQuery] = useState("");
|
|
27
|
+
const rootRef = useRef(null);
|
|
28
|
+
const [placement, setPlacement] = useState(null);
|
|
29
|
+
const onCancelRef = useRef(onCancel);
|
|
30
|
+
onCancelRef.current = onCancel;
|
|
31
|
+
useLayoutEffect(() => {
|
|
32
|
+
const el = rootRef.current;
|
|
33
|
+
if (!el)
|
|
34
|
+
return;
|
|
35
|
+
const margin = 8;
|
|
36
|
+
const { width, height } = el.getBoundingClientRect();
|
|
37
|
+
const viewportWidth = window.innerWidth;
|
|
38
|
+
const viewportHeight = window.innerHeight;
|
|
39
|
+
const overflowsRight = x + width > viewportWidth - margin;
|
|
40
|
+
const left = overflowsRight ? x - width : x;
|
|
41
|
+
const clampedLeft = Math.min(Math.max(margin, left), Math.max(margin, viewportWidth - width - margin));
|
|
42
|
+
const overflowsBottom = y + height > viewportHeight - margin;
|
|
43
|
+
const top = overflowsBottom ? y - height : y;
|
|
44
|
+
const clampedTop = Math.min(Math.max(margin, top), Math.max(margin, viewportHeight - margin));
|
|
45
|
+
setPlacement({ left: clampedLeft, top: clampedTop, maxHeight: viewportHeight - clampedTop - margin });
|
|
46
|
+
}, [x, y]);
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
const dismissIfOutside = (event) => {
|
|
49
|
+
if (rootRef.current && !rootRef.current.contains(event.target))
|
|
50
|
+
onCancelRef.current();
|
|
51
|
+
};
|
|
52
|
+
window.addEventListener("pointerdown", dismissIfOutside, true);
|
|
53
|
+
window.addEventListener("mousedown", dismissIfOutside, true);
|
|
54
|
+
return () => {
|
|
55
|
+
window.removeEventListener("pointerdown", dismissIfOutside, true);
|
|
56
|
+
window.removeEventListener("mousedown", dismissIfOutside, true);
|
|
57
|
+
};
|
|
58
|
+
}, []);
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
const onWindowKeyDown = (event) => {
|
|
61
|
+
if (event.key === "Escape")
|
|
62
|
+
onCancelRef.current();
|
|
63
|
+
};
|
|
64
|
+
window.addEventListener("keydown", onWindowKeyDown);
|
|
65
|
+
return () => window.removeEventListener("keydown", onWindowKeyDown);
|
|
66
|
+
}, []);
|
|
67
|
+
const filtered = useMemo(() => {
|
|
68
|
+
const needle = query.trim().toLowerCase();
|
|
69
|
+
return FORMULA_LIBRARY.filter((entry) => (domain === "all" || entry.domain === domain) && (!needle || entry.name.toLowerCase().includes(needle)));
|
|
70
|
+
}, [domain, query]);
|
|
71
|
+
return (_jsxs("div", { ref: rootRef, "data-srte-formula-library": "true", role: "dialog", "aria-label": "Formula library", style: {
|
|
72
|
+
position: "fixed",
|
|
73
|
+
left: placement?.left ?? x,
|
|
74
|
+
top: placement?.top ?? y,
|
|
75
|
+
visibility: placement ? "visible" : "hidden",
|
|
76
|
+
zIndex: 70,
|
|
77
|
+
width: 420,
|
|
78
|
+
maxWidth: "calc(100vw - 16px)",
|
|
79
|
+
maxHeight: placement ? placement.maxHeight : "calc(100vh - 16px)",
|
|
80
|
+
display: "flex",
|
|
81
|
+
flexDirection: "column",
|
|
82
|
+
boxSizing: "border-box",
|
|
83
|
+
background: "var(--srte-menu-bg)",
|
|
84
|
+
color: "var(--srte-menu-text)",
|
|
85
|
+
border: "1px solid var(--srte-border)",
|
|
86
|
+
borderRadius: 12,
|
|
87
|
+
boxShadow: "var(--srte-menu-shadow)",
|
|
88
|
+
overflow: "hidden",
|
|
89
|
+
}, children: [_jsxs("div", { style: { padding: "12px 14px 8px", borderBottom: "1px solid var(--srte-border)" }, children: [_jsx("input", { "data-srte-formula-search": "true", value: query, onChange: (event) => setQuery(event.target.value), placeholder: "Search formulas\u2026 (e.g. \u201Cquadratic\u201D)", "aria-label": "Search formulas", autoComplete: "off", style: {
|
|
90
|
+
width: "100%", height: 34, boxSizing: "border-box", padding: "0 10px",
|
|
91
|
+
border: "1px solid var(--srte-input-border)", borderRadius: 8, outline: "none",
|
|
92
|
+
background: "var(--srte-input-bg)", color: "var(--srte-input-text)", font: "inherit", fontSize: 13.5,
|
|
93
|
+
} }), _jsxs("div", { role: "tablist", "aria-label": "Formula domain", style: { display: "flex", gap: 4, flexWrap: "wrap", marginTop: 10 }, children: [_jsx("button", { type: "button", role: "tab", "aria-selected": domain === "all", onMouseDown: (event) => event.preventDefault(), onClick: () => setDomain("all"), style: tabStyle(domain === "all"), children: "All" }), FORMULA_DOMAINS.map((entry) => (_jsx("button", { type: "button", role: "tab", "aria-selected": domain === entry.id, onMouseDown: (event) => event.preventDefault(), onClick: () => setDomain(entry.id), style: tabStyle(domain === entry.id), children: entry.label }, entry.id)))] })] }), _jsxs("div", { "data-srte-formula-grid": "true", style: {
|
|
94
|
+
display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(120px, 1fr))", gap: 8,
|
|
95
|
+
padding: 12, overflowY: "auto",
|
|
96
|
+
}, children: [filtered.length === 0 && (_jsxs("div", { style: { gridColumn: "1 / -1", fontSize: 13, color: "var(--srte-menu-text)", opacity: 0.7, padding: "12px 4px" }, children: ["No formulas match \u201C", query, "\u201D."] })), filtered.map((entry) => (_jsxs("button", { type: "button", "data-srte-formula-entry": entry.id, "aria-label": `Insert ${entry.name}`, title: entry.name, onMouseDown: (event) => event.preventDefault(), onClick: () => onInsert(entry), style: {
|
|
97
|
+
display: "flex", flexDirection: "column", alignItems: "center", gap: 8,
|
|
98
|
+
border: "1px solid var(--srte-input-border)", borderRadius: 10, padding: "10px 8px",
|
|
99
|
+
background: "var(--srte-input-bg)", cursor: "pointer", minHeight: 76,
|
|
100
|
+
}, children: [_jsx("span", { "aria-hidden": "true", style: { fontSize: 14, lineHeight: 1.2, minHeight: 22, display: "flex", alignItems: "center", justifyContent: "center", overflow: "hidden", maxWidth: "100%" }, dangerouslySetInnerHTML: { __html: renderPreview(entry.latex) } }), _jsx("span", { style: { fontSize: 11, color: "var(--srte-menu-text)", opacity: 0.75, textAlign: "center" }, children: entry.name })] }, entry.id)))] })] }));
|
|
101
|
+
}
|
|
102
|
+
const tabStyle = (active) => ({
|
|
103
|
+
fontSize: 12, fontWeight: 600, padding: "5px 10px", borderRadius: 7, cursor: "pointer",
|
|
104
|
+
border: "1px solid " + (active ? "var(--srte-primary)" : "transparent"),
|
|
105
|
+
background: active ? "var(--srte-accent-bg)" : "transparent",
|
|
106
|
+
color: active ? "var(--srte-primary)" : "var(--srte-menu-text)",
|
|
107
|
+
});
|
|
@@ -12,9 +12,11 @@ export interface LinkEditorPopoverProps {
|
|
|
12
12
|
showTextInput?: boolean;
|
|
13
13
|
showOpen?: boolean;
|
|
14
14
|
showRemove?: boolean;
|
|
15
|
+
/** Defaults to true (the original toolbar-Link-button behavior: the user asked to edit, so jump straight to the href field). Pass false when the popover opened itself just because the caret entered a link - autofocusing there would steal focus from the editor on every ordinary click/caret-move, trapping the cursor the overlay exists specifically to avoid. */
|
|
16
|
+
autoFocus?: boolean;
|
|
15
17
|
onApply: (value: LinkEditorApplyValue) => void;
|
|
16
18
|
onOpen?: () => void;
|
|
17
19
|
onRemove?: () => void;
|
|
18
20
|
onCancel: () => void;
|
|
19
21
|
}
|
|
20
|
-
export declare function LinkEditorPopover({ x, y, initialHref, initialText, initialOpenInNewTab, showTextInput, showOpen, showRemove, onApply, onOpen, onRemove, onCancel, }: LinkEditorPopoverProps): import("react/jsx-runtime").JSX.Element;
|
|
22
|
+
export declare function LinkEditorPopover({ x, y, initialHref, initialText, initialOpenInNewTab, showTextInput, showOpen, showRemove, autoFocus, onApply, onOpen, onRemove, onCancel, }: LinkEditorPopoverProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useId, useRef, useState } from "react";
|
|
3
|
-
import { normalizeLinkInput } from "smartrte-core";
|
|
3
|
+
import { normalizeLinkInput } from "smartrte-core/legacy";
|
|
4
4
|
const inputStyle = {
|
|
5
5
|
width: "100%",
|
|
6
6
|
height: 36,
|
|
@@ -24,7 +24,7 @@ const buttonStyle = {
|
|
|
24
24
|
cursor: "pointer",
|
|
25
25
|
fontWeight: 500,
|
|
26
26
|
};
|
|
27
|
-
export function LinkEditorPopover({ x, y, initialHref = "", initialText = "", initialOpenInNewTab = false, showTextInput = false, showOpen = false, showRemove = false, onApply, onOpen, onRemove, onCancel, }) {
|
|
27
|
+
export function LinkEditorPopover({ x, y, initialHref = "", initialText = "", initialOpenInNewTab = false, showTextInput = false, showOpen = false, showRemove = false, autoFocus = true, onApply, onOpen, onRemove, onCancel, }) {
|
|
28
28
|
const [href, setHref] = useState(initialHref);
|
|
29
29
|
const [text, setText] = useState(initialText);
|
|
30
30
|
const [openInNewTab, setOpenInNewTab] = useState(initialOpenInNewTab);
|
|
@@ -33,8 +33,24 @@ export function LinkEditorPopover({ x, y, initialHref = "", initialText = "", in
|
|
|
33
33
|
const titleId = useId();
|
|
34
34
|
const errorId = useId();
|
|
35
35
|
useEffect(() => {
|
|
36
|
+
if (!autoFocus)
|
|
37
|
+
return;
|
|
36
38
|
hrefRef.current?.focus();
|
|
37
39
|
hrefRef.current?.select();
|
|
40
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
41
|
+
}, []);
|
|
42
|
+
// Escape must dismiss regardless of where focus actually is - when
|
|
43
|
+
// autoFocus is false (the auto-triggered case), focus deliberately stays
|
|
44
|
+
// on the editor, so the div's own onKeyDown below (which only fires while
|
|
45
|
+
// focus is inside the popover) would never see the keypress.
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
const onWindowKeyDown = (event) => {
|
|
48
|
+
if (event.key === "Escape")
|
|
49
|
+
onCancel();
|
|
50
|
+
};
|
|
51
|
+
window.addEventListener("keydown", onWindowKeyDown);
|
|
52
|
+
return () => window.removeEventListener("keydown", onWindowKeyDown);
|
|
53
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
38
54
|
}, []);
|
|
39
55
|
const apply = () => {
|
|
40
56
|
const normalized = normalizeLinkInput(href);
|
|
@@ -69,10 +85,10 @@ export function LinkEditorPopover({ x, y, initialHref = "", initialText = "", in
|
|
|
69
85
|
boxShadow: "var(--srte-menu-shadow)",
|
|
70
86
|
padding: 14,
|
|
71
87
|
}, onKeyDown: (event) => {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
88
|
+
// Escape is handled by the window-level listener above (needs to
|
|
89
|
+
// work even when autoFocus=false and focus never entered the
|
|
90
|
+
// popover) - not duplicated here to avoid a double-dismiss when
|
|
91
|
+
// focus *is* inside.
|
|
76
92
|
if (event.key === "Enter" && !event.shiftKey) {
|
|
77
93
|
event.preventDefault();
|
|
78
94
|
apply();
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export type MediaAlign = "left" | "center" | "right" | undefined;
|
|
2
|
+
export interface MediaDetailsDraft {
|
|
3
|
+
alt: string;
|
|
4
|
+
href: string;
|
|
5
|
+
openInNewTab: boolean;
|
|
6
|
+
width: number | undefined;
|
|
7
|
+
borderRadius: number | undefined;
|
|
8
|
+
align: MediaAlign;
|
|
9
|
+
licenseDescription: string;
|
|
10
|
+
licenseSourceUrl: string;
|
|
11
|
+
licenseType: string;
|
|
12
|
+
licenseVersion: string;
|
|
13
|
+
licenseAttribution: string;
|
|
14
|
+
}
|
|
15
|
+
export interface MediaDetailsPopoverProps {
|
|
16
|
+
x: number;
|
|
17
|
+
y: number;
|
|
18
|
+
initial: MediaDetailsDraft;
|
|
19
|
+
onApply: (draft: MediaDetailsDraft) => void;
|
|
20
|
+
onCancel: () => void;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* "Media details" - replaces the crude window.prompt("Alt text") edit path
|
|
24
|
+
* (docs/bugs/media-details-old-editor-field-parity.md) with a real settings
|
|
25
|
+
* panel matching the old editor's own field set, following this project's
|
|
26
|
+
* established compact-popover pattern (position:fixed + JS-measured
|
|
27
|
+
* placement, Apply/Cancel commit - same shape as TableBorderPopover for a
|
|
28
|
+
* multi-field dialog like this one, rather than LinkEditorPopover's simpler
|
|
29
|
+
* live-Enter-to-apply form).
|
|
30
|
+
*
|
|
31
|
+
* Opened directly by an image's right-click interaction; the details panel
|
|
32
|
+
* is the image editing surface, while resize handles remain available
|
|
33
|
+
* independently around the selected image.
|
|
34
|
+
*/
|
|
35
|
+
export declare function MediaDetailsPopover({ x, y, initial, onApply, onCancel }: MediaDetailsPopoverProps): import("react/jsx-runtime").JSX.Element;
|