facebook / lexical

Lexical is an extensible text editor framework that provides excellent reliability, accessibility and performance.
https://lexical.dev
MIT License
19.95k stars 1.7k forks source link

[lexical-website] Documentation Update: Add Documentation for html Property in Lexical Editor Configuration #6770

Closed Kingscliq closed 2 weeks ago

Kingscliq commented 3 weeks ago

Description

Closes #6762

Test plan

Before

The html property in CreateEditorArgs is undocumented, lacking examples for its import and export properties. These functions have similar functionality to importDOM and exportDOM but are undocumented, creating ambiguity for users looking to extend or customize HTML handling.

After

The html property is now fully documented, with examples illustrating the import and export properties. The import property demonstrates mappings for transforming HTML elements into Lexical nodes, while export shows how to convert nodes back to HTML. These examples provide clarity and guide users in extending or customizing their editor’s HTML handling, building on the concepts of importDOM and exportDOM.

vercel[bot] commented 3 weeks ago

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
lexical ✅ Ready (Inspect) Visit Preview 💬 Add feedback Nov 5, 2024 7:39pm
lexical-playground ✅ Ready (Inspect) Visit Preview 💬 Add feedback Nov 5, 2024 7:39pm
github-actions[bot] commented 3 weeks ago

size-limit report 📦

Path Size
lexical - cjs 29.92 KB (0%)
lexical - esm 29.78 KB (0%)
@lexical/rich-text - cjs 38.57 KB (0%)
@lexical/rich-text - esm 31.63 KB (0%)
@lexical/plain-text - cjs 37.22 KB (0%)
@lexical/plain-text - esm 28.94 KB (0%)
@lexical/react - cjs 40.33 KB (0%)
@lexical/react - esm 33.06 KB (0%)
etrepum commented 3 weeks ago

Was this written with ChatGPT or something like that? The code example doesn't really seem close to correct. I would recommend writing an example of this functionality in one of the example packages or the playground so you can verify that the types are correct and it behaves as expected. The theme usage isn't relevant here, I'd remove that.

Kingscliq commented 3 weeks ago

Was this written with ChatGPT or something like that? The code example doesn't really seem close to correct. I would recommend writing an example of this functionality in one of the example packages or the playground so you can verify that the types are correct and it behaves as expected. The theme usage isn't relevant here, I'd remove that.

Thanks for the feedback @etrepum , I will create an example I think that's probably the best way to verify the functionality.

Quick question though, can I use the pattern for the exportDom feature on the playground, like a button that calls the html.export() assuming that's how it is called to behave exactly like the exportDom feature, I highlighted earlier

etrepum commented 3 weeks ago

If you were using the playground, or another example that has the debug tree view, then the "Export DOM" button is sufficient. You don't need to add any additional UI. Only the configuration passed to createEditor or LexicalComposer would change to add the html property. The whole purpose is that this configuration can be used to alter the behavior of existing export functionality.

What the example would do is provide some configuration that makes it so some node (or perhaps multiple nodes) have a different output than their default exportDOM implementation. If this is configured correctly, then the change would be apparent in the tree view after clicking Export DOM. The export HTML output can also be observed by copying text from the editor and then pasting it somewhere to see the text/html content such as https://evercoder.github.io/clipboard-inspector/

Possibly a simple and useful example would be some configuration that removes style from the default output. <span style="white-space: pre-wrap;">Welcome to the playground</span> is how a TextNode in the playground is exported, but maybe the example could change that to <span>Welcome to the playground</span>

Kingscliq commented 3 weeks ago

> If you were using the playground, or another example that has the debug tree view, then the "Export DOM" button is sufficient. You don't need to add any additional UI. Only the configuration passed to createEditor or LexicalComposer would change to add the html property. The whole purpose is that this configuration can be used to alter the behavior of existing export functionality.

What the example would do is provide some configuration that makes it so some node (or perhaps multiple nodes) have a different output than their default exportDOM implementation. If this is configured correctly, then the change would be apparent in the tree view after clicking Export DOM. The export HTML output can also be observed by copying text from the editor and then pasting it somewhere to see the text/html content such as https://evercoder.github.io/clipboard-inspector/

Possibly a simple and useful example would be some configuration that removes style from the default output. <span style="white-space: pre-wrap;">Welcome to the playground</span> is how a TextNode in the playground is exported, but maybe the example could change that to <span>Welcome to the playground</span>

Got it! Just scanning through the examples dir, so do you suggest I create a new installation or use an already existing installation, from what I am seeing I think the react-rich dir is quite okay for what I am trying to achieve, what do you think?

I noticed that the TreeViewPlugin isn’t directly editable from the examples, as it’s part of an installed package. To modify it, I’d need to make changes within the lexical-react package itself if I want my examples to work with the React package. The same approach would apply to the lexical-playground. Could you confirm if the examples package and similar dependencies pull changes directly from the monorepo files, or if publishing is required before these changes are accessible?

etrepum commented 3 weeks ago

You don't need to touch the TreeViewPlugin. Its "Export DOM" functionality does exactly the right thing to demonstrate this without any modification. The only thing you will need to change is the configuration passed to createEditor or LexicalComposer.

Any of the examples is fine, or the playground.

Kingscliq commented 3 weeks ago

You don't need to touch the TreeViewPlugin. Its "Export DOM" functionality does exactly the right thing to demonstrate this without any modification. The only thing you will need to change is the configuration passed to createEditor or LexicalComposer.

Any of the examples is fine, or the playground.

Okay, got it!, I will go through the LexicalComposer API and see how I can update the configuration

Kingscliq commented 3 weeks ago

Hi @etrepum I was going through the source and realized that LexicalComposer was used in a lot of places, which means I can create a new react application with a new LexicalComposer Instance and ensure that I add the html attribute to the to the configuration and pass it as initialConfig on the LexicalComposer do you think it will overwrite the exportDOM called on the TreeView plugin?

Just like we have in


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 ExampleTheme from './ExampleTheme';
import ToolbarPlugin from './plugins/ToolbarPlugin';
import TreeViewPlugin from './plugins/TreeViewPlugin';

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

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

export default function App() {
  return (
    <LexicalComposer initialConfig={editorConfig}>
      <div className="editor-container">
        <ToolbarPlugin />
        <div className="editor-inner">
          <RichTextPlugin
            contentEditable={
              <ContentEditable
                className="editor-input"
                aria-placeholder={placeholder}
                placeholder={
                  <div className="editor-placeholder">{placeholder}</div>
                }
              />
            }
            ErrorBoundary={LexicalErrorBoundary}
          />
          <HistoryPlugin />
          <AutoFocusPlugin />
          <TreeViewPlugin />
        </div>
      </div>
    </LexicalComposer>
  );
}

I got this from examples/react-rich

etrepum commented 3 weeks ago

Yes, if the html property is set on that initialConfig then any effect its export property has would be observable from the TreeViewPlugin or the clipboard.

Kingscliq commented 3 weeks ago

Yes, if the html property is set on that initialConfig then any effect its export property has would be observable from the TreeViewPlugin or the clipboard.

okay! thanks, will do that then.

Can I create a new react-app on the examples folder?, what name do I give it(is there a convention for naming)?, or do I link a stackblitz URL to the .md file that have the field documented?

etrepum commented 3 weeks ago

There is not a documented convention for the naming of example packages. It would be a lot less work to add this configuration example to an existing example or the playground, I would recommend that approach.

Kingscliq commented 3 weeks ago

There is not a documented convention for the naming of example packages. It would be a lot less work to add this configuration example to an existing example or the playground, I would recommend that approach.

I wanted to talk about the html property for a bit, I am not sure how and what configuration it needs if I am trying to pass it to the editorConfiguration

for example

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 ExampleTheme from './ExampleTheme';
import ToolbarPlugin from './plugins/ToolbarPlugin';
import TreeViewPlugin from './plugins/TreeViewPlugin';

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

const editorConfig = {
  namespace: 'React.js Demo',
  nodes: [],
  // Handling of errors during update
  onError(error: Error) {
    throw error;
  },
  // The editor theme
  theme: ExampleTheme,
  html: {
      import: {} // with some values or configs,
  export: {
// with some values or config
}
  } , 

};

export default function App() {
  return (
    <LexicalComposer initialConfig={editorConfig}>
      <div className="editor-container">
        <ToolbarPlugin />
        <div className="editor-inner">
          <RichTextPlugin
            contentEditable={
              <ContentEditable
                className="editor-input"
                aria-placeholder={placeholder}
                placeholder={
                  <div className="editor-placeholder">{placeholder}</div>
                }
              />
            }
            ErrorBoundary={LexicalErrorBoundary}
          />
          <HistoryPlugin />
          <AutoFocusPlugin />
          <TreeViewPlugin />
        </div>
      </div>
    </LexicalComposer>
  );
}

