Skip to content

Commit c462bf2

Browse files
authored
feat(richtext-lexical)!: add FixedToolbarFeature (#6192)
BREAKING: - The default inline toolbar has now been extracted into an `InlineToolbarFeature`. While it's part of the defaultFeatures, you might have to add it to your editor features if you are not including the defaultFeatures and still want to keep the inline toolbar (floating toolbar) - Some types have been renamed, e.g. `InlineToolbarGroup` is now `ToolbarGroup`, and `InlineToolbarGroupItem` is now `ToolbarGroupItem` - The `displayName` property of SlashMenuGroup and SlashMenuItem has been renamed to `label` to match the `label` prop of the toolbars - The `inlineToolbarFeatureButtonsGroupWithItem`, `inlineToolbarFormatGroupWithItems` and `inlineToolbarTextDropdownGroupWithItems` exports have been renamed to `toolbarTextDropdownGroupWithItems`, `toolbarFormatGroupWithItems`, `toolbarFeatureButtonsGroupWithItems`
1 parent 8a452c4 commit c462bf2

File tree

61 files changed

+1761
-756
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+1761
-756
lines changed

packages/richtext-lexical/src/exports/components.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
export { RichTextCell } from '../cell/index.js'
2-
export { RichTextField } from '../field/index.js'
2+
export { ToolbarButton } from '../field/features/toolbars/shared/ToolbarButton/index.js'
33

4+
export { ToolbarDropdown } from '../field/features/toolbars/shared/ToolbarDropdown/index.js'
5+
export { RichTextField } from '../field/index.js'
6+
export {
7+
type EditorFocusContextType,
8+
EditorFocusProvider,
9+
useEditorFocus,
10+
} from '../field/lexical/EditorFocusProvider.js'
411
export { defaultEditorLexicalConfig } from '../field/lexical/config/client/default.js'
5-
export { ToolbarButton } from '../field/lexical/plugins/toolbars/inline/ToolbarButton/index.js'
6-
export { ToolbarDropdown } from '../field/lexical/plugins/toolbars/inline/ToolbarDropdown/index.js'

packages/richtext-lexical/src/field/features/align/feature.client.tsx

Lines changed: 136 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,156 @@
11
'use client'
22

3-
import { FORMAT_ELEMENT_COMMAND } from 'lexical'
3+
import { $isElementNode, $isRangeSelection, FORMAT_ELEMENT_COMMAND } from 'lexical'
44

5+
import type { ToolbarGroup } from '../toolbars/types.js'
56
import type { FeatureProviderProviderClient } from '../types.js'
67

78
import { AlignCenterIcon } from '../../lexical/ui/icons/AlignCenter/index.js'
89
import { AlignJustifyIcon } from '../../lexical/ui/icons/AlignJustify/index.js'
910
import { AlignLeftIcon } from '../../lexical/ui/icons/AlignLeft/index.js'
1011
import { AlignRightIcon } from '../../lexical/ui/icons/AlignRight/index.js'
1112
import { createClientComponent } from '../createClientComponent.js'
12-
import { alignGroupWithItems } from './inlineToolbarAlignGroup.js'
13+
import { toolbarAlignGroupWithItems } from './toolbarAlignGroup.js'
14+
15+
const toolbarGroups: ToolbarGroup[] = [
16+
toolbarAlignGroupWithItems([
17+
{
18+
ChildComponent: AlignLeftIcon,
19+
isActive: ({ selection }) => {
20+
if (!$isRangeSelection(selection)) {
21+
return false
22+
}
23+
for (const node of selection.getNodes()) {
24+
if ($isElementNode(node)) {
25+
if (node.getFormatType() === 'left') {
26+
continue
27+
}
28+
}
29+
30+
const parent = node.getParent()
31+
if ($isElementNode(parent)) {
32+
if (parent.getFormatType() === 'left') {
33+
continue
34+
}
35+
}
36+
37+
return false
38+
}
39+
return true
40+
},
41+
key: 'alignLeft',
42+
label: `Align Left`,
43+
onSelect: ({ editor }) => {
44+
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'left')
45+
},
46+
order: 1,
47+
},
48+
{
49+
ChildComponent: AlignCenterIcon,
50+
isActive: ({ selection }) => {
51+
if (!$isRangeSelection(selection)) {
52+
return false
53+
}
54+
for (const node of selection.getNodes()) {
55+
if ($isElementNode(node)) {
56+
if (node.getFormatType() === 'center') {
57+
continue
58+
}
59+
}
60+
61+
const parent = node.getParent()
62+
if ($isElementNode(parent)) {
63+
if (parent.getFormatType() === 'center') {
64+
continue
65+
}
66+
}
67+
68+
return false
69+
}
70+
return true
71+
},
72+
key: 'alignCenter',
73+
label: `Align Center`,
74+
onSelect: ({ editor }) => {
75+
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'center')
76+
},
77+
order: 2,
78+
},
79+
{
80+
ChildComponent: AlignRightIcon,
81+
isActive: ({ selection }) => {
82+
if (!$isRangeSelection(selection)) {
83+
return false
84+
}
85+
for (const node of selection.getNodes()) {
86+
if ($isElementNode(node)) {
87+
if (node.getFormatType() === 'right') {
88+
continue
89+
}
90+
}
91+
92+
const parent = node.getParent()
93+
if ($isElementNode(parent)) {
94+
if (parent.getFormatType() === 'right') {
95+
continue
96+
}
97+
}
98+
99+
return false
100+
}
101+
return true
102+
},
103+
key: 'alignRight',
104+
label: `Align Right`,
105+
onSelect: ({ editor }) => {
106+
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'right')
107+
},
108+
order: 3,
109+
},
110+
{
111+
ChildComponent: AlignJustifyIcon,
112+
isActive: ({ selection }) => {
113+
if (!$isRangeSelection(selection)) {
114+
return false
115+
}
116+
for (const node of selection.getNodes()) {
117+
if ($isElementNode(node)) {
118+
if (node.getFormatType() === 'justify') {
119+
continue
120+
}
121+
}
122+
123+
const parent = node.getParent()
124+
if ($isElementNode(parent)) {
125+
if (parent.getFormatType() === 'justify') {
126+
continue
127+
}
128+
}
129+
130+
return false
131+
}
132+
return true
133+
},
134+
key: 'alignJustify',
135+
label: `Align Justify`,
136+
onSelect: ({ editor }) => {
137+
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'justify')
138+
},
139+
order: 4,
140+
},
141+
]),
142+
]
13143

