-
-
Notifications
You must be signed in to change notification settings - Fork 165
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fixes #92
- Loading branch information
Showing
14 changed files
with
693 additions
and
2 deletions.
There are no files selected for viewing
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,59 @@ | ||
--- | ||
title: HTML Support | ||
slug: html-support | ||
position: 98 | ||
--- | ||
|
||
# HTML Support | ||
|
||
Markdown documents can occasionally include additional HTML elements. Out of the box, MDXEditor converts those into | ||
generic HTML nodes, which extend [Lexical's Element nodes](https://lexical.dev/docs/concepts/nodes#elementnode). This allows the user to edit the HTML content (i.e. the nested markdown inside those elements). | ||
|
||
**Note:** while using HTML can be tempting (and easy when it comes to parsing/rendering afterwards), it goes against the principles of markdown being human-readable, limited by intention format. If you need to extend the tooling available, it's better to consider directives and custom JSX components instead. | ||
|
||
Out of the box, the editor does not include UI that allows the user to add, remove or configure the HTML elements' properties. You can, however use the Lexical API to build toolbar components that do so. Below is a simple example of a toolbar component that lets the user change the CSS class of the element under the cursor. You can replace the input with a dropdown or an UI of your choice. | ||
|
||
|
||
```tsx | ||
const HTMLToolbarComponent = () => { | ||
const [currentSelection, activeEditor] = corePluginHooks.useEmitterValues('currentSelection', 'activeEditor') | ||
|
||
const currentHTMLNode = React.useMemo(() => { | ||
return ( | ||
activeEditor?.getEditorState().read(() => { | ||
const selectedNodes = currentSelection?.getNodes() || [] | ||
if (selectedNodes.length === 1) { | ||
return $getNearestNodeOfType(selectedNodes[0], GenericHTMLNode) | ||
} else { | ||
return null | ||
} | ||
}) || null | ||
) | ||
}, [currentSelection, activeEditor]) | ||
|
||
return ( | ||
<> | ||
<input | ||
disabled={currentHTMLNode === null} | ||
value={getCssClass(currentHTMLNode)} | ||
onChange={(e) => { | ||
activeEditor?.update( | ||
() => { | ||
const attributesWithoutClass = currentHTMLNode?.getAttributes().filter((attr) => attr.name !== 'class') || [] | ||
const newClassAttr: MdxJsxAttribute = { type: 'mdxJsxAttribute', name: 'class', value: e.target.value } | ||
currentHTMLNode?.updateAttributes([...attributesWithoutClass, newClassAttr]) | ||
}, | ||
{ discrete: true } | ||
) | ||
e.target.focus() | ||
}} | ||
/> | ||
</> | ||
) | ||
} | ||
|
||
function getCssClass(node: GenericHTMLNode | null) { | ||
return (node?.getAttributes().find((attr) => attr.name === 'class')?.value as string) ?? '' | ||
} | ||
``` | ||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
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,181 @@ | ||
import React from 'react' | ||
import { | ||
DiffSourceToggleWrapper, | ||
MDXEditor, | ||
corePluginHooks, | ||
diffSourcePlugin, | ||
headingsPlugin, | ||
toolbarPlugin, | ||
$createGenericHTMLNode | ||
} from '../' | ||
import { $patchStyleText } from '@lexical/selection' | ||
import { $getRoot, $getSelection, $isRangeSelection, $isTextNode, ElementNode, LexicalNode } from 'lexical' | ||
import { $isGenericHTMLNode } from '@/plugins/core/GenericHTMLNode' | ||
import { GenericHTMLNode } from '@/plugins/core/GenericHTMLNode' | ||
import { MdxJsxAttribute } from 'mdast-util-mdx' | ||
import { $getNearestNodeOfType } from '@lexical/utils' | ||
|
||
const markdownWithSpan = ` | ||
# Hello World | ||
A paragraph with <span style="color: red" class="some">some red text <span style="color: blue">with some blue nesting.</span> in here.</span> in it. | ||
` | ||
|
||
export function SpanWithColor() { | ||
return ( | ||
<> | ||
<MDXEditor | ||
markdown={markdownWithSpan} | ||
plugins={[ | ||
headingsPlugin(), | ||
diffSourcePlugin(), | ||
toolbarPlugin({ | ||
toolbarContents: () => ( | ||
<DiffSourceToggleWrapper> | ||
<HTMLToolbarComponent /> | ||
</DiffSourceToggleWrapper> | ||
) | ||
}) | ||
]} | ||
onChange={(md) => { | ||
console.log('change', md) | ||
}} | ||
/> | ||
</> | ||
) | ||
} | ||
|
||
const HTMLToolbarComponent = () => { | ||
const [currentSelection, activeEditor] = corePluginHooks.useEmitterValues('currentSelection', 'activeEditor') | ||
|
||
const currentStyle = React.useMemo(() => { | ||
return ( | ||
activeEditor?.getEditorState().read(() => { | ||
const selectedNodes = currentSelection?.getNodes() || [] | ||
if (selectedNodes.length === 1) { | ||
let node: ElementNode | LexicalNode | null | undefined = selectedNodes[0] | ||
let style = '' | ||
while (!style && node && node !== $getRoot()) { | ||
if ($isTextNode(node) || $isGenericHTMLNode(node)) { | ||
style = node.getStyle() | ||
} | ||
node = node?.getParent() | ||
} | ||
return style | ||
} else { | ||
return '' | ||
} | ||
}) || '' | ||
) | ||
}, [currentSelection, activeEditor]) | ||
|
||
const currentHTMLNode = React.useMemo(() => { | ||
return ( | ||
activeEditor?.getEditorState().read(() => { | ||
const selectedNodes = currentSelection?.getNodes() || [] | ||
if (selectedNodes.length === 1) { | ||
return $getNearestNodeOfType(selectedNodes[0], GenericHTMLNode) | ||
} else { | ||
return null | ||
} | ||
}) || null | ||
) | ||
}, [currentSelection, activeEditor]) | ||
|
||
return ( | ||
<> | ||
<button | ||
onClick={() => { | ||
if (activeEditor !== null && currentSelection !== null) { | ||
activeEditor.update(() => { | ||
$patchStyleText(currentSelection, { color: 'orange' }) | ||
}) | ||
} | ||
}} | ||
> | ||
Make selection orange | ||
</button> | ||
<button | ||
onClick={() => { | ||
if (activeEditor !== null && currentSelection !== null) { | ||
activeEditor.update(() => { | ||
$patchStyleText(currentSelection, { 'font-size': '20px' }) | ||
}) | ||
} | ||
}} | ||
> | ||
Big font size | ||
</button> | ||
{currentStyle && <div>Current style: {currentStyle}</div>} | ||
current css class:{' '} | ||
<input | ||
disabled={currentHTMLNode === null} | ||
value={getCssClass(currentHTMLNode)} | ||
onChange={(e) => { | ||
activeEditor?.update( | ||
() => { | ||
const attributesWithoutClass = currentHTMLNode?.getAttributes().filter((attr) => attr.name !== 'class') || [] | ||
const newClassAttr: MdxJsxAttribute = { type: 'mdxJsxAttribute', name: 'class', value: e.target.value } | ||
currentHTMLNode?.updateAttributes([...attributesWithoutClass, newClassAttr]) | ||
}, | ||
{ discrete: true } | ||
) | ||
e.target.focus() | ||
}} | ||
/> | ||
<button | ||
disabled={currentHTMLNode === null} | ||
onClick={() => { | ||
if (activeEditor !== null && currentSelection !== null) { | ||
activeEditor.update(() => { | ||
// const children = currentHTMLNode?.getChildren() || [] | ||
currentHTMLNode?.remove() | ||
const selection = $getSelection() | ||
selection?.insertNodes(currentHTMLNode?.getChildren() || []) | ||
}) | ||
} | ||
}} | ||
> | ||
remove HTML node | ||
</button> | ||
<button | ||
onClick={() => { | ||
if (activeEditor !== null && currentSelection !== null) { | ||
activeEditor.update(() => { | ||
const selection = $getSelection() | ||
if ($isRangeSelection(selection)) { | ||
const selectedNodes = selection.getNodes() | ||
const currentTextNode = selectedNodes.length === 1 && $isTextNode(selectedNodes[0]) ? selectedNodes[0] : null | ||
if (currentTextNode) { | ||
const attributes = Object.entries({ style: 'color: green' }).map(([name, value]) => ({ | ||
type: 'mdxJsxAttribute' as const, | ||
name, | ||
value | ||
})) | ||
|
||
const newNode = $createGenericHTMLNode('span', 'mdxJsxTextElement', attributes) | ||
selection?.insertNodes([newNode]) | ||
|
||
// newNode.insertAfter(slicedPortion) | ||
// newNode.append(slicedPortion) | ||
|
||
/* | ||
$wrapNodeInElement(slicedPortion, () => ) | ||
*/ | ||
} | ||
} | ||
}) | ||
} | ||
}} | ||
> | ||
wrap in a red span | ||
</button> | ||
</> | ||
) | ||
} | ||
|
||
function getCssClass(node: GenericHTMLNode | null) { | ||
return (node?.getAttributes().find((attr) => attr.name === 'class')?.value as string) ?? '' | ||
} |
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
Oops, something went wrong.