Skip to content

Commit

Permalink
fix indent export and import
Browse files Browse the repository at this point in the history
  • Loading branch information
GermanJablo committed Oct 2, 2024
1 parent ffcbab7 commit 4de37a7
Show file tree
Hide file tree
Showing 7 changed files with 215 additions and 14 deletions.
2 changes: 1 addition & 1 deletion packages/lexical-list/src/LexicalListNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ export class ListNode extends ElementNode {
}

exportDOM(editor: LexicalEditor): DOMExportOutput {
const {element} = super.exportDOM(editor);
const element = this.createDOM(editor._config, editor);
if (element && isHTMLElement(element)) {
if (this.__start !== 1) {
element.setAttribute('start', String(this.__start));
Expand Down
175 changes: 174 additions & 1 deletion packages/lexical-playground/__tests__/unit/docSerialization.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,13 @@
*/

import {serializedDocumentFromEditorState} from '@lexical/file';
import {$createParagraphNode, $createTextNode, $getRoot} from 'lexical';
import {$generateHtmlFromNodes, $generateNodesFromDOM} from '@lexical/html';
import {
$createParagraphNode,
$createTextNode,
$getRoot,
$insertNodes,
} from 'lexical';
import {initializeUnitTest} from 'lexical/src/__tests__/utils';

import {docFromHash, docToHash} from '../../src/utils/docSerialization';
Expand Down Expand Up @@ -48,5 +54,172 @@ describe('docSerialization', () => {
expect(await docFromHash(await docToHash(doc))).toEqual(doc);
});
});

describe('Preserve indent serializing HTML <-> Lexical', () => {
it('preserves indentation', async () => {
const {editor} = testEnv;
const parser = new DOMParser();
const htmlString = `<p class="PlaygroundEditorTheme__paragraph" dir="ltr">
<span style="white-space: pre-wrap;">paragraph</span>
</p>
<h1 class="PlaygroundEditorTheme__h1" dir="ltr">
<span style="white-space: pre-wrap;">heading</span>
</h1>
<blockquote class="PlaygroundEditorTheme__quote" dir="ltr">
<span style="white-space: pre-wrap;">quote</span>
</blockquote>
<p class="PlaygroundEditorTheme__paragraph" dir="ltr" style="padding-inline-start: 80px;">
<span style="white-space: pre-wrap;">paragraph</span>
</p>
<h1 class="PlaygroundEditorTheme__h1" dir="ltr" style="padding-inline-start: 80px;">
<span style="white-space: pre-wrap;">heading</span>
</h1>
<blockquote class="PlaygroundEditorTheme__quote" dir="ltr" style="padding-inline-start: 80px;">
<span style="white-space: pre-wrap;">quote</span>
</blockquote>`;
const dom = parser.parseFromString(htmlString, 'text/html');
await editor.update(() => {
const nodes = $generateNodesFromDOM(editor, dom);
$getRoot().select();
$insertNodes(nodes);
});

const expectedEditorState = {
root: {
children: [
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'paragraph',
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
textFormat: 0,
textStyle: '',
type: 'paragraph',
version: 1,
},
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'heading',
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
tag: 'h1',
type: 'heading',
version: 1,
},
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'quote',
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'quote',
version: 1,
},
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'paragraph',
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 2,
textFormat: 0,
textStyle: '',
type: 'paragraph',
version: 1,
},
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'heading',
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 2,
tag: 'h1',
type: 'heading',
version: 1,
},
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'quote',
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 2,
type: 'quote',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'root',
version: 1,
},
};

const editorState = editor.getEditorState().toJSON();
expect(editorState).toEqual(expectedEditorState);
let htmlString2;
await editor.update(() => {
htmlString2 = $generateHtmlFromNodes(editor);
});
expect(htmlString2).toBe(
'<p dir="ltr"><span style="white-space: pre-wrap;">paragraph</span></p><h1 dir="ltr"><span style="white-space: pre-wrap;">heading</span></h1><blockquote dir="ltr"><span style="white-space: pre-wrap;">quote</span></blockquote><p style="padding-inline-start: 80px;" dir="ltr"><span style="white-space: pre-wrap;">paragraph</span></p><h1 style="padding-inline-start: 80px;" dir="ltr"><span style="white-space: pre-wrap;">heading</span></h1><blockquote style="padding-inline-start: 80px;" dir="ltr"><span style="white-space: pre-wrap;">quote</span></blockquote>',
);
});
});
});
});
3 changes: 3 additions & 0 deletions packages/lexical-rich-text/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ import {
PASTE_COMMAND,
REMOVE_TEXT_COMMAND,
SELECT_ALL_COMMAND,
setNodeIndentFromDOM,
} from 'lexical';
import caretFromPoint from 'shared/caretFromPoint';
import {
Expand Down Expand Up @@ -415,6 +416,7 @@ function $convertHeadingElement(element: HTMLElement): DOMConversionOutput {
) {
node = $createHeadingNode(nodeName);
if (element.style !== null) {
setNodeIndentFromDOM(element, node);
node.setFormat(element.style.textAlign as ElementFormatType);
}
}
Expand All @@ -425,6 +427,7 @@ function $convertBlockquoteElement(element: HTMLElement): DOMConversionOutput {
const node = $createQuoteNode();
if (element.style !== null) {
node.setFormat(element.style.textAlign as ElementFormatType);
setNodeIndentFromDOM(element, node);
}
return {node};
}
Expand Down
9 changes: 9 additions & 0 deletions packages/lexical/src/LexicalUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1820,3 +1820,12 @@ export function $cloneWithProperties<T extends LexicalNode>(latestNode: T): T {
}
return mutableNode;
}

