Skip to content

Commit

Permalink
[lexical-website] Documentation Update: Add Documentation for html Pr…
Browse files Browse the repository at this point in the history
…operty in Lexical Editor Configuration (#6770)

Co-authored-by: Bob Ippolito <bob@redivi.com>
  • Loading branch information
Kingscliq and etrepum authored Nov 5, 2024
1 parent dd65f59 commit 86eba22
Show file tree
Hide file tree
Showing 4 changed files with 168 additions and 3 deletions.
116 changes: 113 additions & 3 deletions examples/react-rich/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,137 @@
* LICENSE file in the root directory of this source tree.
*
*/

import {AutoFocusPlugin} from '@lexical/react/LexicalAutoFocusPlugin';
import {LexicalComposer} from '@lexical/react/LexicalComposer';
import {ContentEditable} from '@lexical/react/LexicalContentEditable';
import {LexicalErrorBoundary} from '@lexical/react/LexicalErrorBoundary';
import {HistoryPlugin} from '@lexical/react/LexicalHistoryPlugin';
import {RichTextPlugin} from '@lexical/react/LexicalRichTextPlugin';
import {
$isTextNode,
DOMConversionMap,
DOMExportOutput,
Klass,
LexicalEditor,
LexicalNode,
ParagraphNode,
TextNode,
} from 'lexical';

import ExampleTheme from './ExampleTheme';
import ToolbarPlugin from './plugins/ToolbarPlugin';
import TreeViewPlugin from './plugins/TreeViewPlugin';
import {parseAllowedColor, parseAllowedFontSize} from './styleConfig';

const placeholder = 'Enter some rich text...';

const removeStylesExportDOM = (
editor: LexicalEditor,
target: LexicalNode,
): DOMExportOutput => {
const output = target.exportDOM(editor);
if (output && output.element instanceof HTMLElement) {
// Remove all inline styles and classes if the element is an HTMLElement
// Children are checked as well since TextNode can be nested
// in i, b, and strong tags.
for (const el of [
output.element,
...output.element.querySelectorAll('[style],[class],[dir="ltr"]'),
]) {
el.removeAttribute('class');
el.removeAttribute('style');
if (el.getAttribute('dir') === 'ltr') {
el.removeAttribute('dir');
}
}
}
return output;
};

const exportMap = new Map<
Klass<LexicalNode>,
(editor: LexicalEditor, target: LexicalNode) => DOMExportOutput
>([
[ParagraphNode, removeStylesExportDOM],
[TextNode, removeStylesExportDOM],
]);

const getExtraStyles = (element: HTMLElement): string => {
// Parse styles from pasted input, but only if they match exactly the
// sort of styles that would be produced by exportDOM
let extraStyles = '';
const fontSize = parseAllowedFontSize(element.style.fontSize);
const backgroundColor = parseAllowedColor(element.style.backgroundColor);
const color = parseAllowedColor(element.style.color);
if (fontSize !== '' && fontSize !== '15px') {
extraStyles += `font-size: ${fontSize};`;
}
if (backgroundColor !== '' && backgroundColor !== 'rgb(255, 255, 255)') {
extraStyles += `background-color: ${backgroundColor};`;
}
if (color !== '' && color !== 'rgb(0, 0, 0)') {
extraStyles += `color: ${color};`;
}
return extraStyles;
};

const constructImportMap = (): DOMConversionMap => {
const importMap: DOMConversionMap = {};

// Wrap all TextNode importers with a function that also imports
// the custom styles implemented by the playground
for (const [tag, fn] of Object.entries(TextNode.importDOM() || {})) {
importMap[tag] = (importNode) => {
const importer = fn(importNode);
if (!importer) {
return null;
}
return {
...importer,
conversion: (element) => {
const output = importer.conversion(element);
if (
output === null ||
output.forChild === undefined ||
output.after !== undefined ||
output.node !== null
) {
return output;
}
const extraStyles = getExtraStyles(element);
if (extraStyles) {
const {forChild} = output;
return {
...output,
forChild: (child, parent) => {
const textNode = forChild(child, parent);
if ($isTextNode(textNode)) {
textNode.setStyle(textNode.getStyle() + extraStyles);
}
return textNode;
},
};
}
return output;
},
};
};
}

return importMap;
};

const editorConfig = {
html: {
export: exportMap,
import: constructImportMap(),
},
namespace: 'React.js Demo',
nodes: [],
// Handling of errors during update
nodes: [ParagraphNode, TextNode],
onError(error: Error) {
throw error;
},
// The editor theme
theme: ExampleTheme,
};

Expand Down
1 change: 1 addition & 0 deletions examples/react-rich/src/ExampleTheme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* LICENSE file in the root directory of this source tree.
*
*/

export default {
code: 'editor-code',
heading: {
Expand Down
25 changes: 25 additions & 0 deletions examples/react-rich/src/styleConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* 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.
*
*/

const MIN_ALLOWED_FONT_SIZE = 8;
const MAX_ALLOWED_FONT_SIZE = 72;

export const parseAllowedFontSize = (input: string): string => {
const match = input.match(/^(\d+(?:\.\d+)?)px$/);
if (match) {
const n = Number(match[1]);
if (n >= MIN_ALLOWED_FONT_SIZE && n <= MAX_ALLOWED_FONT_SIZE) {
return input;
}
}
return '';
};

export function parseAllowedColor(input: string) {
return /^rgb\(\d+, \d+, \d+\)$/.test(input) ? input : '';
}
29 changes: 29 additions & 0 deletions packages/lexical-website/docs/concepts/serialization.md
Original file line number Diff line number Diff line change
Expand Up @@ -437,3 +437,32 @@ function patchStyleConversion(
};
}
```
### `html` Property for Import and Export Configuration
The `html` property in `CreateEditorArgs` provides an alternate way to configure HTML import and export behavior in Lexical without subclassing or node replacement. It includes two properties:
- `import` - Similar to `importDOM`, it controls how HTML elements are transformed into `LexicalNodes`. However, instead of defining conversions directly on each `LexicalNode`, `html.import` provides a configuration that can be overridden easily in the editor setup.
- `export` - Similar to `exportDOM`, this property customizes how `LexicalNodes` are serialized into HTML. With `html.export`, users can specify transformations for various nodes collectively, offering a flexible override mechanism that can adapt without needing to extend or replace specific `LexicalNodes`.
#### Key Differences from `importDOM` and `exportDOM`
While `importDOM` and `exportDOM` allow for highly customized, node-specific conversions by defining them directly within the `LexicalNode` class, the `html` property enables broader, editor-wide configurations. This setup benefits situations where:
- **Consistent Transformations**: You want uniform import/export behavior across different nodes without adjusting each node individually.
- **No Subclassing Required**: Overrides to import and export logic are applied at the editor configuration level, simplifying customization and reducing the need for extensive subclassing.
#### Type Definitions
```typescript
type HTMLConfig = {
export?: DOMExportOutputMap; // Optional map defining how nodes are exported to HTML.
import?: DOMConversionMap; // Optional record defining how HTML is converted into nodes.
};
```
#### Example of a use case for the `html` Property for Import and Export Configuration:
[Rich text sandbox](https://stackblitz.com/github/facebook/lexical/tree/main/examples/react-rich?embed=1&file=src%2FApp.tsx&terminalHeight=0&ctl=1&showSidebar=0&devtoolsheight=0&view=preview)

0 comments on commit 86eba22

Please sign in to comment.