From 57068e5496703e921dc8b5d8155ec8a7291fbcd9 Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Thu, 22 Feb 2024 10:59:14 -0800 Subject: [PATCH] fix: unify class name splitting/filtering with normalizeClassNames --- .flowconfig | 1 + jest.config.js | 2 ++ .../lexical-list/src/LexicalListItemNode.ts | 6 ++--- packages/lexical-list/src/LexicalListNode.ts | 6 ++--- .../unit/LexicalElementHelpers.test.ts | 23 ++++++++++++++++++- packages/lexical-utils/src/index.ts | 20 ++++++++-------- packages/lexical/src/LexicalReconciler.ts | 5 ++-- packages/lexical/src/LexicalUtils.ts | 5 ++-- packages/shared/src/normalizeClassNames.ts | 21 +++++++++++++++++ tsconfig.json | 3 +++ 10 files changed, 70 insertions(+), 22 deletions(-) create mode 100644 packages/shared/src/normalizeClassNames.ts diff --git a/.flowconfig b/.flowconfig index 0675ce902b5..0cb79250d82 100644 --- a/.flowconfig +++ b/.flowconfig @@ -100,6 +100,7 @@ module.name_mapper='^@lexical/yjs$' -> '/packages/lexical-yjs/flow module.name_mapper='^shared/simpleDiffWithCursor' -> '/packages/shared/src/simpleDiffWithCursor.js' module.name_mapper='^shared/invariant' -> '/packages/shared/src/invariant.js' +module.name_mapper='^shared/normalizeClassNames' -> '/packages/shared/src/normalizeClassNames.js' module.name_mapper='^shared/warnOnlyOnce' -> '/packages/shared/src/warnOnlyOnce.js' module.name_mapper='^shared/environment' -> '/packages/shared/src/environment.js' module.name_mapper='^shared/useLayoutEffect' -> '/packages/shared/src/useLayoutEffect.js' diff --git a/jest.config.js b/jest.config.js index 523e92dca91..bd1a7644f77 100644 --- a/jest.config.js +++ b/jest.config.js @@ -95,6 +95,8 @@ module.exports = { '/packages/shared/src/caretFromPoint.ts', '^shared/environment$': '/packages/shared/src/environment.ts', '^shared/invariant$': '/packages/shared/src/invariant.ts', + '^shared/normalizeClassNames$': + '/packages/shared/src/normalizeClassNames.ts', '^shared/simpleDiffWithCursor$': '/packages/shared/src/simpleDiffWithCursor.ts', '^shared/useLayoutEffect$': diff --git a/packages/lexical-list/src/LexicalListItemNode.ts b/packages/lexical-list/src/LexicalListItemNode.ts index b6e39d5dadf..97d22cbfcfb 100644 --- a/packages/lexical-list/src/LexicalListItemNode.ts +++ b/packages/lexical-list/src/LexicalListItemNode.ts @@ -37,6 +37,7 @@ import { LexicalEditor, } from 'lexical'; import invariant from 'shared/invariant'; +import normalizeClassNames from 'shared/normalizeClassNames'; import {$createListNode, $isListNode} from './'; import {$handleIndent, $handleOutdent, mergeLists} from './formatList'; @@ -434,8 +435,7 @@ function $setListItemThemeClassNames( } if (listItemClassName !== undefined) { - const listItemClasses = listItemClassName.split(' '); - classesToAdd.push(...listItemClasses); + classesToAdd.push(...normalizeClassNames(listItemClassName)); } if (listTheme) { @@ -460,7 +460,7 @@ function $setListItemThemeClassNames( } if (nestedListItemClassName !== undefined) { - const nestedListItemClasses = nestedListItemClassName.split(' '); + const nestedListItemClasses = normalizeClassNames(nestedListItemClassName); if (node.getChildren().some((child) => $isListNode(child))) { classesToAdd.push(...nestedListItemClasses); diff --git a/packages/lexical-list/src/LexicalListNode.ts b/packages/lexical-list/src/LexicalListNode.ts index 1a0f6e02281..4fdc52d22b6 100644 --- a/packages/lexical-list/src/LexicalListNode.ts +++ b/packages/lexical-list/src/LexicalListNode.ts @@ -28,6 +28,7 @@ import { Spread, } from 'lexical'; import invariant from 'shared/invariant'; +import normalizeClassNames from 'shared/normalizeClassNames'; import {$createListItemNode, $isListItemNode, ListItemNode} from '.'; import {updateChildrenListItemValue} from './formatList'; @@ -243,8 +244,7 @@ function setListThemeClassNames( } if (listLevelClassName !== undefined) { - const listItemClasses = listLevelClassName.split(' '); - classesToAdd.push(...listItemClasses); + classesToAdd.push(...normalizeClassNames(listLevelClassName)); for (let i = 0; i < listLevelsClassNames.length; i++) { if (i !== normalizedListDepth) { classesToRemove.push(node.__tag + i); @@ -253,7 +253,7 @@ function setListThemeClassNames( } if (nestedListClassName !== undefined) { - const nestedListItemClasses = nestedListClassName.split(' '); + const nestedListItemClasses = normalizeClassNames(nestedListClassName); if (listDepth > 1) { classesToAdd.push(...nestedListItemClasses); diff --git a/packages/lexical-utils/src/__tests__/unit/LexicalElementHelpers.test.ts b/packages/lexical-utils/src/__tests__/unit/LexicalElementHelpers.test.ts index 4ae83fc6c77..0bca8a9ea64 100644 --- a/packages/lexical-utils/src/__tests__/unit/LexicalElementHelpers.test.ts +++ b/packages/lexical-utils/src/__tests__/unit/LexicalElementHelpers.test.ts @@ -26,7 +26,16 @@ describe('LexicalElementHelpers tests', () => { test('empty', async () => { const element = document.createElement('div'); - addClassNamesToElement(element, null, undefined, false, true, ''); + addClassNamesToElement( + element, + null, + undefined, + false, + true, + '', + ' ', + ' \t\n', + ); expect(element.className).toEqual(''); }); @@ -53,4 +62,16 @@ describe('LexicalElementHelpers tests', () => { expect(element.className).toEqual(''); }); }); + + test('multiple spaces', async () => { + const classNames = ' a b c \t\n '; + const element = document.createElement('div'); + addClassNamesToElement(element, classNames); + + expect(element.className).toEqual('a b c'); + + removeClassNamesFromElement(element, classNames); + + expect(element.className).toEqual(''); + }); }); diff --git a/packages/lexical-utils/src/index.ts b/packages/lexical-utils/src/index.ts index 71aac0589a2..f597a40056f 100644 --- a/packages/lexical-utils/src/index.ts +++ b/packages/lexical-utils/src/index.ts @@ -26,6 +26,7 @@ import { LexicalNode, } from 'lexical'; import invariant from 'shared/invariant'; +import normalizeClassNames from 'shared/normalizeClassNames'; export {default as markSelection} from './markSelection'; export {default as mergeRegister} from './mergeRegister'; @@ -49,12 +50,10 @@ export function addClassNamesToElement( element: HTMLElement, ...classNames: Array ): void { - classNames.forEach((className) => { - if (typeof className === 'string') { - const classesToAdd = className.split(' ').filter((n) => n !== ''); - element.classList.add(...classesToAdd); - } - }); + const classesToAdd = normalizeClassNames(...classNames); + if (classesToAdd.length > 0) { + element.classList.add(...classesToAdd); + } } /** @@ -69,11 +68,10 @@ export function removeClassNamesFromElement( element: HTMLElement, ...classNames: Array ): void { - classNames.forEach((className) => { - if (typeof className === 'string') { - element.classList.remove(...className.split(' ')); - } - }); + const classesToRemove = normalizeClassNames(...classNames); + if (classesToRemove.length > 0) { + element.classList.remove(...classesToRemove); + } } /** diff --git a/packages/lexical/src/LexicalReconciler.ts b/packages/lexical/src/LexicalReconciler.ts index 175da8fc53d..5359298a408 100644 --- a/packages/lexical/src/LexicalReconciler.ts +++ b/packages/lexical/src/LexicalReconciler.ts @@ -17,6 +17,7 @@ import type {NodeKey, NodeMap} from './LexicalNode'; import type {ElementNode} from './nodes/LexicalElementNode'; import invariant from 'shared/invariant'; +import normalizeClassNames from 'shared/normalizeClassNames'; import { $isDecoratorNode, @@ -368,7 +369,7 @@ function reconcileBlockDirection(element: ElementNode, dom: HTMLElement): void { // Remove the old theme classes if they exist if (previousDirectionTheme !== undefined) { if (typeof previousDirectionTheme === 'string') { - const classNamesArr = previousDirectionTheme.split(' '); + const classNamesArr = normalizeClassNames(previousDirectionTheme); previousDirectionTheme = theme[previousDirection] = classNamesArr; } @@ -386,7 +387,7 @@ function reconcileBlockDirection(element: ElementNode, dom: HTMLElement): void { // Apply the new theme classes if they exist if (nextDirectionTheme !== undefined) { if (typeof nextDirectionTheme === 'string') { - const classNamesArr = nextDirectionTheme.split(' '); + const classNamesArr = normalizeClassNames(nextDirectionTheme); // @ts-expect-error: intentional nextDirectionTheme = theme[direction] = classNamesArr; } diff --git a/packages/lexical/src/LexicalUtils.ts b/packages/lexical/src/LexicalUtils.ts index b653d0e0caf..ff3eff86643 100644 --- a/packages/lexical/src/LexicalUtils.ts +++ b/packages/lexical/src/LexicalUtils.ts @@ -32,6 +32,7 @@ import type {TextFormatType, TextNode} from './nodes/LexicalTextNode'; import {CAN_USE_DOM} from 'shared/canUseDOM'; import {IS_APPLE, IS_APPLE_WEBKIT, IS_IOS, IS_SAFARI} from 'shared/environment'; import invariant from 'shared/invariant'; +import normalizeClassNames from 'shared/normalizeClassNames'; import { $createTextNode, @@ -1035,7 +1036,7 @@ export function getCachedClassNameArray( // className tokens to an array that can be // applied to classList.add()/remove(). if (typeof classNames === 'string') { - const classNamesArr = classNames.split(' '); + const classNamesArr = normalizeClassNames(classNames); classNamesCache[classNameThemeType] = classNamesArr; return classNamesArr; } @@ -1404,7 +1405,7 @@ function createBlockCursorElement(editorConfig: EditorConfig): HTMLDivElement { let blockCursorTheme = theme.blockCursor; if (blockCursorTheme !== undefined) { if (typeof blockCursorTheme === 'string') { - const classNamesArr = blockCursorTheme.split(' '); + const classNamesArr = normalizeClassNames(blockCursorTheme); // @ts-expect-error: intentional blockCursorTheme = theme.blockCursor = classNamesArr; } diff --git a/packages/shared/src/normalizeClassNames.ts b/packages/shared/src/normalizeClassNames.ts new file mode 100644 index 00000000000..22ea3a940bf --- /dev/null +++ b/packages/shared/src/normalizeClassNames.ts @@ -0,0 +1,21 @@ +/** + * 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. + * + */ + +export default function normalizeClassNames( + ...classNames: Array +): Array { + const rval = []; + for (const className of classNames) { + if (className && typeof className === 'string') { + for (const [s] of className.matchAll(/\S+/g)) { + rval.push(s); + } + } + } + return rval; +} diff --git a/tsconfig.json b/tsconfig.json index d12fa850af3..ac108156659 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -186,6 +186,9 @@ "packages/shared/src/simpleDiffWithCursor.ts" ], "shared/invariant": ["./packages/shared/src/invariant.ts"], + "shared/normalizeClassNames": [ + "./packages/shared/src/normalizeClassNames.ts" + ], "shared/warnOnlyOnce": ["./packages/shared/src/warnOnlyOnce.ts"], "shared/environment": ["./packages/shared/src/environment.ts"], "shared/useLayoutEffect": ["./packages/shared/src/useLayoutEffect.ts"]