export function setNodeIndentFromDOM(
elementDom: HTMLElement,
elementNode: ElementNode,
) {
const indentSize = parseInt(elementDom.style.paddingInlineStart, 10) || 0;
const indent = indentSize / 40;
elementNode.setIndent(indent);
}
1 change: 1 addition & 0 deletions packages/lexical/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ export {
isSelectionCapturedInDecoratorInput,
isSelectionWithinEditor,
resetRandomKey,
setNodeIndentFromDOM,
} from './LexicalUtils';
export {ArtificialNode__DO_NOT_USE} from './nodes/ArtificialNode';
export {$isDecoratorNode, DecoratorNode} from './nodes/LexicalDecoratorNode';
Expand Down
27 changes: 25 additions & 2 deletions packages/lexical/src/nodes/LexicalElementNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,17 @@
*
*/

import type {NodeKey, SerializedLexicalNode} from '../LexicalNode';
import type {
DOMExportOutput,
NodeKey,
SerializedLexicalNode,
} from '../LexicalNode';
import type {
BaseSelection,
PointType,
RangeSelection,
} from '../LexicalSelection';
import type {KlassConstructor, Spread} from 'lexical';
import type {KlassConstructor, LexicalEditor, Spread} from 'lexical';

import invariant from 'shared/invariant';

Expand All @@ -33,6 +37,7 @@ import {errorOnReadOnly, getActiveEditor} from '../LexicalUpdates';
import {
$getNodeByKey,
$isRootOrShadowRoot,
isHTMLElement,
removeFromParent,
} from '../LexicalUtils';

Expand Down Expand Up @@ -523,6 +528,24 @@ export class ElementNode extends LexicalNode {

return writableSelf;
}
exportDOM(editor: LexicalEditor): DOMExportOutput {
const {element} = super.exportDOM(editor);
if (element && isHTMLElement(element)) {
const indent = this.getIndent();
if (indent > 0) {
// padding-inline-start is not widely supported in email HTML
// (see https://www.caniemail.com/features/css-padding-inline-start-end/),
// If you want to use HTML output for email, consider overriding the serialization
// to use `padding-right` in RTL languages, `padding-left` in `LTR` languages, or
// `text-indent` if you are ok with first-line indents.
// We recommend keeping multiples of 40px to maintain consistency with list-items
// (see https://github.com/facebook/lexical/pull/4025)
element.style.paddingInlineStart = `${indent * 40}px`;
}
}

return {element};
}
// JSON serialization
exportJSON(): SerializedElementNode {
return {
Expand Down
12 changes: 2 additions & 10 deletions packages/lexical/src/nodes/LexicalParagraphNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
$applyNodeReplacement,
getCachedClassNameArray,
isHTMLElement,
setNodeIndentFromDOM,
toggleTextFormatType,
} from '../LexicalUtils';
import {ElementNode} from './LexicalElementNode';
Expand Down Expand Up @@ -151,12 +152,6 @@ export class ParagraphNode extends ElementNode {
if (direction) {
element.dir = direction;
}
const indent = this.getIndent();
if (indent > 0) {
// padding-inline-start is not widely supported in email HTML, but
// Lexical Reconciler uses padding-inline-start. Using text-indent instead.
element.style.textIndent = `${indent * 20}px`;
}
}

return {
Expand Down Expand Up @@ -229,10 +224,7 @@ function $convertParagraphElement(element: HTMLElement): DOMConversionOutput {
const node = $createParagraphNode();
if (element.style) {
node.setFormat(element.style.textAlign as ElementFormatType);
const indent = parseInt(element.style.textIndent, 10) / 20;
if (indent > 0) {
node.setIndent(indent);
}
setNodeIndentFromDOM(element, node);
}
return {node};
}
Expand Down

0 comments on commit 4de37a7

Please sign in to comment.