@byline/richtext-lexical 3.4.1 → 3.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/field/extensions/admonition/admonition-commands.d.ts +25 -0
- package/dist/field/extensions/admonition/admonition-commands.js +4 -0
- package/dist/field/extensions/admonition/admonition-extension.d.ts +2 -3
- package/dist/field/extensions/admonition/admonition-extension.js +138 -24
- package/dist/field/extensions/admonition/admonition-node.css +112 -0
- package/dist/field/extensions/admonition/admonition-node.d.ts +26 -14
- package/dist/field/extensions/admonition/admonition-node.js +121 -72
- package/dist/field/extensions/admonition/index.js +1 -0
- package/dist/field/extensions/admonition/node-types.d.ts +10 -4
- package/dist/field/extensions/floating-text-format/index.d.ts +8 -1
- package/dist/field/extensions/floating-text-format/index.js +6 -4
- package/dist/field/markdown/transformers.js +11 -14
- package/package.json +5 -5
- package/src/field/extensions/admonition/admonition-commands.ts +31 -0
- package/src/field/extensions/admonition/admonition-extension.tsx +248 -37
- package/src/field/extensions/admonition/admonition-node.css +113 -0
- package/src/field/extensions/admonition/admonition-node.tsx +172 -93
- package/src/field/extensions/admonition/index.ts +4 -8
- package/src/field/extensions/admonition/node-types.ts +10 -10
- package/src/field/extensions/floating-text-format/index.tsx +19 -3
- package/src/field/markdown/admonition-roundtrip.test.node.ts +110 -0
- package/src/field/markdown/transformers.ts +45 -24
- package/dist/field/extensions/admonition/admonition-node-component.css +0 -119
- package/dist/field/extensions/admonition/admonition-node-component.d.ts +0 -17
- package/dist/field/extensions/admonition/admonition-node-component.js +0 -196
- package/dist/field/extensions/admonition/icons/danger-icon.d.ts +0 -7
- package/dist/field/extensions/admonition/icons/index.d.ts +0 -4
- package/dist/field/extensions/admonition/icons/note-icon.d.ts +0 -7
- package/dist/field/extensions/admonition/icons/tip-icon.d.ts +0 -7
- package/dist/field/extensions/admonition/icons/warning-icon.d.ts +0 -7
- package/src/field/extensions/admonition/admonition-node-component.css +0 -115
- package/src/field/extensions/admonition/admonition-node-component.tsx +0 -257
|
@@ -5,16 +5,22 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
|
-
import type {
|
|
8
|
+
import type { NodeKey, SerializedElementNode, Spread } from 'lexical';
|
|
9
9
|
export type AdmonitionType = 'note' | 'tip' | 'warning' | 'danger';
|
|
10
10
|
export interface AdmonitionAttributes {
|
|
11
11
|
admonitionType: AdmonitionType;
|
|
12
12
|
title: string;
|
|
13
|
-
content?: LexicalEditor;
|
|
14
13
|
key?: NodeKey;
|
|
15
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* The admonition is an `ElementNode` — its body lives as real children in
|
|
17
|
+
* the main editor tree (paragraphs + inline content), so the serialized
|
|
18
|
+
* shape extends `SerializedElementNode` and carries `children`. `type` and
|
|
19
|
+
* `title` are node-level attributes set from the Insert/Edit modal; they
|
|
20
|
+
* ride the opening Docusaurus fence (`:::type[title]`) on markdown export,
|
|
21
|
+
* not the body.
|
|
22
|
+
*/
|
|
16
23
|
export type SerializedAdmonitionNode = Spread<{
|
|
17
24
|
admonitionType: AdmonitionType;
|
|
18
25
|
title: string;
|
|
19
|
-
|
|
20
|
-
}, SerializedLexicalNode>;
|
|
26
|
+
}, SerializedElementNode>;
|
|
@@ -7,6 +7,13 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import './index.css';
|
|
9
9
|
import type * as React from 'react';
|
|
10
|
-
export declare function FloatingTextFormatToolbarPlugin({ anchorElem, }: {
|
|
10
|
+
export declare function FloatingTextFormatToolbarPlugin({ anchorElem, shouldShow, }: {
|
|
11
11
|
anchorElem?: HTMLElement;
|
|
12
|
+
/**
|
|
13
|
+
* Optional gate evaluated inside an editor read context. Return `false`
|
|
14
|
+
* to suppress the popover for the current selection — used to scope the
|
|
15
|
+
* toolbar to admonition bodies on editors where the global
|
|
16
|
+
* `FloatingTextFormatExtension` is removed.
|
|
17
|
+
*/
|
|
18
|
+
shouldShow?: () => boolean;
|
|
12
19
|
}): React.JSX.Element | null;
|
|
@@ -207,7 +207,7 @@ function TextFormatFloatingToolbar({ editor, anchorElem, isLink, isBold, isItali
|
|
|
207
207
|
})
|
|
208
208
|
});
|
|
209
209
|
}
|
|
210
|
-
function useFloatingTextFormatToolbar(editor, anchorElem) {
|
|
210
|
+
function useFloatingTextFormatToolbar(editor, anchorElem, shouldShow) {
|
|
211
211
|
const [isText, setIsText] = useState(false);
|
|
212
212
|
const [isLink, setIsLink] = useState(false);
|
|
213
213
|
const [isBold, setIsBold] = useState(false);
|
|
@@ -225,6 +225,7 @@ function useFloatingTextFormatToolbar(editor, anchorElem) {
|
|
|
225
225
|
const rootElement = editor.getRootElement();
|
|
226
226
|
if (null !== nativeSelection && (!$isRangeSelection(selection) || null === rootElement || !rootElement.contains(nativeSelection.anchorNode))) return void setIsText(false);
|
|
227
227
|
if (!$isRangeSelection(selection)) return;
|
|
228
|
+
if (null != shouldShow && !shouldShow()) return void setIsText(false);
|
|
228
229
|
const node = getSelectedNode(selection);
|
|
229
230
|
setIsBold(selection.hasFormat('bold'));
|
|
230
231
|
setIsItalic(selection.hasFormat('italic'));
|
|
@@ -240,7 +241,8 @@ function useFloatingTextFormatToolbar(editor, anchorElem) {
|
|
|
240
241
|
if (!selection.isCollapsed() && '' === rawTextContent) setIsText(false);
|
|
241
242
|
});
|
|
242
243
|
}, [
|
|
243
|
-
editor
|
|
244
|
+
editor,
|
|
245
|
+
shouldShow
|
|
244
246
|
]);
|
|
245
247
|
useEffect(()=>{
|
|
246
248
|
document.addEventListener('selectionchange', updatePopup);
|
|
@@ -272,8 +274,8 @@ function useFloatingTextFormatToolbar(editor, anchorElem) {
|
|
|
272
274
|
isCode: isCode
|
|
273
275
|
}), anchorElem);
|
|
274
276
|
}
|
|
275
|
-
function FloatingTextFormatToolbarPlugin({ anchorElem = document.body }) {
|
|
277
|
+
function FloatingTextFormatToolbarPlugin({ anchorElem = document.body, shouldShow }) {
|
|
276
278
|
const [editor] = useLexicalComposerContext();
|
|
277
|
-
return useFloatingTextFormatToolbar(editor, anchorElem);
|
|
279
|
+
return useFloatingTextFormatToolbar(editor, anchorElem, shouldShow);
|
|
278
280
|
}
|
|
279
281
|
export { FloatingTextFormatToolbarPlugin };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { $convertFromMarkdownString, $convertToMarkdownString, TEXT_FORMAT_TRANSFORMERS, TRANSFORMERS } from "@lexical/markdown";
|
|
1
|
+
import { $convertFromMarkdownString, $convertToMarkdownString, LINK, TEXT_FORMAT_TRANSFORMERS, TRANSFORMERS } from "@lexical/markdown";
|
|
2
2
|
import { $createTableCellNode, $createTableNode, $createTableRowNode, $isTableCellNode, $isTableNode, $isTableRowNode, TableCellHeaderStates, TableCellNode, TableNode, TableRowNode } from "@lexical/table";
|
|
3
|
-
import { $isParagraphNode, $isTextNode } from "lexical";
|
|
3
|
+
import { $createParagraphNode, $isParagraphNode, $isTextNode } from "lexical";
|
|
4
4
|
import { $createAdmonitionNode, $isAdmonitionNode, AdmonitionNode } from "../extensions/admonition/admonition-node.js";
|
|
5
5
|
const TABLE_ROW_REG_EXP = /^(?:\|)(.+)(?:\|)\s?$/;
|
|
6
6
|
const TABLE_ROW_DIVIDER_REG_EXP = /^(\| ?:?-+:? ?)+\|?\s?$/;
|
|
@@ -103,7 +103,8 @@ const ADMONITION_TYPES = new Set([
|
|
|
103
103
|
const ADMONITION_START_REG_EXP = /^:::(note|tip|warning|danger)(?:\[([^\]]*)\])?\s*$/;
|
|
104
104
|
const ADMONITION_END_REG_EXP = /^:::\s*$/;
|
|
105
105
|
const ADMONITION_BODY_TRANSFORMERS = [
|
|
106
|
-
...TEXT_FORMAT_TRANSFORMERS
|
|
106
|
+
...TEXT_FORMAT_TRANSFORMERS,
|
|
107
|
+
LINK
|
|
107
108
|
];
|
|
108
109
|
const ADMONITION = {
|
|
109
110
|
dependencies: [
|
|
@@ -113,10 +114,7 @@ const ADMONITION = {
|
|
|
113
114
|
if (!$isAdmonitionNode(node)) return null;
|
|
114
115
|
const type = node.getAdmonitionType();
|
|
115
116
|
const title = node.getTitle();
|
|
116
|
-
|
|
117
|
-
node.__content.read(()=>{
|
|
118
|
-
body = $convertToMarkdownString(ADMONITION_BODY_TRANSFORMERS).trim();
|
|
119
|
-
});
|
|
117
|
+
const body = $convertToMarkdownString(ADMONITION_BODY_TRANSFORMERS, node).trim();
|
|
120
118
|
const heading = title ? `:::${type}[${title}]` : `:::${type}`;
|
|
121
119
|
return body ? `${heading}\n${body}\n:::` : `${heading}\n:::`;
|
|
122
120
|
},
|
|
@@ -125,7 +123,7 @@ const ADMONITION = {
|
|
|
125
123
|
optional: true,
|
|
126
124
|
regExp: ADMONITION_END_REG_EXP
|
|
127
125
|
},
|
|
128
|
-
replace: (rootNode, children, startMatch, _endMatch, linesInBetween)=>{
|
|
126
|
+
replace: (rootNode, children, startMatch, _endMatch, linesInBetween, isImport)=>{
|
|
129
127
|
const rawType = startMatch[1];
|
|
130
128
|
if (!ADMONITION_TYPES.has(rawType)) return false;
|
|
131
129
|
const admonitionType = rawType;
|
|
@@ -134,15 +132,14 @@ const ADMONITION = {
|
|
|
134
132
|
admonitionType,
|
|
135
133
|
title
|
|
136
134
|
});
|
|
137
|
-
if (
|
|
135
|
+
if (null != children) node.append(...children);
|
|
136
|
+
else if (null != linesInBetween) {
|
|
138
137
|
const body = linesInBetween.join('\n').trim();
|
|
139
|
-
if (body) node
|
|
140
|
-
$convertFromMarkdownString(body, ADMONITION_BODY_TRANSFORMERS);
|
|
141
|
-
}, {
|
|
142
|
-
discrete: true
|
|
143
|
-
});
|
|
138
|
+
if (body) $convertFromMarkdownString(body, ADMONITION_BODY_TRANSFORMERS, node);
|
|
144
139
|
}
|
|
140
|
+
if (0 === node.getChildrenSize()) node.append($createParagraphNode());
|
|
145
141
|
rootNode.append(node);
|
|
142
|
+
if (!isImport) node.selectStart();
|
|
146
143
|
},
|
|
147
144
|
type: 'multiline-element'
|
|
148
145
|
};
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"private": false,
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MPL-2.0",
|
|
6
|
-
"version": "3.
|
|
6
|
+
"version": "3.5.1",
|
|
7
7
|
"engines": {
|
|
8
8
|
"node": ">=20.9.0"
|
|
9
9
|
},
|
|
@@ -77,10 +77,10 @@
|
|
|
77
77
|
"npm-run-all": "^4.1.5",
|
|
78
78
|
"prism-react-renderer": "^2.4.1",
|
|
79
79
|
"react-error-boundary": "^6.1.1",
|
|
80
|
-
"@byline/
|
|
81
|
-
"@byline/
|
|
82
|
-
"@byline/ui": "3.
|
|
83
|
-
"@byline/core": "3.
|
|
80
|
+
"@byline/admin": "3.5.1",
|
|
81
|
+
"@byline/client": "3.5.1",
|
|
82
|
+
"@byline/ui": "3.5.1",
|
|
83
|
+
"@byline/core": "3.5.1"
|
|
84
84
|
},
|
|
85
85
|
"peerDependencies": {
|
|
86
86
|
"react": "^19.0.0",
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Leaf module for the admonition Lexical commands. Lives apart from
|
|
11
|
+
* `admonition-extension.tsx` so the node class (`admonition-node.tsx`)
|
|
12
|
+
* can dispatch the "open modal" command from its `createDOM` chrome
|
|
13
|
+
* without importing the extension (which imports the node — a cycle).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { createCommand, type LexicalCommand, type NodeKey } from 'lexical'
|
|
17
|
+
|
|
18
|
+
import type { AdmonitionAttributes } from './node-types'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Opens the admonition modal. Payload of `null` means "insert a new
|
|
22
|
+
* admonition"; a `{ nodeKey }` payload means "edit the existing node"
|
|
23
|
+
* — dispatched by the per-node Edit button rendered in `createDOM`.
|
|
24
|
+
*/
|
|
25
|
+
export const OPEN_ADMONITION_MODAL_COMMAND: LexicalCommand<{ nodeKey: NodeKey } | null> =
|
|
26
|
+
createCommand('OPEN_ADMONITION_MODAL_COMMAND')
|
|
27
|
+
|
|
28
|
+
/** Inserts a new admonition (type + title come from the modal). */
|
|
29
|
+
export const INSERT_ADMONITION_COMMAND: LexicalCommand<AdmonitionAttributes> = createCommand(
|
|
30
|
+
'INSERT_ADMONITION_COMMAND'
|
|
31
|
+
)
|
|
@@ -13,43 +13,192 @@ import { useEffect } from 'react'
|
|
|
13
13
|
|
|
14
14
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
|
15
15
|
import { ReactExtension } from '@lexical/react/ReactExtension'
|
|
16
|
-
import {
|
|
16
|
+
import { useOptionalExtensionDependency } from '@lexical/react/useExtensionComponent'
|
|
17
|
+
import { $findMatchingParent, $insertNodeToNearestRoot, mergeRegister } from '@lexical/utils'
|
|
17
18
|
import {
|
|
19
|
+
$createParagraphNode,
|
|
20
|
+
$createTextNode,
|
|
21
|
+
$getNodeByKey,
|
|
18
22
|
$getSelection,
|
|
23
|
+
$isDecoratorNode,
|
|
24
|
+
$isElementNode,
|
|
25
|
+
$isLineBreakNode,
|
|
26
|
+
$isParagraphNode,
|
|
19
27
|
$isRangeSelection,
|
|
28
|
+
$isTextNode,
|
|
20
29
|
COMMAND_PRIORITY_EDITOR,
|
|
30
|
+
COMMAND_PRIORITY_LOW,
|
|
21
31
|
COMMAND_PRIORITY_NORMAL,
|
|
22
32
|
configExtension,
|
|
23
|
-
createCommand,
|
|
24
33
|
declarePeerDependency,
|
|
25
34
|
defineExtension,
|
|
26
|
-
type
|
|
35
|
+
type ElementNode,
|
|
36
|
+
KEY_ARROW_DOWN_COMMAND,
|
|
37
|
+
KEY_ARROW_LEFT_COMMAND,
|
|
38
|
+
KEY_ARROW_RIGHT_COMMAND,
|
|
39
|
+
KEY_ARROW_UP_COMMAND,
|
|
40
|
+
type NodeKey,
|
|
27
41
|
} from 'lexical'
|
|
28
42
|
|
|
29
43
|
import { useToolbarActiveEditor } from '../../plugins/toolbar-plugin/toolbar-active-editor'
|
|
30
44
|
import { DropDownItem } from '../../ui/dropdown'
|
|
45
|
+
import {
|
|
46
|
+
type BylineFloatingUIConfig,
|
|
47
|
+
BylineFloatingUIExtension,
|
|
48
|
+
type BylineFloatingUIProps,
|
|
49
|
+
} from '../byline-floating-ui/byline-floating-ui-extension'
|
|
31
50
|
import {
|
|
32
51
|
type BylineToolbarConfig,
|
|
33
52
|
BylineToolbarExtension,
|
|
34
53
|
} from '../byline-toolbar/byline-toolbar-extension'
|
|
54
|
+
import { FloatingTextFormatExtension } from '../floating-text-format/floating-text-format-extension'
|
|
55
|
+
import { FloatingTextFormatToolbarPlugin } from '../floating-text-format/index'
|
|
56
|
+
import { INSERT_ADMONITION_COMMAND, OPEN_ADMONITION_MODAL_COMMAND } from './admonition-commands'
|
|
35
57
|
import { AdmonitionModal } from './admonition-modal'
|
|
36
|
-
import { $createAdmonitionNode, AdmonitionNode } from './admonition-node'
|
|
37
|
-
import type { AdmonitionAttributes } from './node-types'
|
|
58
|
+
import { $createAdmonitionNode, $isAdmonitionNode, AdmonitionNode } from './admonition-node'
|
|
59
|
+
import type { AdmonitionAttributes, AdmonitionType } from './node-types'
|
|
38
60
|
import type { AdmonitionData } from './types'
|
|
39
61
|
|
|
40
62
|
export type InsertAdmonitionPayload = Readonly<AdmonitionAttributes>
|
|
41
63
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
64
|
+
// Re-exported for backwards compatibility — the commands now live in their
|
|
65
|
+
// own leaf module so the node can dispatch the "open modal" command without
|
|
66
|
+
// importing this extension.
|
|
67
|
+
export { INSERT_ADMONITION_COMMAND, OPEN_ADMONITION_MODAL_COMMAND }
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// Body restriction — structure-enforcing transform.
|
|
71
|
+
//
|
|
72
|
+
// The admonition body is deliberately limited to formatted text + links
|
|
73
|
+
// (paragraphs only): no nested admonitions, no images / embeds, no headings /
|
|
74
|
+
// lists / tables / code blocks. Insert-command guards prevent the common
|
|
75
|
+
// entry points; this transform is the backstop for paste and markdown import.
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
/** True when the selection's anchor sits inside an admonition body. */
|
|
79
|
+
function $selectionInsideAdmonition(): boolean {
|
|
80
|
+
const selection = $getSelection()
|
|
81
|
+
if (!$isRangeSelection(selection)) {
|
|
82
|
+
return false
|
|
83
|
+
}
|
|
84
|
+
return $findMatchingParent(selection.anchor.getNode(), $isAdmonitionNode) != null
|
|
85
|
+
}
|
|
45
86
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
)
|
|
87
|
+
/** Flatten a disallowed block child to a paragraph, preserving inline content. */
|
|
88
|
+
function $flattenToParagraph(element: ElementNode): void {
|
|
89
|
+
const paragraph = $createParagraphNode()
|
|
90
|
+
const children = element.getChildren()
|
|
91
|
+
const allInline =
|
|
92
|
+
children.length > 0 &&
|
|
93
|
+
children.every((child) => $isTextNode(child) || $isLineBreakNode(child) || child.isInline())
|
|
94
|
+
if (allInline) {
|
|
95
|
+
paragraph.append(...children)
|
|
96
|
+
} else {
|
|
97
|
+
const text = element.getTextContent()
|
|
98
|
+
if (text.length > 0) {
|
|
99
|
+
paragraph.append($createTextNode(text))
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
element.replace(paragraph)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function $normalizeAdmonition(node: AdmonitionNode): void {
|
|
106
|
+
// Never allow an admonition to nest inside another — unwrap this one,
|
|
107
|
+
// hoisting its children to sit before it in the outer admonition.
|
|
108
|
+
const parent = node.getParent()
|
|
109
|
+
const ancestorAdmonition =
|
|
110
|
+
parent != null ? $findMatchingParent(parent, (candidate) => $isAdmonitionNode(candidate)) : null
|
|
111
|
+
if (ancestorAdmonition != null) {
|
|
112
|
+
for (const child of node.getChildren()) {
|
|
113
|
+
node.insertBefore(child)
|
|
114
|
+
}
|
|
115
|
+
node.remove()
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (node.getChildrenSize() === 0) {
|
|
120
|
+
node.append($createParagraphNode())
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
for (const child of node.getChildren()) {
|
|
125
|
+
if ($isParagraphNode(child)) {
|
|
126
|
+
continue
|
|
127
|
+
}
|
|
128
|
+
// Inline / text / linebreak stragglers → wrap in a paragraph.
|
|
129
|
+
if ($isTextNode(child) || $isLineBreakNode(child) || child.isInline()) {
|
|
130
|
+
const paragraph = $createParagraphNode()
|
|
131
|
+
child.insertBefore(paragraph)
|
|
132
|
+
paragraph.append(child)
|
|
133
|
+
continue
|
|
134
|
+
}
|
|
135
|
+
// Decorators (images, embeds, horizontal rule) aren't allowed in the body.
|
|
136
|
+
if ($isDecoratorNode(child)) {
|
|
137
|
+
child.remove()
|
|
138
|
+
continue
|
|
139
|
+
}
|
|
140
|
+
// Other block elements (headings, lists, quotes, tables, code) collapse
|
|
141
|
+
// to a paragraph.
|
|
142
|
+
if ($isElementNode(child)) {
|
|
143
|
+
$flattenToParagraph(child)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// Shadow-root escape — insert a paragraph above/below when the caret is at the
|
|
150
|
+
// admonition's outer edge, so the block never traps the cursor. Mirrors the
|
|
151
|
+
// Lexical Collapsible plugin.
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
function $onEscapeUp(): boolean {
|
|
155
|
+
const selection = $getSelection()
|
|
156
|
+
if ($isRangeSelection(selection) && selection.isCollapsed() && selection.anchor.offset === 0) {
|
|
157
|
+
const admonition = $findMatchingParent(selection.anchor.getNode(), $isAdmonitionNode)
|
|
158
|
+
if ($isAdmonitionNode(admonition)) {
|
|
159
|
+
const parent = admonition.getParent()
|
|
160
|
+
if (
|
|
161
|
+
parent != null &&
|
|
162
|
+
parent.getFirstChild() === admonition &&
|
|
163
|
+
selection.anchor.key === admonition.getFirstDescendant()?.getKey()
|
|
164
|
+
) {
|
|
165
|
+
admonition.insertBefore($createParagraphNode())
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return false
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function $onEscapeDown(): boolean {
|
|
173
|
+
const selection = $getSelection()
|
|
174
|
+
if ($isRangeSelection(selection) && selection.isCollapsed()) {
|
|
175
|
+
const admonition = $findMatchingParent(selection.anchor.getNode(), $isAdmonitionNode)
|
|
176
|
+
if ($isAdmonitionNode(admonition)) {
|
|
177
|
+
const parent = admonition.getParent()
|
|
178
|
+
if (parent != null && parent.getLastChild() === admonition) {
|
|
179
|
+
const lastParagraph = admonition.getLastDescendant()
|
|
180
|
+
if (
|
|
181
|
+
lastParagraph != null &&
|
|
182
|
+
selection.anchor.key === lastParagraph.getKey() &&
|
|
183
|
+
selection.anchor.offset === lastParagraph.getTextContentSize()
|
|
184
|
+
) {
|
|
185
|
+
admonition.insertAfter($createParagraphNode())
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return false
|
|
191
|
+
}
|
|
49
192
|
|
|
50
193
|
export function AdmonitionPlugin(): React.JSX.Element {
|
|
51
194
|
const [editor] = useLexicalComposerContext()
|
|
52
195
|
const [open, setOpen] = React.useState(false)
|
|
196
|
+
// Null target = insert a new admonition; a key = edit that existing node.
|
|
197
|
+
const [editNodeKey, setEditNodeKey] = React.useState<NodeKey | null>(null)
|
|
198
|
+
const [modalData, setModalData] = React.useState<{
|
|
199
|
+
title: string
|
|
200
|
+
admonitionType?: AdmonitionType
|
|
201
|
+
}>({ title: '', admonitionType: undefined })
|
|
53
202
|
|
|
54
203
|
useEffect(() => {
|
|
55
204
|
if (!editor.hasNodes([AdmonitionNode])) {
|
|
@@ -57,10 +206,26 @@ export function AdmonitionPlugin(): React.JSX.Element {
|
|
|
57
206
|
}
|
|
58
207
|
|
|
59
208
|
return mergeRegister(
|
|
60
|
-
|
|
61
|
-
editor.registerCommand<null>(
|
|
209
|
+
editor.registerCommand<{ nodeKey: NodeKey } | null>(
|
|
62
210
|
OPEN_ADMONITION_MODAL_COMMAND,
|
|
63
|
-
() => {
|
|
211
|
+
(payload) => {
|
|
212
|
+
if (payload == null) {
|
|
213
|
+
setEditNodeKey(null)
|
|
214
|
+
setModalData({ title: '', admonitionType: undefined })
|
|
215
|
+
setOpen(true)
|
|
216
|
+
return true
|
|
217
|
+
}
|
|
218
|
+
const data = editor.getEditorState().read(() => {
|
|
219
|
+
const node = $getNodeByKey(payload.nodeKey)
|
|
220
|
+
return $isAdmonitionNode(node)
|
|
221
|
+
? { title: node.getTitle(), admonitionType: node.getAdmonitionType() }
|
|
222
|
+
: null
|
|
223
|
+
})
|
|
224
|
+
if (data == null) {
|
|
225
|
+
return false
|
|
226
|
+
}
|
|
227
|
+
setEditNodeKey(payload.nodeKey)
|
|
228
|
+
setModalData(data)
|
|
64
229
|
setOpen(true)
|
|
65
230
|
return true
|
|
66
231
|
},
|
|
@@ -69,47 +234,85 @@ export function AdmonitionPlugin(): React.JSX.Element {
|
|
|
69
234
|
|
|
70
235
|
editor.registerCommand<InsertAdmonitionPayload>(
|
|
71
236
|
INSERT_ADMONITION_COMMAND,
|
|
72
|
-
(payload
|
|
73
|
-
// return true
|
|
237
|
+
(payload) => {
|
|
74
238
|
const selection = $getSelection()
|
|
75
|
-
|
|
76
239
|
if (!$isRangeSelection(selection)) {
|
|
77
240
|
return false
|
|
78
241
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (focusNode !== null) {
|
|
83
|
-
const admonitionNode = $createAdmonitionNode(payload)
|
|
84
|
-
$insertNodeToNearestRoot(admonitionNode)
|
|
242
|
+
// No nesting — refuse (silently) when already inside an admonition.
|
|
243
|
+
if ($selectionInsideAdmonition()) {
|
|
244
|
+
return true
|
|
85
245
|
}
|
|
246
|
+
const admonition = $createAdmonitionNode(payload)
|
|
247
|
+
admonition.append($createParagraphNode())
|
|
248
|
+
$insertNodeToNearestRoot(admonition)
|
|
249
|
+
admonition.selectStart()
|
|
86
250
|
return true
|
|
87
251
|
},
|
|
88
252
|
COMMAND_PRIORITY_EDITOR
|
|
89
|
-
)
|
|
253
|
+
),
|
|
254
|
+
|
|
255
|
+
// Structure / restriction backstop.
|
|
256
|
+
editor.registerNodeTransform(AdmonitionNode, $normalizeAdmonition),
|
|
257
|
+
|
|
258
|
+
// Shadow-root caret escape.
|
|
259
|
+
editor.registerCommand(KEY_ARROW_DOWN_COMMAND, $onEscapeDown, COMMAND_PRIORITY_LOW),
|
|
260
|
+
editor.registerCommand(KEY_ARROW_RIGHT_COMMAND, $onEscapeDown, COMMAND_PRIORITY_LOW),
|
|
261
|
+
editor.registerCommand(KEY_ARROW_UP_COMMAND, $onEscapeUp, COMMAND_PRIORITY_LOW),
|
|
262
|
+
editor.registerCommand(KEY_ARROW_LEFT_COMMAND, $onEscapeUp, COMMAND_PRIORITY_LOW)
|
|
90
263
|
)
|
|
91
264
|
}, [editor])
|
|
92
265
|
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
editor.
|
|
266
|
+
const handleSubmit = ({ admonitionType, title }: AdmonitionData): void => {
|
|
267
|
+
setOpen(false)
|
|
268
|
+
if (admonitionType == null) {
|
|
269
|
+
return
|
|
270
|
+
}
|
|
271
|
+
const key = editNodeKey
|
|
272
|
+
if (key != null) {
|
|
273
|
+
editor.update(() => {
|
|
274
|
+
const node = $getNodeByKey(key)
|
|
275
|
+
if ($isAdmonitionNode(node)) {
|
|
276
|
+
node.update({ admonitionType, title })
|
|
277
|
+
}
|
|
278
|
+
})
|
|
101
279
|
} else {
|
|
102
|
-
|
|
280
|
+
editor.dispatchCommand(INSERT_ADMONITION_COMMAND, { admonitionType, title })
|
|
103
281
|
}
|
|
104
|
-
|
|
282
|
+
setEditNodeKey(null)
|
|
105
283
|
}
|
|
106
284
|
|
|
107
285
|
return (
|
|
108
286
|
<AdmonitionModal
|
|
109
287
|
open={open}
|
|
110
|
-
data={
|
|
111
|
-
onClose={() =>
|
|
112
|
-
|
|
288
|
+
data={modalData}
|
|
289
|
+
onClose={() => {
|
|
290
|
+
setOpen(false)
|
|
291
|
+
setEditNodeKey(null)
|
|
292
|
+
}}
|
|
293
|
+
onSubmit={handleSubmit}
|
|
294
|
+
/>
|
|
295
|
+
)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Floating text-format toolbar scoped to admonition bodies. Only mounts when
|
|
300
|
+
* the global `FloatingTextFormatExtension` is *absent* — when it's present, it
|
|
301
|
+
* already covers admonition bodies (and everywhere else), so this would just
|
|
302
|
+
* double up. This is what gives the body its bold/italic/link popover on
|
|
303
|
+
* editors (e.g. the AI editor) that suppress the global one.
|
|
304
|
+
*/
|
|
305
|
+
function AdmonitionFloatingTextFormat({
|
|
306
|
+
anchorElem,
|
|
307
|
+
}: BylineFloatingUIProps): React.JSX.Element | null {
|
|
308
|
+
const hasGlobalToolbar = useOptionalExtensionDependency(FloatingTextFormatExtension) !== undefined
|
|
309
|
+
if (hasGlobalToolbar) {
|
|
310
|
+
return null
|
|
311
|
+
}
|
|
312
|
+
return (
|
|
313
|
+
<FloatingTextFormatToolbarPlugin
|
|
314
|
+
anchorElem={anchorElem}
|
|
315
|
+
shouldShow={$selectionInsideAdmonition}
|
|
113
316
|
/>
|
|
114
317
|
)
|
|
115
318
|
}
|
|
@@ -144,5 +347,13 @@ export const AdmonitionExtension = defineExtension({
|
|
|
144
347
|
},
|
|
145
348
|
],
|
|
146
349
|
} satisfies Partial<BylineToolbarConfig>),
|
|
350
|
+
declarePeerDependency<typeof BylineFloatingUIExtension>(BylineFloatingUIExtension.name, {
|
|
351
|
+
items: [
|
|
352
|
+
{
|
|
353
|
+
id: '@byline/richtext-lexical/Admonition/floating-text-format',
|
|
354
|
+
Component: AdmonitionFloatingTextFormat,
|
|
355
|
+
},
|
|
356
|
+
],
|
|
357
|
+
} satisfies Partial<BylineFloatingUIConfig>),
|
|
147
358
|
],
|
|
148
359
|
})
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admonition (callout) block — ElementNode chrome.
|
|
3
|
+
*
|
|
4
|
+
* The container is a real block in the main editor tree. The header
|
|
5
|
+
* (icon + title + Edit button) is non-editable chrome injected in
|
|
6
|
+
* `createDOM`; the body paragraphs render into `.AdmonitionNode__content`
|
|
7
|
+
* (the DOM slot returned by `getDOMSlot`).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
.LexicalEditor__admonition {
|
|
11
|
+
position: relative;
|
|
12
|
+
margin-top: 0.5rem;
|
|
13
|
+
margin-bottom: 1.5rem;
|
|
14
|
+
padding: 8px 12px 4px;
|
|
15
|
+
border: solid 1px #666666;
|
|
16
|
+
border-left-width: 4px;
|
|
17
|
+
border-radius: 3px;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/* Per-type accent — colours the left border + header icon. */
|
|
21
|
+
.LexicalEditor__admonition.admonition-note {
|
|
22
|
+
border-left-color: #4a90d9;
|
|
23
|
+
}
|
|
24
|
+
.LexicalEditor__admonition.admonition-tip {
|
|
25
|
+
border-left-color: #3fa45b;
|
|
26
|
+
}
|
|
27
|
+
.LexicalEditor__admonition.admonition-warning {
|
|
28
|
+
border-left-color: #d9a334;
|
|
29
|
+
}
|
|
30
|
+
.LexicalEditor__admonition.admonition-danger {
|
|
31
|
+
border-left-color: #d94a4a;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.LexicalEditor__admonition .AdmonitionNode__header {
|
|
35
|
+
display: flex;
|
|
36
|
+
align-items: center;
|
|
37
|
+
gap: 6px;
|
|
38
|
+
font-weight: bold;
|
|
39
|
+
font-size: 1rem;
|
|
40
|
+
text-transform: capitalize;
|
|
41
|
+
margin-bottom: 0.25rem;
|
|
42
|
+
user-select: none;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.LexicalEditor__admonition .AdmonitionNode__icon {
|
|
46
|
+
display: inline-flex;
|
|
47
|
+
align-items: center;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.LexicalEditor__admonition .AdmonitionNode__icon svg {
|
|
51
|
+
width: 20px;
|
|
52
|
+
height: 20px;
|
|
53
|
+
fill: currentColor;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.LexicalEditor__admonition.admonition-note .AdmonitionNode__icon {
|
|
57
|
+
color: #4a90d9;
|
|
58
|
+
}
|
|
59
|
+
.LexicalEditor__admonition.admonition-tip .AdmonitionNode__icon {
|
|
60
|
+
color: #3fa45b;
|
|
61
|
+
}
|
|
62
|
+
.LexicalEditor__admonition.admonition-warning .AdmonitionNode__icon {
|
|
63
|
+
color: #d9a334;
|
|
64
|
+
}
|
|
65
|
+
.LexicalEditor__admonition.admonition-danger .AdmonitionNode__icon {
|
|
66
|
+
color: #d94a4a;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.LexicalEditor__admonition .AdmonitionNode__title {
|
|
70
|
+
flex: 1 1 auto;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.LexicalEditor__admonition .AdmonitionNode__edit-button {
|
|
74
|
+
flex: 0 0 auto;
|
|
75
|
+
font-size: 0.8rem;
|
|
76
|
+
padding: 2px 8px;
|
|
77
|
+
border: 1px solid rgba(0, 0, 0, 0.2);
|
|
78
|
+
border-radius: 5px;
|
|
79
|
+
background-color: rgba(0, 0, 0, 0.04);
|
|
80
|
+
color: inherit;
|
|
81
|
+
cursor: pointer;
|
|
82
|
+
user-select: none;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.LexicalEditor__admonition .AdmonitionNode__edit-button:hover {
|
|
86
|
+
background-color: rgba(60, 132, 244, 0.18);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.LexicalEditor__admonition .AdmonitionNode__content {
|
|
90
|
+
font-size: 0.925em;
|
|
91
|
+
color: #4b5563;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.LexicalEditor__admonition .AdmonitionNode__content > * {
|
|
95
|
+
margin-bottom: 0.25em;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.LexicalEditor__admonition .AdmonitionNode__content > *:last-child {
|
|
99
|
+
margin-bottom: 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.dark .LexicalEditor__admonition {
|
|
103
|
+
border-color: #444444;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.dark .LexicalEditor__admonition .AdmonitionNode__content {
|
|
107
|
+
color: #9ca3af;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.dark .LexicalEditor__admonition .AdmonitionNode__edit-button {
|
|
111
|
+
border-color: rgba(255, 255, 255, 0.2);
|
|
112
|
+
background-color: rgba(255, 255, 255, 0.08);
|
|
113
|
+
}
|