From 3f1fd82793663f4d6e6058068df3f7fec6f9ced9 Mon Sep 17 00:00:00 2001 From: bedre7 Date: Mon, 11 Nov 2024 12:34:47 +0300 Subject: [PATCH 01/21] WIP: create capitalization node --- packages/lexical-playground/src/Editor.tsx | 3 +- .../src/nodes/CapitalizationNode.tsx | 81 +++++++++++++++++++ .../src/nodes/PlaygroundNodes.ts | 2 + .../plugins/CapitalizationPlugin/index.tsx | 49 +++++++++++ 4 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 packages/lexical-playground/src/nodes/CapitalizationNode.tsx create mode 100644 packages/lexical-playground/src/plugins/CapitalizationPlugin/index.tsx diff --git a/packages/lexical-playground/src/Editor.tsx b/packages/lexical-playground/src/Editor.tsx index 2c4f0419575..26d1b10579c 100644 --- a/packages/lexical-playground/src/Editor.tsx +++ b/packages/lexical-playground/src/Editor.tsx @@ -34,6 +34,7 @@ import ActionsPlugin from './plugins/ActionsPlugin'; import AutocompletePlugin from './plugins/AutocompletePlugin'; import AutoEmbedPlugin from './plugins/AutoEmbedPlugin'; import AutoLinkPlugin from './plugins/AutoLinkPlugin'; +import CapitalizationPlugin from './plugins/CapitalizationPlugin'; import CodeActionMenuPlugin from './plugins/CodeActionMenuPlugin'; import CodeHighlightPlugin from './plugins/CodeHighlightPlugin'; import CollapsiblePlugin from './plugins/CollapsiblePlugin'; @@ -160,7 +161,7 @@ export default function Editor(): JSX.Element { - + diff --git a/packages/lexical-playground/src/nodes/CapitalizationNode.tsx b/packages/lexical-playground/src/nodes/CapitalizationNode.tsx new file mode 100644 index 00000000000..fce4cf4059c --- /dev/null +++ b/packages/lexical-playground/src/nodes/CapitalizationNode.tsx @@ -0,0 +1,81 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { + EditorConfig, + LexicalNode, + NodeKey, + SerializedTextNode, + Spread, +} from 'lexical'; + +import {TextNode} from 'lexical'; + +export type SerializedCapitalizationNode = Spread< + { + className: string; + }, + SerializedTextNode +>; + +// eslint-disable-next-line no-shadow +export enum Capitalization { + Titlecase = 'capitalize', + Uppercase = 'uppercase', + Lowercase = 'lowercase', +} + +export class CapitalizationNode extends TextNode { + __capitalization: Capitalization; + + static getType(): string { + return 'capitalization'; + } + + static clone(node: CapitalizationNode): CapitalizationNode { + return new CapitalizationNode( + node.__capitalization, + node.__text, + node.__key, + ); + } + + constructor(capitalization: Capitalization, text: string, key?: NodeKey) { + super(text, key); + this.__capitalization = capitalization; + } + + createDOM(_: EditorConfig): HTMLElement { + const dom = document.createElement('span'); + dom.style.textTransform = this.__capitalization; + dom.textContent = this.__text; + + return dom; + } + + // static importJSON(json: SerializedCapitalizationNode): CapitalizationNode { + // //todo + // } + + // exportJSON(): SerializedCapitalizationNode { + // //todo + // } +} + +export function $isCapitalizationNode( + node: LexicalNode | null | undefined, +): node is CapitalizationNode { + return node instanceof CapitalizationNode; +} + +export function $createCapitalizationNode( + capitalization: Capitalization, + text: string, +): CapitalizationNode { + return new CapitalizationNode(capitalization, text); +} diff --git a/packages/lexical-playground/src/nodes/PlaygroundNodes.ts b/packages/lexical-playground/src/nodes/PlaygroundNodes.ts index e44931905ba..167462040d6 100644 --- a/packages/lexical-playground/src/nodes/PlaygroundNodes.ts +++ b/packages/lexical-playground/src/nodes/PlaygroundNodes.ts @@ -22,6 +22,7 @@ import {CollapsibleContainerNode} from '../plugins/CollapsiblePlugin/Collapsible import {CollapsibleContentNode} from '../plugins/CollapsiblePlugin/CollapsibleContentNode'; import {CollapsibleTitleNode} from '../plugins/CollapsiblePlugin/CollapsibleTitleNode'; import {AutocompleteNode} from './AutocompleteNode'; +import {CapitalizationNode} from './CapitalizationNode'; import {EmojiNode} from './EmojiNode'; import {EquationNode} from './EquationNode'; import {ExcalidrawNode} from './ExcalidrawNode'; @@ -73,6 +74,7 @@ const PlaygroundNodes: Array> = [ PageBreakNode, LayoutContainerNode, LayoutItemNode, + CapitalizationNode, ]; export default PlaygroundNodes; diff --git a/packages/lexical-playground/src/plugins/CapitalizationPlugin/index.tsx b/packages/lexical-playground/src/plugins/CapitalizationPlugin/index.tsx new file mode 100644 index 00000000000..3e69d3fc02b --- /dev/null +++ b/packages/lexical-playground/src/plugins/CapitalizationPlugin/index.tsx @@ -0,0 +1,49 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; +import {mergeRegister} from '@lexical/utils'; +import {LexicalEditor, TextNode} from 'lexical'; +import {useEffect} from 'react'; + +import { + $createCapitalizationNode, + Capitalization, + CapitalizationNode, +} from '../../nodes/CapitalizationNode'; + +function useCapitalization(editor: LexicalEditor): void { + useEffect(() => { + if (!editor.hasNodes([CapitalizationNode])) { + throw new Error( + 'CapitalizationPlugin: CapitalizationNode not registered on editor', + ); + } + + function $handleCapitalizationCommand(node: TextNode): TextNode { + const targetNode = node; + + const capitalizationNode = $createCapitalizationNode( + Capitalization.Titlecase, + node.getTextContent(), + ); + targetNode.replace(capitalizationNode); + + return targetNode; + } + + return mergeRegister( + editor.registerNodeTransform(TextNode, $handleCapitalizationCommand), + ); + }, [editor]); +} + +export default function CapitalizationPlugin(): JSX.Element | null { + const [editor] = useLexicalComposerContext(); + return null; + useCapitalization(editor); +} From 4bb207107e4ffb9f0c9ea35ad0ff0932a7f23271 Mon Sep 17 00:00:00 2001 From: bedre7 Date: Mon, 18 Nov 2024 21:46:07 +0300 Subject: [PATCH 02/21] add capitalization types to lexical text node format types --- packages/lexical-rich-text/src/index.ts | 44 +++++++++++++++++++ packages/lexical/src/LexicalConstants.ts | 11 ++++- packages/lexical/src/LexicalEditor.ts | 3 ++ packages/lexical/src/LexicalUtils.ts | 9 ++++ packages/lexical/src/nodes/LexicalTextNode.ts | 5 ++- 5 files changed, 70 insertions(+), 2 deletions(-) diff --git a/packages/lexical-rich-text/src/index.ts b/packages/lexical-rich-text/src/index.ts index bf53a8acdd4..239a95b7606 100644 --- a/packages/lexical-rich-text/src/index.ts +++ b/packages/lexical-rich-text/src/index.ts @@ -88,6 +88,8 @@ import { KEY_DELETE_COMMAND, KEY_ENTER_COMMAND, KEY_ESCAPE_COMMAND, + KEY_SPACE_COMMAND, + KEY_TAB_COMMAND, OUTDENT_CONTENT_COMMAND, PASTE_COMMAND, REMOVE_TEXT_COMMAND, @@ -549,6 +551,19 @@ function $isSelectionAtEndOfRoot(selection: RangeSelection) { return focus.key === 'root' && focus.offset === $getRoot().getChildrenSize(); } +function $resetCapitalization(selection: RangeSelection): void { + const capitalizationTypes: TextFormatType[] = [ + 'lowercase', + 'titlecase', + 'uppercase', + ]; + capitalizationTypes.forEach((type) => { + if (selection.hasFormat(type)) { + selection.toggleFormat(type); + } + }); +} + export function registerRichText(editor: LexicalEditor): () => void { const removeListener = mergeRegister( editor.registerCommand( @@ -900,6 +915,7 @@ export function registerRichText(editor: LexicalEditor): () => void { if (!$isRangeSelection(selection)) { return false; } + $resetCapitalization(selection); if (event !== null) { // If we have beforeinput, then we can avoid blocking // the default behavior. This ensures that the iOS can @@ -1065,6 +1081,34 @@ export function registerRichText(editor: LexicalEditor): () => void { }, COMMAND_PRIORITY_EDITOR, ), + editor.registerCommand( + KEY_SPACE_COMMAND, + (_) => { + const selection = $getSelection(); + if (!$isRangeSelection(selection)) { + return false; + } + + $resetCapitalization(selection); + + return false; + }, + COMMAND_PRIORITY_EDITOR, + ), + editor.registerCommand( + KEY_TAB_COMMAND, + (_) => { + const selection = $getSelection(); + if (!$isRangeSelection(selection)) { + return false; + } + + $resetCapitalization(selection); + + return false; + }, + COMMAND_PRIORITY_EDITOR, + ), ); return removeListener; } diff --git a/packages/lexical/src/LexicalConstants.ts b/packages/lexical/src/LexicalConstants.ts index 81b86a372ef..c2193d1e161 100644 --- a/packages/lexical/src/LexicalConstants.ts +++ b/packages/lexical/src/LexicalConstants.ts @@ -44,6 +44,9 @@ export const IS_CODE = 1 << 4; export const IS_SUBSCRIPT = 1 << 5; export const IS_SUPERSCRIPT = 1 << 6; export const IS_HIGHLIGHT = 1 << 7; +export const IS_LOWERCASE = 1 << 8; +export const IS_TITLECASE = 1 << 9; +export const IS_UPPERCASE = 1 << 10; export const IS_ALL_FORMATTING = IS_BOLD | @@ -53,7 +56,10 @@ export const IS_ALL_FORMATTING = IS_CODE | IS_SUBSCRIPT | IS_SUPERSCRIPT | - IS_HIGHLIGHT; + IS_HIGHLIGHT | + IS_LOWERCASE | + IS_TITLECASE | + IS_UPPERCASE; // Text node details export const IS_DIRECTIONLESS = 1; @@ -100,10 +106,13 @@ export const TEXT_TYPE_TO_FORMAT: Record = { code: IS_CODE, highlight: IS_HIGHLIGHT, italic: IS_ITALIC, + lowercase: IS_LOWERCASE, strikethrough: IS_STRIKETHROUGH, subscript: IS_SUBSCRIPT, superscript: IS_SUPERSCRIPT, + titlecase: IS_TITLECASE, underline: IS_UNDERLINE, + uppercase: IS_UPPERCASE, }; export const DETAIL_TYPE_TO_DETAIL: Record = { diff --git a/packages/lexical/src/LexicalEditor.ts b/packages/lexical/src/LexicalEditor.ts index 6016ae84956..aa819cc1fa1 100644 --- a/packages/lexical/src/LexicalEditor.ts +++ b/packages/lexical/src/LexicalEditor.ts @@ -69,6 +69,9 @@ export type TextNodeThemeClasses = { code?: EditorThemeClassName; highlight?: EditorThemeClassName; italic?: EditorThemeClassName; + lowercase?: EditorThemeClassName; + titlecase?: EditorThemeClassName; + uppercase?: EditorThemeClassName; strikethrough?: EditorThemeClassName; subscript?: EditorThemeClassName; superscript?: EditorThemeClassName; diff --git a/packages/lexical/src/LexicalUtils.ts b/packages/lexical/src/LexicalUtils.ts index b1a409a9f36..01c5ff8017d 100644 --- a/packages/lexical/src/LexicalUtils.ts +++ b/packages/lexical/src/LexicalUtils.ts @@ -221,6 +221,15 @@ export function toggleTextFormatType( newFormat &= ~TEXT_TYPE_TO_FORMAT.superscript; } else if (type === 'superscript') { newFormat &= ~TEXT_TYPE_TO_FORMAT.subscript; + } else if (type === 'lowercase') { + newFormat &= ~TEXT_TYPE_TO_FORMAT.uppercase; + newFormat &= ~TEXT_TYPE_TO_FORMAT.titlecase; + } else if (type === 'uppercase') { + newFormat &= ~TEXT_TYPE_TO_FORMAT.lowercase; + newFormat &= ~TEXT_TYPE_TO_FORMAT.titlecase; + } else if (type === 'titlecase') { + 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 fad639a1c72..9c6c4ee3665 100644 --- a/packages/lexical/src/nodes/LexicalTextNode.ts +++ b/packages/lexical/src/nodes/LexicalTextNode.ts @@ -90,7 +90,10 @@ export type TextFormatType = | 'highlight' | 'code' | 'subscript' - | 'superscript'; + | 'superscript' + | 'lowercase' + | 'titlecase' + | 'uppercase'; export type TextModeType = 'normal' | 'token' | 'segmented'; From d88f2dbd0ec0b0620006fa9306918a267a9d27c9 Mon Sep 17 00:00:00 2001 From: bedre7 Date: Mon, 18 Nov 2024 21:47:33 +0300 Subject: [PATCH 03/21] add capitalization format to the lexical playground --- packages/lexical-playground/src/Editor.tsx | 2 - .../src/context/ToolbarContext.tsx | 5 ++ .../src/nodes/CapitalizationNode.tsx | 81 ------------------- .../src/nodes/PlaygroundNodes.ts | 2 - .../plugins/CapitalizationPlugin/index.tsx | 49 ----------- .../src/plugins/ShortcutsPlugin/shortcuts.ts | 3 + .../src/plugins/ToolbarPlugin/index.tsx | 48 +++++++++++ .../src/themes/PlaygroundEditorTheme.css | 9 +++ .../src/themes/PlaygroundEditorTheme.ts | 3 + 9 files changed, 68 insertions(+), 134 deletions(-) delete mode 100644 packages/lexical-playground/src/nodes/CapitalizationNode.tsx delete mode 100644 packages/lexical-playground/src/plugins/CapitalizationPlugin/index.tsx diff --git a/packages/lexical-playground/src/Editor.tsx b/packages/lexical-playground/src/Editor.tsx index 26d1b10579c..e3064103a4d 100644 --- a/packages/lexical-playground/src/Editor.tsx +++ b/packages/lexical-playground/src/Editor.tsx @@ -34,7 +34,6 @@ import ActionsPlugin from './plugins/ActionsPlugin'; import AutocompletePlugin from './plugins/AutocompletePlugin'; import AutoEmbedPlugin from './plugins/AutoEmbedPlugin'; import AutoLinkPlugin from './plugins/AutoLinkPlugin'; -import CapitalizationPlugin from './plugins/CapitalizationPlugin'; import CodeActionMenuPlugin from './plugins/CodeActionMenuPlugin'; import CodeHighlightPlugin from './plugins/CodeHighlightPlugin'; import CollapsiblePlugin from './plugins/CollapsiblePlugin'; @@ -161,7 +160,6 @@ export default function Editor(): JSX.Element { - diff --git a/packages/lexical-playground/src/context/ToolbarContext.tsx b/packages/lexical-playground/src/context/ToolbarContext.tsx index 266c584f7ef..f1ee3c06072 100644 --- a/packages/lexical-playground/src/context/ToolbarContext.tsx +++ b/packages/lexical-playground/src/context/ToolbarContext.tsx @@ -40,6 +40,8 @@ export const blockTypeToBlockName = { quote: 'Quote', }; +//disable eslint sorting rule for quick reference to toolbar state +/* eslint-disable sort-keys-fix/sort-keys-fix */ const INITIAL_TOOLBAR_STATE = { bgColor: '#fff', blockType: 'paragraph' as keyof typeof blockTypeToBlockName, @@ -63,6 +65,9 @@ const INITIAL_TOOLBAR_STATE = { isSubscript: false, isSuperscript: false, isUnderline: false, + isLowercase: false, + isTitlecase: false, + isUppercase: false, rootType: 'root' as keyof typeof rootTypeToRootName, }; diff --git a/packages/lexical-playground/src/nodes/CapitalizationNode.tsx b/packages/lexical-playground/src/nodes/CapitalizationNode.tsx deleted file mode 100644 index fce4cf4059c..00000000000 --- a/packages/lexical-playground/src/nodes/CapitalizationNode.tsx +++ /dev/null @@ -1,81 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -import type { - EditorConfig, - LexicalNode, - NodeKey, - SerializedTextNode, - Spread, -} from 'lexical'; - -import {TextNode} from 'lexical'; - -export type SerializedCapitalizationNode = Spread< - { - className: string; - }, - SerializedTextNode ->; - -// eslint-disable-next-line no-shadow -export enum Capitalization { - Titlecase = 'capitalize', - Uppercase = 'uppercase', - Lowercase = 'lowercase', -} - -export class CapitalizationNode extends TextNode { - __capitalization: Capitalization; - - static getType(): string { - return 'capitalization'; - } - - static clone(node: CapitalizationNode): CapitalizationNode { - return new CapitalizationNode( - node.__capitalization, - node.__text, - node.__key, - ); - } - - constructor(capitalization: Capitalization, text: string, key?: NodeKey) { - super(text, key); - this.__capitalization = capitalization; - } - - createDOM(_: EditorConfig): HTMLElement { - const dom = document.createElement('span'); - dom.style.textTransform = this.__capitalization; - dom.textContent = this.__text; - - return dom; - } - - // static importJSON(json: SerializedCapitalizationNode): CapitalizationNode { - // //todo - // } - - // exportJSON(): SerializedCapitalizationNode { - // //todo - // } -} - -export function $isCapitalizationNode( - node: LexicalNode | null | undefined, -): node is CapitalizationNode { - return node instanceof CapitalizationNode; -} - -export function $createCapitalizationNode( - capitalization: Capitalization, - text: string, -): CapitalizationNode { - return new CapitalizationNode(capitalization, text); -} diff --git a/packages/lexical-playground/src/nodes/PlaygroundNodes.ts b/packages/lexical-playground/src/nodes/PlaygroundNodes.ts index 167462040d6..e44931905ba 100644 --- a/packages/lexical-playground/src/nodes/PlaygroundNodes.ts +++ b/packages/lexical-playground/src/nodes/PlaygroundNodes.ts @@ -22,7 +22,6 @@ import {CollapsibleContainerNode} from '../plugins/CollapsiblePlugin/Collapsible import {CollapsibleContentNode} from '../plugins/CollapsiblePlugin/CollapsibleContentNode'; import {CollapsibleTitleNode} from '../plugins/CollapsiblePlugin/CollapsibleTitleNode'; import {AutocompleteNode} from './AutocompleteNode'; -import {CapitalizationNode} from './CapitalizationNode'; import {EmojiNode} from './EmojiNode'; import {EquationNode} from './EquationNode'; import {ExcalidrawNode} from './ExcalidrawNode'; @@ -74,7 +73,6 @@ const PlaygroundNodes: Array> = [ PageBreakNode, LayoutContainerNode, LayoutItemNode, - CapitalizationNode, ]; export default PlaygroundNodes; diff --git a/packages/lexical-playground/src/plugins/CapitalizationPlugin/index.tsx b/packages/lexical-playground/src/plugins/CapitalizationPlugin/index.tsx deleted file mode 100644 index 3e69d3fc02b..00000000000 --- a/packages/lexical-playground/src/plugins/CapitalizationPlugin/index.tsx +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ -import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; -import {mergeRegister} from '@lexical/utils'; -import {LexicalEditor, TextNode} from 'lexical'; -import {useEffect} from 'react'; - -import { - $createCapitalizationNode, - Capitalization, - CapitalizationNode, -} from '../../nodes/CapitalizationNode'; - -function useCapitalization(editor: LexicalEditor): void { - useEffect(() => { - if (!editor.hasNodes([CapitalizationNode])) { - throw new Error( - 'CapitalizationPlugin: CapitalizationNode not registered on editor', - ); - } - - function $handleCapitalizationCommand(node: TextNode): TextNode { - const targetNode = node; - - const capitalizationNode = $createCapitalizationNode( - Capitalization.Titlecase, - node.getTextContent(), - ); - targetNode.replace(capitalizationNode); - - return targetNode; - } - - return mergeRegister( - editor.registerNodeTransform(TextNode, $handleCapitalizationCommand), - ); - }, [editor]); -} - -export default function CapitalizationPlugin(): JSX.Element | null { - const [editor] = useLexicalComposerContext(); - return null; - useCapitalization(editor); -} diff --git a/packages/lexical-playground/src/plugins/ShortcutsPlugin/shortcuts.ts b/packages/lexical-playground/src/plugins/ShortcutsPlugin/shortcuts.ts index 4a959f9dcac..3c1fa224103 100644 --- a/packages/lexical-playground/src/plugins/ShortcutsPlugin/shortcuts.ts +++ b/packages/lexical-playground/src/plugins/ShortcutsPlugin/shortcuts.ts @@ -27,6 +27,9 @@ export const SHORTCUTS = Object.freeze({ DECREASE_FONT_SIZE: IS_APPLE ? '⌘+Shift+,' : 'Ctrl+Shift+,', INSERT_CODE_BLOCK: IS_APPLE ? '⌘+Shift+C' : 'Ctrl+Shift+C', STRIKETHROUGH: IS_APPLE ? '⌘+Shift+S' : 'Ctrl+Shift+S', + LOWERCASE: IS_APPLE ? '⌘+Shift+1' : 'Ctrl+Shift+1', + TITLECASE: IS_APPLE ? '⌘+Shift+2' : 'Ctrl+Shift+2', + UPPERCASE: 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', diff --git a/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx b/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx index ed5da202bca..8e45f22b899 100644 --- a/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx @@ -615,6 +615,9 @@ export default function ToolbarPlugin({ 'fontSize', $getSelectionStyleValueForProperty(selection, 'font-size', '15px'), ); + updateToolbarState('isLowercase', selection.hasFormat('lowercase')); + updateToolbarState('isTitlecase', selection.hasFormat('titlecase')); + updateToolbarState('isUppercase', selection.hasFormat('uppercase')); } }, [activeEditor, editor, updateToolbarState]); @@ -888,6 +891,51 @@ export default function ToolbarPlugin({ buttonLabel="" buttonAriaLabel="Formatting options for additional text styles" buttonIconClassName="icon dropdown-more"> + { + activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'lowercase'); + }} + className={ + 'item wide ' + dropDownActiveClass(toolbarState.isLowercase) + } + title="Lowercase" + aria-label="Format text to lowercase"> +
+ + lowercase +
+ {SHORTCUTS.LOWERCASE} +
+ { + activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'titlecase'); + }} + className={ + 'item wide ' + dropDownActiveClass(toolbarState.isTitlecase) + } + title="Titlecase" + aria-label="Format text to titlecase"> +
+ + Titlecase +
+ {SHORTCUTS.TITLECASE} +
+ { + activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'uppercase'); + }} + className={ + 'item wide ' + dropDownActiveClass(toolbarState.isUppercase) + } + title="Uppercase" + aria-label="Format text to uppercase"> +
+ + UPPERCASE +
+ {SHORTCUTS.UPPERCASE} +
{ activeEditor.dispatchCommand( diff --git a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css index 22d27e4145e..d4a94cab4cb 100644 --- a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css +++ b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css @@ -77,6 +77,15 @@ font-family: Menlo, Consolas, Monaco, monospace; font-size: 94%; } +.PlaygroundEditorTheme__textLowercase { + text-transform: lowercase; +} +.PlaygroundEditorTheme__textUppercase { + text-transform: uppercase; +} +.PlaygroundEditorTheme__textTitlecase { + 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 c29d9d1434d..bdc93c61480 100644 --- a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.ts +++ b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.ts @@ -109,11 +109,14 @@ const theme: EditorThemeClasses = { bold: 'PlaygroundEditorTheme__textBold', code: 'PlaygroundEditorTheme__textCode', italic: 'PlaygroundEditorTheme__textItalic', + lowercase: 'PlaygroundEditorTheme__textLowercase', strikethrough: 'PlaygroundEditorTheme__textStrikethrough', subscript: 'PlaygroundEditorTheme__textSubscript', superscript: 'PlaygroundEditorTheme__textSuperscript', + titlecase: 'PlaygroundEditorTheme__textTitlecase', underline: 'PlaygroundEditorTheme__textUnderline', underlineStrikethrough: 'PlaygroundEditorTheme__textUnderlineStrikethrough', + uppercase: 'PlaygroundEditorTheme__textUppercase', }, }; From e0aec5beb35721e0712ccfb6e707c0f4e18bb43f Mon Sep 17 00:00:00 2001 From: bedre7 Date: Tue, 19 Nov 2024 19:57:46 +0300 Subject: [PATCH 04/21] unit test for capitalization formats --- .../__tests__/unit/LexicalTextNode.test.tsx | 85 ++++++++++++++++++- 1 file changed, 82 insertions(+), 3 deletions(-) diff --git a/packages/lexical/src/nodes/__tests__/unit/LexicalTextNode.test.tsx b/packages/lexical/src/nodes/__tests__/unit/LexicalTextNode.test.tsx index 37191abc831..78fc661c508 100644 --- a/packages/lexical/src/nodes/__tests__/unit/LexicalTextNode.test.tsx +++ b/packages/lexical/src/nodes/__tests__/unit/LexicalTextNode.test.tsx @@ -35,10 +35,13 @@ import { IS_CODE, IS_HIGHLIGHT, IS_ITALIC, + IS_LOWERCASE, IS_STRIKETHROUGH, IS_SUBSCRIPT, IS_SUPERSCRIPT, + IS_TITLECASE, IS_UNDERLINE, + IS_UPPERCASE, } from '../../../LexicalConstants'; import { $getCompositionKey, @@ -54,9 +57,12 @@ const editorConfig = Object.freeze({ code: 'my-code-class', highlight: 'my-highlight-class', italic: 'my-italic-class', + lowercase: 'my-lowercase-class', strikethrough: 'my-strikethrough-class', + titlecase: 'my-titlecase-class', underline: 'my-underline-class', underlineStrikethrough: 'my-underline-strikethrough-class', + uppercase: 'my-uppercase-class', }, }, }); @@ -210,6 +216,9 @@ describe('LexicalTextNode tests', () => { ['subscript', IS_SUBSCRIPT], ['superscript', IS_SUPERSCRIPT], ['highlight', IS_HIGHLIGHT], + ['lowercase', IS_LOWERCASE], + ['titlecase', IS_TITLECASE], + ['uppercase', IS_UPPERCASE], ] as const)('%s flag', (formatFlag: TextFormatType, stateFormat: number) => { const flagPredicate = (node: TextNode) => node.hasFormat(formatFlag); const flagToggle = (node: TextNode) => node.toggleFormat(formatFlag); @@ -318,6 +327,57 @@ describe('LexicalTextNode tests', () => { }); }); + test('capitalization formats are mutually exclusive', async () => { + await update(() => { + const paragraphNode = $createParagraphNode(); + const textNode = $createTextNode('Hello World'); + paragraphNode.append(textNode); + $getRoot().append(paragraphNode); + + textNode.toggleFormat('lowercase'); + expect(textNode.hasFormat('lowercase')).toBe(true); + expect(textNode.hasFormat('titlecase')).toBe(false); + expect(textNode.hasFormat('uppercase')).toBe(false); + + textNode.toggleFormat('titlecase'); + expect(textNode.hasFormat('titlecase')).toBe(true); + expect(textNode.hasFormat('lowercase')).toBe(false); + expect(textNode.hasFormat('uppercase')).toBe(false); + + textNode.toggleFormat('uppercase'); + expect(textNode.hasFormat('uppercase')).toBe(true); + expect(textNode.hasFormat('lowercase')).toBe(false); + expect(textNode.hasFormat('titlecase')).toBe(false); + }); + }); + + test('clearing one capitalization format does not set another', async () => { + await update(() => { + const paragraphNode = $createParagraphNode(); + const textNode = $createTextNode('Hello World'); + paragraphNode.append(textNode); + $getRoot().append(paragraphNode); + + textNode.toggleFormat('lowercase'); + textNode.toggleFormat('lowercase'); + expect(textNode.hasFormat('lowercase')).toBe(false); + expect(textNode.hasFormat('titlecase')).toBe(false); + expect(textNode.hasFormat('uppercase')).toBe(false); + + textNode.toggleFormat('titlecase'); + textNode.toggleFormat('titlecase'); + expect(textNode.hasFormat('titlecase')).toBe(false); + expect(textNode.hasFormat('lowercase')).toBe(false); + expect(textNode.hasFormat('uppercase')).toBe(false); + + textNode.toggleFormat('uppercase'); + textNode.toggleFormat('uppercase'); + expect(textNode.hasFormat('uppercase')).toBe(false); + expect(textNode.hasFormat('lowercase')).toBe(false); + expect(textNode.hasFormat('titlecase')).toBe(false); + }); + }); + test('selectPrevious()', async () => { await update(() => { const paragraphNode = $createParagraphNode(); @@ -636,6 +696,24 @@ describe('LexicalTextNode tests', () => { 'My text node', 'My text node', ], + [ + 'lowercase', + IS_LOWERCASE, + 'My text node', + 'My text node', + ], + [ + 'titlecase', + IS_TITLECASE, + 'My text node', + 'My text node', + ], + [ + 'uppercase', + IS_UPPERCASE, + 'My text node', + 'My text node', + ], [ 'underline + strikethrough', IS_UNDERLINE | IS_STRIKETHROUGH, @@ -669,15 +747,16 @@ describe('LexicalTextNode tests', () => { 'My text node', ], [ - 'code + underline + strikethrough + bold + italic + highlight', + 'code + underline + strikethrough + bold + italic + highlight + uppercase', IS_CODE | IS_UNDERLINE | IS_STRIKETHROUGH | IS_BOLD | IS_ITALIC | - IS_HIGHLIGHT, + IS_HIGHLIGHT | + IS_UPPERCASE, 'My text node', - 'My text node', + 'My text node', ], ])('%s text format type', async (_type, format, contents, expectedHTML) => { await update(() => { From e5f931e6d4a9bccb32559903a131a8653bee251d Mon Sep 17 00:00:00 2001 From: bedre7 Date: Tue, 19 Nov 2024 20:07:01 +0300 Subject: [PATCH 05/21] refactor unit test --- .../__tests__/unit/LexicalTextNode.test.tsx | 50 +++++++------------ 1 file changed, 19 insertions(+), 31 deletions(-) diff --git a/packages/lexical/src/nodes/__tests__/unit/LexicalTextNode.test.tsx b/packages/lexical/src/nodes/__tests__/unit/LexicalTextNode.test.tsx index 78fc661c508..23076ee5e8f 100644 --- a/packages/lexical/src/nodes/__tests__/unit/LexicalTextNode.test.tsx +++ b/packages/lexical/src/nodes/__tests__/unit/LexicalTextNode.test.tsx @@ -334,20 +334,18 @@ describe('LexicalTextNode tests', () => { paragraphNode.append(textNode); $getRoot().append(paragraphNode); - textNode.toggleFormat('lowercase'); - expect(textNode.hasFormat('lowercase')).toBe(true); - expect(textNode.hasFormat('titlecase')).toBe(false); - expect(textNode.hasFormat('uppercase')).toBe(false); - - textNode.toggleFormat('titlecase'); - expect(textNode.hasFormat('titlecase')).toBe(true); - expect(textNode.hasFormat('lowercase')).toBe(false); - expect(textNode.hasFormat('uppercase')).toBe(false); - - textNode.toggleFormat('uppercase'); - expect(textNode.hasFormat('uppercase')).toBe(true); - expect(textNode.hasFormat('lowercase')).toBe(false); - expect(textNode.hasFormat('titlecase')).toBe(false); + const formats: TextFormatType[] = ['lowercase', 'titlecase', 'uppercase']; + + for (const format of formats) { + textNode.toggleFormat(format); + formats.forEach((f) => { + if (f === format) { + expect(textNode.hasFormat(f)).toBe(true); + } else { + expect(textNode.hasFormat(f)).toBe(false); + } + }); + } }); }); @@ -358,23 +356,13 @@ describe('LexicalTextNode tests', () => { paragraphNode.append(textNode); $getRoot().append(paragraphNode); - textNode.toggleFormat('lowercase'); - textNode.toggleFormat('lowercase'); - expect(textNode.hasFormat('lowercase')).toBe(false); - expect(textNode.hasFormat('titlecase')).toBe(false); - expect(textNode.hasFormat('uppercase')).toBe(false); - - textNode.toggleFormat('titlecase'); - textNode.toggleFormat('titlecase'); - expect(textNode.hasFormat('titlecase')).toBe(false); - expect(textNode.hasFormat('lowercase')).toBe(false); - expect(textNode.hasFormat('uppercase')).toBe(false); - - textNode.toggleFormat('uppercase'); - textNode.toggleFormat('uppercase'); - expect(textNode.hasFormat('uppercase')).toBe(false); - expect(textNode.hasFormat('lowercase')).toBe(false); - expect(textNode.hasFormat('titlecase')).toBe(false); + const formats: TextFormatType[] = ['lowercase', 'titlecase', 'uppercase']; + + for (const format of formats) { + textNode.toggleFormat(format); + textNode.toggleFormat(format); + formats.forEach((f) => expect(textNode.hasFormat(f)).toBe(false)); + } }); }); From 284a189d72ee1d25285e5c236b74118cb781d031 Mon Sep 17 00:00:00 2001 From: bedre7 Date: Tue, 19 Nov 2024 21:51:25 +0300 Subject: [PATCH 06/21] add icons to capitalization toolbar buttons --- .../src/context/ToolbarContext.tsx | 2 +- .../src/images/icons/type-lowercase.svg | 3 ++ .../src/images/icons/type-titlecase.svg | 1 + .../src/images/icons/type-uppercase.svg | 3 ++ packages/lexical-playground/src/index.css | 12 ++++++++ .../src/plugins/ShortcutsPlugin/shortcuts.ts | 4 +-- .../src/plugins/ToolbarPlugin/index.tsx | 30 +++++++++---------- 7 files changed, 37 insertions(+), 18 deletions(-) create mode 100644 packages/lexical-playground/src/images/icons/type-lowercase.svg create mode 100644 packages/lexical-playground/src/images/icons/type-titlecase.svg create mode 100644 packages/lexical-playground/src/images/icons/type-uppercase.svg diff --git a/packages/lexical-playground/src/context/ToolbarContext.tsx b/packages/lexical-playground/src/context/ToolbarContext.tsx index f1ee3c06072..83d4694d4a2 100644 --- a/packages/lexical-playground/src/context/ToolbarContext.tsx +++ b/packages/lexical-playground/src/context/ToolbarContext.tsx @@ -66,8 +66,8 @@ const INITIAL_TOOLBAR_STATE = { isSuperscript: false, isUnderline: false, isLowercase: false, - isTitlecase: false, isUppercase: false, + isTitlecase: false, rootType: 'root' as keyof typeof rootTypeToRootName, }; diff --git a/packages/lexical-playground/src/images/icons/type-lowercase.svg b/packages/lexical-playground/src/images/icons/type-lowercase.svg new file mode 100644 index 00000000000..5d097d7a57b --- /dev/null +++ b/packages/lexical-playground/src/images/icons/type-lowercase.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/lexical-playground/src/images/icons/type-titlecase.svg b/packages/lexical-playground/src/images/icons/type-titlecase.svg new file mode 100644 index 00000000000..0414fab9107 --- /dev/null +++ b/packages/lexical-playground/src/images/icons/type-titlecase.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/lexical-playground/src/images/icons/type-uppercase.svg b/packages/lexical-playground/src/images/icons/type-uppercase.svg new file mode 100644 index 00000000000..d0887b5d2f5 --- /dev/null +++ b/packages/lexical-playground/src/images/icons/type-uppercase.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/lexical-playground/src/index.css b/packages/lexical-playground/src/index.css index e5362290c69..9a810af09f0 100644 --- a/packages/lexical-playground/src/index.css +++ b/packages/lexical-playground/src/index.css @@ -394,6 +394,18 @@ i.underline { background-image: url(images/icons/type-underline.svg); } +i.uppercase { + background-image: url(images/icons/type-uppercase.svg); +} + +i.lowercase { + background-image: url(images/icons/type-lowercase.svg); +} + +i.titlecase { + background-image: url(images/icons/type-titlecase.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 3c1fa224103..342d95e704f 100644 --- a/packages/lexical-playground/src/plugins/ShortcutsPlugin/shortcuts.ts +++ b/packages/lexical-playground/src/plugins/ShortcutsPlugin/shortcuts.ts @@ -28,8 +28,8 @@ export const SHORTCUTS = Object.freeze({ INSERT_CODE_BLOCK: IS_APPLE ? '⌘+Shift+C' : 'Ctrl+Shift+C', STRIKETHROUGH: IS_APPLE ? '⌘+Shift+S' : 'Ctrl+Shift+S', LOWERCASE: IS_APPLE ? '⌘+Shift+1' : 'Ctrl+Shift+1', - TITLECASE: IS_APPLE ? '⌘+Shift+2' : 'Ctrl+Shift+2', - UPPERCASE: IS_APPLE ? '⌘+Shift+3' : 'Ctrl+Shift+3', + UPPERCASE: IS_APPLE ? '⌘+Shift+2' : 'Ctrl+Shift+2', + TITLECASE: 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', diff --git a/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx b/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx index 8e45f22b899..f189bbd5a8b 100644 --- a/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx @@ -616,8 +616,8 @@ export default function ToolbarPlugin({ $getSelectionStyleValueForProperty(selection, 'font-size', '15px'), ); updateToolbarState('isLowercase', selection.hasFormat('lowercase')); - updateToolbarState('isTitlecase', selection.hasFormat('titlecase')); updateToolbarState('isUppercase', selection.hasFormat('uppercase')); + updateToolbarState('isTitlecase', selection.hasFormat('titlecase')); } }, [activeEditor, editor, updateToolbarState]); @@ -908,33 +908,33 @@ export default function ToolbarPlugin({ { - activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'titlecase'); + activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'uppercase'); }} className={ - 'item wide ' + dropDownActiveClass(toolbarState.isTitlecase) + 'item wide ' + dropDownActiveClass(toolbarState.isUppercase) } - title="Titlecase" - aria-label="Format text to titlecase"> + title="Uppercase" + aria-label="Format text to uppercase">
- - Titlecase + + UPPERCASE
- {SHORTCUTS.TITLECASE} + {SHORTCUTS.UPPERCASE}
{ - activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'uppercase'); + activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'titlecase'); }} className={ - 'item wide ' + dropDownActiveClass(toolbarState.isUppercase) + 'item wide ' + dropDownActiveClass(toolbarState.isTitlecase) } - title="Uppercase" - aria-label="Format text to uppercase"> + title="Titlecase" + aria-label="Format text to titlecase">
- - UPPERCASE + + Title Case
- {SHORTCUTS.UPPERCASE} + {SHORTCUTS.TITLECASE}
{ From 7760eb33282bafdbf4980ccd69002456e868dd1f Mon Sep 17 00:00:00 2001 From: bedre7 Date: Tue, 19 Nov 2024 21:52:27 +0300 Subject: [PATCH 07/21] add new capitalization formats to flow --- packages/lexical-rich-text/src/index.ts | 2 +- packages/lexical/flow/Lexical.js.flow | 6 +++++- packages/lexical/src/LexicalConstants.ts | 8 ++++---- packages/lexical/src/LexicalEditor.ts | 2 +- packages/lexical/src/nodes/LexicalTextNode.ts | 4 ++-- .../__tests__/unit/LexicalTextNode.test.tsx | 18 +++++++++--------- 6 files changed, 22 insertions(+), 18 deletions(-) diff --git a/packages/lexical-rich-text/src/index.ts b/packages/lexical-rich-text/src/index.ts index 239a95b7606..22f3b13a0be 100644 --- a/packages/lexical-rich-text/src/index.ts +++ b/packages/lexical-rich-text/src/index.ts @@ -554,8 +554,8 @@ function $isSelectionAtEndOfRoot(selection: RangeSelection) { function $resetCapitalization(selection: RangeSelection): void { const capitalizationTypes: TextFormatType[] = [ 'lowercase', - 'titlecase', 'uppercase', + 'titlecase', ]; capitalizationTypes.forEach((type) => { if (selection.hasFormat(type)) { diff --git a/packages/lexical/flow/Lexical.js.flow b/packages/lexical/flow/Lexical.js.flow index bc32e05bff6..587eff86235 100644 --- a/packages/lexical/flow/Lexical.js.flow +++ b/packages/lexical/flow/Lexical.js.flow @@ -584,7 +584,11 @@ export type TextFormatType = | 'highlight' | 'code' | 'subscript' - | 'superscript'; + | 'superscript' + | 'lowercase' + | 'uppercase' + | 'titlecase'; + type TextModeType = 'normal' | 'token' | 'segmented'; declare export class TextNode extends LexicalNode { diff --git a/packages/lexical/src/LexicalConstants.ts b/packages/lexical/src/LexicalConstants.ts index c2193d1e161..3e35be76867 100644 --- a/packages/lexical/src/LexicalConstants.ts +++ b/packages/lexical/src/LexicalConstants.ts @@ -45,8 +45,8 @@ export const IS_SUBSCRIPT = 1 << 5; export const IS_SUPERSCRIPT = 1 << 6; export const IS_HIGHLIGHT = 1 << 7; export const IS_LOWERCASE = 1 << 8; -export const IS_TITLECASE = 1 << 9; -export const IS_UPPERCASE = 1 << 10; +export const IS_UPPERCASE = 1 << 9; +export const IS_TITLECASE = 1 << 10; export const IS_ALL_FORMATTING = IS_BOLD | @@ -58,8 +58,8 @@ export const IS_ALL_FORMATTING = IS_SUPERSCRIPT | IS_HIGHLIGHT | IS_LOWERCASE | - IS_TITLECASE | - IS_UPPERCASE; + IS_UPPERCASE | + IS_TITLECASE; // Text node details export const IS_DIRECTIONLESS = 1; diff --git a/packages/lexical/src/LexicalEditor.ts b/packages/lexical/src/LexicalEditor.ts index aa819cc1fa1..5f336acda03 100644 --- a/packages/lexical/src/LexicalEditor.ts +++ b/packages/lexical/src/LexicalEditor.ts @@ -70,8 +70,8 @@ export type TextNodeThemeClasses = { highlight?: EditorThemeClassName; italic?: EditorThemeClassName; lowercase?: EditorThemeClassName; - titlecase?: EditorThemeClassName; uppercase?: EditorThemeClassName; + titlecase?: EditorThemeClassName; strikethrough?: EditorThemeClassName; subscript?: EditorThemeClassName; superscript?: EditorThemeClassName; diff --git a/packages/lexical/src/nodes/LexicalTextNode.ts b/packages/lexical/src/nodes/LexicalTextNode.ts index 9c6c4ee3665..6b378241df3 100644 --- a/packages/lexical/src/nodes/LexicalTextNode.ts +++ b/packages/lexical/src/nodes/LexicalTextNode.ts @@ -92,8 +92,8 @@ export type TextFormatType = | 'subscript' | 'superscript' | 'lowercase' - | 'titlecase' - | 'uppercase'; + | 'uppercase' + | 'titlecase'; 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 23076ee5e8f..a39efa7beb8 100644 --- a/packages/lexical/src/nodes/__tests__/unit/LexicalTextNode.test.tsx +++ b/packages/lexical/src/nodes/__tests__/unit/LexicalTextNode.test.tsx @@ -217,8 +217,8 @@ describe('LexicalTextNode tests', () => { ['superscript', IS_SUPERSCRIPT], ['highlight', IS_HIGHLIGHT], ['lowercase', IS_LOWERCASE], - ['titlecase', IS_TITLECASE], ['uppercase', IS_UPPERCASE], + ['titlecase', IS_TITLECASE], ] as const)('%s flag', (formatFlag: TextFormatType, stateFormat: number) => { const flagPredicate = (node: TextNode) => node.hasFormat(formatFlag); const flagToggle = (node: TextNode) => node.toggleFormat(formatFlag); @@ -334,7 +334,7 @@ describe('LexicalTextNode tests', () => { paragraphNode.append(textNode); $getRoot().append(paragraphNode); - const formats: TextFormatType[] = ['lowercase', 'titlecase', 'uppercase']; + const formats: TextFormatType[] = ['lowercase', 'uppercase', 'titlecase']; for (const format of formats) { textNode.toggleFormat(format); @@ -356,7 +356,7 @@ describe('LexicalTextNode tests', () => { paragraphNode.append(textNode); $getRoot().append(paragraphNode); - const formats: TextFormatType[] = ['lowercase', 'titlecase', 'uppercase']; + const formats: TextFormatType[] = ['lowercase', 'uppercase', 'titlecase']; for (const format of formats) { textNode.toggleFormat(format); @@ -690,18 +690,18 @@ describe('LexicalTextNode tests', () => { 'My text node', 'My text node', ], - [ - 'titlecase', - IS_TITLECASE, - 'My text node', - 'My text node', - ], [ 'uppercase', IS_UPPERCASE, 'My text node', 'My text node', ], + [ + 'titlecase', + IS_TITLECASE, + 'My text node', + 'My text node', + ], [ 'underline + strikethrough', IS_UNDERLINE | IS_STRIKETHROUGH, From 2b926a6d7c7de7d6b938ed4721e12d3f32c9b1f4 Mon Sep 17 00:00:00 2001 From: bedre7 Date: Thu, 21 Nov 2024 17:34:05 +0300 Subject: [PATCH 08/21] update toolbar labels for new formats --- .../lexical-playground/src/plugins/ToolbarPlugin/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx b/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx index f189bbd5a8b..a01337a7130 100644 --- a/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx @@ -902,7 +902,7 @@ export default function ToolbarPlugin({ aria-label="Format text to lowercase">
- lowercase + Lowercase
{SHORTCUTS.LOWERCASE}
@@ -917,7 +917,7 @@ export default function ToolbarPlugin({ aria-label="Format text to uppercase">
- UPPERCASE + Uppercase
{SHORTCUTS.UPPERCASE} From e512eab8e9158ac1a30715d73cd3be721cf13824 Mon Sep 17 00:00:00 2001 From: bedre7 Date: Fri, 22 Nov 2024 21:54:00 +0300 Subject: [PATCH 09/21] remove titlecase --- .../src/context/ToolbarContext.tsx | 1 - .../src/images/icons/type-titlecase.svg | 1 - packages/lexical-playground/src/index.css | 4 - .../src/plugins/ShortcutsPlugin/shortcuts.ts | 1 - .../src/plugins/ToolbarPlugin/index.tsx | 16 --- .../src/themes/PlaygroundEditorTheme.css | 3 - .../src/themes/PlaygroundEditorTheme.ts | 1 - packages/lexical-rich-text/src/index.ts | 9 +- packages/lexical/flow/Lexical.js.flow | 7 +- 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 | 102 ++++-------------- 14 files changed, 28 insertions(+), 131 deletions(-) delete mode 100644 packages/lexical-playground/src/images/icons/type-titlecase.svg diff --git a/packages/lexical-playground/src/context/ToolbarContext.tsx b/packages/lexical-playground/src/context/ToolbarContext.tsx index 83d4694d4a2..921de34eb1b 100644 --- a/packages/lexical-playground/src/context/ToolbarContext.tsx +++ b/packages/lexical-playground/src/context/ToolbarContext.tsx @@ -67,7 +67,6 @@ const INITIAL_TOOLBAR_STATE = { isUnderline: false, isLowercase: false, isUppercase: false, - isTitlecase: false, rootType: 'root' as keyof typeof rootTypeToRootName, }; diff --git a/packages/lexical-playground/src/images/icons/type-titlecase.svg b/packages/lexical-playground/src/images/icons/type-titlecase.svg deleted file mode 100644 index 0414fab9107..00000000000 --- a/packages/lexical-playground/src/images/icons/type-titlecase.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/lexical-playground/src/index.css b/packages/lexical-playground/src/index.css index 9a810af09f0..2da5dc83868 100644 --- a/packages/lexical-playground/src/index.css +++ b/packages/lexical-playground/src/index.css @@ -402,10 +402,6 @@ i.lowercase { background-image: url(images/icons/type-lowercase.svg); } -i.titlecase { - background-image: url(images/icons/type-titlecase.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 342d95e704f..f171c380b70 100644 --- a/packages/lexical-playground/src/plugins/ShortcutsPlugin/shortcuts.ts +++ b/packages/lexical-playground/src/plugins/ShortcutsPlugin/shortcuts.ts @@ -29,7 +29,6 @@ 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', - TITLECASE: 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', diff --git a/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx b/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx index a01337a7130..eeed31b493b 100644 --- a/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx @@ -617,7 +617,6 @@ export default function ToolbarPlugin({ ); updateToolbarState('isLowercase', selection.hasFormat('lowercase')); updateToolbarState('isUppercase', selection.hasFormat('uppercase')); - updateToolbarState('isTitlecase', selection.hasFormat('titlecase')); } }, [activeEditor, editor, updateToolbarState]); @@ -921,21 +920,6 @@ export default function ToolbarPlugin({ {SHORTCUTS.UPPERCASE} - { - activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'titlecase'); - }} - className={ - 'item wide ' + dropDownActiveClass(toolbarState.isTitlecase) - } - title="Titlecase" - aria-label="Format text to titlecase"> -
- - Title Case -
- {SHORTCUTS.TITLECASE} -
{ activeEditor.dispatchCommand( diff --git a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css index d4a94cab4cb..fb3d88cee9b 100644 --- a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css +++ b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.css @@ -83,9 +83,6 @@ .PlaygroundEditorTheme__textUppercase { text-transform: uppercase; } -.PlaygroundEditorTheme__textTitlecase { - 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 bdc93c61480..046bacebaba 100644 --- a/packages/lexical-playground/src/themes/PlaygroundEditorTheme.ts +++ b/packages/lexical-playground/src/themes/PlaygroundEditorTheme.ts @@ -113,7 +113,6 @@ const theme: EditorThemeClasses = { strikethrough: 'PlaygroundEditorTheme__textStrikethrough', subscript: 'PlaygroundEditorTheme__textSubscript', superscript: 'PlaygroundEditorTheme__textSuperscript', - titlecase: 'PlaygroundEditorTheme__textTitlecase', underline: 'PlaygroundEditorTheme__textUnderline', underlineStrikethrough: 'PlaygroundEditorTheme__textUnderlineStrikethrough', uppercase: 'PlaygroundEditorTheme__textUppercase', diff --git a/packages/lexical-rich-text/src/index.ts b/packages/lexical-rich-text/src/index.ts index 22f3b13a0be..0d9ebd9e9fd 100644 --- a/packages/lexical-rich-text/src/index.ts +++ b/packages/lexical-rich-text/src/index.ts @@ -552,16 +552,11 @@ function $isSelectionAtEndOfRoot(selection: RangeSelection) { } function $resetCapitalization(selection: RangeSelection): void { - const capitalizationTypes: TextFormatType[] = [ - 'lowercase', - 'uppercase', - 'titlecase', - ]; - capitalizationTypes.forEach((type) => { + for (const type of ['lowercase', 'uppercase'] as const) { if (selection.hasFormat(type)) { selection.toggleFormat(type); } - }); + } } export function registerRichText(editor: LexicalEditor): () => void { diff --git a/packages/lexical/flow/Lexical.js.flow b/packages/lexical/flow/Lexical.js.flow index 587eff86235..2853cd9376a 100644 --- a/packages/lexical/flow/Lexical.js.flow +++ b/packages/lexical/flow/Lexical.js.flow @@ -80,7 +80,9 @@ declare export var IS_ITALIC: number; declare export var IS_STRIKETHROUGH: number; declare export var IS_SUBSCRIPT: number; declare export var IS_SUPERSCRIPT: number; -declare export var IS_UNDERLIN: number; +declare export var IS_UNDERLINE: number; +declare export var IS_UPPERCASE: number; +declare export var IS_LOWERCASE: number; declare export var TEXT_TYPE_TO_FORMAT: Record; /** @@ -586,8 +588,7 @@ export type TextFormatType = | 'subscript' | 'superscript' | 'lowercase' - | 'uppercase' - | 'titlecase'; + | 'uppercase'; type TextModeType = 'normal' | 'token' | 'segmented'; diff --git a/packages/lexical/src/LexicalConstants.ts b/packages/lexical/src/LexicalConstants.ts index 3e35be76867..570d26e5cf4 100644 --- a/packages/lexical/src/LexicalConstants.ts +++ b/packages/lexical/src/LexicalConstants.ts @@ -46,7 +46,6 @@ 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_TITLECASE = 1 << 10; export const IS_ALL_FORMATTING = IS_BOLD | @@ -58,8 +57,7 @@ export const IS_ALL_FORMATTING = IS_SUPERSCRIPT | IS_HIGHLIGHT | IS_LOWERCASE | - IS_UPPERCASE | - IS_TITLECASE; + IS_UPPERCASE; // Text node details export const IS_DIRECTIONLESS = 1; @@ -110,7 +108,6 @@ export const TEXT_TYPE_TO_FORMAT: Record = { strikethrough: IS_STRIKETHROUGH, subscript: IS_SUBSCRIPT, superscript: IS_SUPERSCRIPT, - titlecase: IS_TITLECASE, underline: IS_UNDERLINE, uppercase: IS_UPPERCASE, }; diff --git a/packages/lexical/src/LexicalEditor.ts b/packages/lexical/src/LexicalEditor.ts index 5f336acda03..8ff45930ac7 100644 --- a/packages/lexical/src/LexicalEditor.ts +++ b/packages/lexical/src/LexicalEditor.ts @@ -71,7 +71,6 @@ export type TextNodeThemeClasses = { italic?: EditorThemeClassName; lowercase?: EditorThemeClassName; uppercase?: EditorThemeClassName; - titlecase?: EditorThemeClassName; strikethrough?: EditorThemeClassName; subscript?: EditorThemeClassName; superscript?: EditorThemeClassName; diff --git a/packages/lexical/src/LexicalUtils.ts b/packages/lexical/src/LexicalUtils.ts index 01c5ff8017d..563a1d2a033 100644 --- a/packages/lexical/src/LexicalUtils.ts +++ b/packages/lexical/src/LexicalUtils.ts @@ -223,13 +223,8 @@ export function toggleTextFormatType( newFormat &= ~TEXT_TYPE_TO_FORMAT.subscript; } else if (type === 'lowercase') { newFormat &= ~TEXT_TYPE_TO_FORMAT.uppercase; - newFormat &= ~TEXT_TYPE_TO_FORMAT.titlecase; } else if (type === 'uppercase') { newFormat &= ~TEXT_TYPE_TO_FORMAT.lowercase; - newFormat &= ~TEXT_TYPE_TO_FORMAT.titlecase; - } else if (type === 'titlecase') { - 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 6b378241df3..344117500e0 100644 --- a/packages/lexical/src/nodes/LexicalTextNode.ts +++ b/packages/lexical/src/nodes/LexicalTextNode.ts @@ -92,8 +92,7 @@ export type TextFormatType = | 'subscript' | 'superscript' | 'lowercase' - | 'uppercase' - | 'titlecase'; + | 'uppercase'; 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 a39efa7beb8..db427e18afb 100644 --- a/packages/lexical/src/nodes/__tests__/unit/LexicalTextNode.test.tsx +++ b/packages/lexical/src/nodes/__tests__/unit/LexicalTextNode.test.tsx @@ -39,7 +39,6 @@ import { IS_STRIKETHROUGH, IS_SUBSCRIPT, IS_SUPERSCRIPT, - IS_TITLECASE, IS_UNDERLINE, IS_UPPERCASE, } from '../../../LexicalConstants'; @@ -59,7 +58,6 @@ const editorConfig = Object.freeze({ italic: 'my-italic-class', lowercase: 'my-lowercase-class', strikethrough: 'my-strikethrough-class', - titlecase: 'my-titlecase-class', underline: 'my-underline-class', underlineStrikethrough: 'my-underline-strikethrough-class', uppercase: 'my-uppercase-class', @@ -218,7 +216,6 @@ describe('LexicalTextNode tests', () => { ['highlight', IS_HIGHLIGHT], ['lowercase', IS_LOWERCASE], ['uppercase', IS_UPPERCASE], - ['titlecase', IS_TITLECASE], ] as const)('%s flag', (formatFlag: TextFormatType, stateFormat: number) => { const flagPredicate = (node: TextNode) => node.hasFormat(formatFlag); const flagToggle = (node: TextNode) => node.toggleFormat(formatFlag); @@ -275,94 +272,41 @@ describe('LexicalTextNode tests', () => { }); }); - test('setting subscript clears superscript', async () => { + test.each([ + ['subscript', 'superscript'], + ['superscript', 'subscript'], + ['lowercase', 'uppercase'], + ['uppercase', 'lowercase'], + ])('setting %s clears %s', async (newFormat, previousFormat) => { await update(() => { const paragraphNode = $createParagraphNode(); const textNode = $createTextNode('Hello World'); paragraphNode.append(textNode); $getRoot().append(paragraphNode); - textNode.toggleFormat('superscript'); - textNode.toggleFormat('subscript'); - expect(textNode.hasFormat('subscript')).toBe(true); - expect(textNode.hasFormat('superscript')).toBe(false); - }); - }); - 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(previousFormat as TextFormatType); + textNode.toggleFormat(newFormat as TextFormatType); + expect(textNode.hasFormat(newFormat as TextFormatType)).toBe(true); + expect(textNode.hasFormat(previousFormat as TextFormatType)).toBe(false); }); }); - test('clearing subscript does not set superscript', async () => { + test.each([ + ['subscript', 'superscript'], + ['superscript', 'subscript'], + ['lowercase', 'uppercase'], + ['uppercase', 'lowercase'], + ])('clearing %s does not set %s', async (formatToClear, otherFormat) => { await update(() => { const paragraphNode = $createParagraphNode(); const textNode = $createTextNode('Hello World'); paragraphNode.append(textNode); $getRoot().append(paragraphNode); - textNode.toggleFormat('subscript'); - textNode.toggleFormat('subscript'); - expect(textNode.hasFormat('subscript')).toBe(false); - expect(textNode.hasFormat('superscript')).toBe(false); - }); - }); - test('clearing superscript does not set subscript', async () => { - await update(() => { - const paragraphNode = $createParagraphNode(); - const textNode = $createTextNode('Hello World'); - paragraphNode.append(textNode); - $getRoot().append(paragraphNode); - textNode.toggleFormat('superscript'); - textNode.toggleFormat('superscript'); - expect(textNode.hasFormat('superscript')).toBe(false); - expect(textNode.hasFormat('subscript')).toBe(false); - }); - }); - - test('capitalization formats are mutually exclusive', async () => { - await update(() => { - const paragraphNode = $createParagraphNode(); - const textNode = $createTextNode('Hello World'); - paragraphNode.append(textNode); - $getRoot().append(paragraphNode); - - const formats: TextFormatType[] = ['lowercase', 'uppercase', 'titlecase']; - - for (const format of formats) { - textNode.toggleFormat(format); - formats.forEach((f) => { - if (f === format) { - expect(textNode.hasFormat(f)).toBe(true); - } else { - expect(textNode.hasFormat(f)).toBe(false); - } - }); - } - }); - }); - - test('clearing one capitalization format does not set another', async () => { - await update(() => { - const paragraphNode = $createParagraphNode(); - const textNode = $createTextNode('Hello World'); - paragraphNode.append(textNode); - $getRoot().append(paragraphNode); - - const formats: TextFormatType[] = ['lowercase', 'uppercase', 'titlecase']; - - for (const format of formats) { - textNode.toggleFormat(format); - textNode.toggleFormat(format); - formats.forEach((f) => expect(textNode.hasFormat(f)).toBe(false)); - } + textNode.toggleFormat(formatToClear as TextFormatType); + textNode.toggleFormat(formatToClear as TextFormatType); + expect(textNode.hasFormat(formatToClear as TextFormatType)).toBe(false); + expect(textNode.hasFormat(otherFormat as TextFormatType)).toBe(false); }); }); @@ -696,12 +640,6 @@ describe('LexicalTextNode tests', () => { 'My text node', 'My text node', ], - [ - 'titlecase', - IS_TITLECASE, - 'My text node', - 'My text node', - ], [ 'underline + strikethrough', IS_UNDERLINE | IS_STRIKETHROUGH, From 53809bd5295d41da6e87ef1dfa6da4b84b48ded7 Mon Sep 17 00:00:00 2001 From: bedre7 Date: Fri, 29 Nov 2024 20:44:20 +0300 Subject: [PATCH 10/21] add keyboard shortcuts for new formatting options --- .../src/plugins/ShortcutsPlugin/index.tsx | 8 ++++++++ .../src/plugins/ShortcutsPlugin/shortcuts.ts | 14 ++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/packages/lexical-playground/src/plugins/ShortcutsPlugin/index.tsx b/packages/lexical-playground/src/plugins/ShortcutsPlugin/index.tsx index 4549d8a10e8..9b7540a3338 100644 --- a/packages/lexical-playground/src/plugins/ShortcutsPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/ShortcutsPlugin/index.tsx @@ -50,11 +50,13 @@ import { isInsertLink, isJustifyAlign, isLeftAlign, + isLowercase, isOutdent, isRightAlign, isStrikeThrough, isSubscript, isSuperscript, + isUppercase, } from './shortcuts'; export default function ShortcutsPlugin({ @@ -96,6 +98,12 @@ export default function ShortcutsPlugin({ } else if (isStrikeThrough(event)) { event.preventDefault(); editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough'); + } else if (isLowercase(event)) { + event.preventDefault(); + editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'lowercase'); + } else if (isUppercase(event)) { + event.preventDefault(); + editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'uppercase'); } else if (isIndent(event)) { event.preventDefault(); editor.dispatchCommand(INDENT_CONTENT_COMMAND, undefined); diff --git a/packages/lexical-playground/src/plugins/ShortcutsPlugin/shortcuts.ts b/packages/lexical-playground/src/plugins/ShortcutsPlugin/shortcuts.ts index f171c380b70..3a9caab6270 100644 --- a/packages/lexical-playground/src/plugins/ShortcutsPlugin/shortcuts.ts +++ b/packages/lexical-playground/src/plugins/ShortcutsPlugin/shortcuts.ts @@ -119,6 +119,20 @@ export function isFormatQuote(event: KeyboardEvent): boolean { ); } +export function isLowercase(event: KeyboardEvent): boolean { + const {code, shiftKey, altKey, metaKey, ctrlKey} = event; + return ( + code === 'Digit1' && shiftKey && !altKey && controlOrMeta(metaKey, ctrlKey) + ); +} + +export function isUppercase(event: KeyboardEvent): boolean { + const {code, shiftKey, altKey, metaKey, ctrlKey} = event; + return ( + code === 'Digit2' && shiftKey && !altKey && controlOrMeta(metaKey, ctrlKey) + ); +} + export function isStrikeThrough(event: KeyboardEvent): boolean { const {code, shiftKey, altKey, metaKey, ctrlKey} = event; return ( From a7891953e52ef4aa8f5d3598395d2b00e4dcf2b8 Mon Sep 17 00:00:00 2001 From: bedre7 Date: Fri, 29 Nov 2024 22:35:53 +0300 Subject: [PATCH 11/21] e2e test for new keyboard shortcuts --- .../__tests__/e2e/KeyboardShortcuts.spec.mjs | 10 ++++++++++ .../__tests__/keyboardShortcuts/index.mjs | 16 ++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/packages/lexical-playground/__tests__/e2e/KeyboardShortcuts.spec.mjs b/packages/lexical-playground/__tests__/e2e/KeyboardShortcuts.spec.mjs index 989a4bbbeb8..c3de65f586b 100644 --- a/packages/lexical-playground/__tests__/e2e/KeyboardShortcuts.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/KeyboardShortcuts.spec.mjs @@ -26,11 +26,13 @@ import { toggleChecklist, toggleInsertCodeBlock, toggleItalic, + toggleLowercase, toggleNumberedList, toggleStrikethrough, toggleSubscript, toggleSuperscript, toggleUnderline, + toggleUppercase, } from '../keyboardShortcuts/index.mjs'; import { assertHTML, @@ -112,6 +114,14 @@ const alignmentTestCases = [ ]; const additionalStylesTestCases = [ + { + applyShortcut: (page) => toggleLowercase(page), + style: 'Lowercase', + }, + { + applyShortcut: (page) => toggleUppercase(page), + style: 'Uppercase', + }, { applyShortcut: (page) => toggleStrikethrough(page), style: 'Strikethrough', diff --git a/packages/lexical-playground/__tests__/keyboardShortcuts/index.mjs b/packages/lexical-playground/__tests__/keyboardShortcuts/index.mjs index 41893cd7900..c1592fb4c89 100644 --- a/packages/lexical-playground/__tests__/keyboardShortcuts/index.mjs +++ b/packages/lexical-playground/__tests__/keyboardShortcuts/index.mjs @@ -261,6 +261,22 @@ export async function toggleInsertCodeBlock(page) { await page.keyboard.up('Shift'); } +export async function toggleLowercase(page) { + await keyDownCtrlOrMeta(page); + await page.keyboard.down('Shift'); + await page.keyboard.press('1'); + await keyUpCtrlOrMeta(page); + await page.keyboard.up('Shift'); +} + +export async function toggleUppercase(page) { + await keyDownCtrlOrMeta(page); + await page.keyboard.down('Shift'); + await page.keyboard.press('2'); + await keyUpCtrlOrMeta(page); + await page.keyboard.up('Shift'); +} + export async function toggleStrikethrough(page) { await keyDownCtrlOrMeta(page); await page.keyboard.down('Shift'); From 73e0a0dce54f7b0165d8a148a382a747dff994b2 Mon Sep 17 00:00:00 2001 From: bedre7 Date: Fri, 29 Nov 2024 22:40:51 +0300 Subject: [PATCH 12/21] add missing type to flow --- packages/lexical/flow/Lexical.js.flow | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/lexical/flow/Lexical.js.flow b/packages/lexical/flow/Lexical.js.flow index 7fa4dad6086..06069be16fb 100644 --- a/packages/lexical/flow/Lexical.js.flow +++ b/packages/lexical/flow/Lexical.js.flow @@ -245,6 +245,8 @@ type TextNodeThemeClasses = { code?: EditorThemeClassName, subscript?: EditorThemeClassName, superscript?: EditorThemeClassName, + uppercase?: EditorThemeClassName, + lowercase?: EditorThemeClassName, }; export type EditorThemeClasses = { characterLimit?: EditorThemeClassName, From 6dcb401ece4ed8a17f8643d48499619ef6e41f76 Mon Sep 17 00:00:00 2001 From: bedre7 Date: Mon, 2 Dec 2024 21:13:44 +0300 Subject: [PATCH 13/21] e2e test for new formatting options --- .../__tests__/e2e/TextFormatting.spec.mjs | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/packages/lexical-playground/__tests__/e2e/TextFormatting.spec.mjs b/packages/lexical-playground/__tests__/e2e/TextFormatting.spec.mjs index 528cec14d12..103d498d90d 100644 --- a/packages/lexical-playground/__tests__/e2e/TextFormatting.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/TextFormatting.spec.mjs @@ -14,7 +14,9 @@ import { selectCharacters, toggleBold, toggleItalic, + toggleLowercase, toggleUnderline, + toggleUppercase, } from '../keyboardShortcuts/index.mjs'; import { assertHTML, @@ -428,6 +430,138 @@ test.describe.parallel('TextFormatting', () => { }); }); + const capitalizationFormats = [ + { + applyCapitalization: toggleLowercase, + className: 'PlaygroundEditorTheme__textLowercase', + format: 'lowercase', + }, + { + applyCapitalization: toggleUppercase, + className: 'PlaygroundEditorTheme__textUppercase', + format: 'uppercase', + }, + ]; + + capitalizationFormats.forEach(({className, format, applyCapitalization}) => { + test(`Can select text and change it to ${format}`, async ({ + page, + isPlainText, + }) => { + test.skip(isPlainText); + + await focusEditor(page); + await page.keyboard.type('Hello world!'); + await moveLeft(page); + await selectCharacters(page, 'left', 5); + + await assertSelection(page, { + anchorOffset: 11, + anchorPath: [0, 0, 0], + focusOffset: 6, + focusPath: [0, 0, 0], + }); + + await applyCapitalization(page); + await assertHTML( + page, + html` +

+ Hello + world + ! +

+ `, + ); + + await assertSelection(page, { + anchorOffset: 5, + anchorPath: [0, 1, 0], + focusOffset: 0, + focusPath: [0, 1, 0], + }); + }); + }); + + const capitalizationResettingTestCases = [ + { + expectedFinalHTML: html` +

+ Hello + world! +

+ `, + key: 'Space', + }, + { + expectedFinalHTML: html` +

+ Hello + + world! +

+ `, + key: 'Tab', + }, + { + expectedFinalHTML: html` +

+ Hello +

+

+ world! +

+ `, + key: 'Enter', + }, + ]; + + capitalizationFormats.forEach(({format, className, applyCapitalization}) => { + capitalizationResettingTestCases.forEach(({key, expectedFinalHTML}) => { + test(`Pressing ${key} resets ${format} format`, async ({ + page, + isPlainText, + }) => { + test.skip(isPlainText); + + await focusEditor(page); + + await applyCapitalization(page); + await page.keyboard.type('Hello'); + + await assertHTML( + page, + html` +

+ Hello +

+ `, + ); + + // Pressing the key should reset the format + await page.keyboard.press(key); + await page.keyboard.type(' world!'); + + await assertHTML( + page, + expectedFinalHTML.replace('$formatClassName', className), + ); + }); + }); + }); + test(`Can select text and increase the font-size`, async ({ page, isPlainText, From 90dd330f6e17f5e251cfda98ea093da4f7256ec5 Mon Sep 17 00:00:00 2001 From: bedre7 Date: Mon, 2 Dec 2024 21:30:56 +0300 Subject: [PATCH 14/21] refactor --- .../src/plugins/ShortcutsPlugin/shortcuts.ts | 10 +++++++-- packages/lexical-rich-text/src/index.ts | 22 +++++++++---------- .../__tests__/unit/LexicalTextNode.test.tsx | 8 ++++--- 3 files changed, 24 insertions(+), 16 deletions(-) diff --git a/packages/lexical-playground/src/plugins/ShortcutsPlugin/shortcuts.ts b/packages/lexical-playground/src/plugins/ShortcutsPlugin/shortcuts.ts index 3a9caab6270..10131c8875f 100644 --- a/packages/lexical-playground/src/plugins/ShortcutsPlugin/shortcuts.ts +++ b/packages/lexical-playground/src/plugins/ShortcutsPlugin/shortcuts.ts @@ -122,14 +122,20 @@ export function isFormatQuote(event: KeyboardEvent): boolean { export function isLowercase(event: KeyboardEvent): boolean { const {code, shiftKey, altKey, metaKey, ctrlKey} = event; return ( - code === 'Digit1' && shiftKey && !altKey && controlOrMeta(metaKey, ctrlKey) + (code === 'Numpad1' || code === 'Digit1') && + shiftKey && + !altKey && + controlOrMeta(metaKey, ctrlKey) ); } export function isUppercase(event: KeyboardEvent): boolean { const {code, shiftKey, altKey, metaKey, ctrlKey} = event; return ( - code === 'Digit2' && shiftKey && !altKey && controlOrMeta(metaKey, ctrlKey) + (code === 'Numpad2' || code === 'Digit2') && + shiftKey && + !altKey && + controlOrMeta(metaKey, ctrlKey) ); } diff --git a/packages/lexical-rich-text/src/index.ts b/packages/lexical-rich-text/src/index.ts index 426666daafc..3e18bcef723 100644 --- a/packages/lexical-rich-text/src/index.ts +++ b/packages/lexical-rich-text/src/index.ts @@ -552,9 +552,9 @@ function $isSelectionAtEndOfRoot(selection: RangeSelection) { } function $resetCapitalization(selection: RangeSelection): void { - for (const type of ['lowercase', 'uppercase'] as const) { - if (selection.hasFormat(type)) { - selection.toggleFormat(type); + for (const format of ['lowercase', 'uppercase'] as const) { + if (selection.hasFormat(format)) { + selection.toggleFormat(format); } } } @@ -919,7 +919,9 @@ export function registerRichText(editor: LexicalEditor): () => void { if (!$isRangeSelection(selection)) { return false; } + $resetCapitalization(selection); + if (event !== null) { // If we have beforeinput, then we can avoid blocking // the default behavior. This ensures that the iOS can @@ -1089,11 +1091,10 @@ export function registerRichText(editor: LexicalEditor): () => void { KEY_SPACE_COMMAND, (_) => { const selection = $getSelection(); - if (!$isRangeSelection(selection)) { - return false; - } - $resetCapitalization(selection); + if ($isRangeSelection(selection)) { + $resetCapitalization(selection); + } return false; }, @@ -1103,11 +1104,10 @@ export function registerRichText(editor: LexicalEditor): () => void { KEY_TAB_COMMAND, (_) => { const selection = $getSelection(); - if (!$isRangeSelection(selection)) { - return false; - } - $resetCapitalization(selection); + if ($isRangeSelection(selection)) { + $resetCapitalization(selection); + } return false; }, diff --git a/packages/lexical/src/nodes/__tests__/unit/LexicalTextNode.test.tsx b/packages/lexical/src/nodes/__tests__/unit/LexicalTextNode.test.tsx index db427e18afb..44c8e4729b6 100644 --- a/packages/lexical/src/nodes/__tests__/unit/LexicalTextNode.test.tsx +++ b/packages/lexical/src/nodes/__tests__/unit/LexicalTextNode.test.tsx @@ -277,17 +277,18 @@ describe('LexicalTextNode tests', () => { ['superscript', 'subscript'], ['lowercase', 'uppercase'], ['uppercase', 'lowercase'], - ])('setting %s clears %s', async (newFormat, previousFormat) => { + ])('setting %s clears %s', async (newFormat, otherFormat) => { await update(() => { const paragraphNode = $createParagraphNode(); const textNode = $createTextNode('Hello World'); paragraphNode.append(textNode); $getRoot().append(paragraphNode); - textNode.toggleFormat(previousFormat as TextFormatType); + textNode.toggleFormat(otherFormat as TextFormatType); textNode.toggleFormat(newFormat as TextFormatType); + expect(textNode.hasFormat(newFormat as TextFormatType)).toBe(true); - expect(textNode.hasFormat(previousFormat as TextFormatType)).toBe(false); + expect(textNode.hasFormat(otherFormat as TextFormatType)).toBe(false); }); }); @@ -305,6 +306,7 @@ describe('LexicalTextNode tests', () => { textNode.toggleFormat(formatToClear as TextFormatType); textNode.toggleFormat(formatToClear as TextFormatType); + expect(textNode.hasFormat(formatToClear as TextFormatType)).toBe(false); expect(textNode.hasFormat(otherFormat as TextFormatType)).toBe(false); }); From d258d0dd3d9fc81d35dfb21caaad0d4d9d4e69a7 Mon Sep 17 00:00:00 2001 From: bedre7 Date: Tue, 3 Dec 2024 19:31:44 +0300 Subject: [PATCH 15/21] 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, From 22faa2c818d69f4e8921abf318d36ee67a6ad3dd Mon Sep 17 00:00:00 2001 From: bedre7 Date: Thu, 5 Dec 2024 16:12:38 +0300 Subject: [PATCH 16/21] fix failing integrity test --- packages/lexical/src/LexicalEditor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lexical/src/LexicalEditor.ts b/packages/lexical/src/LexicalEditor.ts index 4798f4d8118..76de3af0278 100644 --- a/packages/lexical/src/LexicalEditor.ts +++ b/packages/lexical/src/LexicalEditor.ts @@ -71,7 +71,7 @@ export type TextNodeThemeClasses = { italic?: EditorThemeClassName; lowercase?: EditorThemeClassName; uppercase?: EditorThemeClassName; - capitalize: EditorThemeClassName; + capitalize?: EditorThemeClassName; strikethrough?: EditorThemeClassName; subscript?: EditorThemeClassName; superscript?: EditorThemeClassName; From 8469d942539b8dc243c76b5904f000b832bae302 Mon Sep 17 00:00:00 2001 From: Fadekemi Adebayo Date: Wed, 11 Dec 2024 22:02:58 +0000 Subject: [PATCH 17/21] test for capitalize format --- .../__tests__/e2e/KeyboardShortcuts.spec.mjs | 5 +++++ .../__tests__/e2e/TextFormatting.spec.mjs | 6 ++++++ .../__tests__/keyboardShortcuts/index.mjs | 8 ++++++++ 3 files changed, 19 insertions(+) diff --git a/packages/lexical-playground/__tests__/e2e/KeyboardShortcuts.spec.mjs b/packages/lexical-playground/__tests__/e2e/KeyboardShortcuts.spec.mjs index c3de65f586b..60997995ceb 100644 --- a/packages/lexical-playground/__tests__/e2e/KeyboardShortcuts.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/KeyboardShortcuts.spec.mjs @@ -23,6 +23,7 @@ import { selectCharacters, toggleBold, toggleBulletList, + toggleCapitalize, toggleChecklist, toggleInsertCodeBlock, toggleItalic, @@ -122,6 +123,10 @@ const additionalStylesTestCases = [ applyShortcut: (page) => toggleUppercase(page), style: 'Uppercase', }, + { + applyShortcut: (page) => toggleCapitalize(page), + style: 'Capitalize', + }, { applyShortcut: (page) => toggleStrikethrough(page), style: 'Strikethrough', diff --git a/packages/lexical-playground/__tests__/e2e/TextFormatting.spec.mjs b/packages/lexical-playground/__tests__/e2e/TextFormatting.spec.mjs index 103d498d90d..b684a3de5c3 100644 --- a/packages/lexical-playground/__tests__/e2e/TextFormatting.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/TextFormatting.spec.mjs @@ -13,6 +13,7 @@ import { moveToLineEnd, selectCharacters, toggleBold, + toggleCapitalize, toggleItalic, toggleLowercase, toggleUnderline, @@ -441,6 +442,11 @@ test.describe.parallel('TextFormatting', () => { className: 'PlaygroundEditorTheme__textUppercase', format: 'uppercase', }, + { + applyCapitalization: toggleCapitalize, + className: 'PlaygroundEditorTheme__textCapitalize', + format: 'capitalize', + }, ]; capitalizationFormats.forEach(({className, format, applyCapitalization}) => { diff --git a/packages/lexical-playground/__tests__/keyboardShortcuts/index.mjs b/packages/lexical-playground/__tests__/keyboardShortcuts/index.mjs index c1592fb4c89..e4bef9db645 100644 --- a/packages/lexical-playground/__tests__/keyboardShortcuts/index.mjs +++ b/packages/lexical-playground/__tests__/keyboardShortcuts/index.mjs @@ -277,6 +277,14 @@ export async function toggleUppercase(page) { await page.keyboard.up('Shift'); } +export async function toggleCapitalize(page) { + await keyDownCtrlOrMeta(page); + await page.keyboard.down('Shift'); + await page.keyboard.press('3'); + await keyUpCtrlOrMeta(page); + await page.keyboard.up('Shift'); +} + export async function toggleStrikethrough(page) { await keyDownCtrlOrMeta(page); await page.keyboard.down('Shift'); From 44f20785fff81b1f2525897b9debaa8c50b33e93 Mon Sep 17 00:00:00 2001 From: Fadekemi Adebayo Date: Thu, 12 Dec 2024 21:42:07 +0000 Subject: [PATCH 18/21] keyboard shortcut handler for capitalize --- .../lexical-playground/src/plugins/ShortcutsPlugin/index.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/lexical-playground/src/plugins/ShortcutsPlugin/index.tsx b/packages/lexical-playground/src/plugins/ShortcutsPlugin/index.tsx index 9b7540a3338..eff896fcafa 100644 --- a/packages/lexical-playground/src/plugins/ShortcutsPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/ShortcutsPlugin/index.tsx @@ -34,6 +34,7 @@ import { UpdateFontSizeType, } from '../ToolbarPlugin/utils'; import { + isCapitalize, isCenterAlign, isClearFormatting, isDecreaseFontSize, @@ -104,6 +105,9 @@ export default function ShortcutsPlugin({ } else if (isUppercase(event)) { event.preventDefault(); editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'uppercase'); + } else if (isCapitalize(event)) { + event.preventDefault(); + editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'capitalize'); } else if (isIndent(event)) { event.preventDefault(); editor.dispatchCommand(INDENT_CONTENT_COMMAND, undefined); From 16937b40fb0cadde394346d419f9c4b21cbeb6a8 Mon Sep 17 00:00:00 2001 From: Fadekemi Adebayo Date: Thu, 12 Dec 2024 22:04:38 +0000 Subject: [PATCH 19/21] add capitalization formats to floating toolbar --- .../FloatingTextFormatToolbarPlugin/index.tsx | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/packages/lexical-playground/src/plugins/FloatingTextFormatToolbarPlugin/index.tsx b/packages/lexical-playground/src/plugins/FloatingTextFormatToolbarPlugin/index.tsx index 2404f88dca9..8b3ec28a4d1 100644 --- a/packages/lexical-playground/src/plugins/FloatingTextFormatToolbarPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/FloatingTextFormatToolbarPlugin/index.tsx @@ -39,6 +39,9 @@ function TextFormatFloatingToolbar({ isBold, isItalic, isUnderline, + isUppercase, + isLowercase, + isCapitalize, isCode, isStrikethrough, isSubscript, @@ -51,6 +54,9 @@ function TextFormatFloatingToolbar({ isCode: boolean; isItalic: boolean; isLink: boolean; + isUppercase: boolean; + isLowercase: boolean; + isCapitalize: boolean; isStrikethrough: boolean; isSubscript: boolean; isSuperscript: boolean; @@ -214,6 +220,33 @@ function TextFormatFloatingToolbar({ aria-label="Format text to underlined"> + + + @@ -208,6 +209,7 @@ function TextFormatFloatingToolbar({ editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic'); }} className={'popup-item spaced ' + (isItalic ? 'active' : '')} + title="Italic" aria-label="Format text as italics"> @@ -217,64 +219,69 @@ function TextFormatFloatingToolbar({ editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'underline'); }} className={'popup-item spaced ' + (isUnderline ? 'active' : '')} + title="Underline" aria-label="Format text to underlined"> @@ -289,6 +297,7 @@ function TextFormatFloatingToolbar({ type="button" onClick={insertLink} className={'popup-item spaced ' + (isLink ? 'active' : '')} + title="Insert link" aria-label="Insert link"> @@ -298,6 +307,7 @@ function TextFormatFloatingToolbar({ type="button" onClick={insertComment} className={'popup-item spaced insert-comment'} + title="Insert comment" aria-label="Insert comment"> diff --git a/packages/lexical-rich-text/src/index.ts b/packages/lexical-rich-text/src/index.ts index 7102bd6eeeb..e91fe4073fb 100644 --- a/packages/lexical-rich-text/src/index.ts +++ b/packages/lexical-rich-text/src/index.ts @@ -551,6 +551,11 @@ function $isSelectionAtEndOfRoot(selection: RangeSelection) { return focus.key === 'root' && focus.offset === $getRoot().getChildrenSize(); } +/** + * Resets the capitalization of the selection to default. + * Called when the user presses space, tab, or enter key. + * @param selection The selection to reset the capitalization of. + */ function $resetCapitalization(selection: RangeSelection): void { for (const format of ['lowercase', 'uppercase', 'capitalize'] as const) { if (selection.hasFormat(format)) { diff --git a/packages/lexical/src/nodes/__tests__/unit/LexicalTextNode.test.tsx b/packages/lexical/src/nodes/__tests__/unit/LexicalTextNode.test.tsx index bdf90b63972..358d2b657dd 100644 --- a/packages/lexical/src/nodes/__tests__/unit/LexicalTextNode.test.tsx +++ b/packages/lexical/src/nodes/__tests__/unit/LexicalTextNode.test.tsx @@ -301,6 +301,32 @@ describe('LexicalTextNode tests', () => { }); }); + test('clearing subscript does not set superscript', async () => { + await update(() => { + const paragraphNode = $createParagraphNode(); + const textNode = $createTextNode('Hello World'); + paragraphNode.append(textNode); + $getRoot().append(paragraphNode); + textNode.toggleFormat('subscript'); + textNode.toggleFormat('subscript'); + expect(textNode.hasFormat('subscript')).toBe(false); + expect(textNode.hasFormat('superscript')).toBe(false); + }); + }); + + test('clearing superscript does not set subscript', async () => { + await update(() => { + const paragraphNode = $createParagraphNode(); + const textNode = $createTextNode('Hello World'); + paragraphNode.append(textNode); + $getRoot().append(paragraphNode); + textNode.toggleFormat('superscript'); + textNode.toggleFormat('superscript'); + expect(textNode.hasFormat('superscript')).toBe(false); + expect(textNode.hasFormat('subscript')).toBe(false); + }); + }); + test('capitalization formats are mutually exclusive', async () => { const capitalizationFormats: TextFormatType[] = [ 'lowercase', @@ -314,13 +340,16 @@ describe('LexicalTextNode tests', () => { paragraphNode.append(textNode); $getRoot().append(paragraphNode); + // Set each format and ensure that the other formats are cleared 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); }); }); From 39132d6effbf25b3c8f49685c938840b14f1f311 Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Sat, 14 Dec 2024 10:20:40 -0800 Subject: [PATCH 21/21] Update packages/lexical/flow/Lexical.js.flow --- packages/lexical/flow/Lexical.js.flow | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/lexical/flow/Lexical.js.flow b/packages/lexical/flow/Lexical.js.flow index cb5d33ef8b7..b050defcd3c 100644 --- a/packages/lexical/flow/Lexical.js.flow +++ b/packages/lexical/flow/Lexical.js.flow @@ -593,7 +593,8 @@ export type TextFormatType = | 'subscript' | 'superscript' | 'lowercase' - | 'uppercase'; + | 'uppercase' + | 'capitalize'; type TextModeType = 'normal' | 'token' | 'segmented';