in this snippet the editorConfig has a bunch of values, the nameSpace allows you to pass a string, and the theme allows you pass a default theme configuration, for the HTML its expects the import and export properties which have their own key-value pairs, how does one know what exactly to pass to those key values to maintain the exact behavior we want, more like the overwrite of the exportDOM and importDOM behaviors

etrepum commented 3 weeks ago

The way you would figure it out is to read through the type definitions to see what types are expected. In above messages you can see that the type of the html property is as follows:

https://github.com/facebook/lexical/blob/07ca5c83c2f512734976504ef9fae5d39b6072ae/packages/lexical/src/LexicalEditor.ts#L173-L176

Then you can look to see what the types of DOMExportOutputMap and DOMConversionMap are

https://github.com/facebook/lexical/blob/07ca5c83c2f512734976504ef9fae5d39b6072ae/packages/lexical/src/LexicalNode.ts#L125-L161

You can also see how importDOM and exportDOM use parts of those types

https://github.com/facebook/lexical/blob/07ca5c83c2f512734976504ef9fae5d39b6072ae/packages/lexical/src/LexicalNode.ts#L270

https://github.com/facebook/lexical/blob/07ca5c83c2f512734976504ef9fae5d39b6072ae/packages/lexical/src/LexicalNode.ts#L850-L853

If you look closely you can see that DOMExportOutputMap and DOMConversionMap have almost exactly the same signature as exportDOM and importDOM. In the exportDOM case it's a Map of Klass<LexicalNode> (this is just the awkward way that we have to refer to the class of a node rather than an instance of that class in typescript) to functions instead of a method of those classes. In the importDOM case it's just exactly the same signature as the static importDOM function

In essence the html property would look something like this:

/* this is the same signature as an importDOM
function removeStylesExportDOM(editor: LexicalEditor, target: LexicalNode): DOMExportOutput {
  // Call the original node's implementation of exportDOM
  // and remove any exported style from the element
  const output = target.exportDOM(editor);
  output.element.style = '';
  return output;
}
const export: DOMExportOutputMap = new Map([
  [ParagraphNode, removeStylesExportDOM],
  [TextNode, removeStylesExportDOM],
]);
const import: DOMConversionMap = /* this would be essentially the same as the return value of an importDOM */;
const editorConfig = {
  html: { import, export },
  /* other values here */
};
Kingscliq commented 3 weeks ago

The way you would figure it out is to read through the type definitions to see what types are expected. In above messages you can see that the type of the html property is as follows:

https://github.com/facebook/lexical/blob/07ca5c83c2f512734976504ef9fae5d39b6072ae/packages/lexical/src/LexicalEditor.ts#L173-L176

Then you can look to see what the types of DOMExportOutputMap and DOMConversionMap are

https://github.com/facebook/lexical/blob/07ca5c83c2f512734976504ef9fae5d39b6072ae/packages/lexical/src/LexicalNode.ts#L125-L161

You can also see how importDOM and exportDOM use parts of those types

https://github.com/facebook/lexical/blob/07ca5c83c2f512734976504ef9fae5d39b6072ae/packages/lexical/src/LexicalNode.ts#L270

https://github.com/facebook/lexical/blob/07ca5c83c2f512734976504ef9fae5d39b6072ae/packages/lexical/src/LexicalNode.ts#L850-L853

If you look closely you can see that DOMExportOutputMap and DOMConversionMap have almost exactly the same signature as exportDOM and importDOM. In the exportDOM case it's a Map of Klass<LexicalNode> (this is just the awkward way that we have to refer to the class of a node rather than an instance of that class in typescript) to functions instead of a method of those classes. In the importDOM case it's just exactly the same signature as the static importDOM function

In essence the html property would look something like this:

