@crystallize/design-system 1.10.0 → 1.11.0
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 +6 -0
- package/dist/index.css +15 -17
- package/dist/index.d.ts +8 -1
- package/dist/index.js +469 -442
- package/dist/index.mjs +407 -385
- package/package.json +1 -1
- package/src/rich-text-editor/i18n/i18n.test.ts +14 -0
- package/src/rich-text-editor/i18n/index.tsx +64 -0
- package/src/rich-text-editor/i18n/translations/en.ts +66 -0
- package/src/rich-text-editor/i18n/types.ts +62 -0
- package/src/rich-text-editor/plugins/ActionsPlugin/index.tsx +5 -22
- package/src/rich-text-editor/plugins/CodeActionMenuPlugin/components/CopyButton/index.tsx +4 -1
- package/src/rich-text-editor/plugins/CodeActionMenuPlugin/components/PrettierButton/index.tsx +11 -1
- package/src/rich-text-editor/plugins/CodeActionMenuPlugin/index.tsx +2 -1
- package/src/rich-text-editor/plugins/FloatingLinkEditorPlugin/index.tsx +23 -5
- package/src/rich-text-editor/plugins/FloatingTextFormatToolbarPlugin/index.tsx +21 -10
- package/src/rich-text-editor/plugins/TabFocusPlugin/index.tsx +4 -12
- package/src/rich-text-editor/plugins/TableActionMenuPlugin/index.tsx +23 -14
- package/src/rich-text-editor/plugins/ToolbarPlugin/index.tsx +33 -33
- package/src/rich-text-editor/plugins/ToolbarPlugin/insert-table.tsx +6 -4
- package/src/rich-text-editor/rich-text-editor.css +6 -0
- package/src/rich-text-editor/rich-text-editor.stories.tsx +10 -0
- package/src/rich-text-editor/rich-text-editor.tsx +15 -9
- package/src/rich-text-editor/ui/LinkPreview.tsx +3 -1
- package/src/rich-text-editor/ui/ContentEditable.css +0 -13
- package/src/rich-text-editor/ui/ContentEditable.tsx +0 -15
package/package.json
CHANGED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { replaceI18nVariablesInString } from '.';
|
|
2
|
+
|
|
3
|
+
describe('RichTextEditor i18n', () => {
|
|
4
|
+
it('replaces variable parts of a translation string correctly', async () => {
|
|
5
|
+
expect(replaceI18nVariablesInString('Hi {{name}}!', 'you')).toBe('Hi you!');
|
|
6
|
+
expect(replaceI18nVariablesInString('Hi {{whatever-goes here}}!', 'you')).toBe('Hi you!');
|
|
7
|
+
expect(replaceI18nVariablesInString('It can be replace {{name}} many times {{name}}', 'you')).toBe(
|
|
8
|
+
'It can be replace you many times you',
|
|
9
|
+
);
|
|
10
|
+
expect(replaceI18nVariablesInString('It needs correct formatting {name}}', 'you')).toBe(
|
|
11
|
+
'It needs correct formatting {name}}',
|
|
12
|
+
);
|
|
13
|
+
});
|
|
14
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { createContext, ReactNode, useContext, useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import type { I18N } from './types';
|
|
4
|
+
|
|
5
|
+
const I18nContext = createContext<I18N | null>(null);
|
|
6
|
+
|
|
7
|
+
export type SupportedLanguages = 'en';
|
|
8
|
+
|
|
9
|
+
export { default as labelTranslationEn } from './translations/en';
|
|
10
|
+
|
|
11
|
+
export function I18nProvider({
|
|
12
|
+
language,
|
|
13
|
+
labelTranslations,
|
|
14
|
+
children,
|
|
15
|
+
}: {
|
|
16
|
+
language: SupportedLanguages;
|
|
17
|
+
labelTranslations?: I18N;
|
|
18
|
+
children: ReactNode;
|
|
19
|
+
}) {
|
|
20
|
+
const [translations, setTranslations] = useState<I18N | null>(labelTranslations || null);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
let unmounted = false;
|
|
24
|
+
(async function load() {
|
|
25
|
+
if (!labelTranslations) {
|
|
26
|
+
const resource = await import(`./translations/${language}.ts`);
|
|
27
|
+
if (!unmounted) {
|
|
28
|
+
setTranslations(resource.default);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
})();
|
|
32
|
+
|
|
33
|
+
return () => {
|
|
34
|
+
unmounted = true;
|
|
35
|
+
};
|
|
36
|
+
}, [language, labelTranslations]);
|
|
37
|
+
|
|
38
|
+
return <I18nContext.Provider value={translations}>{children}</I18nContext.Provider>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function replaceI18nVariablesInString(str: string, replaceWith: string) {
|
|
42
|
+
return str.replace(/({{[^}]+}})/g, replaceWith);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function useTr() {
|
|
46
|
+
const context = useContext(I18nContext);
|
|
47
|
+
|
|
48
|
+
return (key: keyof I18N, units?: number) => {
|
|
49
|
+
const thereAreUnits = typeof units === 'number';
|
|
50
|
+
const keyToUse = thereAreUnits && units > 1 ? `${key}_plural` : key;
|
|
51
|
+
|
|
52
|
+
if (context && keyToUse in context) {
|
|
53
|
+
// @ts-expect-error
|
|
54
|
+
const tr: string = context[keyToUse];
|
|
55
|
+
|
|
56
|
+
if (thereAreUnits) {
|
|
57
|
+
return replaceI18nVariablesInString(tr, units.toString());
|
|
58
|
+
}
|
|
59
|
+
return tr;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return '';
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { I18N } from '../types';
|
|
2
|
+
|
|
3
|
+
const translations: I18N = {
|
|
4
|
+
actionClearTextFormatting: 'Clear text formatting',
|
|
5
|
+
actionTextFormattingOptions: 'Formatting options for additional text styles',
|
|
6
|
+
actionFormatAsStrongLabel: 'Format text as bold',
|
|
7
|
+
actionFormatAsStrongTitle: 'Bold (Ctrl+B)',
|
|
8
|
+
actionFormatAsStrongTitleApple: 'Bold (⌘B)',
|
|
9
|
+
actionFormatAsEmphasizedLabel: 'Format text as italics',
|
|
10
|
+
actionFormatAsEmphasizedTitle: 'Italic (Ctrl+I)',
|
|
11
|
+
actionFormatAsEmphasizedTitleApple: 'Italic (⌘I)',
|
|
12
|
+
actionFormatAsUnderlinedLabel: 'Underline text',
|
|
13
|
+
actionFormatAsUnderlinedTitle: 'Underlined (Ctrl+U)',
|
|
14
|
+
actionFormatAsUnderlinedTitleApple: 'Underlined (⌘U)',
|
|
15
|
+
actionFormatWithStrikethroughLabel: 'Format text with a strikethrough',
|
|
16
|
+
actionFormatWithStrikethroughTitle: 'Strikethrough',
|
|
17
|
+
actionFormatWithSubscriptLabel: 'Format text as subscript',
|
|
18
|
+
actionFormatWithSubscriptTitle: 'Subscript',
|
|
19
|
+
actionFormatWithSuperscriptLabel: 'Format text as superscript',
|
|
20
|
+
actionFormatWithSuperscriptTitle: 'Superscript',
|
|
21
|
+
actionInsertCodeBlock: 'Insert code block',
|
|
22
|
+
actionInsertlink: 'Insert link',
|
|
23
|
+
actionCopyJSON: 'Copy JSON',
|
|
24
|
+
actionCopyCode: 'Copy code',
|
|
25
|
+
actionClear: 'Clear editor',
|
|
26
|
+
actionFormatCode: 'Format code with Prettier',
|
|
27
|
+
actionTableInsertRowsAbove: 'Insert row above',
|
|
28
|
+
actionTableInsertRowsAbove_plural: 'Insert {{rows}} rows above',
|
|
29
|
+
actionTableInsertRowsBelow: 'Insert row above',
|
|
30
|
+
actionTableInsertRowsBelow_plural: 'Insert {{rows}} rows above',
|
|
31
|
+
actionTableInsertColumnsBefore: 'Insert column left',
|
|
32
|
+
actionTableInsertColumnsBefore_plural: 'Insert {{columns}} columns left',
|
|
33
|
+
actionTableInsertColumnsAfter: 'Insert column right',
|
|
34
|
+
actionTableInsertColumnsAfter_plural: 'Insert {{columns}} columns right',
|
|
35
|
+
actionTableAddRowHeader: 'Add row header',
|
|
36
|
+
actionTableRemoveRowHeader: 'Remove row header',
|
|
37
|
+
actionTableAddColumnHeader: 'Add column header',
|
|
38
|
+
actionTableRemoveColumnHeader: 'Remove column header',
|
|
39
|
+
actionTableDeleteColumn: 'Delete column',
|
|
40
|
+
actionTableDeleteRow: 'Delete row',
|
|
41
|
+
actionTableDeleteTable: 'Delete table',
|
|
42
|
+
actionTableOpenOptions: 'Open table options',
|
|
43
|
+
actionUndoLabel: 'Undo',
|
|
44
|
+
actionUndoTitle: 'Undo (Ctrl+Z)',
|
|
45
|
+
actionUndoTitleApple: 'Undo (⌘Z)',
|
|
46
|
+
actionRedoLabel: 'Undo',
|
|
47
|
+
actionRedoTitle: 'Undo (Ctrl+Y)',
|
|
48
|
+
actionRedoTitleApple: 'Redo (⌘Y)',
|
|
49
|
+
codeSelectLanguage: 'Select language',
|
|
50
|
+
linkEditorLink: 'Link',
|
|
51
|
+
linkEditorRel: 'Rel',
|
|
52
|
+
linkEditorTarget: 'Target',
|
|
53
|
+
linkEditorCommit: 'Done',
|
|
54
|
+
linkEditorEdit: 'Edit',
|
|
55
|
+
linkPreviewReplaceTextWithTitle: 'Replace link text with its title',
|
|
56
|
+
horizontalRule: 'Horizontal rule',
|
|
57
|
+
table: 'Table',
|
|
58
|
+
insertTableTitle: 'Insert table',
|
|
59
|
+
insertTableDescription:
|
|
60
|
+
'Define your starting point of a table, you can add and remove columns and rows after creation.',
|
|
61
|
+
insertTableRows: 'Rows',
|
|
62
|
+
insertTableColumns: 'Columns',
|
|
63
|
+
insertTableCommit: 'Insert table',
|
|
64
|
+
} as const;
|
|
65
|
+
|
|
66
|
+
export default translations;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export type I18NKeys =
|
|
2
|
+
| 'actionClearTextFormatting'
|
|
3
|
+
| 'actionTextFormattingOptions'
|
|
4
|
+
| 'actionFormatAsStrongLabel'
|
|
5
|
+
| 'actionFormatAsStrongTitle'
|
|
6
|
+
| 'actionFormatAsStrongTitleApple'
|
|
7
|
+
| 'actionFormatAsEmphasizedLabel'
|
|
8
|
+
| 'actionFormatAsEmphasizedTitle'
|
|
9
|
+
| 'actionFormatAsEmphasizedTitleApple'
|
|
10
|
+
| 'actionFormatAsUnderlinedLabel'
|
|
11
|
+
| 'actionFormatAsUnderlinedTitle'
|
|
12
|
+
| 'actionFormatAsUnderlinedTitleApple'
|
|
13
|
+
| 'actionFormatWithStrikethroughLabel'
|
|
14
|
+
| 'actionFormatWithStrikethroughTitle'
|
|
15
|
+
| 'actionFormatWithSubscriptLabel'
|
|
16
|
+
| 'actionFormatWithSubscriptTitle'
|
|
17
|
+
| 'actionFormatWithSuperscriptLabel'
|
|
18
|
+
| 'actionFormatWithSuperscriptTitle'
|
|
19
|
+
| 'actionCopyJSON'
|
|
20
|
+
| 'actionClear'
|
|
21
|
+
| 'actionFormatCode'
|
|
22
|
+
| 'actionCopyCode'
|
|
23
|
+
| 'actionInsertCodeBlock'
|
|
24
|
+
| 'actionInsertlink'
|
|
25
|
+
| 'actionTableInsertRowsAbove'
|
|
26
|
+
| 'actionTableInsertRowsAbove_plural'
|
|
27
|
+
| 'actionTableInsertRowsBelow'
|
|
28
|
+
| 'actionTableInsertRowsBelow_plural'
|
|
29
|
+
| 'actionTableInsertColumnsBefore'
|
|
30
|
+
| 'actionTableInsertColumnsBefore_plural'
|
|
31
|
+
| 'actionTableInsertColumnsAfter'
|
|
32
|
+
| 'actionTableInsertColumnsAfter_plural'
|
|
33
|
+
| 'actionTableAddRowHeader'
|
|
34
|
+
| 'actionTableRemoveRowHeader'
|
|
35
|
+
| 'actionTableAddColumnHeader'
|
|
36
|
+
| 'actionTableRemoveColumnHeader'
|
|
37
|
+
| 'actionTableDeleteColumn'
|
|
38
|
+
| 'actionTableDeleteRow'
|
|
39
|
+
| 'actionTableDeleteTable'
|
|
40
|
+
| 'actionTableOpenOptions'
|
|
41
|
+
| 'actionUndoLabel'
|
|
42
|
+
| 'actionUndoTitle'
|
|
43
|
+
| 'actionUndoTitleApple'
|
|
44
|
+
| 'actionRedoLabel'
|
|
45
|
+
| 'actionRedoTitle'
|
|
46
|
+
| 'actionRedoTitleApple'
|
|
47
|
+
| 'codeSelectLanguage'
|
|
48
|
+
| 'linkEditorLink'
|
|
49
|
+
| 'linkEditorRel'
|
|
50
|
+
| 'linkEditorTarget'
|
|
51
|
+
| 'linkEditorCommit'
|
|
52
|
+
| 'linkEditorEdit'
|
|
53
|
+
| 'linkPreviewReplaceTextWithTitle'
|
|
54
|
+
| 'horizontalRule'
|
|
55
|
+
| 'table'
|
|
56
|
+
| 'insertTableTitle'
|
|
57
|
+
| 'insertTableDescription'
|
|
58
|
+
| 'insertTableRows'
|
|
59
|
+
| 'insertTableColumns'
|
|
60
|
+
| 'insertTableCommit';
|
|
61
|
+
|
|
62
|
+
export type I18N = Record<I18NKeys, string>;
|
|
@@ -11,6 +11,7 @@ import { CLEAR_EDITOR_COMMAND, LexicalEditor } from 'lexical';
|
|
|
11
11
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
12
12
|
|
|
13
13
|
import { ActionMenu } from '../../../action-menu';
|
|
14
|
+
import { useTr } from '../../i18n';
|
|
14
15
|
import { lexicalToCrystallizeRichText } from '../../model/lexical-to-crystallize';
|
|
15
16
|
import type { CrystallizeRichTextActionMenuItem } from '../../types/types';
|
|
16
17
|
|
|
@@ -24,25 +25,6 @@ async function copyJson(editor: LexicalEditor) {
|
|
|
24
25
|
}
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
async function exportJson(editor: LexicalEditor) {
|
|
28
|
-
const json = lexicalToCrystallizeRichText({ editorState: editor.getEditorState() });
|
|
29
|
-
|
|
30
|
-
const blob = new Blob([JSON.stringify(json, null, 1)], {
|
|
31
|
-
type: 'application/json',
|
|
32
|
-
});
|
|
33
|
-
const href = URL.createObjectURL(blob);
|
|
34
|
-
|
|
35
|
-
const link = document.createElement('a');
|
|
36
|
-
link.href = href;
|
|
37
|
-
link.download = 'crystallizeRichText.json';
|
|
38
|
-
|
|
39
|
-
document.body.appendChild(link);
|
|
40
|
-
link.click();
|
|
41
|
-
document.body.removeChild(link);
|
|
42
|
-
|
|
43
|
-
URL.revokeObjectURL(href);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
28
|
export default function ActionsPlugin({
|
|
47
29
|
append,
|
|
48
30
|
prepend,
|
|
@@ -51,6 +33,8 @@ export default function ActionsPlugin({
|
|
|
51
33
|
prepend?: CrystallizeRichTextActionMenuItem[];
|
|
52
34
|
}): JSX.Element {
|
|
53
35
|
const [editor] = useLexicalComposerContext();
|
|
36
|
+
const tr = useTr();
|
|
37
|
+
|
|
54
38
|
return (
|
|
55
39
|
<div className="z-50 flex items-center ">
|
|
56
40
|
<div></div>
|
|
@@ -66,8 +50,7 @@ export default function ActionsPlugin({
|
|
|
66
50
|
{actionItem.title}
|
|
67
51
|
</ActionMenu.Item>
|
|
68
52
|
))}
|
|
69
|
-
<ActionMenu.Item onSelect={() => copyJson(editor)}>
|
|
70
|
-
<ActionMenu.Item onSelect={() => exportJson(editor)}>Export JSON</ActionMenu.Item>
|
|
53
|
+
<ActionMenu.Item onSelect={() => copyJson(editor)}>{tr('actionCopyJSON')}</ActionMenu.Item>
|
|
71
54
|
<ActionMenu.Item
|
|
72
55
|
className="danger"
|
|
73
56
|
onSelect={() => {
|
|
@@ -75,7 +58,7 @@ export default function ActionsPlugin({
|
|
|
75
58
|
editor.focus();
|
|
76
59
|
}}
|
|
77
60
|
>
|
|
78
|
-
|
|
61
|
+
{tr('actionClear')}
|
|
79
62
|
</ActionMenu.Item>
|
|
80
63
|
{!append
|
|
81
64
|
? null
|
|
@@ -12,6 +12,8 @@ import { $getNearestNodeFromDOMNode, $getSelection, $setSelection, LexicalEditor
|
|
|
12
12
|
import { useDebouncedCallback } from 'use-debounce';
|
|
13
13
|
import { $isCodeNode } from '@lexical/code';
|
|
14
14
|
|
|
15
|
+
import { useTr } from '../../../../i18n';
|
|
16
|
+
|
|
15
17
|
interface Props {
|
|
16
18
|
editor: LexicalEditor;
|
|
17
19
|
getCodeDOMNode: () => HTMLElement | null;
|
|
@@ -19,6 +21,7 @@ interface Props {
|
|
|
19
21
|
|
|
20
22
|
export function CopyButton({ editor, getCodeDOMNode }: Props) {
|
|
21
23
|
const [isCopyCompleted, setCopyCompleted] = useState<boolean>(false);
|
|
24
|
+
const tr = useTr();
|
|
22
25
|
|
|
23
26
|
const removeSuccessIcon = useDebouncedCallback(() => {
|
|
24
27
|
setCopyCompleted(false);
|
|
@@ -54,7 +57,7 @@ export function CopyButton({ editor, getCodeDOMNode }: Props) {
|
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
return (
|
|
57
|
-
<button className="menu-item" onClick={handleClick} aria-label=
|
|
60
|
+
<button className="menu-item" onClick={handleClick} aria-label={tr('actionCopyCode')}>
|
|
58
61
|
{isCopyCompleted ? <i className="format success" /> : <i className="format copy" />}
|
|
59
62
|
</button>
|
|
60
63
|
);
|
package/src/rich-text-editor/plugins/CodeActionMenuPlugin/components/PrettierButton/index.tsx
CHANGED
|
@@ -12,6 +12,8 @@ import { $getNearestNodeFromDOMNode, LexicalEditor } from 'lexical';
|
|
|
12
12
|
import type { Options } from 'prettier';
|
|
13
13
|
import { $isCodeNode } from '@lexical/code';
|
|
14
14
|
|
|
15
|
+
import { useTr } from '../../../../i18n';
|
|
16
|
+
|
|
15
17
|
interface Props {
|
|
16
18
|
lang: string;
|
|
17
19
|
editor: LexicalEditor;
|
|
@@ -69,6 +71,7 @@ function getPrettierOptions(lang: string): Options {
|
|
|
69
71
|
export function PrettierButton({ lang, editor, getCodeDOMNode }: Props) {
|
|
70
72
|
const [syntaxError, setSyntaxError] = useState<string>('');
|
|
71
73
|
const [tipsVisible, setTipsVisible] = useState<boolean>(false);
|
|
74
|
+
const tr = useTr();
|
|
72
75
|
|
|
73
76
|
async function handleClick(): Promise<void> {
|
|
74
77
|
const codeDOMNode = getCodeDOMNode();
|
|
@@ -92,6 +95,13 @@ export function PrettierButton({ lang, editor, getCodeDOMNode }: Props) {
|
|
|
92
95
|
|
|
93
96
|
parsed = format(content, options);
|
|
94
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Remove EOF from prettier output. This is useful when
|
|
100
|
+
* using prettier on files, but becomes weird when the code
|
|
101
|
+
* is embedded within a larger portion of text.
|
|
102
|
+
*/
|
|
103
|
+
parsed = parsed.replace(/[\r\n]+$/, '');
|
|
104
|
+
|
|
95
105
|
if (parsed !== '') {
|
|
96
106
|
const selection = codeNode.select(0);
|
|
97
107
|
selection.insertText(parsed);
|
|
@@ -129,7 +139,7 @@ export function PrettierButton({ lang, editor, getCodeDOMNode }: Props) {
|
|
|
129
139
|
onClick={handleClick}
|
|
130
140
|
onMouseEnter={handleMouseEnter}
|
|
131
141
|
onMouseLeave={handleMouseLeave}
|
|
132
|
-
aria-label=
|
|
142
|
+
aria-label={tr('actionFormatCode')}
|
|
133
143
|
>
|
|
134
144
|
{syntaxError ? <i className="format prettier-error" /> : <i className="format prettier" />}
|
|
135
145
|
</button>
|
|
@@ -29,7 +29,7 @@ function CodeActionMenuContainer({ anchorElem }: { anchorElem: HTMLElement }): J
|
|
|
29
29
|
|
|
30
30
|
const [lang, setLang] = useState('');
|
|
31
31
|
const [isShown, setShown] = useState<boolean>(false);
|
|
32
|
-
const [shouldListenMouseMove, setShouldListenMouseMove] = useState<boolean>(
|
|
32
|
+
const [shouldListenMouseMove, setShouldListenMouseMove] = useState<boolean>(true);
|
|
33
33
|
const [position, setPosition] = useState<Position>({
|
|
34
34
|
right: '0',
|
|
35
35
|
top: '0',
|
|
@@ -43,6 +43,7 @@ function CodeActionMenuContainer({ anchorElem }: { anchorElem: HTMLElement }): J
|
|
|
43
43
|
|
|
44
44
|
const debouncedOnMouseMove = useDebouncedCallback((event: MouseEvent) => {
|
|
45
45
|
const { codeDOMNode, isOutside } = getMouseInfo(event);
|
|
46
|
+
|
|
46
47
|
if (isOutside) {
|
|
47
48
|
setShown(false);
|
|
48
49
|
return;
|
|
@@ -30,6 +30,7 @@ import { Button } from '../../../button';
|
|
|
30
30
|
import { IconButton } from '../../../icon-button';
|
|
31
31
|
import { Icon } from '../../../iconography';
|
|
32
32
|
import { InputWithLabel } from '../../../input-with-label';
|
|
33
|
+
import { useTr } from '../../i18n';
|
|
33
34
|
import LinkPreview from '../../ui/LinkPreview';
|
|
34
35
|
import { getSelectedNode } from '../../utils/getSelectedNode';
|
|
35
36
|
import { setFloatingElemPosition } from '../../utils/setFloatingElemPosition';
|
|
@@ -53,6 +54,7 @@ function FloatingLinkEditor({
|
|
|
53
54
|
const [target, setTarget] = useState<string | null>(null);
|
|
54
55
|
const [isEditMode, setEditMode] = useState(false);
|
|
55
56
|
const [lastSelection, setLastSelection] = useState<RangeSelection | GridSelection | NodeSelection | null>(null);
|
|
57
|
+
const tr = useTr();
|
|
56
58
|
|
|
57
59
|
const updateLinkEditor = useCallback(() => {
|
|
58
60
|
const selection = $getSelection();
|
|
@@ -187,14 +189,29 @@ function FloatingLinkEditor({
|
|
|
187
189
|
{isEditMode ? (
|
|
188
190
|
<div>
|
|
189
191
|
<div className="border-0 border-b border-gray-100-800 border-solid px-3">
|
|
190
|
-
<InputWithLabel
|
|
192
|
+
<InputWithLabel
|
|
193
|
+
label={tr('linkEditorLink')}
|
|
194
|
+
type="text"
|
|
195
|
+
value={linkUrl}
|
|
196
|
+
onChange={e => setLinkUrl(e.target.value)}
|
|
197
|
+
/>
|
|
191
198
|
</div>
|
|
192
199
|
|
|
193
200
|
<div className="border-0 border-b border-gray-100-800 border-solid px-3">
|
|
194
|
-
<InputWithLabel
|
|
201
|
+
<InputWithLabel
|
|
202
|
+
label={tr('linkEditorRel')}
|
|
203
|
+
type="text"
|
|
204
|
+
value={rel ?? ''}
|
|
205
|
+
onChange={e => setRel(e.target.value)}
|
|
206
|
+
/>
|
|
195
207
|
</div>
|
|
196
208
|
<div className="border-0 border-b border-gray-100-800 border-solid px-3">
|
|
197
|
-
<InputWithLabel
|
|
209
|
+
<InputWithLabel
|
|
210
|
+
label={tr('linkEditorTarget')}
|
|
211
|
+
type="text"
|
|
212
|
+
value={target ?? ''}
|
|
213
|
+
onChange={e => setTarget(e.target.value)}
|
|
214
|
+
/>
|
|
198
215
|
</div>
|
|
199
216
|
<div className="flex px-6 py-2 justify-end">
|
|
200
217
|
<Button
|
|
@@ -211,14 +228,14 @@ function FloatingLinkEditor({
|
|
|
211
228
|
}
|
|
212
229
|
}}
|
|
213
230
|
>
|
|
214
|
-
|
|
231
|
+
{tr('linkEditorCommit')}
|
|
215
232
|
</Button>
|
|
216
233
|
</div>
|
|
217
234
|
</div>
|
|
218
235
|
) : (
|
|
219
236
|
<>
|
|
220
237
|
<div className="link-input !flex flex-nowrap justify-between items-center max-w-full ">
|
|
221
|
-
<div className="
|
|
238
|
+
<div className="grid">
|
|
222
239
|
<a href={linkUrl} target="_blank" rel="noopener noreferrer">
|
|
223
240
|
{linkUrl}
|
|
224
241
|
</a>
|
|
@@ -241,6 +258,7 @@ function FloatingLinkEditor({
|
|
|
241
258
|
tabIndex={0}
|
|
242
259
|
onMouseDown={event => event.preventDefault()}
|
|
243
260
|
onClick={() => setEditMode(true)}
|
|
261
|
+
aria-label={tr('linkEditorEdit')}
|
|
244
262
|
>
|
|
245
263
|
<Icon.Edit />
|
|
246
264
|
</IconButton>
|
|
@@ -25,6 +25,8 @@ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext
|
|
|
25
25
|
import { mergeRegister } from '@lexical/utils';
|
|
26
26
|
|
|
27
27
|
import { IconButton } from '../../../icon-button';
|
|
28
|
+
import { useTr } from '../../i18n';
|
|
29
|
+
import { IS_APPLE } from '../../utils/environment';
|
|
28
30
|
import { getDOMRangeRect } from '../../utils/getDOMRangeRect';
|
|
29
31
|
import { getSelectedNode } from '../../utils/getSelectedNode';
|
|
30
32
|
import { setFloatingElemPosition } from '../../utils/setFloatingElemPosition';
|
|
@@ -53,6 +55,7 @@ function TextFormatFloatingToolbar({
|
|
|
53
55
|
isUnderline: boolean;
|
|
54
56
|
}): JSX.Element {
|
|
55
57
|
const popupCharStylesEditorRef = useRef<HTMLDivElement | null>(null);
|
|
58
|
+
const tr = useTr();
|
|
56
59
|
|
|
57
60
|
const insertLink = useCallback(() => {
|
|
58
61
|
if (!isLink) {
|
|
@@ -139,7 +142,8 @@ function TextFormatFloatingToolbar({
|
|
|
139
142
|
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold');
|
|
140
143
|
}}
|
|
141
144
|
style={{ padding: 0, overflow: 'hidden' }}
|
|
142
|
-
|
|
145
|
+
title={tr(IS_APPLE ? 'actionFormatAsStrongTitleApple' : 'actionFormatAsStrongTitle')}
|
|
146
|
+
aria-label={tr('actionFormatAsStrongLabel')}
|
|
143
147
|
>
|
|
144
148
|
<i
|
|
145
149
|
className={`format bold w-full h-full bg-[length:18px_18px] bg-no-repeat bg-center ${
|
|
@@ -152,7 +156,8 @@ function TextFormatFloatingToolbar({
|
|
|
152
156
|
onClick={() => {
|
|
153
157
|
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic');
|
|
154
158
|
}}
|
|
155
|
-
|
|
159
|
+
title={tr('actionFormatAsEmphasizedTitle')}
|
|
160
|
+
aria-label={tr('actionFormatAsEmphasizedLabel')}
|
|
156
161
|
>
|
|
157
162
|
<i
|
|
158
163
|
className={`format italic w-full h-full bg-[length:18px_18px] bg-no-repeat bg-center ${
|
|
@@ -165,7 +170,8 @@ function TextFormatFloatingToolbar({
|
|
|
165
170
|
onClick={() => {
|
|
166
171
|
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'underline');
|
|
167
172
|
}}
|
|
168
|
-
|
|
173
|
+
title={tr('actionFormatAsUnderlinedTitle')}
|
|
174
|
+
aria-label={tr('actionFormatAsUnderlinedLabel')}
|
|
169
175
|
>
|
|
170
176
|
<i
|
|
171
177
|
className={`format underline w-full h-full bg-[length:18px_18px] bg-no-repeat bg-center ${
|
|
@@ -178,7 +184,8 @@ function TextFormatFloatingToolbar({
|
|
|
178
184
|
onClick={() => {
|
|
179
185
|
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough');
|
|
180
186
|
}}
|
|
181
|
-
|
|
187
|
+
title={tr('actionFormatWithStrikethroughTitle')}
|
|
188
|
+
aria-label={tr('actionFormatWithStrikethroughLabel')}
|
|
182
189
|
>
|
|
183
190
|
<i
|
|
184
191
|
className={`format strikethrough w-full h-full bg-[length:18px_18px] bg-no-repeat bg-center ${
|
|
@@ -191,8 +198,8 @@ function TextFormatFloatingToolbar({
|
|
|
191
198
|
onClick={() => {
|
|
192
199
|
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'subscript');
|
|
193
200
|
}}
|
|
194
|
-
title=
|
|
195
|
-
aria-label=
|
|
201
|
+
title={tr('actionFormatWithSubscriptTitle')}
|
|
202
|
+
aria-label={tr('actionFormatWithSubscriptLabel')}
|
|
196
203
|
>
|
|
197
204
|
<i
|
|
198
205
|
className={`format subscript w-full h-full bg-[length:18px_18px] bg-no-repeat bg-center ${
|
|
@@ -205,8 +212,8 @@ function TextFormatFloatingToolbar({
|
|
|
205
212
|
onClick={() => {
|
|
206
213
|
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'superscript');
|
|
207
214
|
}}
|
|
208
|
-
title=
|
|
209
|
-
aria-label=
|
|
215
|
+
title={tr('actionFormatWithSuperscriptTitle')}
|
|
216
|
+
aria-label={tr('actionFormatWithSuperscriptLabel')}
|
|
210
217
|
>
|
|
211
218
|
<i
|
|
212
219
|
className={`format superscript w-full h-full bg-[length:18px_18px] bg-no-repeat bg-center ${
|
|
@@ -219,7 +226,7 @@ function TextFormatFloatingToolbar({
|
|
|
219
226
|
onClick={() => {
|
|
220
227
|
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'code');
|
|
221
228
|
}}
|
|
222
|
-
aria-label=
|
|
229
|
+
aria-label={tr('actionInsertCodeBlock')}
|
|
223
230
|
>
|
|
224
231
|
<i
|
|
225
232
|
className={`format code w-full h-full bg-[length:18px_18px] bg-no-repeat bg-center ${
|
|
@@ -227,7 +234,11 @@ function TextFormatFloatingToolbar({
|
|
|
227
234
|
}`}
|
|
228
235
|
/>
|
|
229
236
|
</IconButton>
|
|
230
|
-
<IconButton
|
|
237
|
+
<IconButton
|
|
238
|
+
style={{ padding: 0, overflow: 'hidden' }}
|
|
239
|
+
onClick={insertLink}
|
|
240
|
+
aria-label={tr('actionInsertlink')}
|
|
241
|
+
>
|
|
231
242
|
<i
|
|
232
243
|
className={`format link w-full h-full bg-[length:18px_18px] bg-no-repeat bg-center ${
|
|
233
244
|
isLink ? 'bg-purple-50-900 opacity-100' : 'opacity-60'
|
|
@@ -6,14 +6,9 @@
|
|
|
6
6
|
*
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
$isRangeSelection,
|
|
13
|
-
$setSelection,
|
|
14
|
-
FOCUS_COMMAND,
|
|
15
|
-
} from 'lexical';
|
|
16
|
-
import {useEffect} from 'react';
|
|
9
|
+
import { useEffect } from 'react';
|
|
10
|
+
import { $getSelection, $isRangeSelection, $setSelection, FOCUS_COMMAND } from 'lexical';
|
|
11
|
+
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
17
12
|
|
|
18
13
|
const COMMAND_PRIORITY_LOW = 1;
|
|
19
14
|
const TAB_TO_FOCUS_INTERVAL = 100;
|
|
@@ -48,10 +43,7 @@ export default function TabFocusPlugin(): null {
|
|
|
48
43
|
(event: FocusEvent) => {
|
|
49
44
|
const selection = $getSelection();
|
|
50
45
|
if ($isRangeSelection(selection)) {
|
|
51
|
-
if (
|
|
52
|
-
lastTabKeyDownTimestamp + TAB_TO_FOCUS_INTERVAL >
|
|
53
|
-
event.timeStamp
|
|
54
|
-
) {
|
|
46
|
+
if (lastTabKeyDownTimestamp + TAB_TO_FOCUS_INTERVAL > event.timeStamp) {
|
|
55
47
|
$setSelection(selection.clone());
|
|
56
48
|
}
|
|
57
49
|
}
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
import { DropdownMenu } from '../../../dropdown-menu';
|
|
34
34
|
import { IconButton } from '../../../icon-button';
|
|
35
35
|
import { Icon } from '../../../iconography';
|
|
36
|
+
import { useTr } from '../../i18n';
|
|
36
37
|
|
|
37
38
|
type TableStats = {
|
|
38
39
|
rows: number;
|
|
@@ -51,6 +52,7 @@ function TableActionMenu({ tableCellNode: _tableCellNode, tableStats }: TableCel
|
|
|
51
52
|
columns: 1,
|
|
52
53
|
rows: 1,
|
|
53
54
|
});
|
|
55
|
+
const tr = useTr();
|
|
54
56
|
|
|
55
57
|
useEffect(() => {
|
|
56
58
|
return editor.registerMutationListener(TableCellNode, nodeMutations => {
|
|
@@ -254,41 +256,48 @@ function TableActionMenu({ tableCellNode: _tableCellNode, tableStats }: TableCel
|
|
|
254
256
|
return (
|
|
255
257
|
<>
|
|
256
258
|
<DropdownMenu.Item onSelect={() => insertTableRowAtSelection(false)}>
|
|
257
|
-
|
|
259
|
+
{tr('actionTableInsertRowsAbove', selectionCounts.rows)}
|
|
258
260
|
</DropdownMenu.Item>
|
|
259
261
|
<DropdownMenu.Item onSelect={() => insertTableRowAtSelection(true)}>
|
|
260
|
-
|
|
262
|
+
{tr('actionTableInsertRowsBelow', selectionCounts.rows)}
|
|
261
263
|
</DropdownMenu.Item>
|
|
262
264
|
<DropdownMenu.Item onSelect={() => insertTableColumnAtSelection(false)}>
|
|
263
|
-
|
|
265
|
+
{tr('actionTableInsertColumnsBefore', selectionCounts.columns)}
|
|
264
266
|
</DropdownMenu.Item>
|
|
265
267
|
<DropdownMenu.Item onSelect={() => insertTableColumnAtSelection(true)}>
|
|
266
|
-
|
|
268
|
+
{tr('actionTableInsertColumnsAfter', selectionCounts.columns)}
|
|
267
269
|
</DropdownMenu.Item>
|
|
268
270
|
<DropdownMenu.Item onSelect={() => toggleTableRowIsHeader()}>
|
|
269
|
-
{(
|
|
270
|
-
|
|
271
|
+
{tr(
|
|
272
|
+
(tableCellNode.__headerState & TableCellHeaderStates.ROW) === TableCellHeaderStates.ROW
|
|
273
|
+
? 'actionTableRemoveRowHeader'
|
|
274
|
+
: 'actionTableAddRowHeader',
|
|
275
|
+
)}
|
|
271
276
|
</DropdownMenu.Item>
|
|
272
277
|
<DropdownMenu.Item onSelect={() => toggleTableColumnIsHeader()}>
|
|
273
|
-
{(
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
278
|
+
{tr(
|
|
279
|
+
(tableCellNode.__headerState & TableCellHeaderStates.COLUMN) === TableCellHeaderStates.COLUMN
|
|
280
|
+
? 'actionTableRemoveColumnHeader'
|
|
281
|
+
: 'actionTableAddColumnHeader',
|
|
282
|
+
)}
|
|
277
283
|
</DropdownMenu.Item>
|
|
278
284
|
<DropdownMenu.Separator />
|
|
279
285
|
{tableStats.columns > 1 && (
|
|
280
|
-
<DropdownMenu.Item onSelect={() => deleteTableColumnAtSelection()}>
|
|
286
|
+
<DropdownMenu.Item onSelect={() => deleteTableColumnAtSelection()}>
|
|
287
|
+
{tr('actionTableDeleteColumn')}
|
|
288
|
+
</DropdownMenu.Item>
|
|
281
289
|
)}
|
|
282
290
|
{tableStats.rows > 1 && (
|
|
283
|
-
<DropdownMenu.Item onSelect={() => deleteTableRowAtSelection()}>
|
|
291
|
+
<DropdownMenu.Item onSelect={() => deleteTableRowAtSelection()}>{tr('actionTableDeleteRow')}</DropdownMenu.Item>
|
|
284
292
|
)}
|
|
285
|
-
<DropdownMenu.Item onSelect={() => deleteTableAtSelection()}>
|
|
293
|
+
<DropdownMenu.Item onSelect={() => deleteTableAtSelection()}>{tr('actionTableDeleteTable')}</DropdownMenu.Item>
|
|
286
294
|
</>
|
|
287
295
|
);
|
|
288
296
|
}
|
|
289
297
|
|
|
290
298
|
function TableCellActionMenuContainer({ anchorElem }: { anchorElem: HTMLElement }) {
|
|
291
299
|
const [editor] = useLexicalComposerContext();
|
|
300
|
+
const tr = useTr();
|
|
292
301
|
|
|
293
302
|
const menuButtonRef = useRef(null);
|
|
294
303
|
|
|
@@ -393,7 +402,7 @@ function TableCellActionMenuContainer({ anchorElem }: { anchorElem: HTMLElement
|
|
|
393
402
|
onOpenChange={isOpen => setIsMenuOpen(isOpen)}
|
|
394
403
|
content={<TableActionMenu tableCellNode={tableCellNode} tableStats={tableStats} />}
|
|
395
404
|
>
|
|
396
|
-
<IconButton size="xs" className="table-cell-action-button">
|
|
405
|
+
<IconButton size="xs" className="table-cell-action-button" aria-label={tr('actionTableOpenOptions')}>
|
|
397
406
|
<Icon.Arrow />
|
|
398
407
|
</IconButton>
|
|
399
408
|
</DropdownMenu.Root>
|