14144
const AlignFeatureClient: FeatureProviderProviderClient<undefined> = (props) => {
15145
return {
16146
clientFeatureProps: props,
17147
feature: () => ({
18148
clientFeatureProps: props,
149+
toolbarFixed: {
150+
groups: toolbarGroups,
151+
},
19152
toolbarInline: {
20-
groups: [
21-
alignGroupWithItems([
22-
{
23-
ChildComponent: AlignLeftIcon,
24-
isActive: () => false,
25-
key: 'alignLeft',
26-
label: `Align Left`,
27-
onSelect: ({ editor }) => {
28-
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'left')
29-
},
30-
order: 1,
31-
},
32-
{
33-
ChildComponent: AlignCenterIcon,
34-
isActive: () => false,
35-
key: 'alignCenter',
36-
label: `Align Center`,
37-
onSelect: ({ editor }) => {
38-
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'center')
39-
},
40-
order: 2,
41-
},
42-
{
43-
ChildComponent: AlignRightIcon,
44-
isActive: () => false,
45-
key: 'alignRight',
46-
label: `Align Right`,
47-
onSelect: ({ editor }) => {
48-
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'right')
49-
},
50-
order: 3,
51-
},
52-
{
53-
ChildComponent: AlignJustifyIcon,
54-
isActive: () => false,
55-
key: 'alignJustify',
56-
label: `Align Justify`,
57-
onSelect: ({ editor }) => {
58-
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'justify')
59-
},
60-
order: 4,
61-
},
62-
]),
63-
],
153+
groups: toolbarGroups,
64154
},
65155
}),
66156
}

packages/richtext-lexical/src/field/features/align/inlineToolbarAlignGroup.ts

Lines changed: 0 additions & 16 deletions
This file was deleted.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type { ToolbarGroup, ToolbarGroupItem } from '../toolbars/types.js'
2+
3+
import { AlignLeftIcon } from '../../lexical/ui/icons/AlignLeft/index.js'
4+
5+
export const toolbarAlignGroupWithItems = (items: ToolbarGroupItem[]): ToolbarGroup => {
6+
return {
7+
type: 'dropdown',
8+
ChildComponent: AlignLeftIcon,
9+
items,
10+
key: 'align',
11+
order: 30,
12+
}
13+
}

