From d258d0dd3d9fc81d35dfb21caaad0d4d9d4e69a7 Mon Sep 17 00:00:00 2001 From: bedre7 Date: Tue, 3 Dec 2024 19:31:44 +0300 Subject: [PATCH] add capitalize format --- .../src/context/ToolbarContext.tsx | 1 + .../src/images/icons/type-capitalize.svg | 1 + packages/lexical-playground/src/index.css | 4 ++ .../src/plugins/ShortcutsPlugin/shortcuts.ts | 11 ++++ .../src/plugins/ToolbarPlugin/index.tsx | 16 +++++ .../src/themes/PlaygroundEditorTheme.css | 3 + .../src/themes/PlaygroundEditorTheme.ts | 1 + packages/lexical-rich-text/src/index.ts | 2 +- packages/lexical/flow/Lexical.js.flow | 4 +- packages/lexical/src/LexicalConstants.ts | 5 +- packages/lexical/src/LexicalEditor.ts | 1 + packages/lexical/src/LexicalUtils.ts | 5 ++ packages/lexical/src/nodes/LexicalTextNode.ts | 3 +- .../__tests__/unit/LexicalTextNode.test.tsx | 64 ++++++++++++------- 14 files changed, 95 insertions(+), 26 deletions(-) create mode 100644 packages/lexical-playground/src/images/icons/type-capitalize.svg diff --git a/packages/lexical-playground/src/context/ToolbarContext.tsx b/packages/lexical-playground/src/context/ToolbarContext.tsx index 921de34eb1b..f8b1c1f082b 100644 --- a/packages/lexical-playground/src/context/ToolbarContext.tsx +++ b/packages/lexical-playground/src/context/ToolbarContext.tsx @@ -67,6 +67,7 @@ const INITIAL_TOOLBAR_STATE = { isUnderline: false, isLowercase: false, isUppercase: false, + isCapitalize: false, rootType: 'root' as keyof typeof rootTypeToRootName, }; diff --git a/packages/lexical-playground/src/images/icons/type-capitalize.svg b/packages/lexical-playground/src/images/icons/type-capitalize.svg new file mode 100644 index 00000000000..359fcd0707c --- /dev/null +++ b/packages/lexical-playground/src/images/icons/type-capitalize.svg @@ -0,0 +1 @@ + diff --git a/packages/lexical-playground/src/index.css b/packages/lexical-playground/src/index.css index 8d7b2c3e353..b57f34b85d5 100644 --- a/packages/lexical-playground/src/index.css +++ b/packages/lexical-playground/src/index.css @@ -403,6 +403,10 @@ i.lowercase { background-image: url(images/icons/type-lowercase.svg); } +i.capitalize { + background-image: url(images/icons/type-capitalize.svg); +} + i.strikethrough { background-image: url(images/icons/type-strikethrough.svg); } diff --git a/packages/lexical-playground/src/plugins/ShortcutsPlugin/shortcuts.ts b/packages/lexical-playground/src/plugins/ShortcutsPlugin/shortcuts.ts index 10131c8875f..5ea8514e98a 100644 --- a/packages/lexical-playground/src/plugins/ShortcutsPlugin/shortcuts.ts +++ b/packages/lexical-playground/src/plugins/ShortcutsPlugin/shortcuts.ts @@ -29,6 +29,7 @@ export const SHORTCUTS = Object.freeze({ STRIKETHROUGH: IS_APPLE ? '⌘+Shift+S' : 'Ctrl+Shift+S', LOWERCASE: IS_APPLE ? '⌘+Shift+1' : 'Ctrl+Shift+1', UPPERCASE: IS_APPLE ? '⌘+Shift+2' : 'Ctrl+Shift+2', + CAPITALIZE: IS_APPLE ? '⌘+Shift+3' : 'Ctrl+Shift+3', CENTER_ALIGN: IS_APPLE ? '⌘+Shift+E' : 'Ctrl+Shift+E', JUSTIFY_ALIGN: IS_APPLE ? '⌘+Shift+J' : 'Ctrl+Shift+J', LEFT_ALIGN: IS_APPLE ? '⌘+Shift+L' : 'Ctrl+Shift+L', @@ -139,6 +140,16 @@ export function isUppercase(event: KeyboardEvent): boolean { ); } +export function isCapitalize(event: KeyboardEvent): boolean { + const {code, shiftKey, altKey, metaKey, ctrlKey} = event; + return ( + (code === 'Numpad3' || code === 'Digit3') && + shiftKey && + !altKey && + controlOrMeta(metaKey, ctrlKey) + ); +} + export function isStrikeThrough(event: KeyboardEvent): boolean { const {code, shiftKey, altKey, metaKey, ctrlKey} = event; return ( diff --git a/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx b/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx index eeed31b493b..1dd6dc066d4 100644 --- a/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx @@ -617,6 +617,7 @@ export default function ToolbarPlugin({ ); updateToolbarState('isLowercase', selection.hasFormat('lowercase')); updateToolbarState('isUppercase', selection.hasFormat('uppercase')); + updateToolbarState('isCapitalize', selection.hasFormat('capitalize')); } }, [activeEditor, editor, updateToolbarState]); @@ -920,6 +921,21 @@ export default function ToolbarPlugin({ {SHORTCUTS.UPPERCASE} + { + activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'capitalize'); + }} + className={ + 'item wide ' + dropDownActiveClass(toolbarState.isCapitalize) + } + title="Capitalize" + aria-label="Format text to capitalize"> +
+ + Capitalize +
+ {SHORTCUTS.CAPITALIZE} +
{ activeEditor.dispatchCommand( diff --git a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css index 1f1b9028c11..60fc2a96675 100644 --- a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css +++ b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css @@ -83,6 +83,9 @@ .PlaygroundEditorTheme__textUppercase { text-transform: uppercase; } +.PlaygroundEditorTheme__textCapitalize { + text-transform: capitalize; +} .PlaygroundEditorTheme__hashtag { background-color: rgba(88, 144, 255, 0.15); border-bottom: 1px solid rgba(88, 144, 255, 0.3); diff --git a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.ts b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.ts index a766fbcca29..9dfd9e95c29 100644 --- a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.ts +++ b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.ts @@ -105,6 +105,7 @@ const theme: EditorThemeClasses = { tableSelection: 'PlaygroundEditorTheme__tableSelection', text: { bold: 'PlaygroundEditorTheme__textBold', + capitalize: 'PlaygroundEditorTheme__textCapitalize', code: 'PlaygroundEditorTheme__textCode', italic: 'PlaygroundEditorTheme__textItalic', lowercase: 'PlaygroundEditorTheme__textLowercase', diff --git a/packages/lexical-rich-text/src/index.ts b/packages/lexical-rich-text/src/index.ts index 3e18bcef723..7102bd6eeeb 100644 --- a/packages/lexical-rich-text/src/index.ts +++ b/packages/lexical-rich-text/src/index.ts @@ -552,7 +552,7 @@ function $isSelectionAtEndOfRoot(selection: RangeSelection) { } function $resetCapitalization(selection: RangeSelection): void { - for (const format of ['lowercase', 'uppercase'] as const) { + for (const format of ['lowercase', 'uppercase', 'capitalize'] as const) { if (selection.hasFormat(format)) { selection.toggleFormat(format); } diff --git a/packages/lexical/flow/Lexical.js.flow b/packages/lexical/flow/Lexical.js.flow index 06069be16fb..cb5d33ef8b7 100644 --- a/packages/lexical/flow/Lexical.js.flow +++ b/packages/lexical/flow/Lexical.js.flow @@ -83,6 +83,7 @@ declare export var IS_SUPERSCRIPT: number; declare export var IS_UNDERLINE: number; declare export var IS_UPPERCASE: number; declare export var IS_LOWERCASE: number; +declare export var IS_CAPITALIZE: number; declare export var TEXT_TYPE_TO_FORMAT: Record; /** @@ -245,8 +246,9 @@ type TextNodeThemeClasses = { code?: EditorThemeClassName, subscript?: EditorThemeClassName, superscript?: EditorThemeClassName, - uppercase?: EditorThemeClassName, lowercase?: EditorThemeClassName, + uppercase?: EditorThemeClassName, + capitalize?: EditorThemeClassName, }; export type EditorThemeClasses = { characterLimit?: EditorThemeClassName, diff --git a/packages/lexical/src/LexicalConstants.ts b/packages/lexical/src/LexicalConstants.ts index 570d26e5cf4..aead3dbddff 100644 --- a/packages/lexical/src/LexicalConstants.ts +++ b/packages/lexical/src/LexicalConstants.ts @@ -46,6 +46,7 @@ export const IS_SUPERSCRIPT = 1 << 6; export const IS_HIGHLIGHT = 1 << 7; export const IS_LOWERCASE = 1 << 8; export const IS_UPPERCASE = 1 << 9; +export const IS_CAPITALIZE = 1 << 10; export const IS_ALL_FORMATTING = IS_BOLD | @@ -57,7 +58,8 @@ export const IS_ALL_FORMATTING = IS_SUPERSCRIPT | IS_HIGHLIGHT | IS_LOWERCASE | - IS_UPPERCASE; + IS_UPPERCASE | + IS_CAPITALIZE; // Text node details export const IS_DIRECTIONLESS = 1; @@ -101,6 +103,7 @@ export const LTR_REGEX = new RegExp('^[^' + RTL + ']*[' + LTR + ']'); export const TEXT_TYPE_TO_FORMAT: Record = { bold: IS_BOLD, + capitalize: IS_CAPITALIZE, code: IS_CODE, highlight: IS_HIGHLIGHT, italic: IS_ITALIC, diff --git a/packages/lexical/src/LexicalEditor.ts b/packages/lexical/src/LexicalEditor.ts index ee4172f6cfe..4798f4d8118 100644 --- a/packages/lexical/src/LexicalEditor.ts +++ b/packages/lexical/src/LexicalEditor.ts @@ -71,6 +71,7 @@ export type TextNodeThemeClasses = { italic?: EditorThemeClassName; lowercase?: EditorThemeClassName; uppercase?: EditorThemeClassName; + capitalize: EditorThemeClassName; strikethrough?: EditorThemeClassName; subscript?: EditorThemeClassName; superscript?: EditorThemeClassName; diff --git a/packages/lexical/src/LexicalUtils.ts b/packages/lexical/src/LexicalUtils.ts index 7d859a1ae2a..d6af132f690 100644 --- a/packages/lexical/src/LexicalUtils.ts +++ b/packages/lexical/src/LexicalUtils.ts @@ -228,8 +228,13 @@ export function toggleTextFormatType( newFormat &= ~TEXT_TYPE_TO_FORMAT.subscript; } else if (type === 'lowercase') { newFormat &= ~TEXT_TYPE_TO_FORMAT.uppercase; + newFormat &= ~TEXT_TYPE_TO_FORMAT.capitalize; } else if (type === 'uppercase') { newFormat &= ~TEXT_TYPE_TO_FORMAT.lowercase; + newFormat &= ~TEXT_TYPE_TO_FORMAT.capitalize; + } else if (type === 'capitalize') { + newFormat &= ~TEXT_TYPE_TO_FORMAT.lowercase; + newFormat &= ~TEXT_TYPE_TO_FORMAT.uppercase; } return newFormat; } diff --git a/packages/lexical/src/nodes/LexicalTextNode.ts b/packages/lexical/src/nodes/LexicalTextNode.ts index 344117500e0..e31b845c738 100644 --- a/packages/lexical/src/nodes/LexicalTextNode.ts +++ b/packages/lexical/src/nodes/LexicalTextNode.ts @@ -92,7 +92,8 @@ export type TextFormatType = | 'subscript' | 'superscript' | 'lowercase' - | 'uppercase'; + | 'uppercase' + | 'capitalize'; export type TextModeType = 'normal' | 'token' | 'segmented'; diff --git a/packages/lexical/src/nodes/__tests__/unit/LexicalTextNode.test.tsx b/packages/lexical/src/nodes/__tests__/unit/LexicalTextNode.test.tsx index 44c8e4729b6..bdf90b63972 100644 --- a/packages/lexical/src/nodes/__tests__/unit/LexicalTextNode.test.tsx +++ b/packages/lexical/src/nodes/__tests__/unit/LexicalTextNode.test.tsx @@ -32,6 +32,7 @@ import { } from '../../../__tests__/utils'; import { IS_BOLD, + IS_CAPITALIZE, IS_CODE, IS_HIGHLIGHT, IS_ITALIC, @@ -53,6 +54,7 @@ const editorConfig = Object.freeze({ theme: { text: { bold: 'my-bold-class', + capitalize: 'my-capitalize-class', code: 'my-code-class', highlight: 'my-highlight-class', italic: 'my-italic-class', @@ -216,6 +218,7 @@ describe('LexicalTextNode tests', () => { ['highlight', IS_HIGHLIGHT], ['lowercase', IS_LOWERCASE], ['uppercase', IS_UPPERCASE], + ['capitalize', IS_CAPITALIZE], ] as const)('%s flag', (formatFlag: TextFormatType, stateFormat: number) => { const flagPredicate = (node: TextNode) => node.hasFormat(formatFlag); const flagToggle = (node: TextNode) => node.toggleFormat(formatFlag); @@ -272,43 +275,54 @@ describe('LexicalTextNode tests', () => { }); }); - test.each([ - ['subscript', 'superscript'], - ['superscript', 'subscript'], - ['lowercase', 'uppercase'], - ['uppercase', 'lowercase'], - ])('setting %s clears %s', async (newFormat, otherFormat) => { + test('setting subscript clears superscript', async () => { await update(() => { const paragraphNode = $createParagraphNode(); const textNode = $createTextNode('Hello World'); paragraphNode.append(textNode); $getRoot().append(paragraphNode); - - textNode.toggleFormat(otherFormat as TextFormatType); - textNode.toggleFormat(newFormat as TextFormatType); - - expect(textNode.hasFormat(newFormat as TextFormatType)).toBe(true); - expect(textNode.hasFormat(otherFormat as TextFormatType)).toBe(false); + textNode.toggleFormat('superscript'); + textNode.toggleFormat('subscript'); + expect(textNode.hasFormat('subscript')).toBe(true); + expect(textNode.hasFormat('superscript')).toBe(false); }); }); - test.each([ - ['subscript', 'superscript'], - ['superscript', 'subscript'], - ['lowercase', 'uppercase'], - ['uppercase', 'lowercase'], - ])('clearing %s does not set %s', async (formatToClear, otherFormat) => { + test('setting superscript clears subscript', async () => { await update(() => { const paragraphNode = $createParagraphNode(); const textNode = $createTextNode('Hello World'); paragraphNode.append(textNode); $getRoot().append(paragraphNode); + textNode.toggleFormat('subscript'); + textNode.toggleFormat('superscript'); + expect(textNode.hasFormat('superscript')).toBe(true); + expect(textNode.hasFormat('subscript')).toBe(false); + }); + }); - textNode.toggleFormat(formatToClear as TextFormatType); - textNode.toggleFormat(formatToClear as TextFormatType); + test('capitalization formats are mutually exclusive', async () => { + const capitalizationFormats: TextFormatType[] = [ + 'lowercase', + 'uppercase', + 'capitalize', + ]; - expect(textNode.hasFormat(formatToClear as TextFormatType)).toBe(false); - expect(textNode.hasFormat(otherFormat as TextFormatType)).toBe(false); + await update(() => { + const paragraphNode = $createParagraphNode(); + const textNode = $createTextNode('Hello World'); + paragraphNode.append(textNode); + $getRoot().append(paragraphNode); + + capitalizationFormats.forEach((formatToSet) => { + textNode.toggleFormat(formatToSet as TextFormatType); + capitalizationFormats + .filter((format) => format !== formatToSet) + .forEach((format) => + expect(textNode.hasFormat(format as TextFormatType)).toBe(false), + ); + expect(textNode.hasFormat(formatToSet as TextFormatType)).toBe(true); + }); }); }); @@ -642,6 +656,12 @@ describe('LexicalTextNode tests', () => { 'My text node', 'My text node', ], + [ + 'capitalize', + IS_CAPITALIZE, + 'My text node', + 'My text node', + ], [ 'underline + strikethrough', IS_UNDERLINE | IS_STRIKETHROUGH,