smartrte-react 1.0.0-beta.1 → 1.0.0-beta.11
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 +49 -0
- package/README.md +99 -0
- package/dist/canonicalEditorRuntime.d.ts +12 -0
- package/dist/canonicalEditorRuntime.js +2 -1
- package/dist/components/CanonicalAuthorityEditor.d.ts +22 -1
- package/dist/components/CanonicalAuthorityEditor.js +271 -57
- package/dist/components/ColorPickerPopover.js +3 -1
- package/dist/components/MediaDetailsPopover.d.ts +35 -0
- package/dist/components/MediaDetailsPopover.js +136 -0
- package/dist/components/MediaManager.js +5 -5
- package/dist/components/MediaOverlay.d.ts +12 -21
- package/dist/components/MediaOverlay.js +129 -64
- package/dist/components/TableBorderPopover.js +3 -1
- package/dist/components/TableResizeHandles.js +22 -0
- package/dist/components/ToolbarPrimitives.d.ts +32 -0
- package/dist/components/ToolbarPrimitives.js +95 -4
- package/dist/components/fixedPositioning.d.ts +42 -0
- package/dist/components/fixedPositioning.js +60 -0
- 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 +90 -33
- package/dist/toolbarTools.d.ts +95 -0
- package/dist/toolbarTools.js +17 -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, 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
|
|
@@ -147,7 +152,7 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
|
|
|
147
152
|
// selection.head differs from this value.
|
|
148
153
|
const [linkOverlayDismissedAt, setLinkOverlayDismissedAt] = useState(null);
|
|
149
154
|
if (!runtimeRef.current)
|
|
150
|
-
runtimeRef.current = new CanonicalEditorRuntime({ initialValue: defaultValue, preset, onChange, onHtmlChange, onClipboardDiagnostic });
|
|
155
|
+
runtimeRef.current = new CanonicalEditorRuntime({ initialValue: defaultValue, preset, renderFormulaHtml, onChange, onHtmlChange, onClipboardDiagnostic });
|
|
151
156
|
const runtime = runtimeRef.current;
|
|
152
157
|
const [, setEditorTick] = useState(0);
|
|
153
158
|
runtime.setCallbacks(onChange, onHtmlChange);
|
|
@@ -267,6 +272,35 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
|
|
|
267
272
|
};
|
|
268
273
|
const blockScope = () => runtime.editor.resolveScope({ want: "block-range" });
|
|
269
274
|
const blockContext = () => ({ schema: runtime.editor.schema, positions: runtime.editor.positions });
|
|
275
|
+
/**
|
|
276
|
+
* `SelectionDescription` (resolveScope({want:"describe"})) tracks mark
|
|
277
|
+
* coverage (markCoverage below) but has no equivalent for block
|
|
278
|
+
* *attributes* - `blockTypes` is the closest thing, and it's just node
|
|
279
|
+
* types, not attrs. This mirrors foundation/block/commands.ts's own
|
|
280
|
+
* (core-internal, not exported) blockScopes/selectedBlockIds traversal -
|
|
281
|
+
* a "mixed" scope is a caret/selection spanning more than one real
|
|
282
|
+
* block-range part (e.g. a paragraph plus a list item), each of which
|
|
283
|
+
* carries its own blockIds.
|
|
284
|
+
*/
|
|
285
|
+
const selectedLineHeightBlockIds = (scope) => {
|
|
286
|
+
const collect = (candidate) => candidate.kind === "block-range" ? candidate.blockIds
|
|
287
|
+
: candidate.kind === "mixed" ? candidate.parts.flatMap(collect)
|
|
288
|
+
: [];
|
|
289
|
+
return [...new Set(collect(scope))];
|
|
290
|
+
};
|
|
291
|
+
/** `undefined` = no block selected/no override anywhere, a number = every selected block shares that exact value, `"mixed"` = selected blocks disagree - same three-state shape as markCoverage's boolean|"mixed"|undefined. */
|
|
292
|
+
const currentLineHeight = () => {
|
|
293
|
+
const ids = selectedLineHeightBlockIds(blockScope());
|
|
294
|
+
if (!ids.length)
|
|
295
|
+
return undefined;
|
|
296
|
+
const values = ids.map((id) => {
|
|
297
|
+
const value = findNode(runtime.editor.document, id)?.attrs?.lineHeight;
|
|
298
|
+
return typeof value === "number" ? value : undefined;
|
|
299
|
+
});
|
|
300
|
+
const unique = [...new Set(values)];
|
|
301
|
+
return unique.length === 1 ? unique[0] : "mixed";
|
|
302
|
+
};
|
|
303
|
+
const setLineHeight = (value) => transactBlock(setBlockAttributes(runtime.editor.document, blockScope(), { attrs: { lineHeight: value } }, blockContext()));
|
|
270
304
|
const listScope = () => runtime.editor.resolveScope({ want: "list-selection" });
|
|
271
305
|
const tableScope = () => runtime.editor.resolveScope({ want: "table-grid" });
|
|
272
306
|
const atomScope = () => runtime.editor.resolveScope({ want: "atomic-node" });
|
|
@@ -320,6 +354,19 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
|
|
|
320
354
|
// an `unknown` node (Phase 10's disable-safety contract, unchanged) -
|
|
321
355
|
// this only hides the *toolbar affordance* for creating new ones.
|
|
322
356
|
const tablesEnabled = Boolean(runtime.editor.schema.nodes.table);
|
|
357
|
+
// Per-tool toolbar visibility (toolbarTools.ts) - `tools` is a pure
|
|
358
|
+
// UI-visibility layer, composed with (never overriding) the deeper
|
|
359
|
+
// existing gates below: a required provider being absent, or a plugin
|
|
360
|
+
// having been excluded via `preset`, still wins even if a tool is
|
|
361
|
+
// explicitly left enabled here.
|
|
362
|
+
const t = resolveToolbarTools(tools);
|
|
363
|
+
const showInsertTable = t.insertTable && tablesEnabled;
|
|
364
|
+
const showImageTool = t.image && Boolean(mediaProvider);
|
|
365
|
+
const showVideoTool = t.video && Boolean(mediaProvider);
|
|
366
|
+
const showAudioTool = t.audio && Boolean(mediaProvider);
|
|
367
|
+
const showVersionHistoryTool = t.versionHistory && Boolean(versionProvider);
|
|
368
|
+
const showCommentsTool = t.comments && Boolean(commentProvider);
|
|
369
|
+
const showSuggestionsTool = t.suggestions && Boolean(suggestionProvider);
|
|
323
370
|
const tableSelected = currentTableScope.kind === "table-grid";
|
|
324
371
|
const selectedTableElement = tableSelected
|
|
325
372
|
? runtime.surface.renderer?.mapping.nodeToDom(currentTableScope.tableId)
|
|
@@ -329,15 +376,16 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
|
|
|
329
376
|
? runtime.surface.renderer?.mapping.nodeToDom(currentAtomScope.nodeId)
|
|
330
377
|
: undefined;
|
|
331
378
|
const selectedAtomNode = atomSelected ? findNode(runtime.editor.document, currentAtomScope.nodeId) : null;
|
|
332
|
-
// A divider (<hr>) is atomic/selectable (so ordinary caret/
|
|
333
|
-
// Delete behavior around it works, and the toolbar's "Delete
|
|
334
|
-
// atom" button applies to it), but
|
|
335
|
-
// all - MediaOverlay and the media-specific Edit/Resize
|
|
336
|
-
// every atom is a real media item, and unconditionally
|
|
337
|
-
//
|
|
338
|
-
// sensible edit or resize target. Media-only UI is gated
|
|
339
|
-
// instead of plain atomSelected; delete stays available for any
|
|
340
|
-
|
|
379
|
+
// A divider (<hr>) or page break is atomic/selectable (so ordinary caret/
|
|
380
|
+
// Backspace/Delete behavior around it works, and the toolbar's "Delete
|
|
381
|
+
// selected atom" button applies to it), but neither has any src/alt/
|
|
382
|
+
// width/height at all - MediaOverlay and the media-specific Edit/Resize
|
|
383
|
+
// actions assume every atom is a real media item, and unconditionally
|
|
384
|
+
// showing them for one of these surfaced nonsensical empty/undefined
|
|
385
|
+
// values with no sensible edit or resize target. Media-only UI is gated
|
|
386
|
+
// on this instead of plain atomSelected; delete stays available for any
|
|
387
|
+
// atom.
|
|
388
|
+
const mediaAtomSelected = atomSelected && selectedAtomNode?.type !== "divider" && selectedAtomNode?.type !== "page_break";
|
|
341
389
|
// Formula (like divider above) has no width/height concept - KaTeX sizes
|
|
342
390
|
// its own rendering from the source/font-size, not stored dimensions, so
|
|
343
391
|
// "Enlarge/Shrink selected media" and MediaOverlay's resize handle had no
|
|
@@ -348,6 +396,22 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
|
|
|
348
396
|
// node.type.includes("formula") for its own correct "Formula source"
|
|
349
397
|
// prompt.
|
|
350
398
|
const resizableAtomSelected = mediaAtomSelected && selectedAtomNode?.type !== "formula" && selectedAtomNode?.type !== "block_formula";
|
|
399
|
+
// A divider/page break has no editable field and no width/height, but it
|
|
400
|
+
// is still a real, selectable, deletable atom - right-clicking one should
|
|
401
|
+
// surface the same kind of Delete-only action menu formula already gets
|
|
402
|
+
// (docs/bugs/horizontal-line-not-visibly-selectable.md), not the silence
|
|
403
|
+
// `mediaAtomSelected` alone produces (that flag exists to hide fields
|
|
404
|
+
// that don't apply, not to hide a delete affordance entirely).
|
|
405
|
+
const actionOnlyAtomSelected = atomSelected && (selectedAtomNode?.type === "divider" || selectedAtomNode?.type === "page_break");
|
|
406
|
+
const selectedAtomId = atomSelected && "nodeId" in currentAtomScope ? currentAtomScope.nodeId : null;
|
|
407
|
+
useEffect(() => {
|
|
408
|
+
if (mediaContextMenu && (!(mediaAtomSelected || actionOnlyAtomSelected) || mediaContextMenu.nodeId !== selectedAtomId))
|
|
409
|
+
setMediaContextMenu(null);
|
|
410
|
+
}, [mediaContextMenu, mediaAtomSelected, actionOnlyAtomSelected, selectedAtomId]);
|
|
411
|
+
useEffect(() => {
|
|
412
|
+
if (mediaResizeTarget && (!mediaAtomSelected || mediaResizeTarget.nodeId !== selectedAtomId))
|
|
413
|
+
setMediaResizeTarget(null);
|
|
414
|
+
}, [mediaResizeTarget, mediaAtomSelected, selectedAtomId]);
|
|
351
415
|
// Context menu scope reduction: link no longer gets a right-click menu -
|
|
352
416
|
// LinkEditorPopover (already built and wired for the toolbar's Link
|
|
353
417
|
// button, per §1's investigation) auto-appears, anchored to the link's
|
|
@@ -803,13 +867,10 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
|
|
|
803
867
|
* the same way Tab special-cases list.indent/outdent's dynamic params.
|
|
804
868
|
*/
|
|
805
869
|
/**
|
|
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.
|
|
870
|
+
* The generic plugin context menu remains table-scoped. Images use their
|
|
871
|
+
* direct right-click details editor below, other media keeps its dedicated
|
|
872
|
+
* quick-action menu, while marks stay on the toolbar and links use the
|
|
873
|
+
* auto-triggered LinkEditorPopover.
|
|
813
874
|
*/
|
|
814
875
|
const resolveContextMenuItems = () => {
|
|
815
876
|
const many = () => ids(128);
|
|
@@ -1028,6 +1089,19 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
|
|
|
1028
1089
|
insertBlockAtom("block_image", {
|
|
1029
1090
|
src: item.url, alt: item.alt || item.title || "Image", decorative: false, status: "ready",
|
|
1030
1091
|
...(item.width ? { width: item.width } : {}), ...(item.height ? { height: item.height } : {}),
|
|
1092
|
+
// docs/bugs/media-details-old-editor-field-parity.md: item.license was
|
|
1093
|
+
// already fetched and shown in MediaManager's own library-browser info
|
|
1094
|
+
// panel (MediaManager.tsx), but had nowhere to persist once inserted -
|
|
1095
|
+
// the atom schema had no license attrs at all, so this real,
|
|
1096
|
+
// already-available data was silently discarded at exactly this call
|
|
1097
|
+
// site. `licenseVersion` has no MediaItem.license equivalent (its
|
|
1098
|
+
// shape has no separate version field) - left for the user to fill in
|
|
1099
|
+
// via the media details panel, same as a freshly uploaded image.
|
|
1100
|
+
...(item.license?.workName || item.license?.licenseText
|
|
1101
|
+
? { licenseDescription: [item.license.workName, item.license.licenseText].filter(Boolean).join(" - ") } : {}),
|
|
1102
|
+
...(item.license?.sourceUrl ? { licenseSourceUrl: item.license.sourceUrl } : {}),
|
|
1103
|
+
...(item.license?.licenseType ? { licenseType: item.license.licenseType } : {}),
|
|
1104
|
+
...(item.license?.author ? { licenseAttribution: item.license.author } : {}),
|
|
1031
1105
|
}, createNodeId());
|
|
1032
1106
|
};
|
|
1033
1107
|
const commentAuthorId = authorId || "anonymous";
|
|
@@ -1211,6 +1285,27 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
|
|
|
1211
1285
|
runtime.focus();
|
|
1212
1286
|
setLinkPopover(null);
|
|
1213
1287
|
};
|
|
1288
|
+
const openMediaDetails = (scope, node, element) => {
|
|
1289
|
+
if (scope.kind !== "atomic-node")
|
|
1290
|
+
return;
|
|
1291
|
+
const rect = (element || selectedAtomElement)?.getBoundingClientRect();
|
|
1292
|
+
setMediaDetailsPopover({
|
|
1293
|
+
x: rect ? rect.left : 0, y: rect ? rect.bottom + 6 : 0, scope,
|
|
1294
|
+
initial: {
|
|
1295
|
+
alt: String(node.attrs?.alt || ""),
|
|
1296
|
+
href: String(node.attrs?.href || ""),
|
|
1297
|
+
openInNewTab: node.attrs?.target === "_blank",
|
|
1298
|
+
width: typeof node.attrs?.width === "number" ? node.attrs.width : undefined,
|
|
1299
|
+
borderRadius: typeof node.attrs?.borderRadius === "number" ? node.attrs.borderRadius : undefined,
|
|
1300
|
+
align: node.attrs?.align === "left" || node.attrs?.align === "center" || node.attrs?.align === "right" ? node.attrs.align : undefined,
|
|
1301
|
+
licenseDescription: String(node.attrs?.licenseDescription || ""),
|
|
1302
|
+
licenseSourceUrl: String(node.attrs?.licenseSourceUrl || ""),
|
|
1303
|
+
licenseType: String(node.attrs?.licenseType || ""),
|
|
1304
|
+
licenseVersion: String(node.attrs?.licenseVersion || ""),
|
|
1305
|
+
licenseAttribution: String(node.attrs?.licenseAttribution || ""),
|
|
1306
|
+
},
|
|
1307
|
+
});
|
|
1308
|
+
};
|
|
1214
1309
|
const editSelectedAtom = (resizeBy) => {
|
|
1215
1310
|
const scope = atomScope();
|
|
1216
1311
|
if (scope.kind !== "atomic-node")
|
|
@@ -1218,16 +1313,59 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
|
|
|
1218
1313
|
const node = findNode(runtime.editor.document, scope.nodeId);
|
|
1219
1314
|
if (!node)
|
|
1220
1315
|
return;
|
|
1316
|
+
if (resizeBy === undefined && !node.type.includes("formula")) {
|
|
1317
|
+
// Images: the old window.prompt("Alt text") one-field edit is
|
|
1318
|
+
// replaced by the full Media details panel (docs/bugs/
|
|
1319
|
+
// media-details-old-editor-field-parity.md) - formulas keep their
|
|
1320
|
+
// existing prompt unchanged below, since this panel's fields
|
|
1321
|
+
// (link/align/radius/license) don't apply to a formula atom at all.
|
|
1322
|
+
openMediaDetails(scope, node);
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1221
1325
|
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())
|
|
1326
|
+
? updateAtom(runtime.editor.document, scope, { attrs: { source: window.prompt("Formula source", String(node.attrs?.source || "")) || node.attrs?.source } }, blockContext())
|
|
1225
1327
|
: resizeAtom(runtime.editor.document, scope, {
|
|
1226
1328
|
width: Math.max(16, Number(node.attrs?.width || 160) + resizeBy),
|
|
1227
1329
|
height: Math.max(16, Number(node.attrs?.height || 90) + resizeBy),
|
|
1228
1330
|
}, blockContext());
|
|
1229
1331
|
runtime.executeOperations(operations, resizeBy === undefined ? {} : { historyGroup: `resize-${scope.nodeId}` });
|
|
1230
1332
|
};
|
|
1333
|
+
const applyMediaDetails = (draft) => {
|
|
1334
|
+
if (!mediaDetailsPopover)
|
|
1335
|
+
return;
|
|
1336
|
+
const { scope } = mediaDetailsPopover;
|
|
1337
|
+
if (scope.kind !== "atomic-node") {
|
|
1338
|
+
setMediaDetailsPopover(null);
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
const node = findNode(runtime.editor.document, scope.nodeId);
|
|
1342
|
+
const currentWidth = typeof node?.attrs?.width === "number" ? node.attrs.width : undefined;
|
|
1343
|
+
const currentHeight = typeof node?.attrs?.height === "number" ? node.attrs.height : undefined;
|
|
1344
|
+
// Width preserves the original aspect ratio (matches the drag handle
|
|
1345
|
+
// and Resize +/- buttons' own behavior) rather than exposing a separate
|
|
1346
|
+
// Height field the old editor's own screenshot didn't have either.
|
|
1347
|
+
const resizedHeight = draft.width && currentWidth && currentHeight
|
|
1348
|
+
? Math.round((draft.width * currentHeight) / currentWidth)
|
|
1349
|
+
: currentHeight;
|
|
1350
|
+
setMediaDetailsPopover(null);
|
|
1351
|
+
runtime.executeOperations(updateAtom(runtime.editor.document, scope, {
|
|
1352
|
+
attrs: {
|
|
1353
|
+
alt: draft.alt,
|
|
1354
|
+
href: draft.href || undefined,
|
|
1355
|
+
target: draft.href && draft.openInNewTab ? "_blank" : undefined,
|
|
1356
|
+
width: draft.width ?? currentWidth,
|
|
1357
|
+
height: draft.width ? resizedHeight : currentHeight,
|
|
1358
|
+
borderRadius: draft.borderRadius || undefined,
|
|
1359
|
+
align: draft.align,
|
|
1360
|
+
licenseDescription: draft.licenseDescription || undefined,
|
|
1361
|
+
licenseSourceUrl: draft.licenseSourceUrl || undefined,
|
|
1362
|
+
licenseType: draft.licenseType || undefined,
|
|
1363
|
+
licenseVersion: draft.licenseVersion || undefined,
|
|
1364
|
+
licenseAttribution: draft.licenseAttribution || undefined,
|
|
1365
|
+
},
|
|
1366
|
+
}, blockContext()));
|
|
1367
|
+
runtime.focus();
|
|
1368
|
+
};
|
|
1231
1369
|
/** MediaOverlay's drag-corner-handle commit - explicit width/height rather than editSelectedAtom's fixed +/-20 increment. */
|
|
1232
1370
|
const resizeSelectedAtomTo = (width, height) => {
|
|
1233
1371
|
const scope = atomScope();
|
|
@@ -1326,26 +1464,72 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
|
|
|
1326
1464
|
// twice; below 1440px (including mobile) this copy is what's reachable,
|
|
1327
1465
|
// same as before. "Code" isn't promoted - see the docs/bugs/ writeup on
|
|
1328
1466
|
// 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) => {
|
|
1467
|
+
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
1468
|
const rect = event.currentTarget.getBoundingClientRect();
|
|
1331
1469
|
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) => {
|
|
1470
|
+
} }), t.backgroundColor && _jsx(ToolbarMenuItem, { icon: "backgroundColor", label: "Background colour", disabled: readOnly, widePromote: true, onClick: (event) => {
|
|
1333
1471
|
const rect = event.currentTarget.getBoundingClientRect();
|
|
1334
1472
|
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 })] });
|
|
1473
|
+
} }), 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 })] });
|
|
1474
|
+
const showMoreTextStyles = t.code || t.superscript || t.subscript || t.textColor || t.backgroundColor || t.fontSize || t.fontFamily;
|
|
1475
|
+
/**
|
|
1476
|
+
* Line spacing - matches Google Docs' own reference shape: a short preset
|
|
1477
|
+
* list plus a custom/exact value entry, with a checkmark on whichever
|
|
1478
|
+
* preset (or "Default") currently applies. "Default" clears the
|
|
1479
|
+
* attribute entirely (setLineHeight(undefined)) rather than writing an
|
|
1480
|
+
* explicit "1" - the block then renders at the browser/font's own
|
|
1481
|
+
* natural line-height, matching how no override ever forces a value for
|
|
1482
|
+
* align/indent either. A mixed-value selection (blocks disagreeing)
|
|
1483
|
+
* shows no checkmark on any entry, matching every comparison below
|
|
1484
|
+
* evaluating false against the "mixed" sentinel with no special-casing
|
|
1485
|
+
* needed.
|
|
1486
|
+
*/
|
|
1487
|
+
const LINE_HEIGHT_PRESETS = [1, 1.15, 1.5, 2, 2.5];
|
|
1488
|
+
const lineHeightMenuItems = _jsxs(_Fragment, { children: [_jsx(ToolbarMenuItem, { label: "Default", pressed: currentLineHeight() === undefined, disabled: readOnly, onClick: () => setLineHeight(undefined) }), LINE_HEIGHT_PRESETS.map((preset) => _jsx(ToolbarMenuItem, { label: String(preset), pressed: currentLineHeight() === preset, disabled: readOnly, onClick: () => setLineHeight(preset) }, preset)), _jsx("div", { className: "srte-menu-separator" }), _jsxs("div", { className: "srte-line-height-custom", style: { padding: "4px 8px", display: "flex", alignItems: "center", gap: 6 }, children: [_jsx("span", { "aria-hidden": "true", style: { fontSize: 12, whiteSpace: "nowrap" }, children: "Custom:" }), _jsx("input", { "aria-label": "Custom line spacing", type: "number", min: 0.1, max: 10, step: 0.05, disabled: readOnly, defaultValue: (() => { const current = currentLineHeight(); return typeof current === "number" && !LINE_HEIGHT_PRESETS.includes(current) ? current : ""; })(), placeholder: "e.g. 1.75", style: { width: "100%", minWidth: 0 }, onKeyDown: (event) => {
|
|
1489
|
+
if (event.key !== "Enter")
|
|
1490
|
+
return;
|
|
1491
|
+
const value = Number(event.currentTarget.value);
|
|
1492
|
+
if (Number.isFinite(value) && value >= 0.1 && value <= 10)
|
|
1493
|
+
setLineHeight(value);
|
|
1494
|
+
event.currentTarget.closest("details")?.removeAttribute("open");
|
|
1495
|
+
}, onBlur: (event) => {
|
|
1496
|
+
const value = Number(event.currentTarget.value);
|
|
1497
|
+
if (event.currentTarget.value && Number.isFinite(value) && value >= 0.1 && value <= 10)
|
|
1498
|
+
setLineHeight(value);
|
|
1499
|
+
} })] })] });
|
|
1336
1500
|
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") })] });
|
|
1501
|
+
/**
|
|
1502
|
+
* Named multi-level marker presets (decimal/lower-alpha/upper-alpha/
|
|
1503
|
+
* roman/outline) - a distinct control from the plain Bulleted/Numbered/
|
|
1504
|
+
* Checklist buttons above, which only toggle a raw list style. Extracted
|
|
1505
|
+
* to a variable so it can be reused verbatim in both the desktop "More
|
|
1506
|
+
* list tools" dropdown and MobileMoreMenu below - it was previously
|
|
1507
|
+
* hardcoded only into the dropdown, meaning a narrow/mobile viewport
|
|
1508
|
+
* (where every ToolbarDropdown hides entirely, per theme.ts's
|
|
1509
|
+
* 639px breakpoint) had no way to reach it at all - see
|
|
1510
|
+
* docs/bugs/list-preset-missing-from-mobile-more-menu.md.
|
|
1511
|
+
*/
|
|
1512
|
+
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) => {
|
|
1513
|
+
const preset = event.target.value;
|
|
1514
|
+
if (!preset || currentListParts.length !== 1)
|
|
1515
|
+
return;
|
|
1516
|
+
// A preset choice applies to the whole list, regardless of how deep
|
|
1517
|
+
// the cursor is nested — see outermostListId.
|
|
1518
|
+
const rootId = outermostListId(currentListParts[0].listId);
|
|
1519
|
+
runtime.executeOperations(setListPreset(runtime.editor.document, { ...currentListParts[0], listId: rootId }, { preset }, blockContext()), { preserveSelectionById: true });
|
|
1520
|
+
}, 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
1521
|
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
1522
|
// Remove link/Insert formula/Special characters also get a standalone
|
|
1339
1523
|
// widePromote ToolbarButton (see textStylesMenuItems's own comment above
|
|
1340
1524
|
// for the mechanism) - Insert video/audio and the selected-media actions
|
|
1341
1525
|
// 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
|
|
1526
|
+
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
1527
|
const rect = event.currentTarget.getBoundingClientRect();
|
|
1344
1528
|
setFormulaLibraryPopover({ x: rect.left, y: rect.bottom + 4 });
|
|
1345
|
-
} }), _jsx(ToolbarMenuItem, { icon: "specialChar", label: "Special characters", disabled: readOnly, widePromote: true, onClick: (event) => {
|
|
1529
|
+
} }), t.specialCharacters && _jsx(ToolbarMenuItem, { icon: "specialChar", label: "Special characters", disabled: readOnly, widePromote: true, onClick: (event) => {
|
|
1346
1530
|
const rect = event.currentTarget.getBoundingClientRect();
|
|
1347
1531
|
setSpecialCharPopover({ x: rect.left, y: rect.bottom + 4 });
|
|
1348
|
-
} }), _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())) })] });
|
|
1532
|
+
} }), t.horizontalLine && _jsx(ToolbarMenuItem, { icon: "divider", label: "Horizontal line", disabled: readOnly, widePromote: true, onClick: () => { insertBlockAtom("divider", {}, createNodeId()); runtime.focus(); } }), t.pageBreak && _jsx(ToolbarMenuItem, { icon: "pageBreak", label: "Page break", disabled: readOnly, widePromote: true, onClick: () => { insertBlockAtom("page_break", {}, createNodeId()); runtime.focus(); } }), _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())) })] });
|
|
1349
1533
|
// "Remove row"/"Remove column"/"Remove table" renamed to "Delete ..." -
|
|
1350
1534
|
// matches the right-click table context menu's existing wording
|
|
1351
1535
|
// (table/plugin.ts's table.contextMenu.removeRow/removeColumn/removeTable
|
|
@@ -1359,32 +1543,33 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
|
|
|
1359
1543
|
const rect = event.currentTarget.getBoundingClientRect();
|
|
1360
1544
|
setTableBorderPopover({ x: rect.left, y: rect.bottom + 4, scope: scope, initial: currentCellBorderDraft(scope) });
|
|
1361
1545
|
} })] });
|
|
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
|
-
|
|
1546
|
+
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: "Print / Save as PDF", onClick: runPdfExport }), t.saveAsSmartRte && _jsx(ToolbarMenuItem, { icon: "json", label: "Save as Smart RTE file", onClick: () => runExport("native") })] });
|
|
1547
|
+
const showSaveCopy = t.saveAsHtml || t.saveAsMarkdown || t.saveAsWord || t.saveAsPdf || t.saveAsSmartRte;
|
|
1548
|
+
// "Review" splits into two independently-toggleable tools (comments,
|
|
1549
|
+
// suggestions) - not an arbitrary split, it maps directly onto the two
|
|
1550
|
+
// already-separate providers this architecture has (commentProvider,
|
|
1551
|
+
// suggestionProvider). Kept as ONE dropdown trigger (labeled "Review")
|
|
1552
|
+
// rather than two, showing whichever half(s) are actually enabled.
|
|
1553
|
+
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) })] });
|
|
1554
|
+
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) })] });
|
|
1555
|
+
const reviewMenuItems = _jsxs(_Fragment, { children: [showCommentsTool && commentsMenuItems, showCommentsTool && showSuggestionsTool && _jsx("div", { className: "srte-menu-separator" }), showSuggestionsTool && suggestionsMenuItems] });
|
|
1556
|
+
const showReviewDropdown = showCommentsTool || showSuggestionsTool;
|
|
1557
|
+
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
1558
|
const rect = event.currentTarget.getBoundingClientRect();
|
|
1366
1559
|
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) => {
|
|
1560
|
+
} }), t.backgroundColor && _jsx(ToolbarButton, { icon: "backgroundColor", label: "Background colour", disabled: readOnly, widePromote: true, onClick: (event) => {
|
|
1368
1561
|
const rect = event.currentTarget.getBoundingClientRect();
|
|
1369
1562
|
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(), {
|
|
1563
|
+
} }), 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
1564
|
type: event.target.value === "paragraph" ? "paragraph" : event.target.value === "code_block" ? "code_block" : "heading",
|
|
1372
1565
|
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) => {
|
|
1566
|
+
}, 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.lineHeight && _jsx(ToolbarDropdown, { icon: "lineHeight", label: "Line spacing", priority: 2, children: lineHeightMenuItems }), 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
1567
|
const rect = event.currentTarget.getBoundingClientRect();
|
|
1383
1568
|
setFormulaLibraryPopover({ x: rect.left, y: rect.bottom + 4 });
|
|
1384
|
-
} }), _jsx(ToolbarButton, { icon: "specialChar", label: "Special characters", disabled: readOnly, widePromote: true, onClick: (event) => {
|
|
1569
|
+
} }), t.specialCharacters && _jsx(ToolbarButton, { icon: "specialChar", label: "Special characters", disabled: readOnly, widePromote: true, onClick: (event) => {
|
|
1385
1570
|
const rect = event.currentTarget.getBoundingClientRect();
|
|
1386
1571
|
setSpecialCharPopover({ x: rect.left, y: rect.bottom + 4 });
|
|
1387
|
-
} }), _jsx(ToolbarDropdown, { icon: "video", label: "More to insert", priority: 2, children: insertMoreMenuItems })] }),
|
|
1572
|
+
} }), t.horizontalLine && _jsx(ToolbarButton, { icon: "divider", label: "Horizontal line", disabled: readOnly, widePromote: true, onClick: () => { insertBlockAtom("divider", {}, createNodeId()); runtime.focus(); } }), t.pageBreak && _jsx(ToolbarButton, { icon: "pageBreak", label: "Page break", disabled: readOnly, widePromote: true, onClick: () => { insertBlockAtom("page_break", {}, createNodeId()); runtime.focus(); } }), _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
1573
|
const rect = event.currentTarget.getBoundingClientRect();
|
|
1389
1574
|
setTableSizePopover({ x: rect.left, y: rect.bottom + 4 });
|
|
1390
1575
|
} }), _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 +1577,11 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
|
|
|
1392
1577
|
if (file)
|
|
1393
1578
|
void runImport(file);
|
|
1394
1579
|
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
|
|
1580
|
+
} }), _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, t.lineHeight && _jsx("div", { className: "srte-menu-separator" }), t.lineHeight && lineHeightMenuItems, _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
1581
|
setLinkOverlayDismissedAt(runtime.editor.selection.head);
|
|
1397
1582
|
setLinkPopover(null);
|
|
1398
1583
|
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"
|
|
1584
|
+
} }), !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 || actionOnlyAtomSelected) && 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, editable: mediaAtomSelected, 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
1585
|
? (colorPopover.target.markId === "textColor" ? "Text colour" : "Background colour")
|
|
1401
1586
|
: 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
1587
|
setTableSizePopover(null);
|
|
@@ -1405,7 +1590,7 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
|
|
|
1405
1590
|
}, onCancel: () => {
|
|
1406
1591
|
setTableSizePopover(null);
|
|
1407
1592
|
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) => {
|
|
1593
|
+
} }), 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
1594
|
// A Ctrl/Cmd+click on a link is meant to open it (onClick below),
|
|
1410
1595
|
// not move the caret into it - but native mousedown's own default
|
|
1411
1596
|
// action *is* the caret-repositioning, and mousedown fires (and
|
|
@@ -1420,27 +1605,45 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
|
|
|
1420
1605
|
// completed render in a varying number of passes, so the overlay
|
|
1421
1606
|
// could already be open before the flag was ever set.)
|
|
1422
1607
|
const anchor = event.target?.closest("a[href]");
|
|
1423
|
-
|
|
1608
|
+
const linkedMedia = event.target?.closest("[data-smart-href]");
|
|
1609
|
+
if ((anchor || linkedMedia) && (event.metaKey || event.ctrlKey))
|
|
1424
1610
|
event.preventDefault();
|
|
1425
1611
|
}, onClick: (event) => {
|
|
1426
1612
|
const anchor = event.target?.closest("a[href]");
|
|
1427
|
-
|
|
1613
|
+
// An image atom with a Link set (docs/bugs/media-details-old-editor-field-parity.md)
|
|
1614
|
+
// - plain data attributes, not a real <a> wrapper, in the live DOM
|
|
1615
|
+
// (see surface/renderer.ts's own reasoning); Ctrl/Cmd+click opens it
|
|
1616
|
+
// the same way a real text link does, matching this project's only
|
|
1617
|
+
// existing link-opening convention rather than inventing a second one.
|
|
1618
|
+
const linkedMedia = event.target?.closest("[data-smart-href]");
|
|
1619
|
+
const href = anchor?.href || linkedMedia?.getAttribute("data-smart-href");
|
|
1620
|
+
if (href && (event.metaKey || event.ctrlKey)) {
|
|
1621
|
+
event.preventDefault();
|
|
1622
|
+
window.open(href, "_blank", "noopener,noreferrer");
|
|
1428
1623
|
return;
|
|
1429
|
-
|
|
1430
|
-
|
|
1624
|
+
}
|
|
1625
|
+
const imageTarget = event.target?.closest("[data-smart-type=\"image\"], [data-smart-type=\"block_image\"]");
|
|
1626
|
+
if (!imageTarget)
|
|
1627
|
+
return;
|
|
1628
|
+
setMediaContextMenu(null);
|
|
1629
|
+
const mappedImage = runtime.surface.renderer?.mapping.domToNode(imageTarget);
|
|
1630
|
+
if (mappedImage)
|
|
1631
|
+
setMediaResizeTarget({ nodeId: mappedImage.nodeId });
|
|
1431
1632
|
}, onContextMenu: (event) => {
|
|
1432
1633
|
if (readOnly)
|
|
1433
1634
|
return;
|
|
1434
1635
|
event.preventDefault();
|
|
1636
|
+
setMediaContextMenu(null);
|
|
1435
1637
|
// Atom selection (unlike caret/text placement) is not native
|
|
1436
1638
|
// browser behavior - it's InputController's own clickListener
|
|
1437
1639
|
// (surface/input.ts), bound to the "click" DOM event, which never
|
|
1438
1640
|
// fires for a right-click. Without this, right-clicking an atom
|
|
1439
1641
|
// directly (no preceding left-click) left whatever selection was
|
|
1440
1642
|
// already there in place, so atomic-node scope never resolved and
|
|
1441
|
-
//
|
|
1442
|
-
// appeared - right-clicking media looked
|
|
1443
|
-
// nothing. Mirrors clickListener's own
|
|
1643
|
+
// the media interaction (details editor for images, quick-action
|
|
1644
|
+
// menu for other media) never appeared - right-clicking media looked
|
|
1645
|
+
// like right-clicking nothing. Mirrors clickListener's own
|
|
1646
|
+
// atom-selection logic
|
|
1444
1647
|
// exactly.
|
|
1445
1648
|
const atomTarget = event.target?.closest("[data-smart-atomic]");
|
|
1446
1649
|
const mapped = atomTarget ? runtime.surface.renderer?.mapping.domToNode(atomTarget) : null;
|
|
@@ -1450,6 +1653,19 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
|
|
|
1450
1653
|
runtime.editor.setSelection({ type: "node", anchor: range.from, head: range.to }, { source: "api" });
|
|
1451
1654
|
runtime.surface.renderer?.render(runtime.editor.document, runtime.editor.selection);
|
|
1452
1655
|
}
|
|
1656
|
+
if (mapped.node.type !== "unknown") {
|
|
1657
|
+
setContextMenu(null);
|
|
1658
|
+
const isImage = mapped.node.type === "image" || mapped.node.type === "block_image";
|
|
1659
|
+
if (isImage) {
|
|
1660
|
+
const scope = runtime.editor.resolveScope({ want: "atomic-node" });
|
|
1661
|
+
setMediaResizeTarget({ nodeId: mapped.nodeId });
|
|
1662
|
+
if (scope.kind === "atomic-node")
|
|
1663
|
+
openMediaDetails(scope, mapped.node, atomTarget || undefined);
|
|
1664
|
+
return;
|
|
1665
|
+
}
|
|
1666
|
+
setMediaContextMenu({ x: event.clientX, y: event.clientY, nodeId: mapped.nodeId });
|
|
1667
|
+
return;
|
|
1668
|
+
}
|
|
1453
1669
|
}
|
|
1454
1670
|
else if (isCollapsedTextSelection(runtime.editor.selection)) {
|
|
1455
1671
|
// Chromium/Firefox reposition the native caret to the click
|
|
@@ -1480,10 +1696,8 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
|
|
|
1480
1696
|
runtime.surface.renderer?.render(runtime.editor.document, runtime.editor.selection);
|
|
1481
1697
|
}
|
|
1482
1698
|
}
|
|
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.
|
|
1699
|
+
// A right-click that is not handled by the media branch above and
|
|
1700
|
+
// does not resolve to a table cell opens no generic menu.
|
|
1487
1701
|
const rightClickTableScope = runtime.editor.resolveScope({ want: "table-grid" });
|
|
1488
1702
|
if ("kind" in rightClickTableScope && rightClickTableScope.kind === "table-grid") {
|
|
1489
1703
|
setContextMenu({ x: event.clientX, y: event.clientY });
|