-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[lexical-playground] Feature: Highlight special strings with format (#…
- Loading branch information
Showing
11 changed files
with
296 additions
and
0 deletions.
There are no files selected for viewing
96 changes: 96 additions & 0 deletions
96
packages/lexical-playground/__tests__/e2e/SpecialTexts.spec.mjs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
/** | ||
* 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 { | ||
assertHTML, | ||
focusEditor, | ||
html, | ||
initialize, | ||
test, | ||
waitForSelector, | ||
} from '../utils/index.mjs'; | ||
|
||
test.describe('Special Text', () => { | ||
test.use({shouldAllowHighlightingWithBrackets: true}); | ||
test.beforeEach(({isCollab, page, shouldAllowHighlightingWithBrackets}) => | ||
initialize({ | ||
isCollab, | ||
page, | ||
shouldAllowHighlightingWithBrackets, | ||
}), | ||
); | ||
test('should handle a single special text', async ({page, isCollab}) => { | ||
await focusEditor(page); | ||
await page.keyboard.type('[MLH Fellowship]'); | ||
await waitForSelector(page, '.PlaygroundEditorTheme__specialText'); | ||
|
||
await assertHTML( | ||
page, | ||
html` | ||
<p | ||
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr" | ||
dir="ltr"> | ||
<span | ||
class="PlaygroundEditorTheme__specialText" | ||
data-lexical-text="true"> | ||
MLH Fellowship | ||
</span> | ||
</p> | ||
`, | ||
); | ||
}); | ||
test('should handle multiple special texts', async ({page, isCollab}) => { | ||
await focusEditor(page); | ||
await page.keyboard.type('[MLH Fellowship] [MLH Fellowship]'); | ||
await waitForSelector(page, '.PlaygroundEditorTheme__specialText'); | ||
await assertHTML( | ||
page, | ||
html` | ||
<p | ||
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr" | ||
dir="ltr"> | ||
<span | ||
class="PlaygroundEditorTheme__specialText" | ||
data-lexical-text="true"> | ||
MLH Fellowship | ||
</span> | ||
<span data-lexical-text="true"></span> | ||
<span | ||
class="PlaygroundEditorTheme__specialText" | ||
data-lexical-text="true"> | ||
MLH Fellowship | ||
</span> | ||
</p> | ||
`, | ||
); | ||
}); | ||
|
||
test('should not work when the option to use brackets for highlighting is disabled', async ({ | ||
page, | ||
isCollab, | ||
shouldAllowHighlightingWithBrackets, | ||
}) => { | ||
await initialize({ | ||
isCollab, | ||
page, | ||
shouldAllowHighlightingWithBrackets: false, | ||
}); | ||
await focusEditor(page); | ||
await page.keyboard.type('[MLH Fellowship]'); | ||
await assertHTML( | ||
page, | ||
html` | ||
<p | ||
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr" | ||
dir="ltr"> | ||
<span data-lexical-text="true">[MLH Fellowship]</span> | ||
</p> | ||
`, | ||
); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
/** | ||
* 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, | ||
} from 'lexical'; | ||
|
||
import {addClassNamesToElement} from '@lexical/utils'; | ||
import {$applyNodeReplacement, TextNode} from 'lexical'; | ||
|
||
/** @noInheritDoc */ | ||
export class SpecialTextNode extends TextNode { | ||
static getType(): string { | ||
return 'specialText'; | ||
} | ||
|
||
static clone(node: SpecialTextNode): SpecialTextNode { | ||
return new SpecialTextNode(node.__text, node.__key); | ||
} | ||
|
||
constructor(text: string, key?: NodeKey) { | ||
super(text, key); | ||
} | ||
|
||
createDOM(config: EditorConfig): HTMLElement { | ||
const dom = document.createElement('span'); | ||
addClassNamesToElement(dom, config.theme.specialText); | ||
dom.textContent = this.getTextContent(); | ||
return dom; | ||
} | ||
|
||
updateDOM( | ||
prevNode: TextNode, | ||
dom: HTMLElement, | ||
config: EditorConfig, | ||
): boolean { | ||
if (prevNode.__text.startsWith('[') && prevNode.__text.endsWith(']')) { | ||
const strippedText = this.__text.substring(1, this.__text.length - 1); // Strip brackets again | ||
dom.textContent = strippedText; // Update the text content | ||
} | ||
|
||
addClassNamesToElement(dom, config.theme.specialText); | ||
|
||
return false; | ||
} | ||
|
||
static importJSON(serializedNode: SerializedTextNode): SpecialTextNode { | ||
const node = $createSpecialTextNode(serializedNode.text); | ||
node.setFormat(serializedNode.format); | ||
node.setStyle(serializedNode.style); | ||
node.setDetail(serializedNode.detail); | ||
node.setMode(serializedNode.mode); | ||
return node; | ||
} | ||
|
||
exportJSON(): SerializedTextNode { | ||
return { | ||
...super.exportJSON(), | ||
type: 'specialText', | ||
}; | ||
} | ||
|
||
isTextEntity(): true { | ||
return true; | ||
} | ||
canInsertTextAfter(): boolean { | ||
return false; // Prevents appending text to this node | ||
} | ||
} | ||
|
||
/** | ||
* Creates a SpecialTextNode with the given text. | ||
* @param text - Text content for the SpecialTextNode. | ||
* @returns A new SpecialTextNode instance. | ||
*/ | ||
export function $createSpecialTextNode(text = ''): SpecialTextNode { | ||
return $applyNodeReplacement(new SpecialTextNode(text)); | ||
} | ||
|
||
/** | ||
* Checks if a node is a SpecialTextNode. | ||
* @param node - Node to check. | ||
* @returns True if the node is a SpecialTextNode. | ||
*/ | ||
export function $isSpecialTextNode( | ||
node: LexicalNode | null | undefined, | ||
): node is SpecialTextNode { | ||
return node instanceof SpecialTextNode; | ||
} |
72 changes: 72 additions & 0 deletions
72
packages/lexical-playground/src/plugins/SpecialTextPlugin/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
/** | ||
* 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 {LexicalEditor} from 'lexical'; | ||
|
||
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; | ||
import {TextNode} from 'lexical'; | ||
import {useEffect} from 'react'; | ||
|
||
import { | ||
$createSpecialTextNode, | ||
SpecialTextNode, | ||
} from '../../nodes/SpecialTextNode'; | ||
|
||
const BRACKETED_TEXT_REGEX = /\[([^\[\]]+)\]/; // eslint-disable-line | ||
|
||
function $findAndTransformText(node: TextNode): null | TextNode { | ||
const text = node.getTextContent(); | ||
|
||
const match = BRACKETED_TEXT_REGEX.exec(text); | ||
if (match) { | ||
const matchedText = match[1]; | ||
const startIndex = match.index; | ||
|
||
let targetNode; | ||
if (startIndex === 0) { | ||
[targetNode] = node.splitText(startIndex + match[0].length); | ||
} else { | ||
[, targetNode] = node.splitText(startIndex, startIndex + match[0].length); | ||
} | ||
|
||
const specialTextNode = $createSpecialTextNode(matchedText); | ||
targetNode.replace(specialTextNode); | ||
return specialTextNode; | ||
} | ||
|
||
return null; | ||
} | ||
|
||
function $textNodeTransform(node: TextNode): void { | ||
let targetNode: TextNode | null = node; | ||
|
||
while (targetNode !== null) { | ||
if (!targetNode.isSimpleText()) { | ||
return; | ||
} | ||
|
||
targetNode = $findAndTransformText(targetNode); | ||
} | ||
} | ||
|
||
function useTextTransformation(editor: LexicalEditor): void { | ||
useEffect(() => { | ||
if (!editor.hasNodes([SpecialTextNode])) { | ||
throw new Error( | ||
'SpecialTextPlugin: SpecialTextNode not registered on editor', | ||
); | ||
} | ||
|
||
return editor.registerNodeTransform(TextNode, $textNodeTransform); | ||
}, [editor]); | ||
} | ||
|
||
export default function SpecialTextPlugin(): JSX.Element | null { | ||
const [editor] = useLexicalComposerContext(); | ||
useTextTransformation(editor); | ||
return null; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters