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">
+
+
+
`,
diff --git a/packages/lexical-playground/src/plugins/FloatingTextFormatToolbarPlugin/index.tsx b/packages/lexical-playground/src/plugins/FloatingTextFormatToolbarPlugin/index.tsx
index 8b3ec28a4d1..0cb2730b632 100644
--- a/packages/lexical-playground/src/plugins/FloatingTextFormatToolbarPlugin/index.tsx
+++ b/packages/lexical-playground/src/plugins/FloatingTextFormatToolbarPlugin/index.tsx
@@ -199,6 +199,7 @@ function TextFormatFloatingToolbar({
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold');
}}
className={'popup-item spaced ' + (isBold ? 'active' : '')}
+ title="Bold"
aria-label="Format text as bold">
@@ -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';