@centreon/ui 23.10.116 → 23.10.118
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/package.json +1 -1
- package/src/Graph/LineChart/LineChart.tsx +5 -2
- package/src/RichTextEditor/RichTextEditor.cypress.spec.tsx +100 -52
- package/src/RichTextEditor/RichTextEditor.tsx +9 -3
- package/src/RichTextEditor/plugins/ToolbarPlugin/AlignPicker.tsx +21 -20
- package/src/RichTextEditor/plugins/ToolbarPlugin/BlockButtons.tsx +35 -77
- package/src/RichTextEditor/plugins/ToolbarPlugin/FormatButtons.tsx +29 -83
- package/src/RichTextEditor/plugins/ToolbarPlugin/LinkButton.tsx +106 -0
- package/src/RichTextEditor/plugins/ToolbarPlugin/ListButton.tsx +155 -0
- package/src/RichTextEditor/plugins/ToolbarPlugin/MacrosButton.tsx +23 -39
- package/src/RichTextEditor/plugins/ToolbarPlugin/ToolbarPlugin.styles.ts +24 -14
- package/src/RichTextEditor/plugins/ToolbarPlugin/UndoRedoButtons.tsx +6 -0
- package/src/RichTextEditor/plugins/ToolbarPlugin/index.tsx +5 -10
- package/src/RichTextEditor/translatedLabels.ts +3 -0
- package/src/components/Menu/Button/MenuButton.tsx +4 -2
- package/src/components/Menu/MenuItem.tsx +5 -3
- package/src/components/Menu/MenuItems.tsx +4 -3
package/package.json
CHANGED
|
@@ -34,6 +34,8 @@ import { useIntersection } from './useLineChartIntersection';
|
|
|
34
34
|
import { CurveType } from './BasicComponents/Lines/models';
|
|
35
35
|
import Thresholds from './BasicComponents/Thresholds';
|
|
36
36
|
|
|
37
|
+
const extraMargin = 10;
|
|
38
|
+
|
|
37
39
|
interface Props extends LineChartProps {
|
|
38
40
|
curve: CurveType;
|
|
39
41
|
graphData: Data;
|
|
@@ -92,7 +94,8 @@ const LineChart = ({
|
|
|
92
94
|
showTooltip: showThresholdTooltip
|
|
93
95
|
} = Tooltip.useTooltip();
|
|
94
96
|
|
|
95
|
-
const graphWidth =
|
|
97
|
+
const graphWidth =
|
|
98
|
+
width > 0 ? width - margin.left - margin.right - extraMargin : 0;
|
|
96
99
|
const graphHeight =
|
|
97
100
|
(height || 0) > 0
|
|
98
101
|
? (height || 0) -
|
|
@@ -185,7 +188,7 @@ const LineChart = ({
|
|
|
185
188
|
width={width}
|
|
186
189
|
/>
|
|
187
190
|
<svg height={graphHeight + margin.top} ref={graphSvgRef} width="100%">
|
|
188
|
-
<Group.Group left={margin.left} top={margin.top}>
|
|
191
|
+
<Group.Group left={margin.left + extraMargin / 2} top={margin.top}>
|
|
189
192
|
<Grids
|
|
190
193
|
height={graphHeight - margin.top}
|
|
191
194
|
leftScale={leftScale}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import RichTextEditor from './RichTextEditor';
|
|
2
2
|
import { standardMacros } from './plugins/ToolbarPlugin/MacrosButton';
|
|
3
|
+
import { labelAlignPicker } from './translatedLabels';
|
|
3
4
|
|
|
4
5
|
interface CheckElementStyleOnRichTextEditorProps {
|
|
5
6
|
check: boolean;
|
|
@@ -47,16 +48,30 @@ describe('Rich Text Editor', () => {
|
|
|
47
48
|
it('displays all elements when RichTextEditor is called with required props', () => {
|
|
48
49
|
cy.findByLabelText('Undo').should('be.visible');
|
|
49
50
|
cy.findByLabelText('Redo').should('be.visible');
|
|
50
|
-
cy.findByLabelText('
|
|
51
|
-
cy.findByLabelText('
|
|
52
|
-
cy.findByLabelText('
|
|
53
|
-
cy.findByLabelText('strikethrough').should('be.visible');
|
|
51
|
+
cy.findByLabelText('align picker').should('be.visible');
|
|
52
|
+
cy.findByLabelText('block').should('be.visible');
|
|
53
|
+
cy.findByLabelText('format').should('be.visible');
|
|
54
54
|
cy.findByLabelText('link').should('be.visible');
|
|
55
|
+
cy.findByLabelText('list').should('be.visible');
|
|
56
|
+
|
|
55
57
|
cy.findByLabelText('RichTextEditor')
|
|
56
58
|
.should('be.visible')
|
|
57
59
|
.and('have.value', '');
|
|
58
60
|
|
|
59
61
|
cy.findByLabelText('RichTextEditor').parent().contains('Type here...');
|
|
62
|
+
|
|
63
|
+
cy.makeSnapshot();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('displays all format actions when the format button is clicked', () => {
|
|
67
|
+
cy.findByLabelText('format').click();
|
|
68
|
+
|
|
69
|
+
cy.findByLabelText('bold').should('be.visible');
|
|
70
|
+
cy.findByLabelText('italic').should('be.visible');
|
|
71
|
+
cy.findByLabelText('underline').should('be.visible');
|
|
72
|
+
cy.findByLabelText('strikethrough').should('be.visible');
|
|
73
|
+
|
|
74
|
+
cy.makeSnapshot();
|
|
60
75
|
});
|
|
61
76
|
|
|
62
77
|
it('displays changes with undo redo buttons', () => {
|
|
@@ -80,21 +95,36 @@ describe('Rich Text Editor', () => {
|
|
|
80
95
|
|
|
81
96
|
it('displays changes with bold, italic, underline and strikethrough buttons', () => {
|
|
82
97
|
cy.get('[data-testid="RichTextEditor"]').type('cypress test');
|
|
98
|
+
|
|
83
99
|
cy.get('[data-testid="RichTextEditor"]').focus().type('{selectAll}');
|
|
84
100
|
|
|
85
|
-
cy.
|
|
86
|
-
cy.
|
|
87
|
-
|
|
88
|
-
cy.
|
|
101
|
+
cy.findByLabelText('format').click();
|
|
102
|
+
cy.findByLabelText('bold').click();
|
|
103
|
+
|
|
104
|
+
cy.findByLabelText('format').click();
|
|
105
|
+
cy.findByLabelText('italic').click();
|
|
106
|
+
|
|
107
|
+
cy.findByLabelText('format').click();
|
|
108
|
+
cy.findByLabelText('underline').click();
|
|
109
|
+
|
|
110
|
+
cy.findByLabelText('format').click();
|
|
111
|
+
cy.findByLabelText('strikethrough').click();
|
|
89
112
|
|
|
90
113
|
cy.findByTestId('RichTextEditor').then((element: HTMLElement) => {
|
|
91
114
|
checkElementStyleOnRichTextEditor({ check: true, element });
|
|
92
115
|
});
|
|
93
116
|
|
|
94
|
-
cy.
|
|
95
|
-
cy.
|
|
96
|
-
|
|
97
|
-
cy.
|
|
117
|
+
cy.findByLabelText('format').click();
|
|
118
|
+
cy.findByLabelText('bold').click();
|
|
119
|
+
|
|
120
|
+
cy.findByLabelText('format').click();
|
|
121
|
+
cy.findByLabelText('italic').click();
|
|
122
|
+
|
|
123
|
+
cy.findByLabelText('format').click();
|
|
124
|
+
cy.findByLabelText('underline').click();
|
|
125
|
+
|
|
126
|
+
cy.findByLabelText('format').click();
|
|
127
|
+
cy.findByLabelText('strikethrough').click();
|
|
98
128
|
|
|
99
129
|
cy.findByTestId('RichTextEditor').then((element: HTMLElement) => {
|
|
100
130
|
checkElementStyleOnRichTextEditor({ check: false, element });
|
|
@@ -105,7 +135,7 @@ describe('Rich Text Editor', () => {
|
|
|
105
135
|
cy.get('[data-testid="RichTextEditor"]').type('cypress');
|
|
106
136
|
cy.get('[data-testid="RichTextEditor"]').focus().type('{selectAll}');
|
|
107
137
|
|
|
108
|
-
cy.
|
|
138
|
+
cy.findByLabelText('link').click();
|
|
109
139
|
|
|
110
140
|
cy.findByLabelText('Saved link').should('have.text', 'https://');
|
|
111
141
|
|
|
@@ -130,7 +160,7 @@ describe('Rich Text Editor', () => {
|
|
|
130
160
|
|
|
131
161
|
cy.get('[data-testid="RichTextEditor"]').focus().type('{selectAll}');
|
|
132
162
|
|
|
133
|
-
cy.
|
|
163
|
+
cy.findByLabelText('link').click();
|
|
134
164
|
|
|
135
165
|
cy.findByText('cypress')
|
|
136
166
|
.parent()
|
|
@@ -153,7 +183,7 @@ describe('Rich Text Editor', () => {
|
|
|
153
183
|
cy.get('[data-testid="RichTextEditor"]').type('cypress');
|
|
154
184
|
cy.get('[data-testid="RichTextEditor"]').focus().type('{selectAll}');
|
|
155
185
|
|
|
156
|
-
cy.
|
|
186
|
+
cy.findByLabelText('link').click();
|
|
157
187
|
|
|
158
188
|
cy.findByLabelText('Saved link').should('have.text', 'https://');
|
|
159
189
|
|
|
@@ -189,11 +219,11 @@ describe('Rich Text Editor', () => {
|
|
|
189
219
|
it('displays editor when editable props is false and an initialState exist', () => {
|
|
190
220
|
cy.findByLabelText('Undo').should('not.exist');
|
|
191
221
|
cy.findByLabelText('Redo').should('not.exist');
|
|
192
|
-
cy.findByLabelText(
|
|
193
|
-
cy.findByLabelText('
|
|
194
|
-
cy.findByLabelText('
|
|
195
|
-
cy.findByLabelText('strikethrough').should('not.exist');
|
|
222
|
+
cy.findByLabelText(labelAlignPicker).should('not.exist');
|
|
223
|
+
cy.findByLabelText('block').should('not.exist');
|
|
224
|
+
cy.findByLabelText('format').should('not.exist');
|
|
196
225
|
cy.findByLabelText('link').should('not.exist');
|
|
226
|
+
cy.findByLabelText('list').should('not.exist');
|
|
197
227
|
});
|
|
198
228
|
});
|
|
199
229
|
|
|
@@ -213,11 +243,11 @@ describe('Rich Text Editor', () => {
|
|
|
213
243
|
it('displays editor when editable props is false and an initialState exist', () => {
|
|
214
244
|
cy.findByLabelText('Undo').should('be.disabled');
|
|
215
245
|
cy.findByLabelText('Redo').should('be.disabled');
|
|
216
|
-
cy.findByLabelText(
|
|
217
|
-
cy.findByLabelText('
|
|
218
|
-
cy.findByLabelText('
|
|
219
|
-
cy.findByLabelText('strikethrough').should('be.disabled');
|
|
246
|
+
cy.findByLabelText(labelAlignPicker).should('be.disabled');
|
|
247
|
+
cy.findByLabelText('block').should('be.disabled');
|
|
248
|
+
cy.findByLabelText('format').should('be.disabled');
|
|
220
249
|
cy.findByLabelText('link').should('be.disabled');
|
|
250
|
+
cy.findByLabelText('list').should('be.disabled');
|
|
221
251
|
});
|
|
222
252
|
});
|
|
223
253
|
|
|
@@ -262,7 +292,6 @@ describe('Rich Text Editor', () => {
|
|
|
262
292
|
});
|
|
263
293
|
|
|
264
294
|
it('displays the Macros button when the "displayMacrosButton" prop is set to true', () => {
|
|
265
|
-
cy.findByLabelText('Macros').should('be.visible');
|
|
266
295
|
cy.findByLabelText('Macros').click();
|
|
267
296
|
|
|
268
297
|
standardMacros.forEach((macro) => {
|
|
@@ -273,10 +302,10 @@ describe('Rich Text Editor', () => {
|
|
|
273
302
|
it('ensures that the selected macro is inserted into the RichTextEditor', () => {
|
|
274
303
|
cy.get('[data-testid="RichTextEditor"]').type('macro : ');
|
|
275
304
|
|
|
276
|
-
cy.findByLabelText('Macros').click({});
|
|
277
|
-
|
|
278
305
|
standardMacros.forEach((macro) => {
|
|
279
|
-
cy.
|
|
306
|
+
cy.findByLabelText('Macros').click();
|
|
307
|
+
cy.findByText(macro).click();
|
|
308
|
+
|
|
280
309
|
cy.get('[data-testid="RichTextEditor"]').should('contain', macro);
|
|
281
310
|
});
|
|
282
311
|
});
|
|
@@ -291,44 +320,59 @@ describe('Rich Text Editor', () => {
|
|
|
291
320
|
|
|
292
321
|
const testCases = [
|
|
293
322
|
{
|
|
294
|
-
blockType: '
|
|
295
|
-
tag: 'h1'
|
|
296
|
-
},
|
|
297
|
-
{
|
|
298
|
-
blockType: 'Heading 2',
|
|
299
|
-
tag: 'h2'
|
|
300
|
-
},
|
|
301
|
-
{
|
|
302
|
-
blockType: 'Heading 3',
|
|
323
|
+
blockType: 'Huge',
|
|
303
324
|
tag: 'h3'
|
|
304
325
|
},
|
|
305
326
|
{
|
|
306
|
-
blockType: '
|
|
307
|
-
tag: '
|
|
327
|
+
blockType: 'Large',
|
|
328
|
+
tag: 'h5'
|
|
308
329
|
},
|
|
309
330
|
{
|
|
310
|
-
blockType: '
|
|
311
|
-
tag: '
|
|
331
|
+
blockType: 'Normal',
|
|
332
|
+
tag: 'p'
|
|
312
333
|
},
|
|
313
334
|
{
|
|
314
|
-
blockType: '
|
|
335
|
+
blockType: 'Small',
|
|
315
336
|
tag: 'h6'
|
|
316
|
-
}
|
|
337
|
+
}
|
|
338
|
+
];
|
|
339
|
+
|
|
340
|
+
testCases.forEach(({ blockType, tag }) => {
|
|
341
|
+
it(`displays ${blockType} when the corresponding block type button is selected`, () => {
|
|
342
|
+
cy.get('[data-testid="RichTextEditor"]').type('Example');
|
|
343
|
+
cy.findByLabelText('block').click();
|
|
344
|
+
cy.findByText(blockType).click();
|
|
345
|
+
|
|
346
|
+
cy.get('[data-testid="RichTextEditor"]').find(tag).should('be.visible');
|
|
347
|
+
|
|
348
|
+
cy.makeSnapshot();
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
describe('List type', () => {
|
|
354
|
+
beforeEach(() => {
|
|
355
|
+
cy.mount({
|
|
356
|
+
Component: <RichTextEditor editable />
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
const testCases = [
|
|
317
361
|
{
|
|
318
|
-
|
|
362
|
+
listType: 'Unordered List',
|
|
319
363
|
tag: 'ul'
|
|
320
364
|
},
|
|
321
365
|
{
|
|
322
|
-
|
|
366
|
+
listType: 'Ordered List',
|
|
323
367
|
tag: 'ol'
|
|
324
368
|
}
|
|
325
369
|
];
|
|
326
370
|
|
|
327
|
-
testCases.forEach(({
|
|
328
|
-
it(`displays ${
|
|
371
|
+
testCases.forEach(({ listType, tag }) => {
|
|
372
|
+
it(`displays ${listType} when the corresponding list type button is selected`, () => {
|
|
329
373
|
cy.get('[data-testid="RichTextEditor"]').type('Example');
|
|
330
|
-
cy.
|
|
331
|
-
cy.
|
|
374
|
+
cy.findByLabelText('list').click();
|
|
375
|
+
cy.findByLabelText(listType).click();
|
|
332
376
|
|
|
333
377
|
cy.get('[data-testid="RichTextEditor"]').find(tag).should('be.visible');
|
|
334
378
|
|
|
@@ -344,14 +388,18 @@ describe('Rich Text Editor', () => {
|
|
|
344
388
|
});
|
|
345
389
|
});
|
|
346
390
|
|
|
347
|
-
const testCases = ['Center', 'Right'];
|
|
391
|
+
const testCases = ['Center', 'Right', 'Left'];
|
|
348
392
|
|
|
349
393
|
testCases.forEach((label) => {
|
|
350
|
-
it(`aligns the text to the ${label} when the corresponding button
|
|
394
|
+
it(`aligns the text to the ${label} when the corresponding button was clicked`, () => {
|
|
351
395
|
cy.get('[data-testid="RichTextEditor"]').type('Example');
|
|
352
396
|
|
|
353
|
-
cy.findByLabelText(
|
|
354
|
-
cy.
|
|
397
|
+
cy.findByLabelText(labelAlignPicker).click();
|
|
398
|
+
cy.findByLabelText(label).click();
|
|
399
|
+
|
|
400
|
+
cy.findByText('Example')
|
|
401
|
+
.parent()
|
|
402
|
+
.should('have.css', 'text-align', label.toLowerCase());
|
|
355
403
|
|
|
356
404
|
cy.makeSnapshot();
|
|
357
405
|
});
|
|
@@ -94,9 +94,9 @@ const useStyles = makeStyles<{ toolbarPositions: 'start' | 'end' }>()(
|
|
|
94
94
|
lineHeight: theme.typography.h5.lineHeight
|
|
95
95
|
},
|
|
96
96
|
h6: {
|
|
97
|
-
fontSize: theme.typography.
|
|
98
|
-
fontWeight: theme.typography.
|
|
99
|
-
lineHeight: theme.typography.
|
|
97
|
+
fontSize: theme.typography.body2.fontSize,
|
|
98
|
+
fontWeight: theme.typography.body2.fontWeight,
|
|
99
|
+
lineHeight: theme.typography.body2.lineHeight
|
|
100
100
|
},
|
|
101
101
|
italic: {
|
|
102
102
|
fontStyle: 'italic'
|
|
@@ -104,6 +104,11 @@ const useStyles = makeStyles<{ toolbarPositions: 'start' | 'end' }>()(
|
|
|
104
104
|
link: {
|
|
105
105
|
color: theme.palette.primary.main
|
|
106
106
|
},
|
|
107
|
+
paragraph: {
|
|
108
|
+
fontSize: theme.typography.body1.fontSize,
|
|
109
|
+
fontWeight: theme.typography.body1.fontWeight,
|
|
110
|
+
lineHeight: theme.typography.body1.lineHeight
|
|
111
|
+
},
|
|
107
112
|
strikethrough: {
|
|
108
113
|
textDecoration: 'line-through'
|
|
109
114
|
},
|
|
@@ -165,6 +170,7 @@ const RichTextEditor = ({
|
|
|
165
170
|
h6: classes.h6
|
|
166
171
|
},
|
|
167
172
|
link: classes.link,
|
|
173
|
+
paragraph: classes.paragraph,
|
|
168
174
|
text: {
|
|
169
175
|
bold: classes.bold,
|
|
170
176
|
italic: classes.italic,
|
|
@@ -18,8 +18,9 @@ import { SvgIconTypeMap } from '@mui/material';
|
|
|
18
18
|
|
|
19
19
|
import { Menu } from '../../../components';
|
|
20
20
|
import { getSelectedNode } from '../../utils/getSelectedNode';
|
|
21
|
+
import { labelAlignPicker } from '../../translatedLabels';
|
|
21
22
|
|
|
22
|
-
import {
|
|
23
|
+
import { useStyles } from './ToolbarPlugin.styles';
|
|
23
24
|
|
|
24
25
|
const formatOptions: Array<{
|
|
25
26
|
Icon: OverridableComponent<SvgIconTypeMap<object, 'svg'>>;
|
|
@@ -48,7 +49,7 @@ interface Props {
|
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
const AlignPicker = ({ disabled }: Props): JSX.Element => {
|
|
51
|
-
const { classes } =
|
|
52
|
+
const { classes } = useStyles();
|
|
52
53
|
|
|
53
54
|
const [elementFormat, setElementFormat] = useState<ElementFormatType>('left');
|
|
54
55
|
|
|
@@ -88,25 +89,25 @@ const AlignPicker = ({ disabled }: Props): JSX.Element => {
|
|
|
88
89
|
|
|
89
90
|
return (
|
|
90
91
|
<Menu>
|
|
91
|
-
<Menu.Button
|
|
92
|
-
{
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
92
|
+
<Menu.Button
|
|
93
|
+
ariaLabel={labelAlignPicker}
|
|
94
|
+
className={classes.button}
|
|
95
|
+
disabled={disabled}
|
|
96
|
+
>
|
|
97
|
+
{selectedFormat && <selectedFormat.Icon />}
|
|
97
98
|
</Menu.Button>
|
|
98
|
-
<Menu.Items>
|
|
99
|
-
{
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
<Icon
|
|
107
|
-
</
|
|
108
|
-
|
|
109
|
-
|
|
99
|
+
<Menu.Items className={classes.menuItems}>
|
|
100
|
+
<div className={classes.menu}>
|
|
101
|
+
{formatOptions.map(({ Icon, value, label }) => (
|
|
102
|
+
<Menu.Item
|
|
103
|
+
isActive={equals(value, elementFormat)}
|
|
104
|
+
key={value}
|
|
105
|
+
onClick={dispatchAlignment(value)}
|
|
106
|
+
>
|
|
107
|
+
<Icon aria-label={label} />
|
|
108
|
+
</Menu.Item>
|
|
109
|
+
))}
|
|
110
|
+
</div>
|
|
110
111
|
</Menu.Items>
|
|
111
112
|
</Menu>
|
|
112
113
|
);
|
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useState } from 'react';
|
|
2
2
|
|
|
3
3
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
4
|
-
import {
|
|
5
|
-
$isListNode,
|
|
6
|
-
INSERT_ORDERED_LIST_COMMAND,
|
|
7
|
-
INSERT_UNORDERED_LIST_COMMAND,
|
|
8
|
-
ListNode,
|
|
9
|
-
REMOVE_LIST_COMMAND
|
|
10
|
-
} from '@lexical/list';
|
|
11
4
|
import {
|
|
12
5
|
$createParagraphNode,
|
|
13
6
|
$getSelection,
|
|
@@ -23,43 +16,27 @@ import {
|
|
|
23
16
|
} from '@lexical/rich-text';
|
|
24
17
|
import { $setBlocksType } from '@lexical/selection';
|
|
25
18
|
import { T, always, cond, equals, isNil } from 'ramda';
|
|
26
|
-
import {
|
|
27
|
-
|
|
28
|
-
$getNearestNodeOfType,
|
|
29
|
-
mergeRegister
|
|
30
|
-
} from '@lexical/utils';
|
|
19
|
+
import { $findMatchingParent, mergeRegister } from '@lexical/utils';
|
|
20
|
+
import { useTranslation } from 'react-i18next';
|
|
31
21
|
|
|
32
|
-
import
|
|
22
|
+
import TextSizeIcon from '@mui/icons-material/TextFields';
|
|
33
23
|
|
|
34
|
-
import {
|
|
24
|
+
import { Menu } from '../../../components';
|
|
25
|
+
|
|
26
|
+
import { useStyles } from './ToolbarPlugin.styles';
|
|
35
27
|
|
|
36
28
|
interface Props {
|
|
37
29
|
disabled: boolean;
|
|
38
30
|
}
|
|
39
31
|
|
|
40
32
|
const blockTypeToBlockName = {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
h3: 'Heading 3',
|
|
45
|
-
h4: 'Heading 4',
|
|
46
|
-
h5: 'Heading 5',
|
|
47
|
-
h6: 'Heading 6',
|
|
48
|
-
number: 'Number List',
|
|
33
|
+
h3: 'Huge',
|
|
34
|
+
h5: 'Large',
|
|
35
|
+
h6: 'Small',
|
|
49
36
|
paragraph: 'Normal'
|
|
50
37
|
};
|
|
51
38
|
|
|
52
|
-
const blockTypes = [
|
|
53
|
-
'h1',
|
|
54
|
-
'h2',
|
|
55
|
-
'h3',
|
|
56
|
-
'h4',
|
|
57
|
-
'h5',
|
|
58
|
-
'h6',
|
|
59
|
-
'bullet',
|
|
60
|
-
'number',
|
|
61
|
-
'paragraph'
|
|
62
|
-
];
|
|
39
|
+
const blockTypes = ['h3', 'h5', 'paragraph', 'h6'];
|
|
63
40
|
|
|
64
41
|
const blockTypeOptions = blockTypes.map((blockType) => ({
|
|
65
42
|
id: blockType,
|
|
@@ -67,7 +44,8 @@ const blockTypeOptions = blockTypes.map((blockType) => ({
|
|
|
67
44
|
}));
|
|
68
45
|
|
|
69
46
|
const BlockButtons = ({ disabled }: Props): JSX.Element => {
|
|
70
|
-
const { classes } =
|
|
47
|
+
const { classes } = useStyles();
|
|
48
|
+
const { t } = useTranslation();
|
|
71
49
|
|
|
72
50
|
const [blockType, setBlockType] =
|
|
73
51
|
useState<keyof typeof blockTypeToBlockName>('paragraph');
|
|
@@ -81,22 +59,6 @@ const BlockButtons = ({ disabled }: Props): JSX.Element => {
|
|
|
81
59
|
});
|
|
82
60
|
};
|
|
83
61
|
|
|
84
|
-
const formatBulletList = (): void => {
|
|
85
|
-
if (blockType !== 'bullet') {
|
|
86
|
-
editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined);
|
|
87
|
-
} else {
|
|
88
|
-
editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined);
|
|
89
|
-
}
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
const formatNumberedList = (): void => {
|
|
93
|
-
if (blockType !== 'number') {
|
|
94
|
-
editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined);
|
|
95
|
-
} else {
|
|
96
|
-
editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined);
|
|
97
|
-
}
|
|
98
|
-
};
|
|
99
|
-
|
|
100
62
|
const formatHeading = (headingSize: HeadingTagType): void => {
|
|
101
63
|
if (blockType !== headingSize) {
|
|
102
64
|
editor.update(() => {
|
|
@@ -108,18 +70,13 @@ const BlockButtons = ({ disabled }: Props): JSX.Element => {
|
|
|
108
70
|
}
|
|
109
71
|
};
|
|
110
72
|
|
|
111
|
-
const changeBlockType = (
|
|
73
|
+
const changeBlockType = (newBlockType): void => {
|
|
112
74
|
const formatFunction = cond<Array<string>, (value?) => void>([
|
|
113
|
-
[equals('bullet'), always(formatBulletList)],
|
|
114
|
-
[equals('number'), always(formatNumberedList)],
|
|
115
|
-
[equals('h1'), always(() => formatHeading('h1'))],
|
|
116
|
-
[equals('h2'), always(() => formatHeading('h2'))],
|
|
117
75
|
[equals('h3'), always(() => formatHeading('h3'))],
|
|
118
|
-
[equals('h4'), always(() => formatHeading('h4'))],
|
|
119
76
|
[equals('h5'), always(() => formatHeading('h5'))],
|
|
120
77
|
[equals('h6'), always(() => formatHeading('h6'))],
|
|
121
78
|
[T, always(formatParagraph)]
|
|
122
|
-
])(newBlockType
|
|
79
|
+
])(newBlockType || '');
|
|
123
80
|
|
|
124
81
|
formatFunction();
|
|
125
82
|
};
|
|
@@ -142,23 +99,13 @@ const BlockButtons = ({ disabled }: Props): JSX.Element => {
|
|
|
142
99
|
return;
|
|
143
100
|
}
|
|
144
101
|
|
|
145
|
-
if ($isListNode(element)) {
|
|
146
|
-
const parentList = $getNearestNodeOfType(anchorNode, ListNode);
|
|
147
|
-
const type = parentList
|
|
148
|
-
? parentList.getListType()
|
|
149
|
-
: element.getListType();
|
|
150
|
-
setBlockType(type);
|
|
151
|
-
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
102
|
const type = $isHeadingNode(element) ? element.getTag() : element.getType();
|
|
103
|
+
|
|
155
104
|
if (type in blockTypeToBlockName) {
|
|
156
105
|
setBlockType(type as keyof typeof blockTypeToBlockName);
|
|
157
106
|
}
|
|
158
107
|
}, [editor]);
|
|
159
108
|
|
|
160
|
-
const value = blockTypeOptions.find((option) => option.id === blockType);
|
|
161
|
-
|
|
162
109
|
useEffect(() => {
|
|
163
110
|
return editor.registerCommand(
|
|
164
111
|
SELECTION_CHANGE_COMMAND,
|
|
@@ -182,15 +129,26 @@ const BlockButtons = ({ disabled }: Props): JSX.Element => {
|
|
|
182
129
|
}, [editor, updateToolbar]);
|
|
183
130
|
|
|
184
131
|
return (
|
|
185
|
-
<
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
132
|
+
<Menu>
|
|
133
|
+
<Menu.Button
|
|
134
|
+
ariaLabel="block"
|
|
135
|
+
className={classes.button}
|
|
136
|
+
disabled={disabled}
|
|
137
|
+
>
|
|
138
|
+
<TextSizeIcon />
|
|
139
|
+
</Menu.Button>
|
|
140
|
+
<Menu.Items className={classes.menuItems}>
|
|
141
|
+
{blockTypeOptions.map(({ id, name }) => (
|
|
142
|
+
<Menu.Item
|
|
143
|
+
isActive={equals(id, blockType)}
|
|
144
|
+
key={name}
|
|
145
|
+
onClick={() => changeBlockType(id)}
|
|
146
|
+
>
|
|
147
|
+
{t(name)}
|
|
148
|
+
</Menu.Item>
|
|
149
|
+
))}
|
|
150
|
+
</Menu.Items>
|
|
151
|
+
</Menu>
|
|
194
152
|
);
|
|
195
153
|
};
|
|
196
154
|
|
|
@@ -1,76 +1,38 @@
|
|
|
1
1
|
import { useState, useCallback, useEffect, useMemo } from 'react';
|
|
2
2
|
|
|
3
|
-
import { makeStyles } from 'tss-react/mui';
|
|
4
3
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
5
4
|
import {
|
|
6
5
|
$getSelection,
|
|
7
6
|
$isRangeSelection,
|
|
8
7
|
FORMAT_TEXT_COMMAND,
|
|
9
|
-
RangeSelection,
|
|
10
8
|
SELECTION_CHANGE_COMMAND,
|
|
11
|
-
TextFormatType
|
|
12
|
-
ElementNode,
|
|
13
|
-
TextNode
|
|
9
|
+
TextFormatType
|
|
14
10
|
} from 'lexical';
|
|
15
|
-
import { $isLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link';
|
|
16
|
-
import { $isAtNodeEnd } from '@lexical/selection';
|
|
17
11
|
import { mergeRegister } from '@lexical/utils';
|
|
18
|
-
import { useAtom } from 'jotai';
|
|
19
12
|
|
|
13
|
+
import FormatTextIcon from '@mui/icons-material/FormatColorText';
|
|
20
14
|
import FormatBoldIcon from '@mui/icons-material/FormatBold';
|
|
21
15
|
import FormatItalicIcon from '@mui/icons-material/FormatItalic';
|
|
22
16
|
import FormatUnderlinedIcon from '@mui/icons-material/FormatUnderlined';
|
|
23
17
|
import StrikethroughSIcon from '@mui/icons-material/StrikethroughS';
|
|
24
|
-
import LinkIcon from '@mui/icons-material/Link';
|
|
25
|
-
import { alpha } from '@mui/material';
|
|
26
18
|
|
|
27
|
-
import {
|
|
28
|
-
import { isInsertingLinkAtom } from '../../atoms';
|
|
19
|
+
import { Menu } from '../../../components';
|
|
29
20
|
|
|
30
|
-
|
|
21
|
+
import { useStyles } from './ToolbarPlugin.styles';
|
|
31
22
|
|
|
32
|
-
const
|
|
33
|
-
buttonSelected: {
|
|
34
|
-
backgroundColor: alpha(
|
|
35
|
-
theme.palette.primary.main,
|
|
36
|
-
theme.palette.action.activatedOpacity
|
|
37
|
-
)
|
|
38
|
-
},
|
|
39
|
-
container: {
|
|
40
|
-
columnGap: theme.spacing(1),
|
|
41
|
-
display: 'flex',
|
|
42
|
-
marginBottom: theme.spacing(1)
|
|
43
|
-
}
|
|
44
|
-
}));
|
|
45
|
-
|
|
46
|
-
const getSelectedNode = (selection: RangeSelection): ElementNode | TextNode => {
|
|
47
|
-
const { anchor } = selection;
|
|
48
|
-
const { focus } = selection;
|
|
49
|
-
const anchorNode = selection.anchor.getNode();
|
|
50
|
-
const focusNode = selection.focus.getNode();
|
|
51
|
-
if (anchorNode === focusNode) {
|
|
52
|
-
return anchorNode;
|
|
53
|
-
}
|
|
54
|
-
const isBackward = selection.isBackward();
|
|
55
|
-
if (isBackward) {
|
|
56
|
-
return $isAtNodeEnd(focus) ? anchorNode : focusNode;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return $isAtNodeEnd(anchor) ? focusNode : anchorNode;
|
|
60
|
-
};
|
|
23
|
+
const LowPriority = 1;
|
|
61
24
|
|
|
62
25
|
interface Props {
|
|
63
26
|
disabled: boolean;
|
|
64
27
|
}
|
|
65
28
|
|
|
66
29
|
const FormatButtons = ({ disabled }: Props): JSX.Element => {
|
|
67
|
-
const { classes
|
|
30
|
+
const { classes } = useStyles();
|
|
68
31
|
|
|
69
32
|
const [isBold, setIsBold] = useState(false);
|
|
70
33
|
const [isItalic, setIsItalic] = useState(false);
|
|
71
34
|
const [isUnderline, setIsUnderline] = useState(false);
|
|
72
35
|
const [isStrikeThrough, setIsStrikeThrough] = useState(false);
|
|
73
|
-
const [isLink, setIsLink] = useAtom(isInsertingLinkAtom);
|
|
74
36
|
|
|
75
37
|
const [editor] = useLexicalComposerContext();
|
|
76
38
|
|
|
@@ -83,24 +45,8 @@ const FormatButtons = ({ disabled }: Props): JSX.Element => {
|
|
|
83
45
|
setIsItalic(selection.hasFormat('italic'));
|
|
84
46
|
setIsUnderline(selection.hasFormat('underline'));
|
|
85
47
|
setIsStrikeThrough(selection.hasFormat('strikethrough'));
|
|
86
|
-
|
|
87
|
-
const node = getSelectedNode(selection);
|
|
88
|
-
const parent = node.getParent();
|
|
89
|
-
if ($isLinkNode(parent) || $isLinkNode(node)) {
|
|
90
|
-
setIsLink(true);
|
|
91
|
-
} else {
|
|
92
|
-
setIsLink(false);
|
|
93
|
-
}
|
|
94
48
|
}, [editor]);
|
|
95
49
|
|
|
96
|
-
const insertLink = useCallback(() => {
|
|
97
|
-
if (!isLink) {
|
|
98
|
-
editor.dispatchCommand(TOGGLE_LINK_COMMAND, 'https://');
|
|
99
|
-
} else {
|
|
100
|
-
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
|
|
101
|
-
}
|
|
102
|
-
}, [editor, isLink]);
|
|
103
|
-
|
|
104
50
|
useEffect(() => {
|
|
105
51
|
return mergeRegister(
|
|
106
52
|
editor.registerCommand(
|
|
@@ -144,34 +90,34 @@ const FormatButtons = ({ disabled }: Props): JSX.Element => {
|
|
|
144
90
|
isSelected: isStrikeThrough,
|
|
145
91
|
onClickFunction: toggleTextFormat('strikethrough'),
|
|
146
92
|
type: 'strikethrough'
|
|
147
|
-
},
|
|
148
|
-
{
|
|
149
|
-
Icon: LinkIcon,
|
|
150
|
-
isSelected: isLink,
|
|
151
|
-
onClickFunction: insertLink,
|
|
152
|
-
type: 'link'
|
|
153
93
|
}
|
|
154
94
|
],
|
|
155
|
-
[isBold, isItalic, isUnderline, isStrikeThrough
|
|
95
|
+
[isBold, isItalic, isUnderline, isStrikeThrough]
|
|
156
96
|
);
|
|
157
97
|
|
|
158
98
|
return (
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
99
|
+
<Menu>
|
|
100
|
+
<Menu.Button
|
|
101
|
+
ariaLabel="format"
|
|
102
|
+
className={classes.button}
|
|
103
|
+
disabled={disabled}
|
|
104
|
+
>
|
|
105
|
+
<FormatTextIcon />
|
|
106
|
+
</Menu.Button>
|
|
107
|
+
<Menu.Items className={classes.menuItems}>
|
|
108
|
+
<div className={classes.menu}>
|
|
109
|
+
{formatButtons.map(({ Icon, onClickFunction, isSelected, type }) => (
|
|
110
|
+
<Menu.Item
|
|
111
|
+
isActive={isSelected}
|
|
112
|
+
key={type}
|
|
113
|
+
onClick={onClickFunction}
|
|
114
|
+
>
|
|
115
|
+
<Icon aria-label={type} />
|
|
116
|
+
</Menu.Item>
|
|
117
|
+
))}
|
|
118
|
+
</div>
|
|
119
|
+
</Menu.Items>
|
|
120
|
+
</Menu>
|
|
175
121
|
);
|
|
176
122
|
};
|
|
177
123
|
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { useCallback, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
4
|
+
import {
|
|
5
|
+
$getSelection,
|
|
6
|
+
$isRangeSelection,
|
|
7
|
+
RangeSelection,
|
|
8
|
+
SELECTION_CHANGE_COMMAND,
|
|
9
|
+
ElementNode,
|
|
10
|
+
TextNode
|
|
11
|
+
} from 'lexical';
|
|
12
|
+
import { $isLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link';
|
|
13
|
+
import { $isAtNodeEnd } from '@lexical/selection';
|
|
14
|
+
import { mergeRegister } from '@lexical/utils';
|
|
15
|
+
import { useAtom } from 'jotai';
|
|
16
|
+
|
|
17
|
+
import LinkIcon from '@mui/icons-material/Link';
|
|
18
|
+
|
|
19
|
+
import { IconButton } from '../../..';
|
|
20
|
+
import { isInsertingLinkAtom } from '../../atoms';
|
|
21
|
+
|
|
22
|
+
import { useStyles } from './ToolbarPlugin.styles';
|
|
23
|
+
|
|
24
|
+
const LowPriority = 1;
|
|
25
|
+
|
|
26
|
+
const getSelectedNode = (selection: RangeSelection): ElementNode | TextNode => {
|
|
27
|
+
const { anchor } = selection;
|
|
28
|
+
const { focus } = selection;
|
|
29
|
+
const anchorNode = selection.anchor.getNode();
|
|
30
|
+
const focusNode = selection.focus.getNode();
|
|
31
|
+
if (anchorNode === focusNode) {
|
|
32
|
+
return anchorNode;
|
|
33
|
+
}
|
|
34
|
+
const isBackward = selection.isBackward();
|
|
35
|
+
if (isBackward) {
|
|
36
|
+
return $isAtNodeEnd(focus) ? anchorNode : focusNode;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return $isAtNodeEnd(anchor) ? focusNode : anchorNode;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
interface Props {
|
|
43
|
+
disabled: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const LinkButton = ({ disabled }: Props): JSX.Element => {
|
|
47
|
+
const { classes, cx } = useStyles();
|
|
48
|
+
|
|
49
|
+
const [isLink, setIsLink] = useAtom(isInsertingLinkAtom);
|
|
50
|
+
|
|
51
|
+
const [editor] = useLexicalComposerContext();
|
|
52
|
+
|
|
53
|
+
const updateToolbar = useCallback((): void => {
|
|
54
|
+
const selection = $getSelection();
|
|
55
|
+
if (!$isRangeSelection(selection)) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const node = getSelectedNode(selection);
|
|
60
|
+
const parent = node.getParent();
|
|
61
|
+
if ($isLinkNode(parent) || $isLinkNode(node)) {
|
|
62
|
+
setIsLink(true);
|
|
63
|
+
} else {
|
|
64
|
+
setIsLink(false);
|
|
65
|
+
}
|
|
66
|
+
}, [editor]);
|
|
67
|
+
|
|
68
|
+
const insertLink = useCallback(() => {
|
|
69
|
+
if (!isLink) {
|
|
70
|
+
editor.dispatchCommand(TOGGLE_LINK_COMMAND, 'https://');
|
|
71
|
+
} else {
|
|
72
|
+
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
|
|
73
|
+
}
|
|
74
|
+
}, [editor, isLink]);
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
return mergeRegister(
|
|
78
|
+
editor.registerCommand(
|
|
79
|
+
SELECTION_CHANGE_COMMAND,
|
|
80
|
+
() => {
|
|
81
|
+
updateToolbar();
|
|
82
|
+
|
|
83
|
+
return false;
|
|
84
|
+
},
|
|
85
|
+
LowPriority
|
|
86
|
+
)
|
|
87
|
+
);
|
|
88
|
+
}, [editor, updateToolbar]);
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<IconButton
|
|
92
|
+
ariaLabel="link"
|
|
93
|
+
className={cx(classes.button, { [classes.buttonSelected]: isLink })}
|
|
94
|
+
disabled={disabled}
|
|
95
|
+
key="link"
|
|
96
|
+
size="medium"
|
|
97
|
+
title="link"
|
|
98
|
+
tooltipPlacement="top"
|
|
99
|
+
onClick={insertLink}
|
|
100
|
+
>
|
|
101
|
+
<LinkIcon />
|
|
102
|
+
</IconButton>
|
|
103
|
+
);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export default LinkButton;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
$isListNode,
|
|
5
|
+
INSERT_ORDERED_LIST_COMMAND,
|
|
6
|
+
INSERT_UNORDERED_LIST_COMMAND,
|
|
7
|
+
ListNode,
|
|
8
|
+
REMOVE_LIST_COMMAND
|
|
9
|
+
} from '@lexical/list';
|
|
10
|
+
import { $getSelection, $isRootOrShadowRoot } from 'lexical';
|
|
11
|
+
import {
|
|
12
|
+
$findMatchingParent,
|
|
13
|
+
$getNearestNodeOfType,
|
|
14
|
+
mergeRegister
|
|
15
|
+
} from '@lexical/utils';
|
|
16
|
+
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
17
|
+
import { equals, isNil } from 'ramda';
|
|
18
|
+
import { useTranslation } from 'react-i18next';
|
|
19
|
+
|
|
20
|
+
import UnorderedListIcon from '@mui/icons-material/FormatListBulleted';
|
|
21
|
+
import OrderedListIcon from '@mui/icons-material/FormatListNumbered';
|
|
22
|
+
|
|
23
|
+
import { Menu } from '../../../components';
|
|
24
|
+
import { labelOrderedList, labelUnorderedList } from '../../translatedLabels';
|
|
25
|
+
|
|
26
|
+
import { useStyles } from './ToolbarPlugin.styles';
|
|
27
|
+
|
|
28
|
+
const options = [
|
|
29
|
+
{
|
|
30
|
+
Icon: UnorderedListIcon,
|
|
31
|
+
label: labelUnorderedList,
|
|
32
|
+
value: 'bullet'
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
Icon: OrderedListIcon,
|
|
36
|
+
label: labelOrderedList,
|
|
37
|
+
value: 'number'
|
|
38
|
+
}
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
interface Props {
|
|
42
|
+
disabled?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const ListButton = ({ disabled }: Props): JSX.Element => {
|
|
46
|
+
const { classes } = useStyles();
|
|
47
|
+
const { t } = useTranslation();
|
|
48
|
+
|
|
49
|
+
const [editor] = useLexicalComposerContext();
|
|
50
|
+
|
|
51
|
+
const [elementList, setElementList] = useState(null);
|
|
52
|
+
|
|
53
|
+
const formatBulletList = (): void => {
|
|
54
|
+
if (elementList !== 'bullet') {
|
|
55
|
+
editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined);
|
|
56
|
+
} else {
|
|
57
|
+
editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const formatNumberedList = (): void => {
|
|
62
|
+
if (elementList !== 'number') {
|
|
63
|
+
editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined);
|
|
64
|
+
} else {
|
|
65
|
+
editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const formatList = (type): void => {
|
|
70
|
+
if (equals(type, elementList)) {
|
|
71
|
+
setElementList(null);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (equals(type, 'bullet')) {
|
|
75
|
+
formatBulletList();
|
|
76
|
+
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
formatNumberedList();
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const updateToolbar = useCallback(() => {
|
|
83
|
+
const selection = $getSelection();
|
|
84
|
+
const anchorNode = selection?.anchor.getNode();
|
|
85
|
+
const element = equals(anchorNode?.getKey(), 'root')
|
|
86
|
+
? anchorNode
|
|
87
|
+
: $findMatchingParent(anchorNode, (e) => {
|
|
88
|
+
const parent = e.getParent();
|
|
89
|
+
|
|
90
|
+
return parent !== null && $isRootOrShadowRoot(parent);
|
|
91
|
+
}) || anchorNode?.getTopLevelElementOrThrow();
|
|
92
|
+
|
|
93
|
+
const elementKey = element?.getKey();
|
|
94
|
+
const elementDOM = editor.getElementByKey(elementKey);
|
|
95
|
+
|
|
96
|
+
if (isNil(elementDOM)) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if ($isListNode(element)) {
|
|
101
|
+
const parentList = $getNearestNodeOfType(anchorNode, ListNode);
|
|
102
|
+
const type = parentList
|
|
103
|
+
? parentList.getListType()
|
|
104
|
+
: element.getListType();
|
|
105
|
+
setElementList(type);
|
|
106
|
+
}
|
|
107
|
+
}, [editor]);
|
|
108
|
+
|
|
109
|
+
const selectedList = options.find(({ value }) => equals(value, elementList));
|
|
110
|
+
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
return editor.registerUpdateListener(({ editorState }) => {
|
|
113
|
+
editorState.read(() => {
|
|
114
|
+
updateToolbar();
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
}, [editor, updateToolbar]);
|
|
118
|
+
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
return mergeRegister(
|
|
121
|
+
editor.registerUpdateListener(({ editorState }) => {
|
|
122
|
+
editorState.read(() => {
|
|
123
|
+
updateToolbar();
|
|
124
|
+
});
|
|
125
|
+
})
|
|
126
|
+
);
|
|
127
|
+
}, [editor, updateToolbar]);
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<Menu>
|
|
131
|
+
<Menu.Button
|
|
132
|
+
ariaLabel="list"
|
|
133
|
+
className={classes.button}
|
|
134
|
+
disabled={disabled}
|
|
135
|
+
>
|
|
136
|
+
{selectedList ? <selectedList.Icon /> : <UnorderedListIcon />}
|
|
137
|
+
</Menu.Button>
|
|
138
|
+
<Menu.Items className={classes.menuItems}>
|
|
139
|
+
<div className={classes.menu}>
|
|
140
|
+
{options.map(({ Icon, value, label }) => (
|
|
141
|
+
<Menu.Item
|
|
142
|
+
isActive={equals(value, elementList)}
|
|
143
|
+
key={value}
|
|
144
|
+
onClick={() => formatList(value)}
|
|
145
|
+
>
|
|
146
|
+
<Icon aria-label={t(label)} />
|
|
147
|
+
</Menu.Item>
|
|
148
|
+
))}
|
|
149
|
+
</div>
|
|
150
|
+
</Menu.Items>
|
|
151
|
+
</Menu>
|
|
152
|
+
);
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
export default ListButton;
|
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
import { useState } from 'react';
|
|
2
|
-
|
|
3
1
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
4
2
|
import { createCommand, LexicalCommand } from 'lexical';
|
|
5
3
|
|
|
6
4
|
import MacrosIcon from '@mui/icons-material/TerminalOutlined';
|
|
7
5
|
|
|
8
|
-
import {
|
|
6
|
+
import { Menu } from '../../../components';
|
|
9
7
|
|
|
10
|
-
import { useStyles } from '.';
|
|
8
|
+
import { useStyles } from './ToolbarPlugin.styles';
|
|
11
9
|
|
|
12
10
|
const LowPriority = 1;
|
|
13
11
|
|
|
@@ -27,15 +25,12 @@ export const standardMacros = [
|
|
|
27
25
|
|
|
28
26
|
const MacrosButton = ({ disabled }: Props): JSX.Element => {
|
|
29
27
|
const { classes } = useStyles();
|
|
30
|
-
const [arMacrosDiplayed, setAreMacrosDiplayed] = useState(false);
|
|
31
28
|
|
|
32
29
|
const [editor] = useLexicalComposerContext();
|
|
33
30
|
|
|
34
31
|
const INSERT_MACROS_COMMAND: LexicalCommand<string> = createCommand();
|
|
35
32
|
|
|
36
|
-
const onClick = (
|
|
37
|
-
const macro = e.target.id || e.target.textContent.trim();
|
|
38
|
-
|
|
33
|
+
const onClick = (macro): void => {
|
|
39
34
|
editor.dispatchCommand(INSERT_MACROS_COMMAND, macro);
|
|
40
35
|
};
|
|
41
36
|
|
|
@@ -53,38 +48,27 @@ const MacrosButton = ({ disabled }: Props): JSX.Element => {
|
|
|
53
48
|
LowPriority
|
|
54
49
|
);
|
|
55
50
|
|
|
56
|
-
const displayMacros = (): void => {
|
|
57
|
-
setAreMacrosDiplayed(true);
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
const actions = standardMacros.map((macro) => ({
|
|
61
|
-
label: macro,
|
|
62
|
-
onClick
|
|
63
|
-
}));
|
|
64
|
-
|
|
65
51
|
return (
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
<
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
)}
|
|
87
|
-
</PopoverMenu>
|
|
52
|
+
<Menu>
|
|
53
|
+
<Menu.Button
|
|
54
|
+
ariaLabel="Macros"
|
|
55
|
+
className={classes.button}
|
|
56
|
+
disabled={disabled}
|
|
57
|
+
>
|
|
58
|
+
<MacrosIcon />
|
|
59
|
+
</Menu.Button>
|
|
60
|
+
<Menu.Items className={classes.menuItems}>
|
|
61
|
+
{standardMacros.map((name) => (
|
|
62
|
+
<Menu.Item
|
|
63
|
+
className={classes.menuItem}
|
|
64
|
+
key={name}
|
|
65
|
+
onClick={(): void => onClick(name)}
|
|
66
|
+
>
|
|
67
|
+
{name}
|
|
68
|
+
</Menu.Item>
|
|
69
|
+
))}
|
|
70
|
+
</Menu.Items>
|
|
71
|
+
</Menu>
|
|
88
72
|
);
|
|
89
73
|
};
|
|
90
74
|
|
|
@@ -1,22 +1,32 @@
|
|
|
1
1
|
import { makeStyles } from 'tss-react/mui';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
autocomplete: {
|
|
5
|
-
width: theme.spacing(17)
|
|
6
|
-
}
|
|
7
|
-
}));
|
|
3
|
+
import { alpha } from '@mui/system';
|
|
8
4
|
|
|
9
|
-
export const
|
|
5
|
+
export const useStyles = makeStyles()((theme) => ({
|
|
10
6
|
button: {
|
|
11
|
-
|
|
12
|
-
display: 'flex',
|
|
13
|
-
flexDirection: 'row',
|
|
14
|
-
gap: theme.spacing(1)
|
|
7
|
+
color: theme.palette.text.primary
|
|
15
8
|
},
|
|
16
|
-
|
|
17
|
-
|
|
9
|
+
buttonSelected: {
|
|
10
|
+
backgroundColor: alpha(
|
|
11
|
+
theme.palette.primary.main,
|
|
12
|
+
theme.palette.action.activatedOpacity
|
|
13
|
+
)
|
|
14
|
+
},
|
|
15
|
+
menu: {
|
|
18
16
|
display: 'flex',
|
|
19
|
-
flexDirection: 'row'
|
|
20
|
-
|
|
17
|
+
flexDirection: 'row'
|
|
18
|
+
},
|
|
19
|
+
menuItem: {
|
|
20
|
+
'&, &.MuiMenuItem-root': {
|
|
21
|
+
fontSize: theme.typography.body2.fontSize
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
menuItems: {
|
|
25
|
+
'& > .MuiPaper-root.MuiMenu-paper': {
|
|
26
|
+
minWidth: 0
|
|
27
|
+
},
|
|
28
|
+
'& ul, & ul.MuiMenu-list': {
|
|
29
|
+
padding: 0
|
|
30
|
+
}
|
|
21
31
|
}
|
|
22
32
|
}));
|
|
@@ -16,6 +16,8 @@ import RedoIcon from '@mui/icons-material/Redo';
|
|
|
16
16
|
import { IconButton } from '../../..';
|
|
17
17
|
import { labelRedo, labelUndo } from '../../translatedLabels';
|
|
18
18
|
|
|
19
|
+
import { useStyles } from './ToolbarPlugin.styles';
|
|
20
|
+
|
|
19
21
|
const LowPriority = 1;
|
|
20
22
|
|
|
21
23
|
interface Props {
|
|
@@ -23,6 +25,8 @@ interface Props {
|
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
const UndoRedoButtons = ({ disabled }: Props): JSX.Element => {
|
|
28
|
+
const { classes } = useStyles();
|
|
29
|
+
|
|
26
30
|
const { t } = useTranslation();
|
|
27
31
|
|
|
28
32
|
const [canUndo, setCanUndo] = useState(false);
|
|
@@ -68,6 +72,7 @@ const UndoRedoButtons = ({ disabled }: Props): JSX.Element => {
|
|
|
68
72
|
<>
|
|
69
73
|
<IconButton
|
|
70
74
|
ariaLabel={translatedUndo}
|
|
75
|
+
className={classes.button}
|
|
71
76
|
disabled={!canUndo || disabled}
|
|
72
77
|
title={translatedUndo}
|
|
73
78
|
tooltipPlacement="top"
|
|
@@ -77,6 +82,7 @@ const UndoRedoButtons = ({ disabled }: Props): JSX.Element => {
|
|
|
77
82
|
</IconButton>
|
|
78
83
|
<IconButton
|
|
79
84
|
ariaLabel={translatedRedo}
|
|
85
|
+
className={classes.button}
|
|
80
86
|
disabled={!canRedo || disabled}
|
|
81
87
|
title={translatedRedo}
|
|
82
88
|
tooltipPlacement="top"
|
|
@@ -5,8 +5,10 @@ import { Divider } from '@mui/material';
|
|
|
5
5
|
import FormatButtons from './FormatButtons';
|
|
6
6
|
import UndoRedoButtons from './UndoRedoButtons';
|
|
7
7
|
import MacrosButton from './MacrosButton';
|
|
8
|
-
import BlockButtons from './BlockButtons';
|
|
9
8
|
import AlignPicker from './AlignPicker';
|
|
9
|
+
import ListButton from './ListButton';
|
|
10
|
+
import LinkButton from './LinkButton';
|
|
11
|
+
import BlockButtons from './BlockButtons';
|
|
10
12
|
|
|
11
13
|
interface Props {
|
|
12
14
|
className?: string;
|
|
@@ -22,14 +24,6 @@ export const useStyles = makeStyles()((theme) => ({
|
|
|
22
24
|
columnGap: theme.spacing(1),
|
|
23
25
|
display: 'flex',
|
|
24
26
|
marginBottom: theme.spacing(1)
|
|
25
|
-
},
|
|
26
|
-
macros: {
|
|
27
|
-
'& span': {
|
|
28
|
-
fontSize: theme.typography.caption.fontSize
|
|
29
|
-
}
|
|
30
|
-
},
|
|
31
|
-
macrosButton: {
|
|
32
|
-
marginBottom: theme.spacing(0.5)
|
|
33
27
|
}
|
|
34
28
|
}));
|
|
35
29
|
|
|
@@ -51,9 +45,10 @@ const ToolbarPlugin = ({
|
|
|
51
45
|
<BlockButtons disabled={disabled} />
|
|
52
46
|
</>
|
|
53
47
|
)}
|
|
54
|
-
<Divider flexItem orientation="vertical" />
|
|
55
48
|
<FormatButtons disabled={disabled} />
|
|
56
49
|
<AlignPicker disabled={disabled} />
|
|
50
|
+
<ListButton disabled={disabled} />
|
|
51
|
+
<LinkButton disabled={disabled} />
|
|
57
52
|
{displayMacrosButton && (
|
|
58
53
|
<>
|
|
59
54
|
<Divider flexItem orientation="vertical" />
|
|
@@ -3,3 +3,6 @@ export const labelRedo = 'Redo';
|
|
|
3
3
|
export const labelInputLink = 'Insert link';
|
|
4
4
|
export const labelSavedLink = 'Saved link';
|
|
5
5
|
export const labelEditLink = 'Edit link';
|
|
6
|
+
export const labelAlignPicker = 'align picker';
|
|
7
|
+
export const labelUnorderedList = 'Unordered List';
|
|
8
|
+
export const labelOrderedList = 'Ordered List';
|
|
@@ -15,6 +15,7 @@ import { useStyles } from './MenuButton.styles';
|
|
|
15
15
|
type MenuButtonProps = {
|
|
16
16
|
ariaLabel?: string;
|
|
17
17
|
children?: ReactNode;
|
|
18
|
+
className?: string;
|
|
18
19
|
hasArrow?: boolean;
|
|
19
20
|
isOpen?: boolean;
|
|
20
21
|
onClick?: (args: { isOpen: boolean }) => void;
|
|
@@ -29,9 +30,10 @@ const MenuButton = ({
|
|
|
29
30
|
size = 'small',
|
|
30
31
|
variant = 'ghost',
|
|
31
32
|
ariaLabel,
|
|
33
|
+
className,
|
|
32
34
|
...attr
|
|
33
35
|
}: MenuButtonProps): ReactElement => {
|
|
34
|
-
const { classes } = useStyles();
|
|
36
|
+
const { cx, classes } = useStyles();
|
|
35
37
|
|
|
36
38
|
const { isMenuOpen, setIsMenuOpen, setAnchorEl, onOpen } = useMenu();
|
|
37
39
|
|
|
@@ -50,7 +52,7 @@ const MenuButton = ({
|
|
|
50
52
|
<Button
|
|
51
53
|
{...attr}
|
|
52
54
|
aria-label={ariaLabel}
|
|
53
|
-
className={classes.menuButton}
|
|
55
|
+
className={cx(classes.menuButton, className)}
|
|
54
56
|
data-is-active={isMenuOpen}
|
|
55
57
|
size={size}
|
|
56
58
|
variant={variant}
|
|
@@ -6,6 +6,7 @@ import { useStyles } from './Menu.styles';
|
|
|
6
6
|
|
|
7
7
|
type MenuItemProps = {
|
|
8
8
|
children?: ReactNode;
|
|
9
|
+
className?: string;
|
|
9
10
|
isActive?: boolean;
|
|
10
11
|
isDisabled?: boolean;
|
|
11
12
|
onClick?: () => void;
|
|
@@ -15,13 +16,14 @@ const MenuItem = ({
|
|
|
15
16
|
children,
|
|
16
17
|
onClick,
|
|
17
18
|
isActive = false,
|
|
18
|
-
isDisabled = false
|
|
19
|
+
isDisabled = false,
|
|
20
|
+
className
|
|
19
21
|
}: MenuItemProps): ReactElement => {
|
|
20
|
-
const { classes } = useStyles();
|
|
22
|
+
const { cx, classes } = useStyles();
|
|
21
23
|
|
|
22
24
|
return (
|
|
23
25
|
<MuiMenuItem
|
|
24
|
-
className={classes.menuItem}
|
|
26
|
+
className={cx(classes.menuItem, className)}
|
|
25
27
|
data-is-active={isActive}
|
|
26
28
|
data-is-disabled={isDisabled}
|
|
27
29
|
disabled={isDisabled}
|
|
@@ -7,9 +7,10 @@ import { useMenu } from './useMenu';
|
|
|
7
7
|
|
|
8
8
|
type MenuItemsProps = {
|
|
9
9
|
children?: ReactNode | Array<ReactNode>;
|
|
10
|
+
className?: string;
|
|
10
11
|
};
|
|
11
|
-
const MenuItems = ({ children }: MenuItemsProps): ReactElement => {
|
|
12
|
-
const { classes } = useStyles();
|
|
12
|
+
const MenuItems = ({ children, className }: MenuItemsProps): ReactElement => {
|
|
13
|
+
const { cx, classes } = useStyles();
|
|
13
14
|
|
|
14
15
|
const { isMenuOpen, setIsMenuOpen, anchorEl, onClose } = useMenu();
|
|
15
16
|
|
|
@@ -21,7 +22,7 @@ const MenuItems = ({ children }: MenuItemsProps): ReactElement => {
|
|
|
21
22
|
return (
|
|
22
23
|
<MuiMenu
|
|
23
24
|
anchorEl={anchorEl}
|
|
24
|
-
className={classes.menuItems}
|
|
25
|
+
className={cx(classes.menuItems, className)}
|
|
25
26
|
open={isMenuOpen}
|
|
26
27
|
variant="menu"
|
|
27
28
|
onClick={onCloseMenu}
|