1
0
mirror of https://github.com/BookStackApp/BookStack.git synced 2025-07-30 04:23:11 +03:00

Lexical: Updated toolbar & text node exporting

- Updated toolbar to match existing editor, including dynamic RTL/LTR
  controls.
- Updated text node handling to not include spans and extra classes when
  not needed. Added & update tests to cover.
This commit is contained in:
Dan Brown
2024-09-23 17:36:16 +01:00
parent 8b32e6c15a
commit a62d8381be
16 changed files with 152 additions and 51 deletions

View File

@ -624,7 +624,28 @@ export class TextNode extends LexicalNode {
element !== null && isHTMLElement(element),
'Expected TextNode createDOM to always return a HTMLElement',
);
element.style.whiteSpace = 'pre-wrap';
// Wrap up to retain space if head/tail whitespace exists
const text = this.getTextContent();
if (/^\s|\s$/.test(text)) {
element.style.whiteSpace = 'pre-wrap';
}
// Strip editor theme classes
for (const className of Array.from(element.classList.values())) {
if (className.startsWith('editor-theme-')) {
element.classList.remove(className);
}
}
if (element.classList.length === 0) {
element.removeAttribute('class');
}
// Remove placeholder tag if redundant
if (element.nodeName === 'SPAN' && !element.getAttribute('style')) {
element = document.createTextNode(text);
}
// This is the only way to properly add support for most clients,
// even if it's semantically incorrect to have to resort to using
// <b>, <u>, <s>, <i> elements.
@ -632,7 +653,7 @@ export class TextNode extends LexicalNode {
element = wrapElementWith(element, 'b');
}
if (this.hasFormat('italic')) {
element = wrapElementWith(element, 'i');
element = wrapElementWith(element, 'em');
}
if (this.hasFormat('strikethrough')) {
element = wrapElementWith(element, 's');
@ -1329,6 +1350,10 @@ function applyTextFormatFromStyle(
// Google Docs uses span tags + vertical-align to specify subscript and superscript
const verticalAlign = style.verticalAlign;
// Styles to copy to node
const color = style.color;
const backgroundColor = style.backgroundColor;
return (lexicalNode: LexicalNode) => {
if (!$isTextNode(lexicalNode)) {
return lexicalNode;
@ -1355,6 +1380,18 @@ function applyTextFormatFromStyle(
lexicalNode.toggleFormat('superscript');
}
// Apply styles
let style = lexicalNode.getStyle();
if (color) {
style += `color: ${color};`;
}
if (backgroundColor && backgroundColor !== 'transparent') {
style += `background-color: ${backgroundColor};`;
}
if (style) {
lexicalNode.setStyle(style);
}
if (shouldApply && !lexicalNode.hasFormat(shouldApply)) {
lexicalNode.toggleFormat(shouldApply);
}

View File

@ -107,7 +107,7 @@ describe('LexicalTabNode tests', () => {
$insertDataTransferForRichText(dataTransfer, selection, editor);
});
expect(testEnv.innerHTML).toBe(
'<p><span data-lexical-text="true">Hello</span><span data-lexical-text="true">\t</span><span data-lexical-text="true">world</span></p><p><span data-lexical-text="true">Hello</span><span data-lexical-text="true">\t</span><span data-lexical-text="true">world</span></p>',
'<p><span style="color: rgb(0, 0, 0);" data-lexical-text="true">Hello</span><span data-lexical-text="true">\t</span><span style="color: rgb(0, 0, 0);" data-lexical-text="true">world</span></p><p><span style="color: rgb(0, 0, 0);" data-lexical-text="true">Hello</span><span data-lexical-text="true">\t</span><span style="color: rgb(0, 0, 0);" data-lexical-text="true">world</span></p>',
);
});

View File

@ -8,7 +8,7 @@
import {
$createParagraphNode,
$createTextNode,
$createTextNode, $getEditor,
$getNodeByKey,
$getRoot,
$getSelection,
@ -41,6 +41,9 @@ import {
$setCompositionKey,
getEditorStateTextContent,
} from '../../../LexicalUtils';
import {Text} from "@codemirror/state";
import {$generateHtmlFromNodes} from "@lexical/html";
import {formatBold} from "@lexical/selection/__tests__/utils";
const editorConfig = Object.freeze({
namespace: '',
@ -792,6 +795,58 @@ describe('LexicalTextNode tests', () => {
);
});
describe('exportDOM()', () => {
test('simple text exports as a text node', async () => {
await update(() => {
const paragraph = $getRoot().getFirstChild<ElementNode>()!;
const textNode = $createTextNode('hello');
paragraph.append(textNode);
const html = $generateHtmlFromNodes($getEditor(), null);
expect(html).toBe('<p>hello</p>');
});
});
test('simple text wrapped in span if leading or ending spacing', async () => {
const textByExpectedHtml = {
'hello ': '<p><span style="white-space: pre-wrap;">hello </span></p>',
' hello': '<p><span style="white-space: pre-wrap;"> hello</span></p>',
' hello ': '<p><span style="white-space: pre-wrap;"> hello </span></p>',
}
await update(() => {
const paragraph = $getRoot().getFirstChild<ElementNode>()!;
for (const [text, expectedHtml] of Object.entries(textByExpectedHtml)) {
paragraph.getChildren().forEach(c => c.remove(true));
const textNode = $createTextNode(text);
paragraph.append(textNode);
const html = $generateHtmlFromNodes($getEditor(), null);
expect(html).toBe(expectedHtml);
}
});
});
test('text with formats exports using format elements instead of classes', async () => {
await update(() => {
const paragraph = $getRoot().getFirstChild<ElementNode>()!;
const textNode = $createTextNode('hello');
textNode.toggleFormat('bold');
textNode.toggleFormat('subscript');
textNode.toggleFormat('italic');
textNode.toggleFormat('underline');
textNode.toggleFormat('code');
paragraph.append(textNode);
const html = $generateHtmlFromNodes($getEditor(), null);
expect(html).toBe('<p><u><em><b><code spellcheck="false"><strong>hello</strong></code></b></em></u></p>');
});
});
});
test('mergeWithSibling', async () => {
await update(() => {
const paragraph = $getRoot().getFirstChild<ElementNode>()!;