/* this is the same signature as an importDOM
function removeStylesExportDOM(editor: LexicalEditor, target: LexicalNode): DOMExportOutput {
  // Call the original node's implementation of exportDOM
  // and remove any exported style from the element
  const output = target.exportDOM(editor);
  output.element.style = '';
  return output;
}
const export: DOMExportOutputMap = new Map([
  [ParagraphNode, removeStylesExportDOM],
  [TextNode, removeStylesExportDOM],
]);
const import: DOMConversionMap = /* this would be essentially the same as the return value of an importDOM */;
const editorConfig = {
  html: { import, export },
  /* other values here */
};

Got it! thanks @etrepum

Kingscliq commented 3 weeks ago

Hi @etrepum thanks for clarifying things for me on this issue

I noticed that

export type HTMLConfig = { export?: DOMExportOutputMap; import?: DOMConversionMap; };

HTMLConfig's import and export has two types imported from the lexical. DOMConversionMap works for import but I couldn't import DOMExportMap from lexical, since I am using react-rich example to add the configurations

How do you think I can import DOMExportOutputMap. to the react project I am using?

etrepum commented 3 weeks ago

We should export that too, if you wanted to fix that, but you could use something like this:

type DOMExportOutputMap = NonNullable<HTMLConfig['export']>;
Kingscliq commented 3 weeks ago

We should export that too, if you wanted to fix that, but you could use something like this:

type DOMExportOutputMap = NonNullable<HTMLConfig['export']>;

Oh great hack, I tried something like this

const removeStylesExportDOM = (
  editor: LexicalEditor,
  target: LexicalNode,
): DOMExportOutput => {
  // Call the original node's implementation of exportDOM
  const output = target.exportDOM(editor);

  if (output && output.element instanceof HTMLElement) {
    // Remove all inline styles if the element is an HTMLElement
    output.element.removeAttribute('style');
  }

  return output;
};

type DOMExportOutputMap = NonNullable<HTMLConfig['export']>;

const exportMap: DOMExportOutputMap = new Map([
  [ParagraphNode, removeStylesExportDOM],
  [TextNode, removeStylesExportDOM],
]);

The Map is Javascript Map right? I used it and getting some typescript overload matches errors

No overload matches this call.
  Overload 1 of 4, '(iterable?: Iterable<readonly [typeof ParagraphNode, (editor: LexicalEditor, target: LexicalNode) => DOMExportOutput]> | null | undefined): Map<...>', gave the following error.
    Argument of type '([typeof ParagraphNode, (editor: LexicalEditor, target: LexicalNode) => DOMExportOutput] | [typeof TextNode, (editor: LexicalEditor, target: LexicalNode) => DOMExportOutput])[]' is not assignable to parameter of type 'Iterable<readonly [typeof ParagraphNode, (editor: LexicalEditor, target: LexicalNode) => DOMExportOutput]>'.
etrepum commented 3 weeks ago

It looks like TypeScript is unable to infer the type of the Map correctly with the initializer, either you can expand the type yourself or you can use the setter form so that TypeScript performs the inference correctly.

const export = new Map<
  Klass<LexicalNode>,
  (editor: LexicalEditor, target: LexicalNode) => DOMExportOutput
>([
  [ParagraphNode, removeStylesExportDOM],
  [TextNode, removeStylesExportDOM],
]);
const export: DOMExportOutputMap = new Map();
export.set(ParagraphNode, removeStylesExportDOM);
export.set(TextNode, removeStylesExportDOM);
Kingscliq commented 3 weeks ago

It looks like TypeScript is unable to infer the type of the Map correctly with the initializer, either you can expand the type yourself or you can use the setter form so that TypeScript performs the inference correctly.

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

Okay I will try this

etrepum commented 2 weeks ago

https://github.com/facebook/lexical/pull/6780 has been merged so it should be fairly straightforward for you to refer to its implementation in the example for the html import config

Kingscliq commented 2 weeks ago

6780 has been merged so it should be fairly straightforward for you to refer to its implementation in the example for the html import config

Great!! hoping on it right now. In terms of linking this example to the markdown files, do I need to refer to the react-rich version or do you think I could copy the code over to Stackblitz and link it?

Kingscliq commented 2 weeks ago
Screenshot 2024-11-05 at 1 33 39 AM

I realized there was no color palette or fontSize on the toolbar so I will have that implemented to test the behaviour of the configuration

Kingscliq commented 2 weeks ago

I think this is very close! For the example to be helpful we need to link to it from the documentation, otherwise people won't know to look here

got it!

Kingscliq commented 2 weeks ago

Updated with a link to the example @etrepum