packages/richtext-lexical/src/field/features/blockquote/feature.client.tsx

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,45 @@
11
'use client'
22

3-
import { $createQuoteNode, QuoteNode } from '@lexical/rich-text'
3+
import { $createQuoteNode, $isQuoteNode, QuoteNode } from '@lexical/rich-text'
44
import { $setBlocksType } from '@lexical/selection'
5-
import { $getSelection } from 'lexical'
5+
import { $getSelection, $isRangeSelection } from 'lexical'
66

7+
import type { ToolbarGroup } from '../toolbars/types.js'
78
import type { FeatureProviderProviderClient } from '../types.js'
89

910
import { BlockquoteIcon } from '../../lexical/ui/icons/Blockquote/index.js'
1011
import { createClientComponent } from '../createClientComponent.js'
11-
import { inlineToolbarTextDropdownGroupWithItems } from '../shared/inlineToolbar/textDropdownGroup.js'
12+
import { toolbarTextDropdownGroupWithItems } from '../shared/toolbar/textDropdownGroup.js'
1213
import { MarkdownTransformer } from './markdownTransformer.js'
1314

15+
const toolbarGroups: ToolbarGroup[] = [
16+
toolbarTextDropdownGroupWithItems([
17+
{
18+
ChildComponent: BlockquoteIcon,
19+
isActive: ({ selection }) => {
20+
if (!$isRangeSelection(selection)) {
21+
return false
22+
}
23+
for (const node of selection.getNodes()) {
24+
if (!$isQuoteNode(node) && !$isQuoteNode(node.getParent())) {
25+
return false
26+
}
27+
}
28+
return true
29+
},
30+
key: 'blockquote',
31+
label: `Blockquote`,
32+
onSelect: ({ editor }) => {
33+
editor.update(() => {
34+
const selection = $getSelection()
35+
$setBlocksType(selection, () => $createQuoteNode())
36+
})
37+
},
38+
order: 20,
39+
},
40+
]),
41+
]
42+
1443
const BlockQuoteFeatureClient: FeatureProviderProviderClient<undefined> = (props) => {
1544
return {
1645
clientFeatureProps: props,
@@ -22,13 +51,12 @@ const BlockQuoteFeatureClient: FeatureProviderProviderClient<undefined> = (props
2251
slashMenu: {
2352
groups: [
2453
{
25-
displayName: 'Basic',
2654
items: [
2755
{
2856
Icon: BlockquoteIcon,
29-
displayName: 'Blockquote',
3057
key: 'blockquote',
3158
keywords: ['quote', 'blockquote'],
59+
label: 'Blockquote',
3260
onSelect: ({ editor }) => {
3361
editor.update(() => {
3462
const selection = $getSelection()
@@ -38,27 +66,15 @@ const BlockQuoteFeatureClient: FeatureProviderProviderClient<undefined> = (props
3866
},
3967
],
4068
key: 'basic',
69+
label: 'Basic',
4170
},
4271
],
4372
},
73+
toolbarFixed: {
74+
groups: toolbarGroups,
75+
},
4476
toolbarInline: {
45-
groups: [
46-
inlineToolbarTextDropdownGroupWithItems([
47-
{
48-
ChildComponent: BlockquoteIcon,
49-
isActive: () => false,
50-
key: 'blockquote',
51-
label: `Blockquote`,
52-
onSelect: ({ editor }) => {
53-
editor.update(() => {
54-
const selection = $getSelection()
55-
$setBlocksType(selection, () => $createQuoteNode())
56-
})
57-
},
58-
order: 20,
59-
},
60-
]),
61-
],
77+
groups: toolbarGroups,
6278
},
6379
}),
6480
}

packages/richtext-lexical/src/field/features/blocks/component/index.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,5 @@ export const BlockComponent: React.FC<Props> = (props) => {
187187
schemaFieldsPath,
188188
path,
189189
]) // Adding formData to the dependencies here might break it
190-
191-
return <div className={baseClass}>{formContent}</div>
190+
return <div className={baseClass + ' ' + baseClass + '-' + formData.blockType}>{formContent}</div>
192191
}

0 commit comments

Comments
 (0)