smartrte-react 1.0.0-beta.5 → 1.0.0-beta.8
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 +15 -0
- package/README.md +94 -0
- package/dist/components/CanonicalAuthorityEditor.d.ts +15 -1
- package/dist/components/CanonicalAuthorityEditor.js +198 -46
- package/dist/components/MediaDetailsPopover.d.ts +35 -0
- package/dist/components/MediaDetailsPopover.js +136 -0
- package/dist/components/MediaOverlay.d.ts +10 -21
- package/dist/components/MediaOverlay.js +128 -63
- package/dist/components/ToolbarPrimitives.d.ts +14 -0
- package/dist/components/ToolbarPrimitives.js +45 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -0
- package/dist/standalone/classic-editor-embed.js +1 -1
- package/dist/theme.d.ts +1 -1
- package/dist/theme.js +6 -2
- package/dist/toolbarTools.d.ts +89 -0
- package/dist/toolbarTools.js +16 -0
- package/package.json +2 -2
|
@@ -19,8 +19,10 @@ import { SpecialCharacterPopover } from "./SpecialCharacterPopover.js";
|
|
|
19
19
|
import { TableBorderPopover, BORDER_WIDTH_PRESETS } from "./TableBorderPopover.js";
|
|
20
20
|
import { TableResizeHandles } from "./TableResizeHandles.js";
|
|
21
21
|
import { MediaOverlay } from "./MediaOverlay.js";
|
|
22
|
+
import { MediaDetailsPopover } from "./MediaDetailsPopover.js";
|
|
22
23
|
import { ContextMenu } from "./ContextMenu.js";
|
|
23
24
|
import { ToolbarButton, ToolbarDropdown, ToolbarGroup, ToolbarMenuItem, MobileMoreMenu } from "./ToolbarPrimitives.js";
|
|
25
|
+
import { resolveToolbarTools } from "../toolbarTools.js";
|
|
24
26
|
import { exportDocxDocument, importStyledDocxDocument, importPdfDocument } from "smartrte-core/foundation";
|
|
25
27
|
import { printSmartDocumentAsPdf } from "../adapters/pdfPrint.js";
|
|
26
28
|
import { CanonicalEditorRuntime, } from "../canonicalEditorRuntime.js";
|
|
@@ -102,7 +104,7 @@ const parseBorderShorthand = (value) => {
|
|
|
102
104
|
const isCollapsedTextSelection = (selection) => selection.type === "text" && selection.anchor.offset === selection.head.offset
|
|
103
105
|
&& selection.anchor.path.length === selection.head.path.length
|
|
104
106
|
&& selection.anchor.path.every((part, index) => part === selection.head.path[index]);
|
|
105
|
-
export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEditor({ defaultValue, preset, renderFormulaHtml, onChange, onHtmlChange, onClipboardDiagnostic, mediaProvider, mediaPicker: MediaPicker = DefaultMediaPicker, mediaManager = true, versionProvider, commentProvider, suggestionProvider, authorId, placeholder = "Type here…", minHeight = 200, maxHeight = 500, readOnly = false, className, onRuntime, }, forwardedRef) {
|
|
107
|
+
export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEditor({ defaultValue, preset, renderFormulaHtml, onChange, onHtmlChange, onClipboardDiagnostic, mediaProvider, mediaPicker: MediaPicker = DefaultMediaPicker, mediaManager = true, tools, versionProvider, commentProvider, suggestionProvider, authorId, placeholder = "Type here…", minHeight = 200, maxHeight = 500, readOnly = false, className, onRuntime, }, forwardedRef) {
|
|
106
108
|
ensureStyleSheet();
|
|
107
109
|
const rootRef = useRef(null);
|
|
108
110
|
const importRef = useRef(null);
|
|
@@ -129,6 +131,7 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
|
|
|
129
131
|
const [recentColors, setRecentColors] = useState({ text: [], background: [], border: [] });
|
|
130
132
|
const [tableSizePopover, setTableSizePopover] = useState(null);
|
|
131
133
|
const [tableBorderPopover, setTableBorderPopover] = useState(null);
|
|
134
|
+
const [mediaDetailsPopover, setMediaDetailsPopover] = useState(null);
|
|
132
135
|
const borderPreviewCheckpointRef = useRef(null);
|
|
133
136
|
const [formulaLibraryPopover, setFormulaLibraryPopover] = useState(null);
|
|
134
137
|
const [specialCharPopover, setSpecialCharPopover] = useState(null);
|
|
@@ -140,6 +143,8 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
|
|
|
140
143
|
// since nothing was ever applied to the model).
|
|
141
144
|
const colorPreviewCheckpointRef = useRef(null);
|
|
142
145
|
const [contextMenu, setContextMenu] = useState(null);
|
|
146
|
+
const [mediaContextMenu, setMediaContextMenu] = useState(null);
|
|
147
|
+
const [mediaResizeTarget, setMediaResizeTarget] = useState(null);
|
|
143
148
|
// Tracks the exact caret position (path+offset) the link overlay was
|
|
144
149
|
// last dismissed at (Escape/outside click) - suppresses an instant
|
|
145
150
|
// re-popup while the caret hasn't moved, without needing a separate
|
|
@@ -320,6 +325,19 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
|
|
|
320
325
|
// an `unknown` node (Phase 10's disable-safety contract, unchanged) -
|
|
321
326
|
// this only hides the *toolbar affordance* for creating new ones.
|
|
322
327
|
const tablesEnabled = Boolean(runtime.editor.schema.nodes.table);
|
|
328
|
+
// Per-tool toolbar visibility (toolbarTools.ts) - `tools` is a pure
|
|
329
|
+
// UI-visibility layer, composed with (never overriding) the deeper
|
|
330
|
+
// existing gates below: a required provider being absent, or a plugin
|
|
331
|
+
// having been excluded via `preset`, still wins even if a tool is
|
|
332
|
+
// explicitly left enabled here.
|
|
333
|
+
const t = resolveToolbarTools(tools);
|
|
334
|
+
const showInsertTable = t.insertTable && tablesEnabled;
|
|
335
|
+
const showImageTool = t.image && Boolean(mediaProvider);
|
|
336
|
+
const showVideoTool = t.video && Boolean(mediaProvider);
|
|
337
|
+
const showAudioTool = t.audio && Boolean(mediaProvider);
|
|
338
|
+
const showVersionHistoryTool = t.versionHistory && Boolean(versionProvider);
|
|
339
|
+
const showCommentsTool = t.comments && Boolean(commentProvider);
|
|
340
|
+
const showSuggestionsTool = t.suggestions && Boolean(suggestionProvider);
|
|
323
341
|
const tableSelected = currentTableScope.kind === "table-grid";
|
|
324
342
|
const selectedTableElement = tableSelected
|
|
325
343
|
? runtime.surface.renderer?.mapping.nodeToDom(currentTableScope.tableId)
|
|
@@ -348,6 +366,15 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
|
|
|
348
366
|
// node.type.includes("formula") for its own correct "Formula source"
|
|
349
367
|
// prompt.
|
|
350
368
|
const resizableAtomSelected = mediaAtomSelected && selectedAtomNode?.type !== "formula" && selectedAtomNode?.type !== "block_formula";
|
|
369
|
+
const selectedAtomId = atomSelected && "nodeId" in currentAtomScope ? currentAtomScope.nodeId : null;
|
|
370
|
+
useEffect(() => {
|
|
371
|
+
if (mediaContextMenu && (!mediaAtomSelected || mediaContextMenu.nodeId !== selectedAtomId))
|
|
372
|
+
setMediaContextMenu(null);
|
|
373
|
+
}, [mediaContextMenu, mediaAtomSelected, selectedAtomId]);
|
|
374
|
+
useEffect(() => {
|
|
375
|
+
if (mediaResizeTarget && (!mediaAtomSelected || mediaResizeTarget.nodeId !== selectedAtomId))
|
|
376
|
+
setMediaResizeTarget(null);
|
|
377
|
+
}, [mediaResizeTarget, mediaAtomSelected, selectedAtomId]);
|
|
351
378
|
// Context menu scope reduction: link no longer gets a right-click menu -
|
|
352
379
|
// LinkEditorPopover (already built and wired for the toolbar's Link
|
|
353
380
|
// button, per §1's investigation) auto-appears, anchored to the link's
|
|
@@ -803,13 +830,10 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
|
|
|
803
830
|
* the same way Tab special-cases list.indent/outdent's dynamic params.
|
|
804
831
|
*/
|
|
805
832
|
/**
|
|
806
|
-
*
|
|
807
|
-
*
|
|
808
|
-
*
|
|
809
|
-
*
|
|
810
|
-
* all (marks/plugin.ts, atom/plugin.ts) or get hardcoded item blocks
|
|
811
|
-
* here - only table plugin contributions (the generic loop) and the
|
|
812
|
-
* cell-colour items remain.
|
|
833
|
+
* The generic plugin context menu remains table-scoped. Images use their
|
|
834
|
+
* direct right-click details editor below, other media keeps its dedicated
|
|
835
|
+
* quick-action menu, while marks stay on the toolbar and links use the
|
|
836
|
+
* auto-triggered LinkEditorPopover.
|
|
813
837
|
*/
|
|
814
838
|
const resolveContextMenuItems = () => {
|
|
815
839
|
const many = () => ids(128);
|
|
@@ -1028,6 +1052,19 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
|
|
|
1028
1052
|
insertBlockAtom("block_image", {
|
|
1029
1053
|
src: item.url, alt: item.alt || item.title || "Image", decorative: false, status: "ready",
|
|
1030
1054
|
...(item.width ? { width: item.width } : {}), ...(item.height ? { height: item.height } : {}),
|
|
1055
|
+
// docs/bugs/media-details-old-editor-field-parity.md: item.license was
|
|
1056
|
+
// already fetched and shown in MediaManager's own library-browser info
|
|
1057
|
+
// panel (MediaManager.tsx), but had nowhere to persist once inserted -
|
|
1058
|
+
// the atom schema had no license attrs at all, so this real,
|
|
1059
|
+
// already-available data was silently discarded at exactly this call
|
|
1060
|
+
// site. `licenseVersion` has no MediaItem.license equivalent (its
|
|
1061
|
+
// shape has no separate version field) - left for the user to fill in
|
|
1062
|
+
// via the media details panel, same as a freshly uploaded image.
|
|
1063
|
+
...(item.license?.workName || item.license?.licenseText
|
|
1064
|
+
? { licenseDescription: [item.license.workName, item.license.licenseText].filter(Boolean).join(" - ") } : {}),
|
|
1065
|
+
...(item.license?.sourceUrl ? { licenseSourceUrl: item.license.sourceUrl } : {}),
|
|
1066
|
+
...(item.license?.licenseType ? { licenseType: item.license.licenseType } : {}),
|
|
1067
|
+
...(item.license?.author ? { licenseAttribution: item.license.author } : {}),
|
|
1031
1068
|
}, createNodeId());
|
|
1032
1069
|
};
|
|
1033
1070
|
const commentAuthorId = authorId || "anonymous";
|
|
@@ -1211,6 +1248,27 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
|
|
|
1211
1248
|
runtime.focus();
|
|
1212
1249
|
setLinkPopover(null);
|
|
1213
1250
|
};
|
|
1251
|
+
const openMediaDetails = (scope, node, element) => {
|
|
1252
|
+
if (scope.kind !== "atomic-node")
|
|
1253
|
+
return;
|
|
1254
|
+
const rect = (element || selectedAtomElement)?.getBoundingClientRect();
|
|
1255
|
+
setMediaDetailsPopover({
|
|
1256
|
+
x: rect ? rect.left : 0, y: rect ? rect.bottom + 6 : 0, scope,
|
|
1257
|
+
initial: {
|
|
1258
|
+
alt: String(node.attrs?.alt || ""),
|
|
1259
|
+
href: String(node.attrs?.href || ""),
|
|
1260
|
+
openInNewTab: node.attrs?.target === "_blank",
|
|
1261
|
+
width: typeof node.attrs?.width === "number" ? node.attrs.width : undefined,
|
|
1262
|
+
borderRadius: typeof node.attrs?.borderRadius === "number" ? node.attrs.borderRadius : undefined,
|
|
1263
|
+
align: node.attrs?.align === "left" || node.attrs?.align === "center" || node.attrs?.align === "right" ? node.attrs.align : undefined,
|
|
1264
|
+
licenseDescription: String(node.attrs?.licenseDescription || ""),
|
|
1265
|
+
licenseSourceUrl: String(node.attrs?.licenseSourceUrl || ""),
|
|
1266
|
+
licenseType: String(node.attrs?.licenseType || ""),
|
|
1267
|
+
licenseVersion: String(node.attrs?.licenseVersion || ""),
|
|
1268
|
+
licenseAttribution: String(node.attrs?.licenseAttribution || ""),
|
|
1269
|
+
},
|
|
1270
|
+
});
|
|
1271
|
+
};
|
|
1214
1272
|
const editSelectedAtom = (resizeBy) => {
|
|
1215
1273
|
const scope = atomScope();
|
|
1216
1274
|
if (scope.kind !== "atomic-node")
|
|
@@ -1218,16 +1276,59 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
|
|
|
1218
1276
|
const node = findNode(runtime.editor.document, scope.nodeId);
|
|
1219
1277
|
if (!node)
|
|
1220
1278
|
return;
|
|
1279
|
+
if (resizeBy === undefined && !node.type.includes("formula")) {
|
|
1280
|
+
// Images: the old window.prompt("Alt text") one-field edit is
|
|
1281
|
+
// replaced by the full Media details panel (docs/bugs/
|
|
1282
|
+
// media-details-old-editor-field-parity.md) - formulas keep their
|
|
1283
|
+
// existing prompt unchanged below, since this panel's fields
|
|
1284
|
+
// (link/align/radius/license) don't apply to a formula atom at all.
|
|
1285
|
+
openMediaDetails(scope, node);
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1221
1288
|
const operations = resizeBy === undefined
|
|
1222
|
-
? updateAtom(runtime.editor.document, scope,
|
|
1223
|
-
? { attrs: { source: window.prompt("Formula source", String(node.attrs?.source || "")) || node.attrs?.source } }
|
|
1224
|
-
: { attrs: { alt: window.prompt("Alt text", String(node.attrs?.alt || "")) ?? node.attrs?.alt } }, blockContext())
|
|
1289
|
+
? updateAtom(runtime.editor.document, scope, { attrs: { source: window.prompt("Formula source", String(node.attrs?.source || "")) || node.attrs?.source } }, blockContext())
|
|
1225
1290
|
: resizeAtom(runtime.editor.document, scope, {
|
|
1226
1291
|
width: Math.max(16, Number(node.attrs?.width || 160) + resizeBy),
|
|
1227
1292
|
height: Math.max(16, Number(node.attrs?.height || 90) + resizeBy),
|
|
1228
1293
|
}, blockContext());
|
|
1229
1294
|
runtime.executeOperations(operations, resizeBy === undefined ? {} : { historyGroup: `resize-${scope.nodeId}` });
|
|
1230
1295
|
};
|
|
1296
|
+
const applyMediaDetails = (draft) => {
|
|
1297
|
+
if (!mediaDetailsPopover)
|
|
1298
|
+
return;
|
|
1299
|
+
const { scope } = mediaDetailsPopover;
|
|
1300
|
+
if (scope.kind !== "atomic-node") {
|
|
1301
|
+
setMediaDetailsPopover(null);
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
const node = findNode(runtime.editor.document, scope.nodeId);
|
|
1305
|
+
const currentWidth = typeof node?.attrs?.width === "number" ? node.attrs.width : undefined;
|
|
1306
|
+
const currentHeight = typeof node?.attrs?.height === "number" ? node.attrs.height : undefined;
|
|
1307
|
+
// Width preserves the original aspect ratio (matches the drag handle
|
|
1308
|
+
// and Resize +/- buttons' own behavior) rather than exposing a separate
|
|
1309
|
+
// Height field the old editor's own screenshot didn't have either.
|
|
1310
|
+
const resizedHeight = draft.width && currentWidth && currentHeight
|
|
1311
|
+
? Math.round((draft.width * currentHeight) / currentWidth)
|
|
1312
|
+
: currentHeight;
|
|
1313
|
+
setMediaDetailsPopover(null);
|
|
1314
|
+
runtime.executeOperations(updateAtom(runtime.editor.document, scope, {
|
|
1315
|
+
attrs: {
|
|
1316
|
+
alt: draft.alt,
|
|
1317
|
+
href: draft.href || undefined,
|
|
1318
|
+
target: draft.href && draft.openInNewTab ? "_blank" : undefined,
|
|
1319
|
+
width: draft.width ?? currentWidth,
|
|
1320
|
+
height: draft.width ? resizedHeight : currentHeight,
|
|
1321
|
+
borderRadius: draft.borderRadius || undefined,
|
|
1322
|
+
align: draft.align,
|
|
1323
|
+
licenseDescription: draft.licenseDescription || undefined,
|
|
1324
|
+
licenseSourceUrl: draft.licenseSourceUrl || undefined,
|
|
1325
|
+
licenseType: draft.licenseType || undefined,
|
|
1326
|
+
licenseVersion: draft.licenseVersion || undefined,
|
|
1327
|
+
licenseAttribution: draft.licenseAttribution || undefined,
|
|
1328
|
+
},
|
|
1329
|
+
}, blockContext()));
|
|
1330
|
+
runtime.focus();
|
|
1331
|
+
};
|
|
1231
1332
|
/** MediaOverlay's drag-corner-handle commit - explicit width/height rather than editSelectedAtom's fixed +/-20 increment. */
|
|
1232
1333
|
const resizeSelectedAtomTo = (width, height) => {
|
|
1233
1334
|
const scope = atomScope();
|
|
@@ -1326,23 +1427,44 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
|
|
|
1326
1427
|
// twice; below 1440px (including mobile) this copy is what's reachable,
|
|
1327
1428
|
// same as before. "Code" isn't promoted - see the docs/bugs/ writeup on
|
|
1328
1429
|
// why it stayed the dropdown's sole remaining item at wide widths.
|
|
1329
|
-
const textStylesMenuItems = _jsxs(_Fragment, { children: [_jsx(ToolbarMenuItem, { icon: "code", label: "Code", pressed: markCoverage("inlineCode"), disabled: readOnly, onClick: () => toggleMark("inlineCode") }), _jsx(ToolbarMenuItem, { icon: "superscript", label: "Superscript", pressed: markCoverage("superscript"), disabled: readOnly, onClick: () => toggleMark("superscript"), widePromote: true }), _jsx(ToolbarMenuItem, { icon: "subscript", label: "Subscript", pressed: markCoverage("subscript"), disabled: readOnly, onClick: () => toggleMark("subscript"), widePromote: true }), _jsx(ToolbarMenuItem, { icon: "textColor", label: "Text colour", disabled: readOnly, widePromote: true, onClick: (event) => {
|
|
1430
|
+
const textStylesMenuItems = _jsxs(_Fragment, { children: [t.code && _jsx(ToolbarMenuItem, { icon: "code", label: "Code", pressed: markCoverage("inlineCode"), disabled: readOnly, onClick: () => toggleMark("inlineCode") }), t.superscript && _jsx(ToolbarMenuItem, { icon: "superscript", label: "Superscript", pressed: markCoverage("superscript"), disabled: readOnly, onClick: () => toggleMark("superscript"), widePromote: true }), t.subscript && _jsx(ToolbarMenuItem, { icon: "subscript", label: "Subscript", pressed: markCoverage("subscript"), disabled: readOnly, onClick: () => toggleMark("subscript"), widePromote: true }), t.textColor && _jsx(ToolbarMenuItem, { icon: "textColor", label: "Text colour", disabled: readOnly, widePromote: true, onClick: (event) => {
|
|
1330
1431
|
const rect = event.currentTarget.getBoundingClientRect();
|
|
1331
1432
|
setColorPopover({ x: rect.left, y: rect.bottom + 4, target: { kind: "mark", markId: "textColor" }, initialValue: currentMarkColor("textColor") });
|
|
1332
|
-
} }), _jsx(ToolbarMenuItem, { icon: "backgroundColor", label: "Background colour", disabled: readOnly, widePromote: true, onClick: (event) => {
|
|
1433
|
+
} }), t.backgroundColor && _jsx(ToolbarMenuItem, { icon: "backgroundColor", label: "Background colour", disabled: readOnly, widePromote: true, onClick: (event) => {
|
|
1333
1434
|
const rect = event.currentTarget.getBoundingClientRect();
|
|
1334
1435
|
setColorPopover({ x: rect.left, y: rect.bottom + 4, target: { kind: "mark", markId: "backgroundColor" }, initialValue: currentMarkColor("backgroundColor") });
|
|
1335
|
-
} }), _jsx(ToolbarMenuItem, { icon: "fontSize", label: "Font size", disabled: readOnly, onClick: () => applyAttributedMark("fontSize"), widePromote: true }), _jsx(ToolbarMenuItem, { icon: "fontFamily", label: "Font family", disabled: readOnly, onClick: () => applyAttributedMark("fontFamily"), widePromote: true })] });
|
|
1436
|
+
} }), t.fontSize && _jsx(ToolbarMenuItem, { icon: "fontSize", label: "Font size", disabled: readOnly, onClick: () => applyAttributedMark("fontSize"), widePromote: true }), t.fontFamily && _jsx(ToolbarMenuItem, { icon: "fontFamily", label: "Font family", disabled: readOnly, onClick: () => applyAttributedMark("fontFamily"), widePromote: true })] });
|
|
1437
|
+
const showMoreTextStyles = t.code || t.superscript || t.subscript || t.textColor || t.backgroundColor || t.fontSize || t.fontFamily;
|
|
1336
1438
|
const paragraphToolsMenuItems = _jsxs(_Fragment, { children: [_jsx(ToolbarMenuItem, { icon: "moveUp", label: "Move block up", disabled: readOnly, onClick: () => runBlock("up") }), _jsx(ToolbarMenuItem, { icon: "moveDown", label: "Move block down", disabled: readOnly, onClick: () => runBlock("down") }), _jsx(ToolbarMenuItem, { icon: "indent", label: "Indent block", disabled: readOnly, onClick: () => runBlock("indent") }), _jsx(ToolbarMenuItem, { icon: "outdent", label: "Outdent block", disabled: readOnly, onClick: () => runBlock("outdent") })] });
|
|
1439
|
+
/**
|
|
1440
|
+
* Named multi-level marker presets (decimal/lower-alpha/upper-alpha/
|
|
1441
|
+
* roman/outline) - a distinct control from the plain Bulleted/Numbered/
|
|
1442
|
+
* Checklist buttons above, which only toggle a raw list style. Extracted
|
|
1443
|
+
* to a variable so it can be reused verbatim in both the desktop "More
|
|
1444
|
+
* list tools" dropdown and MobileMoreMenu below - it was previously
|
|
1445
|
+
* hardcoded only into the dropdown, meaning a narrow/mobile viewport
|
|
1446
|
+
* (where every ToolbarDropdown hides entirely, per theme.ts's
|
|
1447
|
+
* 639px breakpoint) had no way to reach it at all - see
|
|
1448
|
+
* docs/bugs/list-preset-missing-from-mobile-more-menu.md.
|
|
1449
|
+
*/
|
|
1450
|
+
const listPresetSelect = t.listPreset && _jsx("div", { style: { padding: "4px 8px" }, children: _jsxs("select", { "aria-label": "List preset", title: "List type / preset", disabled: readOnly || currentListParts.length !== 1, value: currentListPreset, onChange: (event) => {
|
|
1451
|
+
const preset = event.target.value;
|
|
1452
|
+
if (!preset || currentListParts.length !== 1)
|
|
1453
|
+
return;
|
|
1454
|
+
// A preset choice applies to the whole list, regardless of how deep
|
|
1455
|
+
// the cursor is nested — see outermostListId.
|
|
1456
|
+
const rootId = outermostListId(currentListParts[0].listId);
|
|
1457
|
+
runtime.executeOperations(setListPreset(runtime.editor.document, { ...currentListParts[0], listId: rootId }, { preset }, blockContext()), { preserveSelectionById: true });
|
|
1458
|
+
}, style: { width: "100%" }, children: [_jsx("option", { value: "", children: "List preset" }), SMART_LIST_PRESETS.map((preset) => _jsx("option", { value: preset.id, children: preset.kind === "bullet" ? `Bullet · ${preset.label}` : `Number · ${preset.label}` }, preset.id))] }) });
|
|
1337
1459
|
const listToolsMenuItems = _jsxs(_Fragment, { children: [_jsx(ToolbarMenuItem, { icon: "checkSquare", label: "Check selected items", pressed: currentListScope.kind === "list-selection" && currentListScope.items.every((item) => findNode(runtime.editor.document, item.itemId)?.attrs?.checked === true), disabled: readOnly || currentList?.attrs?.checkable !== true, onClick: toggleCheckedItems }), _jsx(ToolbarMenuItem, { icon: "indent", label: "Indent list item", disabled: readOnly || !canIndent, onClick: () => runList("indent") }), _jsx(ToolbarMenuItem, { icon: "outdent", label: "Outdent list item", disabled: readOnly || currentListParts.length === 0, onClick: () => runList("outdent") }), _jsx(ToolbarMenuItem, { icon: "moveUp", label: "Move item up", disabled: readOnly || !canMoveUp, onClick: () => runList("up") }), _jsx(ToolbarMenuItem, { icon: "moveDown", label: "Move item down", disabled: readOnly || !canMoveDown, onClick: () => runList("down") }), _jsx(ToolbarMenuItem, { icon: "restart", label: "Restart numbering", disabled: readOnly || !orderedList, onClick: restartNumbering }), _jsx(ToolbarMenuItem, { icon: "continueNumbering", label: "Continue numbering", disabled: readOnly || !orderedList, onClick: () => runtime.executeOperations(continueListNumbering(runtime.editor.document, currentListScope, {}, blockContext()), { preserveSelectionById: true }) })] });
|
|
1338
1460
|
// Remove link/Insert formula/Special characters also get a standalone
|
|
1339
1461
|
// widePromote ToolbarButton (see textStylesMenuItems's own comment above
|
|
1340
1462
|
// for the mechanism) - Insert video/audio and the selected-media actions
|
|
1341
1463
|
// stay dropdown-only, matching the report's own named tool list.
|
|
1342
|
-
const insertMoreMenuItems = _jsxs(_Fragment, { children: [_jsx(ToolbarMenuItem, { icon: "unlink", label: "Remove link", disabled: readOnly, onClick: removeLink, widePromote: true }), _jsx(ToolbarMenuItem, { icon: "video", label: "Insert video", disabled: readOnly
|
|
1464
|
+
const insertMoreMenuItems = _jsxs(_Fragment, { children: [t.removeLink && _jsx(ToolbarMenuItem, { icon: "unlink", label: "Remove link", disabled: readOnly, onClick: removeLink, widePromote: true }), showVideoTool && _jsx(ToolbarMenuItem, { icon: "video", label: "Insert video", disabled: readOnly, onClick: () => setMediaKind("video") }), showAudioTool && _jsx(ToolbarMenuItem, { icon: "audio", label: "Insert audio", disabled: readOnly, onClick: () => setMediaKind("audio") }), t.insertFormula && _jsx(ToolbarMenuItem, { icon: "formula", label: "Insert formula", disabled: readOnly, widePromote: true, onClick: (event) => {
|
|
1343
1465
|
const rect = event.currentTarget.getBoundingClientRect();
|
|
1344
1466
|
setFormulaLibraryPopover({ x: rect.left, y: rect.bottom + 4 });
|
|
1345
|
-
} }), _jsx(ToolbarMenuItem, { icon: "specialChar", label: "Special characters", disabled: readOnly, widePromote: true, onClick: (event) => {
|
|
1467
|
+
} }), t.specialCharacters && _jsx(ToolbarMenuItem, { icon: "specialChar", label: "Special characters", disabled: readOnly, widePromote: true, onClick: (event) => {
|
|
1346
1468
|
const rect = event.currentTarget.getBoundingClientRect();
|
|
1347
1469
|
setSpecialCharPopover({ x: rect.left, y: rect.bottom + 4 });
|
|
1348
1470
|
} }), _jsx(ToolbarMenuItem, { icon: "edit", label: "Edit selected media", disabled: readOnly || !mediaAtomSelected, onClick: () => editSelectedAtom() }), _jsx(ToolbarMenuItem, { icon: "zoomIn", label: "Enlarge selected media", disabled: readOnly || !resizableAtomSelected, onClick: () => editSelectedAtom(20) }), _jsx(ToolbarMenuItem, { icon: "zoomOut", label: "Shrink selected media", disabled: readOnly || !resizableAtomSelected, onClick: () => editSelectedAtom(-20) }), _jsx(ToolbarMenuItem, { icon: "delete", label: "Delete selected media", disabled: readOnly || !atomSelected, onClick: () => runtime.executeOperations(deleteAtom(runtime.editor.document, atomScope(), {}, blockContext())) })] });
|
|
@@ -1359,32 +1481,33 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
|
|
|
1359
1481
|
const rect = event.currentTarget.getBoundingClientRect();
|
|
1360
1482
|
setTableBorderPopover({ x: rect.left, y: rect.bottom + 4, scope: scope, initial: currentCellBorderDraft(scope) });
|
|
1361
1483
|
} })] });
|
|
1362
|
-
const saveCopyMenuItems = _jsxs(_Fragment, { children: [_jsx(ToolbarMenuItem, { label: "Save as HTML", onClick: () => runExport("html") }), _jsx(ToolbarMenuItem, { label: "Save as Markdown", onClick: () => runExport("markdown") }), _jsx(ToolbarMenuItem, { label: "Save as Word document", onClick: () => void runDocxExport() }), _jsx(ToolbarMenuItem, { label: "Save as PDF", onClick: runPdfExport }), _jsx(ToolbarMenuItem, { icon: "json", label: "Save as Smart RTE file", onClick: () => runExport("native") })] });
|
|
1363
|
-
const
|
|
1364
|
-
|
|
1484
|
+
const saveCopyMenuItems = _jsxs(_Fragment, { children: [t.saveAsHtml && _jsx(ToolbarMenuItem, { label: "Save as HTML", onClick: () => runExport("html") }), t.saveAsMarkdown && _jsx(ToolbarMenuItem, { label: "Save as Markdown", onClick: () => runExport("markdown") }), t.saveAsWord && _jsx(ToolbarMenuItem, { label: "Save as Word document", onClick: () => void runDocxExport() }), t.saveAsPdf && _jsx(ToolbarMenuItem, { label: "Save as PDF", onClick: runPdfExport }), t.saveAsSmartRte && _jsx(ToolbarMenuItem, { icon: "json", label: "Save as Smart RTE file", onClick: () => runExport("native") })] });
|
|
1485
|
+
const showSaveCopy = t.saveAsHtml || t.saveAsMarkdown || t.saveAsWord || t.saveAsPdf || t.saveAsSmartRte;
|
|
1486
|
+
// "Review" splits into two independently-toggleable tools (comments,
|
|
1487
|
+
// suggestions) - not an arbitrary split, it maps directly onto the two
|
|
1488
|
+
// already-separate providers this architecture has (commentProvider,
|
|
1489
|
+
// suggestionProvider). Kept as ONE dropdown trigger (labeled "Review")
|
|
1490
|
+
// rather than two, showing whichever half(s) are actually enabled.
|
|
1491
|
+
const commentsMenuItems = _jsxs(_Fragment, { children: [_jsx(ToolbarMenuItem, { icon: "addComment", label: "Add comment", disabled: !canComment, onClick: startComment }), _jsx(ToolbarMenuItem, { icon: "comments", label: "Comments", pressed: commentPanelOpen, disabled: !commentProvider, onClick: () => setCommentPanelOpen((open) => !open) })] });
|
|
1492
|
+
const suggestionsMenuItems = _jsxs(_Fragment, { children: [_jsx(ToolbarMenuItem, { icon: "suggest", label: "Suggest deletion", disabled: !canSuggestDelete, onClick: suggestDelete }), _jsx(ToolbarMenuItem, { icon: "suggest", label: "Suggest insertion", disabled: readOnly || !suggestionProvider, onClick: startSuggestInsert }), _jsx(ToolbarMenuItem, { icon: "suggest", label: "Suggest removing this", disabled: readOnly || !suggestionProvider, onClick: suggestBlockRemoval }), _jsx(ToolbarMenuItem, { icon: "suggestions", label: "Suggestions", pressed: suggestionPanelOpen, disabled: !suggestionProvider, onClick: () => setSuggestionPanelOpen((open) => !open) }), _jsx(ToolbarMenuItem, { icon: "showEdits", label: "Show edits", pressed: trackChangesEnabled, title: "Tracks ordinary typing and edits within a single paragraph as suggestions. Merging paragraphs together - typing over a selection that spans multiple paragraphs, or pressing Backspace/Delete at a paragraph boundary - still applies directly and can't be reviewed as a suggestion.", disabled: readOnly || !suggestionProvider, onClick: () => setTrackChangesEnabled((enabled) => !enabled) })] });
|
|
1493
|
+
const reviewMenuItems = _jsxs(_Fragment, { children: [showCommentsTool && commentsMenuItems, showCommentsTool && showSuggestionsTool && _jsx("div", { className: "srte-menu-separator" }), showSuggestionsTool && suggestionsMenuItems] });
|
|
1494
|
+
const showReviewDropdown = showCommentsTool || showSuggestionsTool;
|
|
1495
|
+
return _jsxs("section", { className: `srte-root srte-editor srte-canonical-authority${className ? ` ${className}` : ""}`, "data-smart-authority": "canonical", children: [_jsxs("div", { className: "srte-toolbar", role: "toolbar", "aria-label": "Formatting toolbar", children: [_jsxs(ToolbarGroup, { children: [t.bold && _jsx(ToolbarButton, { icon: "bold", label: "Bold", pressed: markCoverage("bold"), disabled: readOnly, onClick: () => toggleMark("bold") }), t.italic && _jsx(ToolbarButton, { icon: "italic", label: "Italic", pressed: markCoverage("italic"), disabled: readOnly, onClick: () => toggleMark("italic") }), t.underline && _jsx(ToolbarButton, { icon: "underline", label: "Underline", pressed: markCoverage("underline"), disabled: readOnly, onClick: () => toggleMark("underline") }), t.strikethrough && _jsx(ToolbarButton, { icon: "strikethrough", label: "Strikethrough", pressed: markCoverage("strikethrough"), disabled: readOnly, onClick: () => toggleMark("strikethrough") }), t.superscript && _jsx(ToolbarButton, { icon: "superscript", label: "Superscript", pressed: markCoverage("superscript"), disabled: readOnly, onClick: () => toggleMark("superscript"), widePromote: true }), t.subscript && _jsx(ToolbarButton, { icon: "subscript", label: "Subscript", pressed: markCoverage("subscript"), disabled: readOnly, onClick: () => toggleMark("subscript"), widePromote: true }), t.textColor && _jsx(ToolbarButton, { icon: "textColor", label: "Text colour", disabled: readOnly, widePromote: true, onClick: (event) => {
|
|
1365
1496
|
const rect = event.currentTarget.getBoundingClientRect();
|
|
1366
1497
|
setColorPopover({ x: rect.left, y: rect.bottom + 4, target: { kind: "mark", markId: "textColor" }, initialValue: currentMarkColor("textColor") });
|
|
1367
|
-
} }), _jsx(ToolbarButton, { icon: "backgroundColor", label: "Background colour", disabled: readOnly, widePromote: true, onClick: (event) => {
|
|
1498
|
+
} }), t.backgroundColor && _jsx(ToolbarButton, { icon: "backgroundColor", label: "Background colour", disabled: readOnly, widePromote: true, onClick: (event) => {
|
|
1368
1499
|
const rect = event.currentTarget.getBoundingClientRect();
|
|
1369
1500
|
setColorPopover({ x: rect.left, y: rect.bottom + 4, target: { kind: "mark", markId: "backgroundColor" }, initialValue: currentMarkColor("backgroundColor") });
|
|
1370
|
-
} }), _jsx(ToolbarButton, { icon: "fontSize", label: "Font size", disabled: readOnly, onClick: () => applyAttributedMark("fontSize"), widePromote: true }), _jsx(ToolbarButton, { icon: "fontFamily", label: "Font family", disabled: readOnly, onClick: () => applyAttributedMark("fontFamily"), widePromote: true }), _jsx(ToolbarDropdown, { icon: "textColor", label: "More text styles", priority: 2, children: textStylesMenuItems })] }), _jsxs(ToolbarGroup, { children: [_jsxs("select", { "aria-label": "Block type", title: "Block type", value: currentBlockType, disabled: readOnly, onChange: (event) => transactBlock(setBlockTypeCommand(runtime.editor.document, blockScope(), {
|
|
1501
|
+
} }), t.fontSize && _jsx(ToolbarButton, { icon: "fontSize", label: "Font size", disabled: readOnly, onClick: () => applyAttributedMark("fontSize"), widePromote: true }), t.fontFamily && _jsx(ToolbarButton, { icon: "fontFamily", label: "Font family", disabled: readOnly, onClick: () => applyAttributedMark("fontFamily"), widePromote: true }), showMoreTextStyles && _jsx(ToolbarDropdown, { icon: "textColor", label: "More text styles", priority: 2, children: textStylesMenuItems })] }), _jsxs(ToolbarGroup, { children: [t.blockType && _jsxs("select", { "aria-label": "Block type", title: "Block type", value: currentBlockType, disabled: readOnly, onChange: (event) => transactBlock(setBlockTypeCommand(runtime.editor.document, blockScope(), {
|
|
1371
1502
|
type: event.target.value === "paragraph" ? "paragraph" : event.target.value === "code_block" ? "code_block" : "heading",
|
|
1372
1503
|
attrs: event.target.value.startsWith("heading-") ? { level: Number(event.target.value.slice(8)) } : {},
|
|
1373
|
-
}, blockContext())), children: [_jsx("option", { value: "paragraph", children: "Paragraph" }), Array.from({ length: 6 }, (_, index) => _jsxs("option", { value: `heading-${index + 1}`, children: ["Heading ", index + 1] }, index + 1)), _jsx("option", { value: "code_block", children: "Code block" })] }), _jsx(ToolbarButton, { icon: "alignLeft", label: "Align left", ariaLabel: "Align left", iconOnly: true, disabled: readOnly, onClick: () => transactBlock(setBlockAttributes(runtime.editor.document, blockScope(), { attrs: { align: "left" } }, blockContext())) }), _jsx(ToolbarButton, { icon: "alignCenter", label: "Align center", ariaLabel: "Align center", iconOnly: true, disabled: readOnly, onClick: () => transactBlock(setBlockAttributes(runtime.editor.document, blockScope(), { attrs: { align: "center" } }, blockContext())) }), _jsx(ToolbarButton, { icon: "alignRight", label: "Align right", ariaLabel: "Align right", iconOnly: true, disabled: readOnly, onClick: () => transactBlock(setBlockAttributes(runtime.editor.document, blockScope(), { attrs: { align: "right" } }, blockContext())) }), _jsx(ToolbarButton, { icon: "alignJustify", label: "Justify", ariaLabel: "Align justify", iconOnly: true, disabled: readOnly, onClick: () => transactBlock(setBlockAttributes(runtime.editor.document, blockScope(), { attrs: { align: "justify" } }, blockContext())) }), _jsx(ToolbarButton, { icon: "quote", label: "Quote", ariaLabel: "Blockquote", disabled: readOnly, onClick: toggleBlockquote }), _jsx(ToolbarDropdown, { icon: "moveUp", label: "More paragraph tools", priority: 2, children: paragraphToolsMenuItems })] }), _jsxs(ToolbarGroup, { children: [_jsx(ToolbarButton, { icon: "bulletedList", label: "Bulleted list", ariaLabel: "Bulleted list", pressed: listStyleActive("disc"), disabled: readOnly, onClick: () => toggleList("disc") }), _jsx(ToolbarButton, { icon: "numberedList", label: "Numbered list", ariaLabel: "Numbered list", pressed: listStyleActive("decimal"), disabled: readOnly, onClick: () => toggleList("decimal") }), _jsx(ToolbarButton, { icon: "checklist", label: "Checklist", ariaLabel: "Checklist", pressed: listStyleActive("disc", true), disabled: readOnly, onClick: () => toggleList("disc", true) }), _jsxs(ToolbarDropdown, { icon: "restart", label: "More list tools", priority: 2, children: [_jsx("div", {
|
|
1374
|
-
const preset = event.target.value;
|
|
1375
|
-
if (!preset || currentListParts.length !== 1)
|
|
1376
|
-
return;
|
|
1377
|
-
// A preset choice applies to the whole list, regardless of how deep
|
|
1378
|
-
// the cursor is nested — see outermostListId.
|
|
1379
|
-
const rootId = outermostListId(currentListParts[0].listId);
|
|
1380
|
-
runtime.executeOperations(setListPreset(runtime.editor.document, { ...currentListParts[0], listId: rootId }, { preset }, blockContext()), { preserveSelectionById: true });
|
|
1381
|
-
}, style: { width: "100%" }, children: [_jsx("option", { value: "", children: "List preset" }), SMART_LIST_PRESETS.map((preset) => _jsx("option", { value: preset.id, children: preset.kind === "bullet" ? `Bullet · ${preset.label}` : `Number · ${preset.label}` }, preset.id))] }) }), _jsx("div", { className: "srte-menu-separator" }), listToolsMenuItems] })] }), _jsxs(ToolbarGroup, { children: [_jsx(ToolbarButton, { icon: "link", label: "Link", ariaLabel: "Insert or edit link", disabled: readOnly, onClick: openLinkPopover }), _jsx(ToolbarButton, { icon: "image", label: "Image", ariaLabel: "Insert image", disabled: readOnly || !mediaProvider, onClick: () => setMediaKind("image") }), _jsx(ToolbarButton, { icon: "unlink", label: "Remove link", disabled: readOnly, onClick: removeLink, widePromote: true }), _jsx(ToolbarButton, { icon: "formula", label: "Insert formula", disabled: readOnly, widePromote: true, onClick: (event) => {
|
|
1504
|
+
}, blockContext())), children: [_jsx("option", { value: "paragraph", children: "Paragraph" }), Array.from({ length: 6 }, (_, index) => _jsxs("option", { value: `heading-${index + 1}`, children: ["Heading ", index + 1] }, index + 1)), _jsx("option", { value: "code_block", children: "Code block" })] }), t.alignLeft && _jsx(ToolbarButton, { icon: "alignLeft", label: "Align left", ariaLabel: "Align left", iconOnly: true, disabled: readOnly, onClick: () => transactBlock(setBlockAttributes(runtime.editor.document, blockScope(), { attrs: { align: "left" } }, blockContext())) }), t.alignCenter && _jsx(ToolbarButton, { icon: "alignCenter", label: "Align center", ariaLabel: "Align center", iconOnly: true, disabled: readOnly, onClick: () => transactBlock(setBlockAttributes(runtime.editor.document, blockScope(), { attrs: { align: "center" } }, blockContext())) }), t.alignRight && _jsx(ToolbarButton, { icon: "alignRight", label: "Align right", ariaLabel: "Align right", iconOnly: true, disabled: readOnly, onClick: () => transactBlock(setBlockAttributes(runtime.editor.document, blockScope(), { attrs: { align: "right" } }, blockContext())) }), t.alignJustify && _jsx(ToolbarButton, { icon: "alignJustify", label: "Justify", ariaLabel: "Align justify", iconOnly: true, disabled: readOnly, onClick: () => transactBlock(setBlockAttributes(runtime.editor.document, blockScope(), { attrs: { align: "justify" } }, blockContext())) }), t.quote && _jsx(ToolbarButton, { icon: "quote", label: "Quote", ariaLabel: "Blockquote", disabled: readOnly, onClick: toggleBlockquote }), _jsx(ToolbarDropdown, { icon: "moveUp", label: "More paragraph tools", priority: 2, children: paragraphToolsMenuItems })] }), _jsxs(ToolbarGroup, { children: [t.bulletedList && _jsx(ToolbarButton, { icon: "bulletedList", label: "Bulleted list", ariaLabel: "Bulleted list", pressed: listStyleActive("disc"), disabled: readOnly, onClick: () => toggleList("disc") }), t.numberedList && _jsx(ToolbarButton, { icon: "numberedList", label: "Numbered list", ariaLabel: "Numbered list", pressed: listStyleActive("decimal"), disabled: readOnly, onClick: () => toggleList("decimal") }), t.checklist && _jsx(ToolbarButton, { icon: "checklist", label: "Checklist", ariaLabel: "Checklist", pressed: listStyleActive("disc", true), disabled: readOnly, onClick: () => toggleList("disc", true) }), _jsxs(ToolbarDropdown, { icon: "restart", label: "More list tools", priority: 2, children: [listPresetSelect, t.listPreset && _jsx("div", { className: "srte-menu-separator" }), listToolsMenuItems] })] }), _jsxs(ToolbarGroup, { children: [t.link && _jsx(ToolbarButton, { icon: "link", label: "Link", ariaLabel: "Insert or edit link", disabled: readOnly, onClick: openLinkPopover }), showImageTool && _jsx(ToolbarButton, { icon: "image", label: "Image", ariaLabel: "Insert image", disabled: readOnly, onClick: () => setMediaKind("image") }), t.removeLink && _jsx(ToolbarButton, { icon: "unlink", label: "Remove link", disabled: readOnly, onClick: removeLink, widePromote: true }), t.insertFormula && _jsx(ToolbarButton, { icon: "formula", label: "Insert formula", disabled: readOnly, widePromote: true, onClick: (event) => {
|
|
1382
1505
|
const rect = event.currentTarget.getBoundingClientRect();
|
|
1383
1506
|
setFormulaLibraryPopover({ x: rect.left, y: rect.bottom + 4 });
|
|
1384
|
-
} }), _jsx(ToolbarButton, { icon: "specialChar", label: "Special characters", disabled: readOnly, widePromote: true, onClick: (event) => {
|
|
1507
|
+
} }), t.specialCharacters && _jsx(ToolbarButton, { icon: "specialChar", label: "Special characters", disabled: readOnly, widePromote: true, onClick: (event) => {
|
|
1385
1508
|
const rect = event.currentTarget.getBoundingClientRect();
|
|
1386
1509
|
setSpecialCharPopover({ x: rect.left, y: rect.bottom + 4 });
|
|
1387
|
-
} }), _jsx(ToolbarDropdown, { icon: "video", label: "More to insert", priority: 2, children: insertMoreMenuItems })] }),
|
|
1510
|
+
} }), _jsx(ToolbarDropdown, { icon: "video", label: "More to insert", priority: 2, children: insertMoreMenuItems })] }), showInsertTable && _jsxs(ToolbarGroup, { children: [_jsx(ToolbarButton, { icon: "table", label: "Insert table", ariaLabel: "Insert table", disabled: readOnly, onClick: (event) => {
|
|
1388
1511
|
const rect = event.currentTarget.getBoundingClientRect();
|
|
1389
1512
|
setTableSizePopover({ x: rect.left, y: rect.bottom + 4 });
|
|
1390
1513
|
} }), _jsx(ToolbarDropdown, { icon: "mergeCells", label: "Table tools", priority: 2, children: tableToolsMenuItems })] }), _jsx("input", { ref: importRef, type: "file", accept: ".html,.htm,.md,.markdown,.docx,.pdf,text/html,text/markdown,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/pdf", hidden: true, onChange: (event) => {
|
|
@@ -1392,11 +1515,11 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
|
|
|
1392
1515
|
if (file)
|
|
1393
1516
|
void runImport(file);
|
|
1394
1517
|
event.currentTarget.value = "";
|
|
1395
|
-
} }), _jsxs(ToolbarGroup, { children: [_jsx(ToolbarButton, { icon: "import", label: "Import", ariaLabel: "Import document", disabled: readOnly, onClick: () => importRef.current?.click() }), _jsx(ToolbarDropdown, { icon: "saveCopy", label: "Save a copy", priority: 2, children: saveCopyMenuItems }), _jsx(ToolbarButton, { icon: "history", label: "Version history", ariaLabel: "Version history", disabled: readOnly
|
|
1518
|
+
} }), _jsxs(ToolbarGroup, { children: [t.import && _jsx(ToolbarButton, { icon: "import", label: "Import", ariaLabel: "Import document", disabled: readOnly, onClick: () => importRef.current?.click() }), showSaveCopy && _jsx(ToolbarDropdown, { icon: "saveCopy", label: "Save a copy", priority: 2, children: saveCopyMenuItems }), showVersionHistoryTool && _jsx(ToolbarButton, { icon: "history", label: "Version history", ariaLabel: "Version history", disabled: readOnly, onClick: () => setVersionHistoryOpen(true) }), showReviewDropdown && _jsx(ToolbarDropdown, { icon: "comments", label: "Review", priority: 2, children: reviewMenuItems })] }), _jsxs(ToolbarGroup, { children: [t.undo && _jsx(ToolbarButton, { icon: "undo", label: "Undo", ariaLabel: "Undo", disabled: readOnly, onClick: () => { runtime.editor.undo(); runtime.focus(); } }), t.redo && _jsx(ToolbarButton, { icon: "redo", label: "Redo", ariaLabel: "Redo", disabled: readOnly, onClick: () => { runtime.editor.redo(); runtime.focus(); } })] }), _jsxs(MobileMoreMenu, { children: [textStylesMenuItems, _jsx("div", { className: "srte-menu-separator" }), paragraphToolsMenuItems, _jsx("div", { className: "srte-menu-separator" }), listPresetSelect, listToolsMenuItems, _jsx("div", { className: "srte-menu-separator" }), insertMoreMenuItems, showInsertTable && _jsxs(_Fragment, { children: [_jsx("div", { className: "srte-menu-separator" }), tableToolsMenuItems] }), showSaveCopy && _jsxs(_Fragment, { children: [_jsx("div", { className: "srte-menu-separator" }), saveCopyMenuItems] }), showReviewDropdown && _jsxs(_Fragment, { children: [_jsx("div", { className: "srte-menu-separator" }), reviewMenuItems] })] })] }), mediaKind === "image" && mediaManager && mediaProvider && _jsx(MediaManager, { open: true, onClose: () => setMediaKind(null), adapter: mediaManagerAdapter, onSelect: selectFromMediaManager }), mediaKind && mediaProvider && !(mediaKind === "image" && mediaManager) && _jsx(MediaPicker, { kind: mediaKind, onPick: (file) => void insertMediaFile(mediaKind, file), onCancel: () => setMediaKind(null) }), showVersionHistoryTool && _jsx(VersionHistoryPanel, { open: versionHistoryOpen, onClose: () => setVersionHistoryOpen(false), runtime: runtime, versionProvider: versionProvider }), showCommentsTool && rootRef.current && runtime.surface.renderer?.mapping && _jsx(CommentMarkers, { positions: runtime.editor.positions, mapping: runtime.surface.renderer.mapping, containerElement: rootRef.current, threads: threads, activeThreadId: activeThreadId, onSelectThread: selectThread }), showCommentsTool && _jsx(CommentThreadPanel, { open: commentPanelOpen, onClose: () => { setCommentPanelOpen(false); setPendingCommentRange(null); }, threads: threads, activeThreadId: activeThreadId, onSelectThread: selectThread, pendingRange: pendingCommentRange, onCreateThread: createThread, onReply: replyToThread, onResolve: setThreadResolved, onDelete: deleteThread, busyThreadId: busyThreadId }), showSuggestionsTool && rootRef.current && runtime.surface.renderer?.mapping && _jsx(StructuralSuggestionMarkers, { positions: runtime.editor.positions, mapping: runtime.surface.renderer.mapping, containerElement: rootRef.current, suggestions: structuralSuggestions, activeSuggestionId: activeSuggestionId, onSelectSuggestion: selectSuggestion }), showSuggestionsTool && _jsx(SuggestionPanel, { open: suggestionPanelOpen, onClose: () => { setSuggestionPanelOpen(false); setPendingSuggestInsertAt(null); }, document: runtime.editor.document, structuralSuggestions: structuralSuggestions, activeId: activeSuggestionId, onSelect: selectSuggestion, pendingInsert: Boolean(pendingSuggestInsertAt), onSubmitInsert: submitSuggestInsert, onAcceptInline: acceptInlineSuggestion, onRejectInline: rejectInlineSuggestion, onAcceptStructural: acceptStructural, onRejectStructural: rejectStructural, busyId: busySuggestionId }), linkPopover && _jsx(LinkEditorPopover, { x: linkPopover.x, y: linkPopover.y, initialHref: linkPopover.href, initialOpenInNewTab: linkPopover.openInNewTab, showTextInput: linkPopover.collapsed && !linkPopover.editingExisting, showRemove: linkPopover.editingExisting, autoFocus: !linkPopover.autoTriggered, onApply: applyLink, onRemove: removeLink, onCancel: () => {
|
|
1396
1519
|
setLinkOverlayDismissedAt(runtime.editor.selection.head);
|
|
1397
1520
|
setLinkPopover(null);
|
|
1398
1521
|
runtime.focus();
|
|
1399
|
-
} }), !readOnly && mediaAtomSelected && selectedAtomElement && _jsx(MediaOverlay, { atomElement: selectedAtomElement, alt: typeof selectedAtomNode?.attrs?.alt === "string" ? selectedAtomNode.attrs.alt : "", width: typeof selectedAtomNode?.attrs?.width === "number" ? selectedAtomNode.attrs.width : undefined, height: typeof selectedAtomNode?.attrs?.height === "number" ? selectedAtomNode.attrs.height : undefined, src: typeof selectedAtomNode?.attrs?.src === "string" ? selectedAtomNode.attrs.src : undefined, resizable: resizableAtomSelected, onEdit: () => editSelectedAtom(), onResize: (by) => editSelectedAtom(by), onResizeTo: resizeSelectedAtomTo, onDelete: deleteSelectedAtom, onDismiss: () => runtime.focus() }), colorPopover && _jsx(ColorPickerPopover, { x: colorPopover.x, y: colorPopover.y, label: colorPopover.target.kind === "mark"
|
|
1522
|
+
} }), !readOnly && mediaResizeTarget && mediaResizeTarget.nodeId === selectedAtomId && mediaAtomSelected && selectedAtomElement && _jsx(MediaOverlay, { atomElement: selectedAtomElement, alt: typeof selectedAtomNode?.attrs?.alt === "string" ? selectedAtomNode.attrs.alt : "", width: typeof selectedAtomNode?.attrs?.width === "number" ? selectedAtomNode.attrs.width : undefined, height: typeof selectedAtomNode?.attrs?.height === "number" ? selectedAtomNode.attrs.height : undefined, src: typeof selectedAtomNode?.attrs?.src === "string" ? selectedAtomNode.attrs.src : undefined, showMenu: false, resizable: resizableAtomSelected, onEdit: () => undefined, onResize: () => undefined, onResizeTo: resizeSelectedAtomTo, onDelete: () => undefined, onDismiss: () => setMediaResizeTarget(null) }), !readOnly && mediaContextMenu && mediaAtomSelected && selectedAtomElement && _jsx(MediaOverlay, { atomElement: selectedAtomElement, alt: typeof selectedAtomNode?.attrs?.alt === "string" ? selectedAtomNode.attrs.alt : "", width: typeof selectedAtomNode?.attrs?.width === "number" ? selectedAtomNode.attrs.width : undefined, height: typeof selectedAtomNode?.attrs?.height === "number" ? selectedAtomNode.attrs.height : undefined, src: typeof selectedAtomNode?.attrs?.src === "string" ? selectedAtomNode.attrs.src : undefined, x: mediaContextMenu.x, y: mediaContextMenu.y, resizable: resizableAtomSelected, onEdit: () => { setMediaContextMenu(null); editSelectedAtom(); }, onResize: (by) => { setMediaContextMenu(null); editSelectedAtom(by); }, onResizeTo: resizeSelectedAtomTo, onDelete: () => { setMediaContextMenu(null); deleteSelectedAtom(); }, onDismiss: () => { setMediaContextMenu(null); runtime.focus(); } }), colorPopover && _jsx(ColorPickerPopover, { x: colorPopover.x, y: colorPopover.y, label: colorPopover.target.kind === "mark"
|
|
1400
1523
|
? (colorPopover.target.markId === "textColor" ? "Text colour" : "Background colour")
|
|
1401
1524
|
: colorPopover.target.attr === "textColor" ? "Cell text colour" : "Cell background colour", ...(colorPopover.initialValue ? { initialValue: colorPopover.initialValue } : {}), recentColors: recentColors[colorBucketFor(colorPopover.target)], onPreview: previewColor, onApply: applyColor, onCancel: cancelColorPopover }), tableSizePopover && _jsx(TableSizePickerPopover, { x: tableSizePopover.x, y: tableSizePopover.y, onInsert: (rows, columns) => {
|
|
1402
1525
|
setTableSizePopover(null);
|
|
@@ -1405,7 +1528,7 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
|
|
|
1405
1528
|
}, onCancel: () => {
|
|
1406
1529
|
setTableSizePopover(null);
|
|
1407
1530
|
runtime.focus();
|
|
1408
|
-
} }), tableBorderPopover && _jsx(TableBorderPopover, { x: tableBorderPopover.x, y: tableBorderPopover.y, initial: tableBorderPopover.initial, recentColors: recentColors.border, onPreview: previewCellBorder, onApply: applyCellBorderCommit, onCancel: cancelTableBorderPopover }), formulaLibraryPopover && _jsx(FormulaLibraryPopover, { x: formulaLibraryPopover.x, y: formulaLibraryPopover.y, onInsert: insertFormulaFromLibrary, onCancel: () => { setFormulaLibraryPopover(null); runtime.focus(); } }), specialCharPopover && _jsx(SpecialCharacterPopover, { x: specialCharPopover.x, y: specialCharPopover.y, recentCharacters: recentSpecialChars, onInsert: insertSpecialCharacter, onCancel: () => { setSpecialCharPopover(null); runtime.focus(); } }), !readOnly && selectedTableElement && _jsx(TableResizeHandles, { tableElement: selectedTableElement, onResizeColumn: resizeTableColumn, onResizeRow: resizeTableRow }), contextMenu && _jsx(ContextMenu, { x: contextMenu.x, y: contextMenu.y, items: resolveContextMenuItems(), onDismiss: () => { setContextMenu(null); runtime.focus(); } }), _jsx("div", { ref: rootRef, className: "srte-editor", "data-placeholder": placeholder, contentEditable: !readOnly, suppressContentEditableWarning: true, role: "textbox", "aria-label": "Smart RTE editing surface", "aria-multiline": "true", onMouseDown: (event) => {
|
|
1531
|
+
} }), tableBorderPopover && _jsx(TableBorderPopover, { x: tableBorderPopover.x, y: tableBorderPopover.y, initial: tableBorderPopover.initial, recentColors: recentColors.border, onPreview: previewCellBorder, onApply: applyCellBorderCommit, onCancel: cancelTableBorderPopover }), mediaDetailsPopover && _jsx(MediaDetailsPopover, { x: mediaDetailsPopover.x, y: mediaDetailsPopover.y, initial: mediaDetailsPopover.initial, onApply: applyMediaDetails, onCancel: () => { setMediaDetailsPopover(null); runtime.focus(); } }), formulaLibraryPopover && _jsx(FormulaLibraryPopover, { x: formulaLibraryPopover.x, y: formulaLibraryPopover.y, onInsert: insertFormulaFromLibrary, onCancel: () => { setFormulaLibraryPopover(null); runtime.focus(); } }), specialCharPopover && _jsx(SpecialCharacterPopover, { x: specialCharPopover.x, y: specialCharPopover.y, recentCharacters: recentSpecialChars, onInsert: insertSpecialCharacter, onCancel: () => { setSpecialCharPopover(null); runtime.focus(); } }), !readOnly && selectedTableElement && _jsx(TableResizeHandles, { tableElement: selectedTableElement, onResizeColumn: resizeTableColumn, onResizeRow: resizeTableRow }), contextMenu && _jsx(ContextMenu, { x: contextMenu.x, y: contextMenu.y, items: resolveContextMenuItems(), onDismiss: () => { setContextMenu(null); runtime.focus(); } }), _jsx("div", { ref: rootRef, className: "srte-editor", "data-placeholder": placeholder, contentEditable: !readOnly, suppressContentEditableWarning: true, role: "textbox", "aria-label": "Smart RTE editing surface", "aria-multiline": "true", onMouseDown: (event) => {
|
|
1409
1532
|
// A Ctrl/Cmd+click on a link is meant to open it (onClick below),
|
|
1410
1533
|
// not move the caret into it - but native mousedown's own default
|
|
1411
1534
|
// action *is* the caret-repositioning, and mousedown fires (and
|
|
@@ -1420,27 +1543,45 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
|
|
|
1420
1543
|
// completed render in a varying number of passes, so the overlay
|
|
1421
1544
|
// could already be open before the flag was ever set.)
|
|
1422
1545
|
const anchor = event.target?.closest("a[href]");
|
|
1423
|
-
|
|
1546
|
+
const linkedMedia = event.target?.closest("[data-smart-href]");
|
|
1547
|
+
if ((anchor || linkedMedia) && (event.metaKey || event.ctrlKey))
|
|
1424
1548
|
event.preventDefault();
|
|
1425
1549
|
}, onClick: (event) => {
|
|
1426
1550
|
const anchor = event.target?.closest("a[href]");
|
|
1427
|
-
|
|
1551
|
+
// An image atom with a Link set (docs/bugs/media-details-old-editor-field-parity.md)
|
|
1552
|
+
// - plain data attributes, not a real <a> wrapper, in the live DOM
|
|
1553
|
+
// (see surface/renderer.ts's own reasoning); Ctrl/Cmd+click opens it
|
|
1554
|
+
// the same way a real text link does, matching this project's only
|
|
1555
|
+
// existing link-opening convention rather than inventing a second one.
|
|
1556
|
+
const linkedMedia = event.target?.closest("[data-smart-href]");
|
|
1557
|
+
const href = anchor?.href || linkedMedia?.getAttribute("data-smart-href");
|
|
1558
|
+
if (href && (event.metaKey || event.ctrlKey)) {
|
|
1559
|
+
event.preventDefault();
|
|
1560
|
+
window.open(href, "_blank", "noopener,noreferrer");
|
|
1428
1561
|
return;
|
|
1429
|
-
|
|
1430
|
-
|
|
1562
|
+
}
|
|
1563
|
+
const imageTarget = event.target?.closest("[data-smart-type=\"image\"], [data-smart-type=\"block_image\"]");
|
|
1564
|
+
if (!imageTarget)
|
|
1565
|
+
return;
|
|
1566
|
+
setMediaContextMenu(null);
|
|
1567
|
+
const mappedImage = runtime.surface.renderer?.mapping.domToNode(imageTarget);
|
|
1568
|
+
if (mappedImage)
|
|
1569
|
+
setMediaResizeTarget({ nodeId: mappedImage.nodeId });
|
|
1431
1570
|
}, onContextMenu: (event) => {
|
|
1432
1571
|
if (readOnly)
|
|
1433
1572
|
return;
|
|
1434
1573
|
event.preventDefault();
|
|
1574
|
+
setMediaContextMenu(null);
|
|
1435
1575
|
// Atom selection (unlike caret/text placement) is not native
|
|
1436
1576
|
// browser behavior - it's InputController's own clickListener
|
|
1437
1577
|
// (surface/input.ts), bound to the "click" DOM event, which never
|
|
1438
1578
|
// fires for a right-click. Without this, right-clicking an atom
|
|
1439
1579
|
// directly (no preceding left-click) left whatever selection was
|
|
1440
1580
|
// already there in place, so atomic-node scope never resolved and
|
|
1441
|
-
//
|
|
1442
|
-
// appeared - right-clicking media looked
|
|
1443
|
-
// nothing. Mirrors clickListener's own
|
|
1581
|
+
// the media interaction (details editor for images, quick-action
|
|
1582
|
+
// menu for other media) never appeared - right-clicking media looked
|
|
1583
|
+
// like right-clicking nothing. Mirrors clickListener's own
|
|
1584
|
+
// atom-selection logic
|
|
1444
1585
|
// exactly.
|
|
1445
1586
|
const atomTarget = event.target?.closest("[data-smart-atomic]");
|
|
1446
1587
|
const mapped = atomTarget ? runtime.surface.renderer?.mapping.domToNode(atomTarget) : null;
|
|
@@ -1450,6 +1591,19 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
|
|
|
1450
1591
|
runtime.editor.setSelection({ type: "node", anchor: range.from, head: range.to }, { source: "api" });
|
|
1451
1592
|
runtime.surface.renderer?.render(runtime.editor.document, runtime.editor.selection);
|
|
1452
1593
|
}
|
|
1594
|
+
if (mapped.node.type !== "divider" && mapped.node.type !== "unknown") {
|
|
1595
|
+
setContextMenu(null);
|
|
1596
|
+
const isImage = mapped.node.type === "image" || mapped.node.type === "block_image";
|
|
1597
|
+
if (isImage) {
|
|
1598
|
+
const scope = runtime.editor.resolveScope({ want: "atomic-node" });
|
|
1599
|
+
setMediaResizeTarget({ nodeId: mapped.nodeId });
|
|
1600
|
+
if (scope.kind === "atomic-node")
|
|
1601
|
+
openMediaDetails(scope, mapped.node, atomTarget || undefined);
|
|
1602
|
+
return;
|
|
1603
|
+
}
|
|
1604
|
+
setMediaContextMenu({ x: event.clientX, y: event.clientY, nodeId: mapped.nodeId });
|
|
1605
|
+
return;
|
|
1606
|
+
}
|
|
1453
1607
|
}
|
|
1454
1608
|
else if (isCollapsedTextSelection(runtime.editor.selection)) {
|
|
1455
1609
|
// Chromium/Firefox reposition the native caret to the click
|
|
@@ -1480,10 +1634,8 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
|
|
|
1480
1634
|
runtime.surface.renderer?.render(runtime.editor.document, runtime.editor.selection);
|
|
1481
1635
|
}
|
|
1482
1636
|
}
|
|
1483
|
-
//
|
|
1484
|
-
//
|
|
1485
|
-
// A right-click that doesn't resolve to a table cell opens nothing
|
|
1486
|
-
// at all, rather than an empty "No actions here" menu.
|
|
1637
|
+
// A right-click that is not handled by the media branch above and
|
|
1638
|
+
// does not resolve to a table cell opens no generic menu.
|
|
1487
1639
|
const rightClickTableScope = runtime.editor.resolveScope({ want: "table-grid" });
|
|
1488
1640
|
if ("kind" in rightClickTableScope && rightClickTableScope.kind === "table-grid") {
|
|
1489
1641
|
setContextMenu({ x: event.clientX, y: event.clientY });
|
|
@@ -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;
|