mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-15 11:44:05 +02:00
Add Javascript Examples Support (#5722)
* chore: moved all ts files for examples to examples/ts * add: tsc to eject js and jsx output * example: add js transpiled examples * example: update example site to show both js and ts code * chore: fix yarn lint * fix(example): getAllExamplesPath
This commit is contained in:
@@ -35,6 +35,7 @@
|
||||
"test:integration-local": "run-p -r serve playwright",
|
||||
"test:mocha": "mocha --require ./config/babel/register.cjs ./packages/{slate,slate-history,slate-hyperscript}/test/**/*.{js,ts}",
|
||||
"test:jest": "jest --config jest.config.js",
|
||||
"tsc:examples": "tsc --project ./site/tsconfig.example.json",
|
||||
"watch": "yarn build:rollup --watch",
|
||||
"playwright": "playwright test"
|
||||
},
|
||||
|
@@ -2,16 +2,16 @@
|
||||
|
||||
This directory contains a set of examples that give you an idea for how you might use Slate to implement your own editor. Take a look around!
|
||||
|
||||
- [**Plain text**](./plaintext.tsx) — showing the most basic case: a glorified `<textarea>`.
|
||||
- [**Rich text**](./richtext.tsx) — showing the features you'd expect from a basic editor.
|
||||
- [**Forced Layout**](./forced-layout.tsx) - showing how to use constraints to enforce a document structure.
|
||||
- [**Markdown Shortcuts**](./markdown-shortcuts.tsx) — showing how to add key handlers for Markdown-like shortcuts.
|
||||
- [**Inlines**](./inlines.tsx) — showing how wrap text in inline nodes with associated data.
|
||||
- [**Images**](./images.tsx) — showing how to use void (text-less) nodes to add images.
|
||||
- [**Hovering toolbar**](./hovering-toolbar.tsx) — showing how a hovering toolbar can be implemented.
|
||||
- [**Tables**](./tables.tsx) — showing how to nest blocks to render more advanced components.
|
||||
- [**Paste HTML**](./paste-html.tsx) — showing how to use an HTML serializer to handle pasted HTML.
|
||||
- [**Code Highlighting**](./code-highlighting.tsx) — showing how to use decorations to dynamically format text.
|
||||
- [**Plain text**](./ts/plaintext.tsx) — showing the most basic case: a glorified `<textarea>`.
|
||||
- [**Rich text**](./ts/richtext.tsx) — showing the features you'd expect from a basic editor.
|
||||
- [**Forced Layout**](./ts/forced-layout.tsx) - showing how to use constraints to enforce a document structure.
|
||||
- [**Markdown Shortcuts**](./ts/markdown-shortcuts.tsx) — showing how to add key handlers for Markdown-like shortcuts.
|
||||
- [**Inlines**](./ts/inlines.tsx) — showing how wrap text in inline nodes with associated data.
|
||||
- [**Images**](./ts/images.tsx) — showing how to use void (text-less) nodes to add images.
|
||||
- [**Hovering toolbar**](./ts/hovering-toolbar.tsx) — showing how a hovering toolbar can be implemented.
|
||||
- [**Tables**](./ts/tables.tsx) — showing how to nest blocks to render more advanced components.
|
||||
- [**Paste HTML**](./ts/paste-html.tsx) — showing how to use an HTML serializer to handle pasted HTML.
|
||||
- [**Code Highlighting**](./ts/code-highlighting.tsx) — showing how to use decorations to dynamically format text.
|
||||
- ...and more!
|
||||
|
||||
If you have an idea for an example that shows a common use case, pull request it!
|
||||
|
176
site/examples/js/check-lists.jsx
Normal file
176
site/examples/js/check-lists.jsx
Normal file
@@ -0,0 +1,176 @@
|
||||
import React, { useMemo, useCallback } from 'react'
|
||||
import {
|
||||
Slate,
|
||||
Editable,
|
||||
withReact,
|
||||
useSlateStatic,
|
||||
useReadOnly,
|
||||
ReactEditor,
|
||||
} from 'slate-react'
|
||||
import {
|
||||
Editor,
|
||||
Transforms,
|
||||
Range,
|
||||
Point,
|
||||
createEditor,
|
||||
Element as SlateElement,
|
||||
} from 'slate'
|
||||
import { css } from '@emotion/css'
|
||||
import { withHistory } from 'slate-history'
|
||||
|
||||
const initialValue = [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
text: 'With Slate you can build complex block types that have their own embedded content and behaviors, like rendering checkboxes inside check list items!',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'check-list-item',
|
||||
checked: true,
|
||||
children: [{ text: 'Slide to the left.' }],
|
||||
},
|
||||
{
|
||||
type: 'check-list-item',
|
||||
checked: true,
|
||||
children: [{ text: 'Slide to the right.' }],
|
||||
},
|
||||
{
|
||||
type: 'check-list-item',
|
||||
checked: false,
|
||||
children: [{ text: 'Criss-cross.' }],
|
||||
},
|
||||
{
|
||||
type: 'check-list-item',
|
||||
checked: true,
|
||||
children: [{ text: 'Criss-cross!' }],
|
||||
},
|
||||
{
|
||||
type: 'check-list-item',
|
||||
checked: false,
|
||||
children: [{ text: 'Cha cha real smooth…' }],
|
||||
},
|
||||
{
|
||||
type: 'check-list-item',
|
||||
checked: false,
|
||||
children: [{ text: "Let's go to work!" }],
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [{ text: 'Try it out for yourself!' }],
|
||||
},
|
||||
]
|
||||
const CheckListsExample = () => {
|
||||
const renderElement = useCallback(props => <Element {...props} />, [])
|
||||
const editor = useMemo(
|
||||
() => withChecklists(withHistory(withReact(createEditor()))),
|
||||
[]
|
||||
)
|
||||
return (
|
||||
<Slate editor={editor} initialValue={initialValue}>
|
||||
<Editable
|
||||
renderElement={renderElement}
|
||||
placeholder="Get to work…"
|
||||
spellCheck
|
||||
autoFocus
|
||||
/>
|
||||
</Slate>
|
||||
)
|
||||
}
|
||||
const withChecklists = editor => {
|
||||
const { deleteBackward } = editor
|
||||
editor.deleteBackward = (...args) => {
|
||||
const { selection } = editor
|
||||
if (selection && Range.isCollapsed(selection)) {
|
||||
const [match] = Editor.nodes(editor, {
|
||||
match: n =>
|
||||
!Editor.isEditor(n) &&
|
||||
SlateElement.isElement(n) &&
|
||||
n.type === 'check-list-item',
|
||||
})
|
||||
if (match) {
|
||||
const [, path] = match
|
||||
const start = Editor.start(editor, path)
|
||||
if (Point.equals(selection.anchor, start)) {
|
||||
const newProperties = {
|
||||
type: 'paragraph',
|
||||
}
|
||||
Transforms.setNodes(editor, newProperties, {
|
||||
match: n =>
|
||||
!Editor.isEditor(n) &&
|
||||
SlateElement.isElement(n) &&
|
||||
n.type === 'check-list-item',
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
deleteBackward(...args)
|
||||
}
|
||||
return editor
|
||||
}
|
||||
const Element = props => {
|
||||
const { attributes, children, element } = props
|
||||
switch (element.type) {
|
||||
case 'check-list-item':
|
||||
return <CheckListItemElement {...props} />
|
||||
default:
|
||||
return <p {...attributes}>{children}</p>
|
||||
}
|
||||
}
|
||||
const CheckListItemElement = ({ attributes, children, element }) => {
|
||||
const editor = useSlateStatic()
|
||||
const readOnly = useReadOnly()
|
||||
const { checked } = element
|
||||
return (
|
||||
<div
|
||||
{...attributes}
|
||||
className={css`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
& + & {
|
||||
margin-top: 0;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<span
|
||||
contentEditable={false}
|
||||
className={css`
|
||||
margin-right: 0.75em;
|
||||
`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={event => {
|
||||
const path = ReactEditor.findPath(editor, element)
|
||||
const newProperties = {
|
||||
checked: event.target.checked,
|
||||
}
|
||||
Transforms.setNodes(editor, newProperties, { at: path })
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
contentEditable={!readOnly}
|
||||
suppressContentEditableWarning
|
||||
className={css`
|
||||
flex: 1;
|
||||
opacity: ${checked ? 0.666 : 1};
|
||||
text-decoration: ${!checked ? 'none' : 'line-through'};
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
`}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default CheckListsExample
|
455
site/examples/js/code-highlighting.jsx
Normal file
455
site/examples/js/code-highlighting.jsx
Normal file
@@ -0,0 +1,455 @@
|
||||
import Prism from 'prismjs'
|
||||
import 'prismjs/components/prism-javascript'
|
||||
import 'prismjs/components/prism-jsx'
|
||||
import 'prismjs/components/prism-typescript'
|
||||
import 'prismjs/components/prism-tsx'
|
||||
import 'prismjs/components/prism-markdown'
|
||||
import 'prismjs/components/prism-python'
|
||||
import 'prismjs/components/prism-php'
|
||||
import 'prismjs/components/prism-sql'
|
||||
import 'prismjs/components/prism-java'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { createEditor, Node, Editor, Element, Transforms } from 'slate'
|
||||
import {
|
||||
withReact,
|
||||
Slate,
|
||||
Editable,
|
||||
useSlate,
|
||||
ReactEditor,
|
||||
useSlateStatic,
|
||||
} from 'slate-react'
|
||||
import { withHistory } from 'slate-history'
|
||||
import isHotkey from 'is-hotkey'
|
||||
import { css } from '@emotion/css'
|
||||
import { normalizeTokens } from './utils/normalize-tokens'
|
||||
import { Button, Icon, Toolbar } from './components'
|
||||
|
||||
const ParagraphType = 'paragraph'
|
||||
const CodeBlockType = 'code-block'
|
||||
const CodeLineType = 'code-line'
|
||||
const CodeHighlightingExample = () => {
|
||||
const [editor] = useState(() => withHistory(withReact(createEditor())))
|
||||
const decorate = useDecorate(editor)
|
||||
const onKeyDown = useOnKeydown(editor)
|
||||
return (
|
||||
<Slate editor={editor} initialValue={initialValue}>
|
||||
<ExampleToolbar />
|
||||
<SetNodeToDecorations />
|
||||
<Editable
|
||||
decorate={decorate}
|
||||
renderElement={ElementWrapper}
|
||||
renderLeaf={renderLeaf}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
<style>{prismThemeCss}</style>
|
||||
</Slate>
|
||||
)
|
||||
}
|
||||
const ElementWrapper = props => {
|
||||
const { attributes, children, element } = props
|
||||
const editor = useSlateStatic()
|
||||
if (element.type === CodeBlockType) {
|
||||
const setLanguage = language => {
|
||||
const path = ReactEditor.findPath(editor, element)
|
||||
Transforms.setNodes(editor, { language }, { at: path })
|
||||
}
|
||||
return (
|
||||
<div
|
||||
{...attributes}
|
||||
className={css(`
|
||||
font-family: monospace;
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
margin-top: 0;
|
||||
background: rgba(0, 20, 60, .03);
|
||||
padding: 5px 13px;
|
||||
`)}
|
||||
style={{ position: 'relative' }}
|
||||
spellCheck={false}
|
||||
>
|
||||
<LanguageSelect
|
||||
value={element.language}
|
||||
onChange={e => setLanguage(e.target.value)}
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (element.type === CodeLineType) {
|
||||
return (
|
||||
<div {...attributes} style={{ position: 'relative' }}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const Tag = editor.isInline(element) ? 'span' : 'div'
|
||||
return (
|
||||
<Tag {...attributes} style={{ position: 'relative' }}>
|
||||
{children}
|
||||
</Tag>
|
||||
)
|
||||
}
|
||||
const ExampleToolbar = () => {
|
||||
return (
|
||||
<Toolbar>
|
||||
<CodeBlockButton />
|
||||
</Toolbar>
|
||||
)
|
||||
}
|
||||
const CodeBlockButton = () => {
|
||||
const editor = useSlateStatic()
|
||||
const handleClick = () => {
|
||||
Transforms.wrapNodes(
|
||||
editor,
|
||||
{ type: CodeBlockType, language: 'html', children: [] },
|
||||
{
|
||||
match: n => Element.isElement(n) && n.type === ParagraphType,
|
||||
split: true,
|
||||
}
|
||||
)
|
||||
Transforms.setNodes(
|
||||
editor,
|
||||
{ type: CodeLineType },
|
||||
{ match: n => Element.isElement(n) && n.type === ParagraphType }
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
data-test-id="code-block-button"
|
||||
active
|
||||
onMouseDown={event => {
|
||||
event.preventDefault()
|
||||
handleClick()
|
||||
}}
|
||||
>
|
||||
<Icon>code</Icon>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
const renderLeaf = props => {
|
||||
const { attributes, children, leaf } = props
|
||||
const { text, ...rest } = leaf
|
||||
return (
|
||||
<span {...attributes} className={Object.keys(rest).join(' ')}>
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
const useDecorate = editor => {
|
||||
return useCallback(
|
||||
([node, path]) => {
|
||||
if (Element.isElement(node) && node.type === CodeLineType) {
|
||||
const ranges = editor.nodeToDecorations.get(node) || []
|
||||
return ranges
|
||||
}
|
||||
return []
|
||||
},
|
||||
[editor.nodeToDecorations]
|
||||
)
|
||||
}
|
||||
const getChildNodeToDecorations = ([block, blockPath]) => {
|
||||
const nodeToDecorations = new Map()
|
||||
const text = block.children.map(line => Node.string(line)).join('\n')
|
||||
const language = block.language
|
||||
const tokens = Prism.tokenize(text, Prism.languages[language])
|
||||
const normalizedTokens = normalizeTokens(tokens) // make tokens flat and grouped by line
|
||||
const blockChildren = block.children
|
||||
for (let index = 0; index < normalizedTokens.length; index++) {
|
||||
const tokens = normalizedTokens[index]
|
||||
const element = blockChildren[index]
|
||||
if (!nodeToDecorations.has(element)) {
|
||||
nodeToDecorations.set(element, [])
|
||||
}
|
||||
let start = 0
|
||||
for (const token of tokens) {
|
||||
const length = token.content.length
|
||||
if (!length) {
|
||||
continue
|
||||
}
|
||||
const end = start + length
|
||||
const path = [...blockPath, index, 0]
|
||||
const range = {
|
||||
anchor: { path, offset: start },
|
||||
focus: { path, offset: end },
|
||||
token: true,
|
||||
...Object.fromEntries(token.types.map(type => [type, true])),
|
||||
}
|
||||
nodeToDecorations.get(element).push(range)
|
||||
start = end
|
||||
}
|
||||
}
|
||||
return nodeToDecorations
|
||||
}
|
||||
// precalculate editor.nodeToDecorations map to use it inside decorate function then
|
||||
const SetNodeToDecorations = () => {
|
||||
const editor = useSlate()
|
||||
const blockEntries = Array.from(
|
||||
Editor.nodes(editor, {
|
||||
at: [],
|
||||
mode: 'highest',
|
||||
match: n => Element.isElement(n) && n.type === CodeBlockType,
|
||||
})
|
||||
)
|
||||
const nodeToDecorations = mergeMaps(
|
||||
...blockEntries.map(getChildNodeToDecorations)
|
||||
)
|
||||
editor.nodeToDecorations = nodeToDecorations
|
||||
return null
|
||||
}
|
||||
const useOnKeydown = editor => {
|
||||
const onKeyDown = useCallback(
|
||||
e => {
|
||||
if (isHotkey('tab', e)) {
|
||||
// handle tab key, insert spaces
|
||||
e.preventDefault()
|
||||
Editor.insertText(editor, ' ')
|
||||
}
|
||||
},
|
||||
[editor]
|
||||
)
|
||||
return onKeyDown
|
||||
}
|
||||
const LanguageSelect = props => {
|
||||
return (
|
||||
<select
|
||||
data-test-id="language-select"
|
||||
contentEditable={false}
|
||||
className={css`
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
z-index: 1;
|
||||
`}
|
||||
{...props}
|
||||
>
|
||||
<option value="css">CSS</option>
|
||||
<option value="html">HTML</option>
|
||||
<option value="java">Java</option>
|
||||
<option value="javascript">JavaScript</option>
|
||||
<option value="jsx">JSX</option>
|
||||
<option value="markdown">Markdown</option>
|
||||
<option value="php">PHP</option>
|
||||
<option value="python">Python</option>
|
||||
<option value="sql">SQL</option>
|
||||
<option value="tsx">TSX</option>
|
||||
<option value="typescript">TypeScript</option>
|
||||
</select>
|
||||
)
|
||||
}
|
||||
const mergeMaps = (...maps) => {
|
||||
const map = new Map()
|
||||
for (const m of maps) {
|
||||
for (const item of m) {
|
||||
map.set(...item)
|
||||
}
|
||||
}
|
||||
return map
|
||||
}
|
||||
const toChildren = content => [{ text: content }]
|
||||
const toCodeLines = content =>
|
||||
content
|
||||
.split('\n')
|
||||
.map(line => ({ type: CodeLineType, children: toChildren(line) }))
|
||||
const initialValue = [
|
||||
{
|
||||
type: ParagraphType,
|
||||
children: toChildren(
|
||||
"Here's one containing a single paragraph block with some text in it:"
|
||||
),
|
||||
},
|
||||
{
|
||||
type: CodeBlockType,
|
||||
language: 'jsx',
|
||||
children: toCodeLines(`// Add the initial value.
|
||||
const initialValue = [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [{ text: 'A line of text in a paragraph.' }]
|
||||
}
|
||||
]
|
||||
|
||||
const App = () => {
|
||||
const [editor] = useState(() => withReact(createEditor()))
|
||||
|
||||
return (
|
||||
<Slate editor={editor} initialValue={initialValue}>
|
||||
<Editable />
|
||||
</Slate>
|
||||
)
|
||||
}`),
|
||||
},
|
||||
{
|
||||
type: ParagraphType,
|
||||
children: toChildren(
|
||||
'If you are using TypeScript, you will also need to extend the Editor with ReactEditor and add annotations as per the documentation on TypeScript. The example below also includes the custom types required for the rest of this example.'
|
||||
),
|
||||
},
|
||||
{
|
||||
type: CodeBlockType,
|
||||
language: 'typescript',
|
||||
children: toCodeLines(`// TypeScript users only add this code
|
||||
import { BaseEditor, Descendant } from 'slate'
|
||||
import { ReactEditor } from 'slate-react'
|
||||
|
||||
type CustomElement = { type: 'paragraph'; children: CustomText[] }
|
||||
type CustomText = { text: string }
|
||||
|
||||
declare module 'slate' {
|
||||
interface CustomTypes {
|
||||
Editor: BaseEditor & ReactEditor
|
||||
Element: CustomElement
|
||||
Text: CustomText
|
||||
}
|
||||
}`),
|
||||
},
|
||||
{
|
||||
type: ParagraphType,
|
||||
children: toChildren('There you have it!'),
|
||||
},
|
||||
]
|
||||
// Prismjs theme stored as a string instead of emotion css function.
|
||||
// It is useful for copy/pasting different themes. Also lets keeping simpler Leaf implementation
|
||||
// In the real project better to use just css file
|
||||
const prismThemeCss = `
|
||||
/**
|
||||
* prism.js default theme for JavaScript, CSS and HTML
|
||||
* Based on dabblet (http://dabblet.com)
|
||||
* @author Lea Verou
|
||||
*/
|
||||
|
||||
code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
color: black;
|
||||
background: none;
|
||||
text-shadow: 0 1px white;
|
||||
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
|
||||
font-size: 1em;
|
||||
text-align: left;
|
||||
white-space: pre;
|
||||
word-spacing: normal;
|
||||
word-break: normal;
|
||||
word-wrap: normal;
|
||||
line-height: 1.5;
|
||||
|
||||
-moz-tab-size: 4;
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
|
||||
-webkit-hyphens: none;
|
||||
-moz-hyphens: none;
|
||||
-ms-hyphens: none;
|
||||
hyphens: none;
|
||||
}
|
||||
|
||||
pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection,
|
||||
code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection {
|
||||
text-shadow: none;
|
||||
background: #b3d4fc;
|
||||
}
|
||||
|
||||
pre[class*="language-"]::selection, pre[class*="language-"] ::selection,
|
||||
code[class*="language-"]::selection, code[class*="language-"] ::selection {
|
||||
text-shadow: none;
|
||||
background: #b3d4fc;
|
||||
}
|
||||
|
||||
@media print {
|
||||
code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
text-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Code blocks */
|
||||
pre[class*="language-"] {
|
||||
padding: 1em;
|
||||
margin: .5em 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
:not(pre) > code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
background: #f5f2f0;
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
:not(pre) > code[class*="language-"] {
|
||||
padding: .1em;
|
||||
border-radius: .3em;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.token.comment,
|
||||
.token.prolog,
|
||||
.token.doctype,
|
||||
.token.cdata {
|
||||
color: slategray;
|
||||
}
|
||||
|
||||
.token.punctuation {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.token.namespace {
|
||||
opacity: .7;
|
||||
}
|
||||
|
||||
.token.property,
|
||||
.token.tag,
|
||||
.token.boolean,
|
||||
.token.number,
|
||||
.token.constant,
|
||||
.token.symbol,
|
||||
.token.deleted {
|
||||
color: #905;
|
||||
}
|
||||
|
||||
.token.selector,
|
||||
.token.attr-name,
|
||||
.token.string,
|
||||
.token.char,
|
||||
.token.builtin,
|
||||
.token.inserted {
|
||||
color: #690;
|
||||
}
|
||||
|
||||
.token.operator,
|
||||
.token.entity,
|
||||
.token.url,
|
||||
.language-css .token.string,
|
||||
.style .token.string {
|
||||
color: #9a6e3a;
|
||||
/* This background color was intended by the author of this theme. */
|
||||
background: hsla(0, 0%, 100%, .5);
|
||||
}
|
||||
|
||||
.token.atrule,
|
||||
.token.attr-value,
|
||||
.token.keyword {
|
||||
color: #07a;
|
||||
}
|
||||
|
||||
.token.function,
|
||||
.token.class-name {
|
||||
color: #DD4A68;
|
||||
}
|
||||
|
||||
.token.regex,
|
||||
.token.important,
|
||||
.token.variable {
|
||||
color: #e90;
|
||||
}
|
||||
|
||||
.token.important,
|
||||
.token.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
.token.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.token.entity {
|
||||
cursor: help;
|
||||
}
|
||||
`
|
||||
export default CodeHighlightingExample
|
140
site/examples/js/components/index.jsx
Normal file
140
site/examples/js/components/index.jsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { cx, css } from '@emotion/css'
|
||||
|
||||
export const Button = React.forwardRef(
|
||||
({ className, active, reversed, ...props }, ref) => (
|
||||
<span
|
||||
{...props}
|
||||
ref={ref}
|
||||
className={cx(
|
||||
className,
|
||||
css`
|
||||
cursor: pointer;
|
||||
color: ${reversed
|
||||
? active
|
||||
? 'white'
|
||||
: '#aaa'
|
||||
: active
|
||||
? 'black'
|
||||
: '#ccc'};
|
||||
`
|
||||
)}
|
||||
/>
|
||||
)
|
||||
)
|
||||
export const EditorValue = React.forwardRef(
|
||||
({ className, value, ...props }, ref) => {
|
||||
const textLines = value.document.nodes
|
||||
.map(node => node.text)
|
||||
.toArray()
|
||||
.join('\n')
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
{...props}
|
||||
className={cx(
|
||||
className,
|
||||
css`
|
||||
margin: 30px -20px 0;
|
||||
`
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={css`
|
||||
font-size: 14px;
|
||||
padding: 5px 20px;
|
||||
color: #404040;
|
||||
border-top: 2px solid #eeeeee;
|
||||
background: #f8f8f8;
|
||||
`}
|
||||
>
|
||||
Slate's value as text
|
||||
</div>
|
||||
<div
|
||||
className={css`
|
||||
color: #404040;
|
||||
font: 12px monospace;
|
||||
white-space: pre-wrap;
|
||||
padding: 10px 20px;
|
||||
div {
|
||||
margin: 0 0 0.5em;
|
||||
}
|
||||
`}
|
||||
>
|
||||
{textLines}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
export const Icon = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<span
|
||||
{...props}
|
||||
ref={ref}
|
||||
className={cx(
|
||||
'material-icons',
|
||||
className,
|
||||
css`
|
||||
font-size: 18px;
|
||||
vertical-align: text-bottom;
|
||||
`
|
||||
)}
|
||||
/>
|
||||
))
|
||||
export const Instruction = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div
|
||||
{...props}
|
||||
ref={ref}
|
||||
className={cx(
|
||||
className,
|
||||
css`
|
||||
white-space: pre-wrap;
|
||||
margin: 0 -20px 10px;
|
||||
padding: 10px 20px;
|
||||
font-size: 14px;
|
||||
background: #f8f8e8;
|
||||
`
|
||||
)}
|
||||
/>
|
||||
))
|
||||
export const Menu = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div
|
||||
{...props}
|
||||
data-test-id="menu"
|
||||
ref={ref}
|
||||
className={cx(
|
||||
className,
|
||||
css`
|
||||
& > * {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
& > * + * {
|
||||
margin-left: 15px;
|
||||
}
|
||||
`
|
||||
)}
|
||||
/>
|
||||
))
|
||||
export const Portal = ({ children }) => {
|
||||
return typeof document === 'object'
|
||||
? ReactDOM.createPortal(children, document.body)
|
||||
: null
|
||||
}
|
||||
export const Toolbar = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<Menu
|
||||
{...props}
|
||||
ref={ref}
|
||||
className={cx(
|
||||
className,
|
||||
css`
|
||||
position: relative;
|
||||
padding: 1px 18px 17px;
|
||||
margin: 0 -20px;
|
||||
border-bottom: 2px solid #eee;
|
||||
margin-bottom: 20px;
|
||||
`
|
||||
)}
|
||||
/>
|
||||
))
|
31
site/examples/js/custom-placeholder.jsx
Normal file
31
site/examples/js/custom-placeholder.jsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import { createEditor } from 'slate'
|
||||
import { Slate, Editable, withReact } from 'slate-react'
|
||||
import { withHistory } from 'slate-history'
|
||||
|
||||
const initialValue = [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [{ text: '' }],
|
||||
},
|
||||
]
|
||||
const PlainTextExample = () => {
|
||||
const editor = useMemo(() => withHistory(withReact(createEditor())), [])
|
||||
return (
|
||||
<Slate editor={editor} initialValue={initialValue}>
|
||||
<Editable
|
||||
placeholder="Type something"
|
||||
renderPlaceholder={({ children, attributes }) => (
|
||||
<div {...attributes}>
|
||||
<p>{children}</p>
|
||||
<pre>
|
||||
Use the renderPlaceholder prop to customize rendering of the
|
||||
placeholder
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</Slate>
|
||||
)
|
||||
}
|
||||
export default PlainTextExample
|
141
site/examples/js/editable-voids.jsx
Normal file
141
site/examples/js/editable-voids.jsx
Normal file
@@ -0,0 +1,141 @@
|
||||
import React, { useState, useMemo } from 'react'
|
||||
import { Transforms, createEditor } from 'slate'
|
||||
import { Slate, Editable, useSlateStatic, withReact } from 'slate-react'
|
||||
import { withHistory } from 'slate-history'
|
||||
import { css } from '@emotion/css'
|
||||
import RichTextEditor from './richtext'
|
||||
import { Button, Icon, Toolbar } from './components'
|
||||
|
||||
const EditableVoidsExample = () => {
|
||||
const editor = useMemo(
|
||||
() => withEditableVoids(withHistory(withReact(createEditor()))),
|
||||
[]
|
||||
)
|
||||
return (
|
||||
<Slate editor={editor} initialValue={initialValue}>
|
||||
<Toolbar>
|
||||
<InsertEditableVoidButton />
|
||||
</Toolbar>
|
||||
|
||||
<Editable
|
||||
renderElement={props => <Element {...props} />}
|
||||
placeholder="Enter some text..."
|
||||
/>
|
||||
</Slate>
|
||||
)
|
||||
}
|
||||
const withEditableVoids = editor => {
|
||||
const { isVoid } = editor
|
||||
editor.isVoid = element => {
|
||||
return element.type === 'editable-void' ? true : isVoid(element)
|
||||
}
|
||||
return editor
|
||||
}
|
||||
const insertEditableVoid = editor => {
|
||||
const text = { text: '' }
|
||||
const voidNode = {
|
||||
type: 'editable-void',
|
||||
children: [text],
|
||||
}
|
||||
Transforms.insertNodes(editor, voidNode)
|
||||
}
|
||||
const Element = props => {
|
||||
const { attributes, children, element } = props
|
||||
switch (element.type) {
|
||||
case 'editable-void':
|
||||
return <EditableVoid {...props} />
|
||||
default:
|
||||
return <p {...attributes}>{children}</p>
|
||||
}
|
||||
}
|
||||
const unsetWidthStyle = css`
|
||||
width: unset;
|
||||
`
|
||||
const EditableVoid = ({ attributes, children, element }) => {
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
return (
|
||||
// Need contentEditable=false or Firefox has issues with certain input types.
|
||||
<div {...attributes} contentEditable={false}>
|
||||
<div
|
||||
className={css`
|
||||
box-shadow: 0 0 0 3px #ddd;
|
||||
padding: 8px;
|
||||
`}
|
||||
>
|
||||
<h4>Name:</h4>
|
||||
<input
|
||||
className={css`
|
||||
margin: 8px 0;
|
||||
`}
|
||||
type="text"
|
||||
value={inputValue}
|
||||
onChange={e => {
|
||||
setInputValue(e.target.value)
|
||||
}}
|
||||
/>
|
||||
<h4>Left or right handed:</h4>
|
||||
<input
|
||||
className={unsetWidthStyle}
|
||||
type="radio"
|
||||
name="handedness"
|
||||
value="left"
|
||||
/>{' '}
|
||||
Left
|
||||
<br />
|
||||
<input
|
||||
className={unsetWidthStyle}
|
||||
type="radio"
|
||||
name="handedness"
|
||||
value="right"
|
||||
/>{' '}
|
||||
Right
|
||||
<h4>Tell us about yourself:</h4>
|
||||
<div
|
||||
className={css`
|
||||
padding: 20px;
|
||||
border: 2px solid #ddd;
|
||||
`}
|
||||
>
|
||||
<RichTextEditor />
|
||||
</div>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const InsertEditableVoidButton = () => {
|
||||
const editor = useSlateStatic()
|
||||
return (
|
||||
<Button
|
||||
onMouseDown={event => {
|
||||
event.preventDefault()
|
||||
insertEditableVoid(editor)
|
||||
}}
|
||||
>
|
||||
<Icon>add</Icon>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
const initialValue = [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
text: 'In addition to nodes that contain editable text, you can insert void nodes, which can also contain editable elements, inputs, or an entire other Slate editor.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'editable-void',
|
||||
children: [{ text: '' }],
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
text: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
export default EditableVoidsExample
|
130
site/examples/js/embeds.jsx
Normal file
130
site/examples/js/embeds.jsx
Normal file
@@ -0,0 +1,130 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import { Transforms, createEditor } from 'slate'
|
||||
import {
|
||||
Slate,
|
||||
Editable,
|
||||
withReact,
|
||||
useSlateStatic,
|
||||
ReactEditor,
|
||||
} from 'slate-react'
|
||||
|
||||
const EmbedsExample = () => {
|
||||
const editor = useMemo(() => withEmbeds(withReact(createEditor())), [])
|
||||
return (
|
||||
<Slate editor={editor} initialValue={initialValue}>
|
||||
<Editable
|
||||
renderElement={props => <Element {...props} />}
|
||||
placeholder="Enter some text..."
|
||||
/>
|
||||
</Slate>
|
||||
)
|
||||
}
|
||||
const withEmbeds = editor => {
|
||||
const { isVoid } = editor
|
||||
editor.isVoid = element => (element.type === 'video' ? true : isVoid(element))
|
||||
return editor
|
||||
}
|
||||
const Element = props => {
|
||||
const { attributes, children, element } = props
|
||||
switch (element.type) {
|
||||
case 'video':
|
||||
return <VideoElement {...props} />
|
||||
default:
|
||||
return <p {...attributes}>{children}</p>
|
||||
}
|
||||
}
|
||||
const allowedSchemes = ['http:', 'https:']
|
||||
const VideoElement = ({ attributes, children, element }) => {
|
||||
const editor = useSlateStatic()
|
||||
const { url } = element
|
||||
const safeUrl = useMemo(() => {
|
||||
let parsedUrl = null
|
||||
try {
|
||||
parsedUrl = new URL(url)
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch {}
|
||||
if (parsedUrl && allowedSchemes.includes(parsedUrl.protocol)) {
|
||||
return parsedUrl.href
|
||||
}
|
||||
return 'about:blank'
|
||||
}, [url])
|
||||
return (
|
||||
<div {...attributes}>
|
||||
<div contentEditable={false}>
|
||||
<div
|
||||
style={{
|
||||
padding: '75% 0 0 0',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<iframe
|
||||
src={`${safeUrl}?title=0&byline=0&portrait=0`}
|
||||
frameBorder="0"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '0',
|
||||
left: '0',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<UrlInput
|
||||
url={url}
|
||||
onChange={val => {
|
||||
const path = ReactEditor.findPath(editor, element)
|
||||
const newProperties = {
|
||||
url: val,
|
||||
}
|
||||
Transforms.setNodes(editor, newProperties, {
|
||||
at: path,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const UrlInput = ({ url, onChange }) => {
|
||||
const [value, setValue] = React.useState(url)
|
||||
return (
|
||||
<input
|
||||
value={value}
|
||||
onClick={e => e.stopPropagation()}
|
||||
style={{
|
||||
marginTop: '5px',
|
||||
boxSizing: 'border-box',
|
||||
}}
|
||||
onChange={e => {
|
||||
const newUrl = e.target.value
|
||||
setValue(newUrl)
|
||||
onChange(newUrl)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
const initialValue = [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
text: 'In addition to simple image nodes, you can actually create complex embedded nodes. For example, this one contains an input element that lets you change the video being rendered!',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'video',
|
||||
url: 'https://player.vimeo.com/video/26689853',
|
||||
children: [{ text: '' }],
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
text: 'Try it out! This editor is built to handle Vimeo embeds, but you could handle any type.',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
export default EmbedsExample
|
100
site/examples/js/forced-layout.jsx
Normal file
100
site/examples/js/forced-layout.jsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import { Slate, Editable, withReact } from 'slate-react'
|
||||
import {
|
||||
Transforms,
|
||||
createEditor,
|
||||
Node,
|
||||
Element as SlateElement,
|
||||
Editor,
|
||||
} from 'slate'
|
||||
import { withHistory } from 'slate-history'
|
||||
|
||||
const withLayout = editor => {
|
||||
const { normalizeNode } = editor
|
||||
editor.normalizeNode = ([node, path]) => {
|
||||
if (path.length === 0) {
|
||||
if (editor.children.length <= 1 && Editor.string(editor, [0, 0]) === '') {
|
||||
const title = {
|
||||
type: 'title',
|
||||
children: [{ text: 'Untitled' }],
|
||||
}
|
||||
Transforms.insertNodes(editor, title, {
|
||||
at: path.concat(0),
|
||||
select: true,
|
||||
})
|
||||
}
|
||||
if (editor.children.length < 2) {
|
||||
const paragraph = {
|
||||
type: 'paragraph',
|
||||
children: [{ text: '' }],
|
||||
}
|
||||
Transforms.insertNodes(editor, paragraph, { at: path.concat(1) })
|
||||
}
|
||||
for (const [child, childPath] of Node.children(editor, path)) {
|
||||
let type
|
||||
const slateIndex = childPath[0]
|
||||
const enforceType = type => {
|
||||
if (SlateElement.isElement(child) && child.type !== type) {
|
||||
const newProperties = { type }
|
||||
Transforms.setNodes(editor, newProperties, {
|
||||
at: childPath,
|
||||
})
|
||||
}
|
||||
}
|
||||
switch (slateIndex) {
|
||||
case 0:
|
||||
type = 'title'
|
||||
enforceType(type)
|
||||
break
|
||||
case 1:
|
||||
type = 'paragraph'
|
||||
enforceType(type)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return normalizeNode([node, path])
|
||||
}
|
||||
return editor
|
||||
}
|
||||
const ForcedLayoutExample = () => {
|
||||
const renderElement = useCallback(props => <Element {...props} />, [])
|
||||
const editor = useMemo(
|
||||
() => withLayout(withHistory(withReact(createEditor()))),
|
||||
[]
|
||||
)
|
||||
return (
|
||||
<Slate editor={editor} initialValue={initialValue}>
|
||||
<Editable
|
||||
renderElement={renderElement}
|
||||
placeholder="Enter a title…"
|
||||
spellCheck
|
||||
autoFocus
|
||||
/>
|
||||
</Slate>
|
||||
)
|
||||
}
|
||||
const Element = ({ attributes, children, element }) => {
|
||||
switch (element.type) {
|
||||
case 'title':
|
||||
return <h2 {...attributes}>{children}</h2>
|
||||
case 'paragraph':
|
||||
return <p {...attributes}>{children}</p>
|
||||
}
|
||||
}
|
||||
const initialValue = [
|
||||
{
|
||||
type: 'title',
|
||||
children: [{ text: 'Enforce Your Layout!' }],
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
text: 'This example shows how to enforce your layout with domain-specific constraints. This document will always have a title block at the top and at least one paragraph in the body. Try deleting them and see what happens!',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
export default ForcedLayoutExample
|
147
site/examples/js/hovering-toolbar.jsx
Normal file
147
site/examples/js/hovering-toolbar.jsx
Normal file
@@ -0,0 +1,147 @@
|
||||
import React, { useMemo, useRef, useEffect } from 'react'
|
||||
import { Slate, Editable, withReact, useSlate, useFocused } from 'slate-react'
|
||||
import { Editor, createEditor, Range } from 'slate'
|
||||
import { css } from '@emotion/css'
|
||||
import { withHistory } from 'slate-history'
|
||||
import { Button, Icon, Menu, Portal } from './components'
|
||||
|
||||
const HoveringMenuExample = () => {
|
||||
const editor = useMemo(() => withHistory(withReact(createEditor())), [])
|
||||
return (
|
||||
<Slate editor={editor} initialValue={initialValue}>
|
||||
<HoveringToolbar />
|
||||
<Editable
|
||||
renderLeaf={props => <Leaf {...props} />}
|
||||
placeholder="Enter some text..."
|
||||
onDOMBeforeInput={event => {
|
||||
switch (event.inputType) {
|
||||
case 'formatBold':
|
||||
event.preventDefault()
|
||||
return toggleMark(editor, 'bold')
|
||||
case 'formatItalic':
|
||||
event.preventDefault()
|
||||
return toggleMark(editor, 'italic')
|
||||
case 'formatUnderline':
|
||||
event.preventDefault()
|
||||
return toggleMark(editor, 'underlined')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Slate>
|
||||
)
|
||||
}
|
||||
const toggleMark = (editor, format) => {
|
||||
const isActive = isMarkActive(editor, format)
|
||||
if (isActive) {
|
||||
Editor.removeMark(editor, format)
|
||||
} else {
|
||||
Editor.addMark(editor, format, true)
|
||||
}
|
||||
}
|
||||
const isMarkActive = (editor, format) => {
|
||||
const marks = Editor.marks(editor)
|
||||
return marks ? marks[format] === true : false
|
||||
}
|
||||
const Leaf = ({ attributes, children, leaf }) => {
|
||||
if (leaf.bold) {
|
||||
children = <strong>{children}</strong>
|
||||
}
|
||||
if (leaf.italic) {
|
||||
children = <em>{children}</em>
|
||||
}
|
||||
if (leaf.underlined) {
|
||||
children = <u>{children}</u>
|
||||
}
|
||||
return <span {...attributes}>{children}</span>
|
||||
}
|
||||
const HoveringToolbar = () => {
|
||||
const ref = useRef()
|
||||
const editor = useSlate()
|
||||
const inFocus = useFocused()
|
||||
useEffect(() => {
|
||||
const el = ref.current
|
||||
const { selection } = editor
|
||||
if (!el) {
|
||||
return
|
||||
}
|
||||
if (
|
||||
!selection ||
|
||||
!inFocus ||
|
||||
Range.isCollapsed(selection) ||
|
||||
Editor.string(editor, selection) === ''
|
||||
) {
|
||||
el.removeAttribute('style')
|
||||
return
|
||||
}
|
||||
const domSelection = window.getSelection()
|
||||
const domRange = domSelection.getRangeAt(0)
|
||||
const rect = domRange.getBoundingClientRect()
|
||||
el.style.opacity = '1'
|
||||
el.style.top = `${rect.top + window.pageYOffset - el.offsetHeight}px`
|
||||
el.style.left = `${
|
||||
rect.left + window.pageXOffset - el.offsetWidth / 2 + rect.width / 2
|
||||
}px`
|
||||
})
|
||||
return (
|
||||
<Portal>
|
||||
<Menu
|
||||
ref={ref}
|
||||
className={css`
|
||||
padding: 8px 7px 6px;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: -10000px;
|
||||
left: -10000px;
|
||||
margin-top: -6px;
|
||||
opacity: 0;
|
||||
background-color: #222;
|
||||
border-radius: 4px;
|
||||
transition: opacity 0.75s;
|
||||
`}
|
||||
onMouseDown={e => {
|
||||
// prevent toolbar from taking focus away from editor
|
||||
e.preventDefault()
|
||||
}}
|
||||
>
|
||||
<FormatButton format="bold" icon="format_bold" />
|
||||
<FormatButton format="italic" icon="format_italic" />
|
||||
<FormatButton format="underlined" icon="format_underlined" />
|
||||
</Menu>
|
||||
</Portal>
|
||||
)
|
||||
}
|
||||
const FormatButton = ({ format, icon }) => {
|
||||
const editor = useSlate()
|
||||
return (
|
||||
<Button
|
||||
reversed
|
||||
active={isMarkActive(editor, format)}
|
||||
onClick={() => toggleMark(editor, format)}
|
||||
>
|
||||
<Icon>{icon}</Icon>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
const initialValue = [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
text: 'This example shows how you can make a hovering menu appear above your content, which you can use to make text ',
|
||||
},
|
||||
{ text: 'bold', bold: true },
|
||||
{ text: ', ' },
|
||||
{ text: 'italic', italic: true },
|
||||
{ text: ', or anything else you might want to do!' },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{ text: 'Try it out yourself! Just ' },
|
||||
{ text: 'select any piece of text and the menu will appear', bold: true },
|
||||
{ text: '.' },
|
||||
],
|
||||
},
|
||||
]
|
||||
export default HoveringMenuExample
|
38
site/examples/js/huge-document.jsx
Normal file
38
site/examples/js/huge-document.jsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import React, { useMemo, useCallback } from 'react'
|
||||
import { faker } from '@faker-js/faker'
|
||||
import { createEditor } from 'slate'
|
||||
import { Slate, Editable, withReact } from 'slate-react'
|
||||
|
||||
const HEADINGS = 100
|
||||
const PARAGRAPHS = 7
|
||||
const initialValue = []
|
||||
for (let h = 0; h < HEADINGS; h++) {
|
||||
initialValue.push({
|
||||
type: 'heading',
|
||||
children: [{ text: faker.lorem.sentence() }],
|
||||
})
|
||||
for (let p = 0; p < PARAGRAPHS; p++) {
|
||||
initialValue.push({
|
||||
type: 'paragraph',
|
||||
children: [{ text: faker.lorem.paragraph() }],
|
||||
})
|
||||
}
|
||||
}
|
||||
const HugeDocumentExample = () => {
|
||||
const renderElement = useCallback(props => <Element {...props} />, [])
|
||||
const editor = useMemo(() => withReact(createEditor()), [])
|
||||
return (
|
||||
<Slate editor={editor} initialValue={initialValue}>
|
||||
<Editable renderElement={renderElement} spellCheck autoFocus />
|
||||
</Slate>
|
||||
)
|
||||
}
|
||||
const Element = ({ attributes, children, element }) => {
|
||||
switch (element.type) {
|
||||
case 'heading':
|
||||
return <h1 {...attributes}>{children}</h1>
|
||||
default:
|
||||
return <p {...attributes}>{children}</p>
|
||||
}
|
||||
}
|
||||
export default HugeDocumentExample
|
140
site/examples/js/iframe.jsx
Normal file
140
site/examples/js/iframe.jsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import isHotkey from 'is-hotkey'
|
||||
import { Editable, withReact, useSlate, Slate, ReactEditor } from 'slate-react'
|
||||
import { Editor, createEditor } from 'slate'
|
||||
import { withHistory } from 'slate-history'
|
||||
import { Button, Icon, Toolbar } from './components'
|
||||
|
||||
const HOTKEYS = {
|
||||
'mod+b': 'bold',
|
||||
'mod+i': 'italic',
|
||||
'mod+u': 'underline',
|
||||
'mod+`': 'code',
|
||||
}
|
||||
const IFrameExample = () => {
|
||||
const renderElement = useCallback(
|
||||
({ attributes, children }) => <p {...attributes}>{children}</p>,
|
||||
[]
|
||||
)
|
||||
const renderLeaf = useCallback(props => <Leaf {...props} />, [])
|
||||
const editor = useMemo(() => withHistory(withReact(createEditor())), [])
|
||||
const handleBlur = useCallback(() => ReactEditor.deselect(editor), [editor])
|
||||
return (
|
||||
<Slate editor={editor} initialValue={initialValue}>
|
||||
<Toolbar>
|
||||
<MarkButton format="bold" icon="format_bold" />
|
||||
<MarkButton format="italic" icon="format_italic" />
|
||||
<MarkButton format="underline" icon="format_underlined" />
|
||||
<MarkButton format="code" icon="code" />
|
||||
</Toolbar>
|
||||
<IFrame onBlur={handleBlur}>
|
||||
<Editable
|
||||
renderElement={renderElement}
|
||||
renderLeaf={renderLeaf}
|
||||
placeholder="Enter some rich text…"
|
||||
spellCheck
|
||||
autoFocus
|
||||
onKeyDown={event => {
|
||||
for (const hotkey in HOTKEYS) {
|
||||
if (isHotkey(hotkey, event)) {
|
||||
event.preventDefault()
|
||||
const mark = HOTKEYS[hotkey]
|
||||
toggleMark(editor, mark)
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</IFrame>
|
||||
</Slate>
|
||||
)
|
||||
}
|
||||
const toggleMark = (editor, format) => {
|
||||
const isActive = isMarkActive(editor, format)
|
||||
if (isActive) {
|
||||
Editor.removeMark(editor, format)
|
||||
} else {
|
||||
Editor.addMark(editor, format, true)
|
||||
}
|
||||
}
|
||||
const isMarkActive = (editor, format) => {
|
||||
const marks = Editor.marks(editor)
|
||||
return marks ? marks[format] === true : false
|
||||
}
|
||||
const Leaf = ({ attributes, children, leaf }) => {
|
||||
if (leaf.bold) {
|
||||
children = <strong>{children}</strong>
|
||||
}
|
||||
if (leaf.code) {
|
||||
children = <code>{children}</code>
|
||||
}
|
||||
if (leaf.italic) {
|
||||
children = <em>{children}</em>
|
||||
}
|
||||
if (leaf.underline) {
|
||||
children = <u>{children}</u>
|
||||
}
|
||||
return <span {...attributes}>{children}</span>
|
||||
}
|
||||
const MarkButton = ({ format, icon }) => {
|
||||
const editor = useSlate()
|
||||
return (
|
||||
<Button
|
||||
active={isMarkActive(editor, format)}
|
||||
onMouseDown={event => {
|
||||
event.preventDefault()
|
||||
toggleMark(editor, format)
|
||||
}}
|
||||
>
|
||||
<Icon>{icon}</Icon>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
const IFrame = ({ children, ...props }) => {
|
||||
const [iframeBody, setIframeBody] = useState(null)
|
||||
const handleLoad = e => {
|
||||
setIframeBody(e.target.contentDocument.body)
|
||||
}
|
||||
return (
|
||||
<iframe srcDoc={`<!DOCTYPE html>`} {...props} onLoad={handleLoad}>
|
||||
{iframeBody && createPortal(children, iframeBody)}
|
||||
</iframe>
|
||||
)
|
||||
}
|
||||
const initialValue = [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
text: 'In this example, the document gets rendered into a controlled ',
|
||||
},
|
||||
{ text: '<iframe>', code: true },
|
||||
{
|
||||
text: '. This is ',
|
||||
},
|
||||
{
|
||||
text: 'particularly',
|
||||
italic: true,
|
||||
},
|
||||
{
|
||||
text: ' useful, when you need to separate the styles for your editor contents from the ones addressing your UI.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
text: 'This also the only reliable method to preview any ',
|
||||
},
|
||||
{
|
||||
text: 'media queries',
|
||||
bold: true,
|
||||
},
|
||||
{
|
||||
text: ' in your CSS.',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
export default IFrameExample
|
188
site/examples/js/images.jsx
Normal file
188
site/examples/js/images.jsx
Normal file
@@ -0,0 +1,188 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import imageExtensions from 'image-extensions'
|
||||
import isUrl from 'is-url'
|
||||
import isHotkey from 'is-hotkey'
|
||||
import { Transforms, createEditor } from 'slate'
|
||||
import {
|
||||
Slate,
|
||||
Editable,
|
||||
useSlateStatic,
|
||||
useSelected,
|
||||
useFocused,
|
||||
withReact,
|
||||
ReactEditor,
|
||||
} from 'slate-react'
|
||||
import { withHistory } from 'slate-history'
|
||||
import { css } from '@emotion/css'
|
||||
import { Button, Icon, Toolbar } from './components'
|
||||
|
||||
const ImagesExample = () => {
|
||||
const editor = useMemo(
|
||||
() => withImages(withHistory(withReact(createEditor()))),
|
||||
[]
|
||||
)
|
||||
return (
|
||||
<Slate editor={editor} initialValue={initialValue}>
|
||||
<Toolbar>
|
||||
<InsertImageButton />
|
||||
</Toolbar>
|
||||
<Editable
|
||||
onKeyDown={event => {
|
||||
if (isHotkey('mod+a', event)) {
|
||||
event.preventDefault()
|
||||
Transforms.select(editor, [])
|
||||
}
|
||||
}}
|
||||
renderElement={props => <Element {...props} />}
|
||||
placeholder="Enter some text..."
|
||||
/>
|
||||
</Slate>
|
||||
)
|
||||
}
|
||||
const withImages = editor => {
|
||||
const { insertData, isVoid } = editor
|
||||
editor.isVoid = element => {
|
||||
return element.type === 'image' ? true : isVoid(element)
|
||||
}
|
||||
editor.insertData = data => {
|
||||
const text = data.getData('text/plain')
|
||||
const { files } = data
|
||||
if (files && files.length > 0) {
|
||||
for (const file of files) {
|
||||
const reader = new FileReader()
|
||||
const [mime] = file.type.split('/')
|
||||
if (mime === 'image') {
|
||||
reader.addEventListener('load', () => {
|
||||
const url = reader.result
|
||||
insertImage(editor, url)
|
||||
})
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
}
|
||||
} else if (isImageUrl(text)) {
|
||||
insertImage(editor, text)
|
||||
} else {
|
||||
insertData(data)
|
||||
}
|
||||
}
|
||||
return editor
|
||||
}
|
||||
const insertImage = (editor, url) => {
|
||||
const text = { text: '' }
|
||||
const image = { type: 'image', url, children: [text] }
|
||||
Transforms.insertNodes(editor, image)
|
||||
Transforms.insertNodes(editor, {
|
||||
type: 'paragraph',
|
||||
children: [{ text: '' }],
|
||||
})
|
||||
}
|
||||
const Element = props => {
|
||||
const { attributes, children, element } = props
|
||||
switch (element.type) {
|
||||
case 'image':
|
||||
return <Image {...props} />
|
||||
default:
|
||||
return <p {...attributes}>{children}</p>
|
||||
}
|
||||
}
|
||||
const Image = ({ attributes, children, element }) => {
|
||||
const editor = useSlateStatic()
|
||||
const path = ReactEditor.findPath(editor, element)
|
||||
const selected = useSelected()
|
||||
const focused = useFocused()
|
||||
return (
|
||||
<div {...attributes}>
|
||||
{children}
|
||||
<div
|
||||
contentEditable={false}
|
||||
className={css`
|
||||
position: relative;
|
||||
`}
|
||||
>
|
||||
<img
|
||||
src={element.url}
|
||||
className={css`
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
max-height: 20em;
|
||||
box-shadow: ${selected && focused ? '0 0 0 3px #B4D5FF' : 'none'};
|
||||
`}
|
||||
/>
|
||||
<Button
|
||||
active
|
||||
onClick={() => Transforms.removeNodes(editor, { at: path })}
|
||||
className={css`
|
||||
display: ${selected && focused ? 'inline' : 'none'};
|
||||
position: absolute;
|
||||
top: 0.5em;
|
||||
left: 0.5em;
|
||||
background-color: white;
|
||||
`}
|
||||
>
|
||||
<Icon>delete</Icon>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const InsertImageButton = () => {
|
||||
const editor = useSlateStatic()
|
||||
return (
|
||||
<Button
|
||||
onMouseDown={event => {
|
||||
event.preventDefault()
|
||||
const url = window.prompt('Enter the URL of the image:')
|
||||
if (url && !isImageUrl(url)) {
|
||||
alert('URL is not an image')
|
||||
return
|
||||
}
|
||||
url && insertImage(editor, url)
|
||||
}}
|
||||
>
|
||||
<Icon>image</Icon>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
const isImageUrl = url => {
|
||||
if (!url) return false
|
||||
if (!isUrl(url)) return false
|
||||
const ext = new URL(url).pathname.split('.').pop()
|
||||
return imageExtensions.includes(ext)
|
||||
}
|
||||
const initialValue = [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
text: 'In addition to nodes that contain editable text, you can also create other types of nodes, like images or videos.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'image',
|
||||
url: 'https://source.unsplash.com/kFrdX5IeQzI',
|
||||
children: [{ text: '' }],
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
text: 'This example shows images in action. It features two ways to add images. You can either add an image via the toolbar icon above, or if you want in on a little secret, copy an image URL to your clipboard and paste it anywhere in the editor!',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
text: 'You can delete images with the cross in the top left. Try deleting this sheep:',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'image',
|
||||
url: 'https://source.unsplash.com/zOwZKwZOZq8',
|
||||
children: [{ text: '' }],
|
||||
},
|
||||
]
|
||||
export default ImagesExample
|
386
site/examples/js/inlines.jsx
Normal file
386
site/examples/js/inlines.jsx
Normal file
@@ -0,0 +1,386 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import isUrl from 'is-url'
|
||||
import { isKeyHotkey } from 'is-hotkey'
|
||||
import { css } from '@emotion/css'
|
||||
import { Editable, withReact, useSlate, useSelected } from 'slate-react'
|
||||
import * as SlateReact from 'slate-react'
|
||||
import {
|
||||
Transforms,
|
||||
Editor,
|
||||
Range,
|
||||
createEditor,
|
||||
Element as SlateElement,
|
||||
} from 'slate'
|
||||
import { withHistory } from 'slate-history'
|
||||
import { Button, Icon, Toolbar } from './components'
|
||||
|
||||
const initialValue = [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
text: 'In addition to block nodes, you can create inline nodes. Here is a ',
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
url: 'https://en.wikipedia.org/wiki/Hypertext',
|
||||
children: [{ text: 'hyperlink' }],
|
||||
},
|
||||
{
|
||||
text: ', and here is a more unusual inline: an ',
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
children: [{ text: 'editable button' }],
|
||||
},
|
||||
{
|
||||
text: '! Here is a read-only inline: ',
|
||||
},
|
||||
{
|
||||
type: 'badge',
|
||||
children: [{ text: 'Approved' }],
|
||||
},
|
||||
{
|
||||
text: '.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
text: 'There are two ways to add links. You can either add a link via the toolbar icon above, or if you want in on a little secret, copy a URL to your keyboard and paste it while a range of text is selected. ',
|
||||
},
|
||||
// The following is an example of an inline at the end of a block.
|
||||
// This is an edge case that can cause issues.
|
||||
{
|
||||
type: 'link',
|
||||
url: 'https://twitter.com/JustMissEmma/status/1448679899531726852',
|
||||
children: [{ text: 'Finally, here is our favorite dog video.' }],
|
||||
},
|
||||
{ text: '' },
|
||||
],
|
||||
},
|
||||
]
|
||||
const InlinesExample = () => {
|
||||
const editor = useMemo(
|
||||
() => withInlines(withHistory(withReact(createEditor()))),
|
||||
[]
|
||||
)
|
||||
const onKeyDown = event => {
|
||||
const { selection } = editor
|
||||
// Default left/right behavior is unit:'character'.
|
||||
// This fails to distinguish between two cursor positions, such as
|
||||
// <inline>foo<cursor/></inline> vs <inline>foo</inline><cursor/>.
|
||||
// Here we modify the behavior to unit:'offset'.
|
||||
// This lets the user step into and out of the inline without stepping over characters.
|
||||
// You may wish to customize this further to only use unit:'offset' in specific cases.
|
||||
if (selection && Range.isCollapsed(selection)) {
|
||||
const { nativeEvent } = event
|
||||
if (isKeyHotkey('left', nativeEvent)) {
|
||||
event.preventDefault()
|
||||
Transforms.move(editor, { unit: 'offset', reverse: true })
|
||||
return
|
||||
}
|
||||
if (isKeyHotkey('right', nativeEvent)) {
|
||||
event.preventDefault()
|
||||
Transforms.move(editor, { unit: 'offset' })
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
return (
|
||||
<SlateReact.Slate editor={editor} initialValue={initialValue}>
|
||||
<Toolbar>
|
||||
<AddLinkButton />
|
||||
<RemoveLinkButton />
|
||||
<ToggleEditableButtonButton />
|
||||
</Toolbar>
|
||||
<Editable
|
||||
renderElement={props => <Element {...props} />}
|
||||
renderLeaf={props => <Text {...props} />}
|
||||
placeholder="Enter some text..."
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
</SlateReact.Slate>
|
||||
)
|
||||
}
|
||||
const withInlines = editor => {
|
||||
const { insertData, insertText, isInline, isElementReadOnly, isSelectable } =
|
||||
editor
|
||||
editor.isInline = element =>
|
||||
['link', 'button', 'badge'].includes(element.type) || isInline(element)
|
||||
editor.isElementReadOnly = element =>
|
||||
element.type === 'badge' || isElementReadOnly(element)
|
||||
editor.isSelectable = element =>
|
||||
element.type !== 'badge' && isSelectable(element)
|
||||
editor.insertText = text => {
|
||||
if (text && isUrl(text)) {
|
||||
wrapLink(editor, text)
|
||||
} else {
|
||||
insertText(text)
|
||||
}
|
||||
}
|
||||
editor.insertData = data => {
|
||||
const text = data.getData('text/plain')
|
||||
if (text && isUrl(text)) {
|
||||
wrapLink(editor, text)
|
||||
} else {
|
||||
insertData(data)
|
||||
}
|
||||
}
|
||||
return editor
|
||||
}
|
||||
const insertLink = (editor, url) => {
|
||||
if (editor.selection) {
|
||||
wrapLink(editor, url)
|
||||
}
|
||||
}
|
||||
const insertButton = editor => {
|
||||
if (editor.selection) {
|
||||
wrapButton(editor)
|
||||
}
|
||||
}
|
||||
const isLinkActive = editor => {
|
||||
const [link] = Editor.nodes(editor, {
|
||||
match: n =>
|
||||
!Editor.isEditor(n) && SlateElement.isElement(n) && n.type === 'link',
|
||||
})
|
||||
return !!link
|
||||
}
|
||||
const isButtonActive = editor => {
|
||||
const [button] = Editor.nodes(editor, {
|
||||
match: n =>
|
||||
!Editor.isEditor(n) && SlateElement.isElement(n) && n.type === 'button',
|
||||
})
|
||||
return !!button
|
||||
}
|
||||
const unwrapLink = editor => {
|
||||
Transforms.unwrapNodes(editor, {
|
||||
match: n =>
|
||||
!Editor.isEditor(n) && SlateElement.isElement(n) && n.type === 'link',
|
||||
})
|
||||
}
|
||||
const unwrapButton = editor => {
|
||||
Transforms.unwrapNodes(editor, {
|
||||
match: n =>
|
||||
!Editor.isEditor(n) && SlateElement.isElement(n) && n.type === 'button',
|
||||
})
|
||||
}
|
||||
const wrapLink = (editor, url) => {
|
||||
if (isLinkActive(editor)) {
|
||||
unwrapLink(editor)
|
||||
}
|
||||
const { selection } = editor
|
||||
const isCollapsed = selection && Range.isCollapsed(selection)
|
||||
const link = {
|
||||
type: 'link',
|
||||
url,
|
||||
children: isCollapsed ? [{ text: url }] : [],
|
||||
}
|
||||
if (isCollapsed) {
|
||||
Transforms.insertNodes(editor, link)
|
||||
} else {
|
||||
Transforms.wrapNodes(editor, link, { split: true })
|
||||
Transforms.collapse(editor, { edge: 'end' })
|
||||
}
|
||||
}
|
||||
const wrapButton = editor => {
|
||||
if (isButtonActive(editor)) {
|
||||
unwrapButton(editor)
|
||||
}
|
||||
const { selection } = editor
|
||||
const isCollapsed = selection && Range.isCollapsed(selection)
|
||||
const button = {
|
||||
type: 'button',
|
||||
children: isCollapsed ? [{ text: 'Edit me!' }] : [],
|
||||
}
|
||||
if (isCollapsed) {
|
||||
Transforms.insertNodes(editor, button)
|
||||
} else {
|
||||
Transforms.wrapNodes(editor, button, { split: true })
|
||||
Transforms.collapse(editor, { edge: 'end' })
|
||||
}
|
||||
}
|
||||
// Put this at the start and end of an inline component to work around this Chromium bug:
|
||||
// https://bugs.chromium.org/p/chromium/issues/detail?id=1249405
|
||||
const InlineChromiumBugfix = () => (
|
||||
<span
|
||||
contentEditable={false}
|
||||
className={css`
|
||||
font-size: 0;
|
||||
`}
|
||||
>
|
||||
{String.fromCodePoint(160) /* Non-breaking space */}
|
||||
</span>
|
||||
)
|
||||
const allowedSchemes = ['http:', 'https:', 'mailto:', 'tel:']
|
||||
const LinkComponent = ({ attributes, children, element }) => {
|
||||
const selected = useSelected()
|
||||
const safeUrl = useMemo(() => {
|
||||
let parsedUrl = null
|
||||
try {
|
||||
parsedUrl = new URL(element.url)
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch {}
|
||||
if (parsedUrl && allowedSchemes.includes(parsedUrl.protocol)) {
|
||||
return parsedUrl.href
|
||||
}
|
||||
return 'about:blank'
|
||||
}, [element.url])
|
||||
return (
|
||||
<a
|
||||
{...attributes}
|
||||
href={safeUrl}
|
||||
className={
|
||||
selected
|
||||
? css`
|
||||
box-shadow: 0 0 0 3px #ddd;
|
||||
`
|
||||
: ''
|
||||
}
|
||||
>
|
||||
<InlineChromiumBugfix />
|
||||
{children}
|
||||
<InlineChromiumBugfix />
|
||||
</a>
|
||||
)
|
||||
}
|
||||
const EditableButtonComponent = ({ attributes, children }) => {
|
||||
return (
|
||||
/*
|
||||
Note that this is not a true button, but a span with button-like CSS.
|
||||
True buttons are display:inline-block, but Chrome and Safari
|
||||
have a bad bug with display:inline-block inside contenteditable:
|
||||
- https://bugs.webkit.org/show_bug.cgi?id=105898
|
||||
- https://bugs.chromium.org/p/chromium/issues/detail?id=1088403
|
||||
Worse, one cannot override the display property: https://github.com/w3c/csswg-drafts/issues/3226
|
||||
The only current workaround is to emulate the appearance of a display:inline button using CSS.
|
||||
*/
|
||||
<span
|
||||
{...attributes}
|
||||
onClick={ev => ev.preventDefault()}
|
||||
// Margin is necessary to clearly show the cursor adjacent to the button
|
||||
className={css`
|
||||
margin: 0 0.1em;
|
||||
|
||||
background-color: #efefef;
|
||||
padding: 2px 6px;
|
||||
border: 1px solid #767676;
|
||||
border-radius: 2px;
|
||||
font-size: 0.9em;
|
||||
`}
|
||||
>
|
||||
<InlineChromiumBugfix />
|
||||
{children}
|
||||
<InlineChromiumBugfix />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
const BadgeComponent = ({ attributes, children, element }) => {
|
||||
const selected = useSelected()
|
||||
return (
|
||||
<span
|
||||
{...attributes}
|
||||
contentEditable={false}
|
||||
className={css`
|
||||
background-color: green;
|
||||
color: white;
|
||||
padding: 2px 6px;
|
||||
border-radius: 2px;
|
||||
font-size: 0.9em;
|
||||
${selected && 'box-shadow: 0 0 0 3px #ddd;'}
|
||||
`}
|
||||
data-playwright-selected={selected}
|
||||
>
|
||||
<InlineChromiumBugfix />
|
||||
{children}
|
||||
<InlineChromiumBugfix />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
const Element = props => {
|
||||
const { attributes, children, element } = props
|
||||
switch (element.type) {
|
||||
case 'link':
|
||||
return <LinkComponent {...props} />
|
||||
case 'button':
|
||||
return <EditableButtonComponent {...props} />
|
||||
case 'badge':
|
||||
return <BadgeComponent {...props} />
|
||||
default:
|
||||
return <p {...attributes}>{children}</p>
|
||||
}
|
||||
}
|
||||
const Text = props => {
|
||||
const { attributes, children, leaf } = props
|
||||
return (
|
||||
<span
|
||||
// The following is a workaround for a Chromium bug where,
|
||||
// if you have an inline at the end of a block,
|
||||
// clicking the end of a block puts the cursor inside the inline
|
||||
// instead of inside the final {text: ''} node
|
||||
// https://github.com/ianstormtaylor/slate/issues/4704#issuecomment-1006696364
|
||||
className={
|
||||
leaf.text === ''
|
||||
? css`
|
||||
padding-left: 0.1px;
|
||||
`
|
||||
: null
|
||||
}
|
||||
{...attributes}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
const AddLinkButton = () => {
|
||||
const editor = useSlate()
|
||||
return (
|
||||
<Button
|
||||
active={isLinkActive(editor)}
|
||||
onMouseDown={event => {
|
||||
event.preventDefault()
|
||||
const url = window.prompt('Enter the URL of the link:')
|
||||
if (!url) return
|
||||
insertLink(editor, url)
|
||||
}}
|
||||
>
|
||||
<Icon>link</Icon>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
const RemoveLinkButton = () => {
|
||||
const editor = useSlate()
|
||||
return (
|
||||
<Button
|
||||
active={isLinkActive(editor)}
|
||||
onMouseDown={event => {
|
||||
if (isLinkActive(editor)) {
|
||||
unwrapLink(editor)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon>link_off</Icon>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
const ToggleEditableButtonButton = () => {
|
||||
const editor = useSlate()
|
||||
return (
|
||||
<Button
|
||||
active
|
||||
onMouseDown={event => {
|
||||
event.preventDefault()
|
||||
if (isButtonActive(editor)) {
|
||||
unwrapButton(editor)
|
||||
} else {
|
||||
insertButton(editor)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon>smart_button</Icon>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
export default InlinesExample
|
117
site/examples/js/markdown-preview.jsx
Normal file
117
site/examples/js/markdown-preview.jsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import Prism from 'prismjs'
|
||||
import 'prismjs/components/prism-markdown'
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import { Slate, Editable, withReact } from 'slate-react'
|
||||
import { Text, createEditor } from 'slate'
|
||||
import { withHistory } from 'slate-history'
|
||||
import { css } from '@emotion/css'
|
||||
|
||||
const MarkdownPreviewExample = () => {
|
||||
const renderLeaf = useCallback(props => <Leaf {...props} />, [])
|
||||
const editor = useMemo(() => withHistory(withReact(createEditor())), [])
|
||||
const decorate = useCallback(([node, path]) => {
|
||||
const ranges = []
|
||||
if (!Text.isText(node)) {
|
||||
return ranges
|
||||
}
|
||||
const getLength = token => {
|
||||
if (typeof token === 'string') {
|
||||
return token.length
|
||||
} else if (typeof token.content === 'string') {
|
||||
return token.content.length
|
||||
} else {
|
||||
return token.content.reduce((l, t) => l + getLength(t), 0)
|
||||
}
|
||||
}
|
||||
const tokens = Prism.tokenize(node.text, Prism.languages.markdown)
|
||||
let start = 0
|
||||
for (const token of tokens) {
|
||||
const length = getLength(token)
|
||||
const end = start + length
|
||||
if (typeof token !== 'string') {
|
||||
ranges.push({
|
||||
[token.type]: true,
|
||||
anchor: { path, offset: start },
|
||||
focus: { path, offset: end },
|
||||
})
|
||||
}
|
||||
start = end
|
||||
}
|
||||
return ranges
|
||||
}, [])
|
||||
return (
|
||||
<Slate editor={editor} initialValue={initialValue}>
|
||||
<Editable
|
||||
decorate={decorate}
|
||||
renderLeaf={renderLeaf}
|
||||
placeholder="Write some markdown..."
|
||||
/>
|
||||
</Slate>
|
||||
)
|
||||
}
|
||||
const Leaf = ({ attributes, children, leaf }) => {
|
||||
return (
|
||||
<span
|
||||
{...attributes}
|
||||
className={css`
|
||||
font-weight: ${leaf.bold && 'bold'};
|
||||
font-style: ${leaf.italic && 'italic'};
|
||||
text-decoration: ${leaf.underlined && 'underline'};
|
||||
${leaf.title &&
|
||||
css`
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
margin: 20px 0 10px 0;
|
||||
`}
|
||||
${leaf.list &&
|
||||
css`
|
||||
padding-left: 10px;
|
||||
font-size: 20px;
|
||||
line-height: 10px;
|
||||
`}
|
||||
${leaf.hr &&
|
||||
css`
|
||||
display: block;
|
||||
text-align: center;
|
||||
border-bottom: 2px solid #ddd;
|
||||
`}
|
||||
${leaf.blockquote &&
|
||||
css`
|
||||
display: inline-block;
|
||||
border-left: 2px solid #ddd;
|
||||
padding-left: 10px;
|
||||
color: #aaa;
|
||||
font-style: italic;
|
||||
`}
|
||||
${leaf.code &&
|
||||
css`
|
||||
font-family: monospace;
|
||||
background-color: #eee;
|
||||
padding: 3px;
|
||||
`}
|
||||
`}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
const initialValue = [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
text: 'Slate is flexible enough to add **decorations** that can format text based on its content. For example, this editor has **Markdown** preview decorations on it, to make it _dead_ simple to make an editor with built-in Markdown previewing.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [{ text: '## Try it out!' }],
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [{ text: 'Try it out for yourself!' }],
|
||||
},
|
||||
]
|
||||
export default MarkdownPreviewExample
|
210
site/examples/js/markdown-shortcuts.jsx
Normal file
210
site/examples/js/markdown-shortcuts.jsx
Normal file
@@ -0,0 +1,210 @@
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import {
|
||||
createEditor,
|
||||
Editor,
|
||||
Element as SlateElement,
|
||||
Node as SlateNode,
|
||||
Point,
|
||||
Range,
|
||||
Transforms,
|
||||
} from 'slate'
|
||||
import { withHistory } from 'slate-history'
|
||||
import { Editable, ReactEditor, Slate, withReact } from 'slate-react'
|
||||
|
||||
const SHORTCUTS = {
|
||||
'*': 'list-item',
|
||||
'-': 'list-item',
|
||||
'+': 'list-item',
|
||||
'>': 'block-quote',
|
||||
'#': 'heading-one',
|
||||
'##': 'heading-two',
|
||||
'###': 'heading-three',
|
||||
'####': 'heading-four',
|
||||
'#####': 'heading-five',
|
||||
'######': 'heading-six',
|
||||
}
|
||||
const MarkdownShortcutsExample = () => {
|
||||
const renderElement = useCallback(props => <Element {...props} />, [])
|
||||
const editor = useMemo(
|
||||
() => withShortcuts(withReact(withHistory(createEditor()))),
|
||||
[]
|
||||
)
|
||||
const handleDOMBeforeInput = useCallback(
|
||||
e => {
|
||||
queueMicrotask(() => {
|
||||
const pendingDiffs = ReactEditor.androidPendingDiffs(editor)
|
||||
const scheduleFlush = pendingDiffs?.some(({ diff, path }) => {
|
||||
if (!diff.text.endsWith(' ')) {
|
||||
return false
|
||||
}
|
||||
const { text } = SlateNode.leaf(editor, path)
|
||||
const beforeText = text.slice(0, diff.start) + diff.text.slice(0, -1)
|
||||
if (!(beforeText in SHORTCUTS)) {
|
||||
return
|
||||
}
|
||||
const blockEntry = Editor.above(editor, {
|
||||
at: path,
|
||||
match: n => SlateElement.isElement(n) && Editor.isBlock(editor, n),
|
||||
})
|
||||
if (!blockEntry) {
|
||||
return false
|
||||
}
|
||||
const [, blockPath] = blockEntry
|
||||
return Editor.isStart(editor, Editor.start(editor, path), blockPath)
|
||||
})
|
||||
if (scheduleFlush) {
|
||||
ReactEditor.androidScheduleFlush(editor)
|
||||
}
|
||||
})
|
||||
},
|
||||
[editor]
|
||||
)
|
||||
return (
|
||||
<Slate editor={editor} initialValue={initialValue}>
|
||||
<Editable
|
||||
onDOMBeforeInput={handleDOMBeforeInput}
|
||||
renderElement={renderElement}
|
||||
placeholder="Write some markdown..."
|
||||
spellCheck
|
||||
autoFocus
|
||||
/>
|
||||
</Slate>
|
||||
)
|
||||
}
|
||||
const withShortcuts = editor => {
|
||||
const { deleteBackward, insertText } = editor
|
||||
editor.insertText = text => {
|
||||
const { selection } = editor
|
||||
if (text.endsWith(' ') && selection && Range.isCollapsed(selection)) {
|
||||
const { anchor } = selection
|
||||
const block = Editor.above(editor, {
|
||||
match: n => SlateElement.isElement(n) && Editor.isBlock(editor, n),
|
||||
})
|
||||
const path = block ? block[1] : []
|
||||
const start = Editor.start(editor, path)
|
||||
const range = { anchor, focus: start }
|
||||
const beforeText = Editor.string(editor, range) + text.slice(0, -1)
|
||||
const type = SHORTCUTS[beforeText]
|
||||
if (type) {
|
||||
Transforms.select(editor, range)
|
||||
if (!Range.isCollapsed(range)) {
|
||||
Transforms.delete(editor)
|
||||
}
|
||||
const newProperties = {
|
||||
type,
|
||||
}
|
||||
Transforms.setNodes(editor, newProperties, {
|
||||
match: n => SlateElement.isElement(n) && Editor.isBlock(editor, n),
|
||||
})
|
||||
if (type === 'list-item') {
|
||||
const list = {
|
||||
type: 'bulleted-list',
|
||||
children: [],
|
||||
}
|
||||
Transforms.wrapNodes(editor, list, {
|
||||
match: n =>
|
||||
!Editor.isEditor(n) &&
|
||||
SlateElement.isElement(n) &&
|
||||
n.type === 'list-item',
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
insertText(text)
|
||||
}
|
||||
editor.deleteBackward = (...args) => {
|
||||
const { selection } = editor
|
||||
if (selection && Range.isCollapsed(selection)) {
|
||||
const match = Editor.above(editor, {
|
||||
match: n => SlateElement.isElement(n) && Editor.isBlock(editor, n),
|
||||
})
|
||||
if (match) {
|
||||
const [block, path] = match
|
||||
const start = Editor.start(editor, path)
|
||||
if (
|
||||
!Editor.isEditor(block) &&
|
||||
SlateElement.isElement(block) &&
|
||||
block.type !== 'paragraph' &&
|
||||
Point.equals(selection.anchor, start)
|
||||
) {
|
||||
const newProperties = {
|
||||
type: 'paragraph',
|
||||
}
|
||||
Transforms.setNodes(editor, newProperties)
|
||||
if (block.type === 'list-item') {
|
||||
Transforms.unwrapNodes(editor, {
|
||||
match: n =>
|
||||
!Editor.isEditor(n) &&
|
||||
SlateElement.isElement(n) &&
|
||||
n.type === 'bulleted-list',
|
||||
split: true,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
deleteBackward(...args)
|
||||
}
|
||||
}
|
||||
return editor
|
||||
}
|
||||
const Element = ({ attributes, children, element }) => {
|
||||
switch (element.type) {
|
||||
case 'block-quote':
|
||||
return <blockquote {...attributes}>{children}</blockquote>
|
||||
case 'bulleted-list':
|
||||
return <ul {...attributes}>{children}</ul>
|
||||
case 'heading-one':
|
||||
return <h1 {...attributes}>{children}</h1>
|
||||
case 'heading-two':
|
||||
return <h2 {...attributes}>{children}</h2>
|
||||
case 'heading-three':
|
||||
return <h3 {...attributes}>{children}</h3>
|
||||
case 'heading-four':
|
||||
return <h4 {...attributes}>{children}</h4>
|
||||
case 'heading-five':
|
||||
return <h5 {...attributes}>{children}</h5>
|
||||
case 'heading-six':
|
||||
return <h6 {...attributes}>{children}</h6>
|
||||
case 'list-item':
|
||||
return <li {...attributes}>{children}</li>
|
||||
default:
|
||||
return <p {...attributes}>{children}</p>
|
||||
}
|
||||
}
|
||||
const initialValue = [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
text: 'The editor gives you full control over the logic you can add. For example, it\'s fairly common to want to add markdown-like shortcuts to editors. So that, when you start a line with "> " you get a blockquote that looks like this:',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'block-quote',
|
||||
children: [{ text: 'A wise quote.' }],
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
text: 'Order when you start a line with "## " you get a level-two heading, like this:',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'heading-two',
|
||||
children: [{ text: 'Try it out!' }],
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
text: 'Try it out for yourself! Try starting a new line with ">", "-", or "#"s.',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
export default MarkdownShortcutsExample
|
692
site/examples/js/mentions.jsx
Normal file
692
site/examples/js/mentions.jsx
Normal file
@@ -0,0 +1,692 @@
|
||||
import React, {
|
||||
useMemo,
|
||||
useCallback,
|
||||
useRef,
|
||||
useEffect,
|
||||
useState,
|
||||
Fragment,
|
||||
} from 'react'
|
||||
import { Editor, Transforms, Range, createEditor } from 'slate'
|
||||
import { withHistory } from 'slate-history'
|
||||
import {
|
||||
Slate,
|
||||
Editable,
|
||||
ReactEditor,
|
||||
withReact,
|
||||
useSelected,
|
||||
useFocused,
|
||||
} from 'slate-react'
|
||||
import { Portal } from './components'
|
||||
import { IS_MAC } from './utils/environment'
|
||||
|
||||
const MentionExample = () => {
|
||||
const ref = useRef()
|
||||
const [target, setTarget] = useState()
|
||||
const [index, setIndex] = useState(0)
|
||||
const [search, setSearch] = useState('')
|
||||
const renderElement = useCallback(props => <Element {...props} />, [])
|
||||
const renderLeaf = useCallback(props => <Leaf {...props} />, [])
|
||||
const editor = useMemo(
|
||||
() => withMentions(withReact(withHistory(createEditor()))),
|
||||
[]
|
||||
)
|
||||
const chars = CHARACTERS.filter(c =>
|
||||
c.toLowerCase().startsWith(search.toLowerCase())
|
||||
).slice(0, 10)
|
||||
const onKeyDown = useCallback(
|
||||
event => {
|
||||
if (target && chars.length > 0) {
|
||||
switch (event.key) {
|
||||
case 'ArrowDown':
|
||||
event.preventDefault()
|
||||
const prevIndex = index >= chars.length - 1 ? 0 : index + 1
|
||||
setIndex(prevIndex)
|
||||
break
|
||||
case 'ArrowUp':
|
||||
event.preventDefault()
|
||||
const nextIndex = index <= 0 ? chars.length - 1 : index - 1
|
||||
setIndex(nextIndex)
|
||||
break
|
||||
case 'Tab':
|
||||
case 'Enter':
|
||||
event.preventDefault()
|
||||
Transforms.select(editor, target)
|
||||
insertMention(editor, chars[index])
|
||||
setTarget(null)
|
||||
break
|
||||
case 'Escape':
|
||||
event.preventDefault()
|
||||
setTarget(null)
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
[chars, editor, index, target]
|
||||
)
|
||||
useEffect(() => {
|
||||
if (target && chars.length > 0) {
|
||||
const el = ref.current
|
||||
const domRange = ReactEditor.toDOMRange(editor, target)
|
||||
const rect = domRange.getBoundingClientRect()
|
||||
el.style.top = `${rect.top + window.pageYOffset + 24}px`
|
||||
el.style.left = `${rect.left + window.pageXOffset}px`
|
||||
}
|
||||
}, [chars.length, editor, index, search, target])
|
||||
return (
|
||||
<Slate
|
||||
editor={editor}
|
||||
initialValue={initialValue}
|
||||
onChange={() => {
|
||||
const { selection } = editor
|
||||
if (selection && Range.isCollapsed(selection)) {
|
||||
const [start] = Range.edges(selection)
|
||||
const wordBefore = Editor.before(editor, start, { unit: 'word' })
|
||||
const before = wordBefore && Editor.before(editor, wordBefore)
|
||||
const beforeRange = before && Editor.range(editor, before, start)
|
||||
const beforeText = beforeRange && Editor.string(editor, beforeRange)
|
||||
const beforeMatch = beforeText && beforeText.match(/^@(\w+)$/)
|
||||
const after = Editor.after(editor, start)
|
||||
const afterRange = Editor.range(editor, start, after)
|
||||
const afterText = Editor.string(editor, afterRange)
|
||||
const afterMatch = afterText.match(/^(\s|$)/)
|
||||
if (beforeMatch && afterMatch) {
|
||||
setTarget(beforeRange)
|
||||
setSearch(beforeMatch[1])
|
||||
setIndex(0)
|
||||
return
|
||||
}
|
||||
}
|
||||
setTarget(null)
|
||||
}}
|
||||
>
|
||||
<Editable
|
||||
renderElement={renderElement}
|
||||
renderLeaf={renderLeaf}
|
||||
onKeyDown={onKeyDown}
|
||||
placeholder="Enter some text..."
|
||||
/>
|
||||
{target && chars.length > 0 && (
|
||||
<Portal>
|
||||
<div
|
||||
ref={ref}
|
||||
style={{
|
||||
top: '-9999px',
|
||||
left: '-9999px',
|
||||
position: 'absolute',
|
||||
zIndex: 1,
|
||||
padding: '3px',
|
||||
background: 'white',
|
||||
borderRadius: '4px',
|
||||
boxShadow: '0 1px 5px rgba(0,0,0,.2)',
|
||||
}}
|
||||
data-cy="mentions-portal"
|
||||
>
|
||||
{chars.map((char, i) => (
|
||||
<div
|
||||
key={char}
|
||||
onClick={() => {
|
||||
Transforms.select(editor, target)
|
||||
insertMention(editor, char)
|
||||
setTarget(null)
|
||||
}}
|
||||
style={{
|
||||
padding: '1px 3px',
|
||||
borderRadius: '3px',
|
||||
cursor: 'pointer',
|
||||
background: i === index ? '#B4D5FF' : 'transparent',
|
||||
}}
|
||||
>
|
||||
{char}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Portal>
|
||||
)}
|
||||
</Slate>
|
||||
)
|
||||
}
|
||||
const withMentions = editor => {
|
||||
const { isInline, isVoid, markableVoid } = editor
|
||||
editor.isInline = element => {
|
||||
return element.type === 'mention' ? true : isInline(element)
|
||||
}
|
||||
editor.isVoid = element => {
|
||||
return element.type === 'mention' ? true : isVoid(element)
|
||||
}
|
||||
editor.markableVoid = element => {
|
||||
return element.type === 'mention' || markableVoid(element)
|
||||
}
|
||||
return editor
|
||||
}
|
||||
const insertMention = (editor, character) => {
|
||||
const mention = {
|
||||
type: 'mention',
|
||||
character,
|
||||
children: [{ text: '' }],
|
||||
}
|
||||
Transforms.insertNodes(editor, mention)
|
||||
Transforms.move(editor)
|
||||
}
|
||||
// Borrow Leaf renderer from the Rich Text example.
|
||||
// In a real project you would get this via `withRichText(editor)` or similar.
|
||||
const Leaf = ({ attributes, children, leaf }) => {
|
||||
if (leaf.bold) {
|
||||
children = <strong>{children}</strong>
|
||||
}
|
||||
if (leaf.code) {
|
||||
children = <code>{children}</code>
|
||||
}
|
||||
if (leaf.italic) {
|
||||
children = <em>{children}</em>
|
||||
}
|
||||
if (leaf.underline) {
|
||||
children = <u>{children}</u>
|
||||
}
|
||||
return <span {...attributes}>{children}</span>
|
||||
}
|
||||
const Element = props => {
|
||||
const { attributes, children, element } = props
|
||||
switch (element.type) {
|
||||
case 'mention':
|
||||
return <Mention {...props} />
|
||||
default:
|
||||
return <p {...attributes}>{children}</p>
|
||||
}
|
||||
}
|
||||
const Mention = ({ attributes, children, element }) => {
|
||||
const selected = useSelected()
|
||||
const focused = useFocused()
|
||||
const style = {
|
||||
padding: '3px 3px 2px',
|
||||
margin: '0 1px',
|
||||
verticalAlign: 'baseline',
|
||||
display: 'inline-block',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: '#eee',
|
||||
fontSize: '0.9em',
|
||||
boxShadow: selected && focused ? '0 0 0 2px #B4D5FF' : 'none',
|
||||
}
|
||||
// See if our empty text child has any styling marks applied and apply those
|
||||
if (element.children[0].bold) {
|
||||
style.fontWeight = 'bold'
|
||||
}
|
||||
if (element.children[0].italic) {
|
||||
style.fontStyle = 'italic'
|
||||
}
|
||||
return (
|
||||
<span
|
||||
{...attributes}
|
||||
contentEditable={false}
|
||||
data-cy={`mention-${element.character.replace(' ', '-')}`}
|
||||
style={style}
|
||||
>
|
||||
{IS_MAC ? (
|
||||
// Mac OS IME https://github.com/ianstormtaylor/slate/issues/3490
|
||||
<Fragment>
|
||||
{children}@{element.character}
|
||||
</Fragment>
|
||||
) : (
|
||||
// Others like Android https://github.com/ianstormtaylor/slate/pull/5360
|
||||
<Fragment>
|
||||
@{element.character}
|
||||
{children}
|
||||
</Fragment>
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
const initialValue = [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
text: 'This example shows how you might implement a simple ',
|
||||
},
|
||||
{
|
||||
text: '@-mentions',
|
||||
bold: true,
|
||||
},
|
||||
{
|
||||
text: ' feature that lets users autocomplete mentioning a user by their username. Which, in this case means Star Wars characters. The ',
|
||||
},
|
||||
{
|
||||
text: 'mentions',
|
||||
bold: true,
|
||||
},
|
||||
{
|
||||
text: ' are rendered as ',
|
||||
},
|
||||
{
|
||||
text: 'void inline elements',
|
||||
code: true,
|
||||
},
|
||||
{
|
||||
text: ' inside the document.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{ text: 'Try mentioning characters, like ' },
|
||||
{
|
||||
type: 'mention',
|
||||
character: 'R2-D2',
|
||||
children: [{ text: '', bold: true }],
|
||||
},
|
||||
{ text: ' or ' },
|
||||
{
|
||||
type: 'mention',
|
||||
character: 'Mace Windu',
|
||||
children: [{ text: '' }],
|
||||
},
|
||||
{ text: '!' },
|
||||
],
|
||||
},
|
||||
]
|
||||
const CHARACTERS = [
|
||||
'Aayla Secura',
|
||||
'Adi Gallia',
|
||||
'Admiral Dodd Rancit',
|
||||
'Admiral Firmus Piett',
|
||||
'Admiral Gial Ackbar',
|
||||
'Admiral Ozzel',
|
||||
'Admiral Raddus',
|
||||
'Admiral Terrinald Screed',
|
||||
'Admiral Trench',
|
||||
'Admiral U.O. Statura',
|
||||
'Agen Kolar',
|
||||
'Agent Kallus',
|
||||
'Aiolin and Morit Astarte',
|
||||
'Aks Moe',
|
||||
'Almec',
|
||||
'Alton Kastle',
|
||||
'Amee',
|
||||
'AP-5',
|
||||
'Armitage Hux',
|
||||
'Artoo',
|
||||
'Arvel Crynyd',
|
||||
'Asajj Ventress',
|
||||
'Aurra Sing',
|
||||
'AZI-3',
|
||||
'Bala-Tik',
|
||||
'Barada',
|
||||
'Bargwill Tomder',
|
||||
'Baron Papanoida',
|
||||
'Barriss Offee',
|
||||
'Baze Malbus',
|
||||
'Bazine Netal',
|
||||
'BB-8',
|
||||
'BB-9E',
|
||||
'Ben Quadinaros',
|
||||
'Berch Teller',
|
||||
'Beru Lars',
|
||||
'Bib Fortuna',
|
||||
'Biggs Darklighter',
|
||||
'Black Krrsantan',
|
||||
'Bo-Katan Kryze',
|
||||
'Boba Fett',
|
||||
'Bobbajo',
|
||||
'Bodhi Rook',
|
||||
'Borvo the Hutt',
|
||||
'Boss Nass',
|
||||
'Bossk',
|
||||
'Breha Antilles-Organa',
|
||||
'Bren Derlin',
|
||||
'Brendol Hux',
|
||||
'BT-1',
|
||||
'C-3PO',
|
||||
'C1-10P',
|
||||
'Cad Bane',
|
||||
'Caluan Ematt',
|
||||
'Captain Gregor',
|
||||
'Captain Phasma',
|
||||
'Captain Quarsh Panaka',
|
||||
'Captain Rex',
|
||||
'Carlist Rieekan',
|
||||
'Casca Panzoro',
|
||||
'Cassian Andor',
|
||||
'Cassio Tagge',
|
||||
'Cham Syndulla',
|
||||
'Che Amanwe Papanoida',
|
||||
'Chewbacca',
|
||||
'Chi Eekway Papanoida',
|
||||
'Chief Chirpa',
|
||||
'Chirrut Îmwe',
|
||||
'Ciena Ree',
|
||||
'Cin Drallig',
|
||||
'Clegg Holdfast',
|
||||
'Cliegg Lars',
|
||||
'Coleman Kcaj',
|
||||
'Coleman Trebor',
|
||||
'Colonel Kaplan',
|
||||
'Commander Bly',
|
||||
'Commander Cody (CC-2224)',
|
||||
'Commander Fil (CC-3714)',
|
||||
'Commander Fox',
|
||||
'Commander Gree',
|
||||
'Commander Jet',
|
||||
'Commander Wolffe',
|
||||
'Conan Antonio Motti',
|
||||
'Conder Kyl',
|
||||
'Constable Zuvio',
|
||||
'Cordé',
|
||||
'Cpatain Typho',
|
||||
'Crix Madine',
|
||||
'Cut Lawquane',
|
||||
'Dak Ralter',
|
||||
'Dapp',
|
||||
'Darth Bane',
|
||||
'Darth Maul',
|
||||
'Darth Tyranus',
|
||||
'Daultay Dofine',
|
||||
'Del Meeko',
|
||||
'Delian Mors',
|
||||
'Dengar',
|
||||
'Depa Billaba',
|
||||
'Derek Klivian',
|
||||
'Dexter Jettster',
|
||||
'Dineé Ellberger',
|
||||
'DJ',
|
||||
'Doctor Aphra',
|
||||
'Doctor Evazan',
|
||||
'Dogma',
|
||||
'Dormé',
|
||||
'Dr. Cylo',
|
||||
'Droidbait',
|
||||
'Droopy McCool',
|
||||
'Dryden Vos',
|
||||
'Dud Bolt',
|
||||
'Ebe E. Endocott',
|
||||
'Echuu Shen-Jon',
|
||||
'Eeth Koth',
|
||||
'Eighth Brother',
|
||||
'Eirtaé',
|
||||
'Eli Vanto',
|
||||
'Ellé',
|
||||
'Ello Asty',
|
||||
'Embo',
|
||||
'Eneb Ray',
|
||||
'Enfys Nest',
|
||||
'EV-9D9',
|
||||
'Evaan Verlaine',
|
||||
'Even Piell',
|
||||
'Ezra Bridger',
|
||||
'Faro Argyus',
|
||||
'Feral',
|
||||
'Fifth Brother',
|
||||
'Finis Valorum',
|
||||
'Finn',
|
||||
'Fives',
|
||||
'FN-1824',
|
||||
'FN-2003',
|
||||
'Fodesinbeed Annodue',
|
||||
'Fulcrum',
|
||||
'FX-7',
|
||||
'GA-97',
|
||||
'Galen Erso',
|
||||
'Gallius Rax',
|
||||
'Garazeb "Zeb" Orrelios',
|
||||
'Gardulla the Hutt',
|
||||
'Garrick Versio',
|
||||
'Garven Dreis',
|
||||
'Gavyn Sykes',
|
||||
'Gideon Hask',
|
||||
'Gizor Dellso',
|
||||
'Gonk droid',
|
||||
'Grand Inquisitor',
|
||||
'Greeata Jendowanian',
|
||||
'Greedo',
|
||||
'Greer Sonnel',
|
||||
'Grievous',
|
||||
'Grummgar',
|
||||
'Gungi',
|
||||
'Hammerhead',
|
||||
'Han Solo',
|
||||
'Harter Kalonia',
|
||||
'Has Obbit',
|
||||
'Hera Syndulla',
|
||||
'Hevy',
|
||||
'Hondo Ohnaka',
|
||||
'Huyang',
|
||||
'Iden Versio',
|
||||
'IG-88',
|
||||
'Ima-Gun Di',
|
||||
'Inquisitors',
|
||||
'Inspector Thanoth',
|
||||
'Jabba',
|
||||
'Jacen Syndulla',
|
||||
'Jan Dodonna',
|
||||
'Jango Fett',
|
||||
'Janus Greejatus',
|
||||
'Jar Jar Binks',
|
||||
'Jas Emari',
|
||||
'Jaxxon',
|
||||
'Jek Tono Porkins',
|
||||
'Jeremoch Colton',
|
||||
'Jira',
|
||||
'Jobal Naberrie',
|
||||
'Jocasta Nu',
|
||||
'Joclad Danva',
|
||||
'Joh Yowza',
|
||||
'Jom Barell',
|
||||
'Joph Seastriker',
|
||||
'Jova Tarkin',
|
||||
'Jubnuk',
|
||||
'Jyn Erso',
|
||||
'K-2SO',
|
||||
'Kanan Jarrus',
|
||||
'Karbin',
|
||||
'Karina the Great',
|
||||
'Kes Dameron',
|
||||
'Ketsu Onyo',
|
||||
'Ki-Adi-Mundi',
|
||||
'King Katuunko',
|
||||
'Kit Fisto',
|
||||
'Kitster Banai',
|
||||
'Klaatu',
|
||||
'Klik-Klak',
|
||||
'Korr Sella',
|
||||
'Kylo Ren',
|
||||
'L3-37',
|
||||
'Lama Su',
|
||||
'Lando Calrissian',
|
||||
'Lanever Villecham',
|
||||
'Leia Organa',
|
||||
'Letta Turmond',
|
||||
'Lieutenant Kaydel Ko Connix',
|
||||
'Lieutenant Thire',
|
||||
'Lobot',
|
||||
'Logray',
|
||||
'Lok Durd',
|
||||
'Longo Two-Guns',
|
||||
'Lor San Tekka',
|
||||
'Lorth Needa',
|
||||
'Lott Dod',
|
||||
'Luke Skywalker',
|
||||
'Lumat',
|
||||
'Luminara Unduli',
|
||||
'Lux Bonteri',
|
||||
'Lyn Me',
|
||||
'Lyra Erso',
|
||||
'Mace Windu',
|
||||
'Malakili',
|
||||
'Mama the Hutt',
|
||||
'Mars Guo',
|
||||
'Mas Amedda',
|
||||
'Mawhonic',
|
||||
'Max Rebo',
|
||||
'Maximilian Veers',
|
||||
'Maz Kanata',
|
||||
'ME-8D9',
|
||||
'Meena Tills',
|
||||
'Mercurial Swift',
|
||||
'Mina Bonteri',
|
||||
'Miraj Scintel',
|
||||
'Mister Bones',
|
||||
'Mod Terrik',
|
||||
'Moden Canady',
|
||||
'Mon Mothma',
|
||||
'Moradmin Bast',
|
||||
'Moralo Eval',
|
||||
'Morley',
|
||||
'Mother Talzin',
|
||||
'Nahdar Vebb',
|
||||
'Nahdonnis Praji',
|
||||
'Nien Nunb',
|
||||
'Niima the Hutt',
|
||||
'Nines',
|
||||
'Norra Wexley',
|
||||
'Nute Gunray',
|
||||
'Nuvo Vindi',
|
||||
'Obi-Wan Kenobi',
|
||||
'Odd Ball',
|
||||
'Ody Mandrell',
|
||||
'Omi',
|
||||
'Onaconda Farr',
|
||||
'Oola',
|
||||
'OOM-9',
|
||||
'Oppo Rancisis',
|
||||
'Orn Free Taa',
|
||||
'Oro Dassyne',
|
||||
'Orrimarko',
|
||||
'Osi Sobeck',
|
||||
'Owen Lars',
|
||||
'Pablo-Jill',
|
||||
'Padmé Amidala',
|
||||
'Pagetti Rook',
|
||||
'Paige Tico',
|
||||
'Paploo',
|
||||
'Petty Officer Thanisson',
|
||||
'Pharl McQuarrie',
|
||||
'Plo Koon',
|
||||
'Po Nudo',
|
||||
'Poe Dameron',
|
||||
'Poggle the Lesser',
|
||||
'Pong Krell',
|
||||
'Pooja Naberrie',
|
||||
'PZ-4CO',
|
||||
'Quarrie',
|
||||
'Quay Tolsite',
|
||||
'Queen Apailana',
|
||||
'Queen Jamillia',
|
||||
'Queen Neeyutnee',
|
||||
'Qui-Gon Jinn',
|
||||
'Quiggold',
|
||||
'Quinlan Vos',
|
||||
'R2-D2',
|
||||
'R2-KT',
|
||||
'R3-S6',
|
||||
'R4-P17',
|
||||
'R5-D4',
|
||||
'RA-7',
|
||||
'Rabé',
|
||||
'Rako Hardeen',
|
||||
'Ransolm Casterfo',
|
||||
'Rappertunie',
|
||||
'Ratts Tyerell',
|
||||
'Raymus Antilles',
|
||||
'Ree-Yees',
|
||||
'Reeve Panzoro',
|
||||
'Rey',
|
||||
'Ric Olié',
|
||||
'Riff Tamson',
|
||||
'Riley',
|
||||
'Rinnriyin Di',
|
||||
'Rio Durant',
|
||||
'Rogue Squadron',
|
||||
'Romba',
|
||||
'Roos Tarpals',
|
||||
'Rose Tico',
|
||||
'Rotta the Hutt',
|
||||
'Rukh',
|
||||
'Rune Haako',
|
||||
'Rush Clovis',
|
||||
'Ruwee Naberrie',
|
||||
'Ryoo Naberrie',
|
||||
'Sabé',
|
||||
'Sabine Wren',
|
||||
'Saché',
|
||||
'Saelt-Marae',
|
||||
'Saesee Tiin',
|
||||
'Salacious B. Crumb',
|
||||
'San Hill',
|
||||
'Sana Starros',
|
||||
'Sarco Plank',
|
||||
'Sarkli',
|
||||
'Satine Kryze',
|
||||
'Savage Opress',
|
||||
'Sebulba',
|
||||
'Senator Organa',
|
||||
'Sergeant Kreel',
|
||||
'Seventh Sister',
|
||||
'Shaak Ti',
|
||||
'Shara Bey',
|
||||
'Shmi Skywalker',
|
||||
'Shu Mai',
|
||||
'Sidon Ithano',
|
||||
'Sifo-Dyas',
|
||||
'Sim Aloo',
|
||||
'Siniir Rath Velus',
|
||||
'Sio Bibble',
|
||||
'Sixth Brother',
|
||||
'Slowen Lo',
|
||||
'Sly Moore',
|
||||
'Snaggletooth',
|
||||
'Snap Wexley',
|
||||
'Snoke',
|
||||
'Sola Naberrie',
|
||||
'Sora Bulq',
|
||||
'Strono Tuggs',
|
||||
'Sy Snootles',
|
||||
'Tallissan Lintra',
|
||||
'Tarfful',
|
||||
'Tasu Leech',
|
||||
'Taun We',
|
||||
'TC-14',
|
||||
'Tee Watt Kaa',
|
||||
'Teebo',
|
||||
'Teedo',
|
||||
'Teemto Pagalies',
|
||||
'Temiri Blagg',
|
||||
'Tessek',
|
||||
'Tey How',
|
||||
'Thane Kyrell',
|
||||
'The Bendu',
|
||||
'The Smuggler',
|
||||
'Thrawn',
|
||||
'Tiaan Jerjerrod',
|
||||
'Tion Medon',
|
||||
'Tobias Beckett',
|
||||
'Tulon Voidgazer',
|
||||
'Tup',
|
||||
'U9-C4',
|
||||
'Unkar Plutt',
|
||||
'Val Beckett',
|
||||
'Vanden Willard',
|
||||
'Vice Admiral Amilyn Holdo',
|
||||
'Vober Dand',
|
||||
'WAC-47',
|
||||
'Wag Too',
|
||||
'Wald',
|
||||
'Walrus Man',
|
||||
'Warok',
|
||||
'Wat Tambor',
|
||||
'Watto',
|
||||
'Wedge Antilles',
|
||||
'Wes Janson',
|
||||
'Wicket W. Warrick',
|
||||
'Wilhuff Tarkin',
|
||||
'Wollivan',
|
||||
'Wuher',
|
||||
'Wullf Yularen',
|
||||
'Xamuel Lennox',
|
||||
'Yaddle',
|
||||
'Yarael Poof',
|
||||
'Yoda',
|
||||
'Zam Wesell',
|
||||
'Zev Senesca',
|
||||
'Ziro the Hutt',
|
||||
'Zuckuss',
|
||||
]
|
||||
export default MentionExample
|
235
site/examples/js/paste-html.jsx
Normal file
235
site/examples/js/paste-html.jsx
Normal file
@@ -0,0 +1,235 @@
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import { jsx } from 'slate-hyperscript'
|
||||
import { Transforms, createEditor } from 'slate'
|
||||
import { withHistory } from 'slate-history'
|
||||
import { css } from '@emotion/css'
|
||||
import {
|
||||
Slate,
|
||||
Editable,
|
||||
withReact,
|
||||
useSelected,
|
||||
useFocused,
|
||||
} from 'slate-react'
|
||||
|
||||
const ELEMENT_TAGS = {
|
||||
A: el => ({ type: 'link', url: el.getAttribute('href') }),
|
||||
BLOCKQUOTE: () => ({ type: 'quote' }),
|
||||
H1: () => ({ type: 'heading-one' }),
|
||||
H2: () => ({ type: 'heading-two' }),
|
||||
H3: () => ({ type: 'heading-three' }),
|
||||
H4: () => ({ type: 'heading-four' }),
|
||||
H5: () => ({ type: 'heading-five' }),
|
||||
H6: () => ({ type: 'heading-six' }),
|
||||
IMG: el => ({ type: 'image', url: el.getAttribute('src') }),
|
||||
LI: () => ({ type: 'list-item' }),
|
||||
OL: () => ({ type: 'numbered-list' }),
|
||||
P: () => ({ type: 'paragraph' }),
|
||||
PRE: () => ({ type: 'code' }),
|
||||
UL: () => ({ type: 'bulleted-list' }),
|
||||
}
|
||||
// COMPAT: `B` is omitted here because Google Docs uses `<b>` in weird ways.
|
||||
const TEXT_TAGS = {
|
||||
CODE: () => ({ code: true }),
|
||||
DEL: () => ({ strikethrough: true }),
|
||||
EM: () => ({ italic: true }),
|
||||
I: () => ({ italic: true }),
|
||||
S: () => ({ strikethrough: true }),
|
||||
STRONG: () => ({ bold: true }),
|
||||
U: () => ({ underline: true }),
|
||||
}
|
||||
export const deserialize = el => {
|
||||
if (el.nodeType === 3) {
|
||||
return el.textContent
|
||||
} else if (el.nodeType !== 1) {
|
||||
return null
|
||||
} else if (el.nodeName === 'BR') {
|
||||
return '\n'
|
||||
}
|
||||
const { nodeName } = el
|
||||
let parent = el
|
||||
if (
|
||||
nodeName === 'PRE' &&
|
||||
el.childNodes[0] &&
|
||||
el.childNodes[0].nodeName === 'CODE'
|
||||
) {
|
||||
parent = el.childNodes[0]
|
||||
}
|
||||
let children = Array.from(parent.childNodes).map(deserialize).flat()
|
||||
if (children.length === 0) {
|
||||
children = [{ text: '' }]
|
||||
}
|
||||
if (el.nodeName === 'BODY') {
|
||||
return jsx('fragment', {}, children)
|
||||
}
|
||||
if (ELEMENT_TAGS[nodeName]) {
|
||||
const attrs = ELEMENT_TAGS[nodeName](el)
|
||||
return jsx('element', attrs, children)
|
||||
}
|
||||
if (TEXT_TAGS[nodeName]) {
|
||||
const attrs = TEXT_TAGS[nodeName](el)
|
||||
return children.map(child => jsx('text', attrs, child))
|
||||
}
|
||||
return children
|
||||
}
|
||||
const PasteHtmlExample = () => {
|
||||
const renderElement = useCallback(props => <Element {...props} />, [])
|
||||
const renderLeaf = useCallback(props => <Leaf {...props} />, [])
|
||||
const editor = useMemo(
|
||||
() => withHtml(withReact(withHistory(createEditor()))),
|
||||
[]
|
||||
)
|
||||
return (
|
||||
<Slate editor={editor} initialValue={initialValue}>
|
||||
<Editable
|
||||
renderElement={renderElement}
|
||||
renderLeaf={renderLeaf}
|
||||
placeholder="Paste in some HTML..."
|
||||
/>
|
||||
</Slate>
|
||||
)
|
||||
}
|
||||
const withHtml = editor => {
|
||||
const { insertData, isInline, isVoid } = editor
|
||||
editor.isInline = element => {
|
||||
return element.type === 'link' ? true : isInline(element)
|
||||
}
|
||||
editor.isVoid = element => {
|
||||
return element.type === 'image' ? true : isVoid(element)
|
||||
}
|
||||
editor.insertData = data => {
|
||||
const html = data.getData('text/html')
|
||||
if (html) {
|
||||
const parsed = new DOMParser().parseFromString(html, 'text/html')
|
||||
const fragment = deserialize(parsed.body)
|
||||
Transforms.insertFragment(editor, fragment)
|
||||
return
|
||||
}
|
||||
insertData(data)
|
||||
}
|
||||
return editor
|
||||
}
|
||||
const Element = props => {
|
||||
const { attributes, children, element } = props
|
||||
switch (element.type) {
|
||||
default:
|
||||
return <p {...attributes}>{children}</p>
|
||||
case 'quote':
|
||||
return <blockquote {...attributes}>{children}</blockquote>
|
||||
case 'code':
|
||||
return (
|
||||
<pre>
|
||||
<code {...attributes}>{children}</code>
|
||||
</pre>
|
||||
)
|
||||
case 'bulleted-list':
|
||||
return <ul {...attributes}>{children}</ul>
|
||||
case 'heading-one':
|
||||
return <h1 {...attributes}>{children}</h1>
|
||||
case 'heading-two':
|
||||
return <h2 {...attributes}>{children}</h2>
|
||||
case 'heading-three':
|
||||
return <h3 {...attributes}>{children}</h3>
|
||||
case 'heading-four':
|
||||
return <h4 {...attributes}>{children}</h4>
|
||||
case 'heading-five':
|
||||
return <h5 {...attributes}>{children}</h5>
|
||||
case 'heading-six':
|
||||
return <h6 {...attributes}>{children}</h6>
|
||||
case 'list-item':
|
||||
return <li {...attributes}>{children}</li>
|
||||
case 'numbered-list':
|
||||
return <ol {...attributes}>{children}</ol>
|
||||
case 'link':
|
||||
return (
|
||||
<SafeLink href={element.url} {...attributes}>
|
||||
{children}
|
||||
</SafeLink>
|
||||
)
|
||||
case 'image':
|
||||
return <ImageElement {...props} />
|
||||
}
|
||||
}
|
||||
const allowedSchemes = ['http:', 'https:', 'mailto:', 'tel:']
|
||||
const SafeLink = ({ attributes, children, href }) => {
|
||||
const safeHref = useMemo(() => {
|
||||
let parsedUrl = null
|
||||
try {
|
||||
parsedUrl = new URL(href)
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch {}
|
||||
if (parsedUrl && allowedSchemes.includes(parsedUrl.protocol)) {
|
||||
return parsedUrl.href
|
||||
}
|
||||
return 'about:blank'
|
||||
}, [href])
|
||||
return (
|
||||
<a href={safeHref} {...attributes}>
|
||||
{children}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
const ImageElement = ({ attributes, children, element }) => {
|
||||
const selected = useSelected()
|
||||
const focused = useFocused()
|
||||
return (
|
||||
<div {...attributes}>
|
||||
{children}
|
||||
<img
|
||||
src={element.url}
|
||||
className={css`
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
max-height: 20em;
|
||||
box-shadow: ${selected && focused ? '0 0 0 2px blue;' : 'none'};
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const Leaf = ({ attributes, children, leaf }) => {
|
||||
if (leaf.bold) {
|
||||
children = <strong>{children}</strong>
|
||||
}
|
||||
if (leaf.code) {
|
||||
children = <code>{children}</code>
|
||||
}
|
||||
if (leaf.italic) {
|
||||
children = <em>{children}</em>
|
||||
}
|
||||
if (leaf.underline) {
|
||||
children = <u>{children}</u>
|
||||
}
|
||||
if (leaf.strikethrough) {
|
||||
children = <del>{children}</del>
|
||||
}
|
||||
return <span {...attributes}>{children}</span>
|
||||
}
|
||||
const initialValue = [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
text: "By default, pasting content into a Slate editor will use the clipboard's ",
|
||||
},
|
||||
{ text: "'text/plain'", code: true },
|
||||
{
|
||||
text: " data. That's okay for some use cases, but sometimes you want users to be able to paste in content and have it maintain its formatting. To do this, your editor needs to handle ",
|
||||
},
|
||||
{ text: "'text/html'", code: true },
|
||||
{ text: ' data. ' },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [{ text: 'This is an example of doing exactly that!' }],
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
text: "Try it out for yourself! Copy and paste some rendered HTML rich text content (not the source code) from another site into this editor and it's formatting should be preserved.",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
export default PasteHtmlExample
|
22
site/examples/js/plaintext.jsx
Normal file
22
site/examples/js/plaintext.jsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import { createEditor } from 'slate'
|
||||
import { Slate, Editable, withReact } from 'slate-react'
|
||||
import { withHistory } from 'slate-history'
|
||||
|
||||
const PlainTextExample = () => {
|
||||
const editor = useMemo(() => withHistory(withReact(createEditor())), [])
|
||||
return (
|
||||
<Slate editor={editor} initialValue={initialValue}>
|
||||
<Editable placeholder="Enter some plain text..." />
|
||||
</Slate>
|
||||
)
|
||||
}
|
||||
const initialValue = [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{ text: 'This is editable plain text, just like a <textarea>!' },
|
||||
],
|
||||
},
|
||||
]
|
||||
export default PlainTextExample
|
23
site/examples/js/read-only.jsx
Normal file
23
site/examples/js/read-only.jsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import { createEditor } from 'slate'
|
||||
import { Slate, Editable, withReact } from 'slate-react'
|
||||
|
||||
const ReadOnlyExample = () => {
|
||||
const editor = useMemo(() => withReact(createEditor()), [])
|
||||
return (
|
||||
<Slate editor={editor} initialValue={initialValue}>
|
||||
<Editable readOnly placeholder="Enter some plain text..." />
|
||||
</Slate>
|
||||
)
|
||||
}
|
||||
const initialValue = [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
text: 'This example shows what happens when the Editor is set to readOnly, it is not editable',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
export default ReadOnlyExample
|
247
site/examples/js/richtext.jsx
Normal file
247
site/examples/js/richtext.jsx
Normal file
@@ -0,0 +1,247 @@
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import isHotkey from 'is-hotkey'
|
||||
import { Editable, withReact, useSlate, Slate } from 'slate-react'
|
||||
import {
|
||||
Editor,
|
||||
Transforms,
|
||||
createEditor,
|
||||
Element as SlateElement,
|
||||
} from 'slate'
|
||||
import { withHistory } from 'slate-history'
|
||||
import { Button, Icon, Toolbar } from './components'
|
||||
|
||||
const HOTKEYS = {
|
||||
'mod+b': 'bold',
|
||||
'mod+i': 'italic',
|
||||
'mod+u': 'underline',
|
||||
'mod+`': 'code',
|
||||
}
|
||||
const LIST_TYPES = ['numbered-list', 'bulleted-list']
|
||||
const TEXT_ALIGN_TYPES = ['left', 'center', 'right', 'justify']
|
||||
const RichTextExample = () => {
|
||||
const renderElement = useCallback(props => <Element {...props} />, [])
|
||||
const renderLeaf = useCallback(props => <Leaf {...props} />, [])
|
||||
const editor = useMemo(() => withHistory(withReact(createEditor())), [])
|
||||
return (
|
||||
<Slate editor={editor} initialValue={initialValue}>
|
||||
<Toolbar>
|
||||
<MarkButton format="bold" icon="format_bold" />
|
||||
<MarkButton format="italic" icon="format_italic" />
|
||||
<MarkButton format="underline" icon="format_underlined" />
|
||||
<MarkButton format="code" icon="code" />
|
||||
<BlockButton format="heading-one" icon="looks_one" />
|
||||
<BlockButton format="heading-two" icon="looks_two" />
|
||||
<BlockButton format="block-quote" icon="format_quote" />
|
||||
<BlockButton format="numbered-list" icon="format_list_numbered" />
|
||||
<BlockButton format="bulleted-list" icon="format_list_bulleted" />
|
||||
<BlockButton format="left" icon="format_align_left" />
|
||||
<BlockButton format="center" icon="format_align_center" />
|
||||
<BlockButton format="right" icon="format_align_right" />
|
||||
<BlockButton format="justify" icon="format_align_justify" />
|
||||
</Toolbar>
|
||||
<Editable
|
||||
renderElement={renderElement}
|
||||
renderLeaf={renderLeaf}
|
||||
placeholder="Enter some rich text…"
|
||||
spellCheck
|
||||
autoFocus
|
||||
onKeyDown={event => {
|
||||
for (const hotkey in HOTKEYS) {
|
||||
if (isHotkey(hotkey, event)) {
|
||||
event.preventDefault()
|
||||
const mark = HOTKEYS[hotkey]
|
||||
toggleMark(editor, mark)
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Slate>
|
||||
)
|
||||
}
|
||||
const toggleBlock = (editor, format) => {
|
||||
const isActive = isBlockActive(
|
||||
editor,
|
||||
format,
|
||||
TEXT_ALIGN_TYPES.includes(format) ? 'align' : 'type'
|
||||
)
|
||||
const isList = LIST_TYPES.includes(format)
|
||||
Transforms.unwrapNodes(editor, {
|
||||
match: n =>
|
||||
!Editor.isEditor(n) &&
|
||||
SlateElement.isElement(n) &&
|
||||
LIST_TYPES.includes(n.type) &&
|
||||
!TEXT_ALIGN_TYPES.includes(format),
|
||||
split: true,
|
||||
})
|
||||
let newProperties
|
||||
if (TEXT_ALIGN_TYPES.includes(format)) {
|
||||
newProperties = {
|
||||
align: isActive ? undefined : format,
|
||||
}
|
||||
} else {
|
||||
newProperties = {
|
||||
type: isActive ? 'paragraph' : isList ? 'list-item' : format,
|
||||
}
|
||||
}
|
||||
Transforms.setNodes(editor, newProperties)
|
||||
if (!isActive && isList) {
|
||||
const block = { type: format, children: [] }
|
||||
Transforms.wrapNodes(editor, block)
|
||||
}
|
||||
}
|
||||
const toggleMark = (editor, format) => {
|
||||
const isActive = isMarkActive(editor, format)
|
||||
if (isActive) {
|
||||
Editor.removeMark(editor, format)
|
||||
} else {
|
||||
Editor.addMark(editor, format, true)
|
||||
}
|
||||
}
|
||||
const isBlockActive = (editor, format, blockType = 'type') => {
|
||||
const { selection } = editor
|
||||
if (!selection) return false
|
||||
const [match] = Array.from(
|
||||
Editor.nodes(editor, {
|
||||
at: Editor.unhangRange(editor, selection),
|
||||
match: n =>
|
||||
!Editor.isEditor(n) &&
|
||||
SlateElement.isElement(n) &&
|
||||
n[blockType] === format,
|
||||
})
|
||||
)
|
||||
return !!match
|
||||
}
|
||||
const isMarkActive = (editor, format) => {
|
||||
const marks = Editor.marks(editor)
|
||||
return marks ? marks[format] === true : false
|
||||
}
|
||||
const Element = ({ attributes, children, element }) => {
|
||||
const style = { textAlign: element.align }
|
||||
switch (element.type) {
|
||||
case 'block-quote':
|
||||
return (
|
||||
<blockquote style={style} {...attributes}>
|
||||
{children}
|
||||
</blockquote>
|
||||
)
|
||||
case 'bulleted-list':
|
||||
return (
|
||||
<ul style={style} {...attributes}>
|
||||
{children}
|
||||
</ul>
|
||||
)
|
||||
case 'heading-one':
|
||||
return (
|
||||
<h1 style={style} {...attributes}>
|
||||
{children}
|
||||
</h1>
|
||||
)
|
||||
case 'heading-two':
|
||||
return (
|
||||
<h2 style={style} {...attributes}>
|
||||
{children}
|
||||
</h2>
|
||||
)
|
||||
case 'list-item':
|
||||
return (
|
||||
<li style={style} {...attributes}>
|
||||
{children}
|
||||
</li>
|
||||
)
|
||||
case 'numbered-list':
|
||||
return (
|
||||
<ol style={style} {...attributes}>
|
||||
{children}
|
||||
</ol>
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<p style={style} {...attributes}>
|
||||
{children}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
}
|
||||
const Leaf = ({ attributes, children, leaf }) => {
|
||||
if (leaf.bold) {
|
||||
children = <strong>{children}</strong>
|
||||
}
|
||||
if (leaf.code) {
|
||||
children = <code>{children}</code>
|
||||
}
|
||||
if (leaf.italic) {
|
||||
children = <em>{children}</em>
|
||||
}
|
||||
if (leaf.underline) {
|
||||
children = <u>{children}</u>
|
||||
}
|
||||
return <span {...attributes}>{children}</span>
|
||||
}
|
||||
const BlockButton = ({ format, icon }) => {
|
||||
const editor = useSlate()
|
||||
return (
|
||||
<Button
|
||||
active={isBlockActive(
|
||||
editor,
|
||||
format,
|
||||
TEXT_ALIGN_TYPES.includes(format) ? 'align' : 'type'
|
||||
)}
|
||||
onMouseDown={event => {
|
||||
event.preventDefault()
|
||||
toggleBlock(editor, format)
|
||||
}}
|
||||
>
|
||||
<Icon>{icon}</Icon>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
const MarkButton = ({ format, icon }) => {
|
||||
const editor = useSlate()
|
||||
return (
|
||||
<Button
|
||||
active={isMarkActive(editor, format)}
|
||||
onMouseDown={event => {
|
||||
event.preventDefault()
|
||||
toggleMark(editor, format)
|
||||
}}
|
||||
>
|
||||
<Icon>{icon}</Icon>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
const initialValue = [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{ text: 'This is editable ' },
|
||||
{ text: 'rich', bold: true },
|
||||
{ text: ' text, ' },
|
||||
{ text: 'much', italic: true },
|
||||
{ text: ' better than a ' },
|
||||
{ text: '<textarea>', code: true },
|
||||
{ text: '!' },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
text: "Since it's rich text, you can do things like turn a selection of text ",
|
||||
},
|
||||
{ text: 'bold', bold: true },
|
||||
{
|
||||
text: ', or add a semantically rendered block quote in the middle of the page, like this:',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'block-quote',
|
||||
children: [{ text: 'A wise quote.' }],
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
align: 'center',
|
||||
children: [{ text: 'Try it out for yourself!' }],
|
||||
},
|
||||
]
|
||||
export default RichTextExample
|
64
site/examples/js/scroll-into-view.jsx
Normal file
64
site/examples/js/scroll-into-view.jsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import { createEditor } from 'slate'
|
||||
import { Slate, Editable, withReact } from 'slate-react'
|
||||
import { withHistory } from 'slate-history'
|
||||
import { css } from '@emotion/css'
|
||||
import range from 'lodash/range'
|
||||
/**
|
||||
* This is an example we can use to test the scrollIntoView functionality in
|
||||
* `Editable`. Keeping it here for now as we may need it to make sure it is
|
||||
* working properly after adding it.
|
||||
*
|
||||
* If all is good, we can remove this example.
|
||||
*
|
||||
* Note:
|
||||
* The example needs to be added to `[example].tsx` before it can be used.
|
||||
*/
|
||||
const ScrollIntoViewExample = () => {
|
||||
return (
|
||||
<div
|
||||
className={css`
|
||||
height: 320px;
|
||||
overflow-y: scroll;
|
||||
`}
|
||||
>
|
||||
<div
|
||||
className={css`
|
||||
height: 160px;
|
||||
background: #e0e0e0;
|
||||
`}
|
||||
/>
|
||||
<div
|
||||
className={css`
|
||||
height: 320px;
|
||||
overflow-y: scroll;
|
||||
`}
|
||||
>
|
||||
<PlainTextEditor />
|
||||
</div>
|
||||
<div
|
||||
className={css`
|
||||
height: 160px;
|
||||
background: #e0e0e0;
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const PlainTextEditor = () => {
|
||||
const editor = useMemo(() => withHistory(withReact(createEditor())), [])
|
||||
return (
|
||||
<Slate editor={editor} initialValue={initialValue}>
|
||||
<Editable placeholder="Enter some plain text..." />
|
||||
</Slate>
|
||||
)
|
||||
}
|
||||
const initialValue = range(5).map(() => ({
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
text: `There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text. All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet. It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which looks reasonable. The generated Lorem Ipsum is therefore always free from repetition, injected humour, or non-characteristic words etc.`,
|
||||
},
|
||||
],
|
||||
}))
|
||||
export default ScrollIntoViewExample
|
127
site/examples/js/search-highlighting.jsx
Normal file
127
site/examples/js/search-highlighting.jsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import React, { useState, useCallback, useMemo } from 'react'
|
||||
import { Slate, Editable, withReact } from 'slate-react'
|
||||
import { Text, createEditor } from 'slate'
|
||||
import { css } from '@emotion/css'
|
||||
import { withHistory } from 'slate-history'
|
||||
import { Icon, Toolbar } from './components'
|
||||
|
||||
const SearchHighlightingExample = () => {
|
||||
const [search, setSearch] = useState()
|
||||
const editor = useMemo(() => withHistory(withReact(createEditor())), [])
|
||||
const decorate = useCallback(
|
||||
([node, path]) => {
|
||||
const ranges = []
|
||||
if (
|
||||
search &&
|
||||
Array.isArray(node.children) &&
|
||||
node.children.every(Text.isText)
|
||||
) {
|
||||
const texts = node.children.map(it => it.text)
|
||||
const str = texts.join('')
|
||||
const length = search.length
|
||||
let start = str.indexOf(search)
|
||||
let index = 0
|
||||
let iterated = 0
|
||||
while (start !== -1) {
|
||||
// Skip already iterated strings
|
||||
while (
|
||||
index < texts.length &&
|
||||
start >= iterated + texts[index].length
|
||||
) {
|
||||
iterated = iterated + texts[index].length
|
||||
index++
|
||||
}
|
||||
// Find the index of array and relative position
|
||||
let offset = start - iterated
|
||||
let remaining = length
|
||||
while (index < texts.length && remaining > 0) {
|
||||
const currentText = texts[index]
|
||||
const currentPath = [...path, index]
|
||||
const taken = Math.min(remaining, currentText.length - offset)
|
||||
ranges.push({
|
||||
anchor: { path: currentPath, offset },
|
||||
focus: { path: currentPath, offset: offset + taken },
|
||||
highlight: true,
|
||||
})
|
||||
remaining = remaining - taken
|
||||
if (remaining > 0) {
|
||||
iterated = iterated + currentText.length
|
||||
// Next block will be indexed from 0
|
||||
offset = 0
|
||||
index++
|
||||
}
|
||||
}
|
||||
// Looking for next search block
|
||||
start = str.indexOf(search, start + search.length)
|
||||
}
|
||||
}
|
||||
return ranges
|
||||
},
|
||||
[search]
|
||||
)
|
||||
return (
|
||||
<Slate editor={editor} initialValue={initialValue}>
|
||||
<Toolbar>
|
||||
<div
|
||||
className={css`
|
||||
position: relative;
|
||||
`}
|
||||
>
|
||||
<Icon
|
||||
className={css`
|
||||
position: absolute;
|
||||
top: 0.3em;
|
||||
left: 0.4em;
|
||||
color: #ccc;
|
||||
`}
|
||||
>
|
||||
search
|
||||
</Icon>
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Search the text..."
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
className={css`
|
||||
padding-left: 2.5em;
|
||||
width: 100%;
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
</Toolbar>
|
||||
<Editable decorate={decorate} renderLeaf={props => <Leaf {...props} />} />
|
||||
</Slate>
|
||||
)
|
||||
}
|
||||
const Leaf = ({ attributes, children, leaf }) => {
|
||||
return (
|
||||
<span
|
||||
{...attributes}
|
||||
{...(leaf.highlight && { 'data-cy': 'search-highlighted' })}
|
||||
className={css`
|
||||
font-weight: ${leaf.bold && 'bold'};
|
||||
background-color: ${leaf.highlight && '#ffeeba'};
|
||||
`}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
const initialValue = [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
text: 'This is editable text that you can search. As you search, it looks for matching strings of text, and adds ',
|
||||
},
|
||||
{ text: 'decorations', bold: true },
|
||||
{ text: ' to them in realtime.' },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{ text: 'Try it out for yourself by typing in the search box above!' },
|
||||
],
|
||||
},
|
||||
]
|
||||
export default SearchHighlightingExample
|
39
site/examples/js/shadow-dom.jsx
Normal file
39
site/examples/js/shadow-dom.jsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import React, { useMemo, useRef, useEffect } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { createEditor } from 'slate'
|
||||
import { Slate, Editable, withReact } from 'slate-react'
|
||||
import { withHistory } from 'slate-history'
|
||||
|
||||
const ShadowDOM = () => {
|
||||
const container = useRef(null)
|
||||
useEffect(() => {
|
||||
if (container.current.shadowRoot) return
|
||||
// Create a shadow DOM
|
||||
const outerShadowRoot = container.current.attachShadow({ mode: 'open' })
|
||||
const host = document.createElement('div')
|
||||
outerShadowRoot.appendChild(host)
|
||||
// Create a nested shadow DOM
|
||||
const innerShadowRoot = host.attachShadow({ mode: 'open' })
|
||||
const reactRoot = document.createElement('div')
|
||||
innerShadowRoot.appendChild(reactRoot)
|
||||
// Render the editor within the nested shadow DOM
|
||||
const root = createRoot(reactRoot)
|
||||
root.render(<ShadowEditor />)
|
||||
})
|
||||
return <div ref={container} data-cy="outer-shadow-root" />
|
||||
}
|
||||
const ShadowEditor = () => {
|
||||
const editor = useMemo(() => withHistory(withReact(createEditor())), [])
|
||||
return (
|
||||
<Slate editor={editor} initialValue={initialValue}>
|
||||
<Editable placeholder="Enter some plain text..." />
|
||||
</Slate>
|
||||
)
|
||||
}
|
||||
const initialValue = [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [{ text: 'This Editor is rendered within a nested Shadow DOM.' }],
|
||||
},
|
||||
]
|
||||
export default ShadowDOM
|
45
site/examples/js/styling.jsx
Normal file
45
site/examples/js/styling.jsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import { createEditor } from 'slate'
|
||||
import { Slate, Editable, withReact } from 'slate-react'
|
||||
import { withHistory } from 'slate-history'
|
||||
|
||||
const StylingExample = () => {
|
||||
const editor1 = useMemo(() => withHistory(withReact(createEditor())), [])
|
||||
const editor2 = useMemo(() => withHistory(withReact(createEditor())), [])
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '40px' }}>
|
||||
<Slate
|
||||
editor={editor1}
|
||||
initialValue={[
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [{ text: 'This editor is styled using the style prop.' }],
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Editable
|
||||
style={{
|
||||
backgroundColor: 'rgb(255, 230, 156)',
|
||||
minHeight: '200px',
|
||||
outline: 'rgb(0, 128, 0) solid 2px',
|
||||
}}
|
||||
/>
|
||||
</Slate>
|
||||
|
||||
<Slate
|
||||
editor={editor2}
|
||||
initialValue={[
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{ text: 'This editor is styled using the className prop.' },
|
||||
],
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Editable className="fancy" disableDefaultStyles />
|
||||
</Slate>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default StylingExample
|
190
site/examples/js/tables.jsx
Normal file
190
site/examples/js/tables.jsx
Normal file
@@ -0,0 +1,190 @@
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import { Slate, Editable, withReact } from 'slate-react'
|
||||
import {
|
||||
Editor,
|
||||
Range,
|
||||
Point,
|
||||
createEditor,
|
||||
Element as SlateElement,
|
||||
} from 'slate'
|
||||
import { withHistory } from 'slate-history'
|
||||
|
||||
const TablesExample = () => {
|
||||
const renderElement = useCallback(props => <Element {...props} />, [])
|
||||
const renderLeaf = useCallback(props => <Leaf {...props} />, [])
|
||||
const editor = useMemo(
|
||||
() => withTables(withHistory(withReact(createEditor()))),
|
||||
[]
|
||||
)
|
||||
return (
|
||||
<Slate editor={editor} initialValue={initialValue}>
|
||||
<Editable renderElement={renderElement} renderLeaf={renderLeaf} />
|
||||
</Slate>
|
||||
)
|
||||
}
|
||||
const withTables = editor => {
|
||||
const { deleteBackward, deleteForward, insertBreak } = editor
|
||||
editor.deleteBackward = unit => {
|
||||
const { selection } = editor
|
||||
if (selection && Range.isCollapsed(selection)) {
|
||||
const [cell] = Editor.nodes(editor, {
|
||||
match: n =>
|
||||
!Editor.isEditor(n) &&
|
||||
SlateElement.isElement(n) &&
|
||||
n.type === 'table-cell',
|
||||
})
|
||||
if (cell) {
|
||||
const [, cellPath] = cell
|
||||
const start = Editor.start(editor, cellPath)
|
||||
if (Point.equals(selection.anchor, start)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
deleteBackward(unit)
|
||||
}
|
||||
editor.deleteForward = unit => {
|
||||
const { selection } = editor
|
||||
if (selection && Range.isCollapsed(selection)) {
|
||||
const [cell] = Editor.nodes(editor, {
|
||||
match: n =>
|
||||
!Editor.isEditor(n) &&
|
||||
SlateElement.isElement(n) &&
|
||||
n.type === 'table-cell',
|
||||
})
|
||||
if (cell) {
|
||||
const [, cellPath] = cell
|
||||
const end = Editor.end(editor, cellPath)
|
||||
if (Point.equals(selection.anchor, end)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
deleteForward(unit)
|
||||
}
|
||||
editor.insertBreak = () => {
|
||||
const { selection } = editor
|
||||
if (selection) {
|
||||
const [table] = Editor.nodes(editor, {
|
||||
match: n =>
|
||||
!Editor.isEditor(n) &&
|
||||
SlateElement.isElement(n) &&
|
||||
n.type === 'table',
|
||||
})
|
||||
if (table) {
|
||||
return
|
||||
}
|
||||
}
|
||||
insertBreak()
|
||||
}
|
||||
return editor
|
||||
}
|
||||
const Element = ({ attributes, children, element }) => {
|
||||
switch (element.type) {
|
||||
case 'table':
|
||||
return (
|
||||
<table>
|
||||
<tbody {...attributes}>{children}</tbody>
|
||||
</table>
|
||||
)
|
||||
case 'table-row':
|
||||
return <tr {...attributes}>{children}</tr>
|
||||
case 'table-cell':
|
||||
return <td {...attributes}>{children}</td>
|
||||
default:
|
||||
return <p {...attributes}>{children}</p>
|
||||
}
|
||||
}
|
||||
const Leaf = ({ attributes, children, leaf }) => {
|
||||
if (leaf.bold) {
|
||||
children = <strong>{children}</strong>
|
||||
}
|
||||
return <span {...attributes}>{children}</span>
|
||||
}
|
||||
const initialValue = [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
text: 'Since the editor is based on a recursive tree model, similar to an HTML document, you can create complex nested structures, like tables:',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'table',
|
||||
children: [
|
||||
{
|
||||
type: 'table-row',
|
||||
children: [
|
||||
{
|
||||
type: 'table-cell',
|
||||
children: [{ text: '' }],
|
||||
},
|
||||
{
|
||||
type: 'table-cell',
|
||||
children: [{ text: 'Human', bold: true }],
|
||||
},
|
||||
{
|
||||
type: 'table-cell',
|
||||
children: [{ text: 'Dog', bold: true }],
|
||||
},
|
||||
{
|
||||
type: 'table-cell',
|
||||
children: [{ text: 'Cat', bold: true }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'table-row',
|
||||
children: [
|
||||
{
|
||||
type: 'table-cell',
|
||||
children: [{ text: '# of Feet', bold: true }],
|
||||
},
|
||||
{
|
||||
type: 'table-cell',
|
||||
children: [{ text: '2' }],
|
||||
},
|
||||
{
|
||||
type: 'table-cell',
|
||||
children: [{ text: '4' }],
|
||||
},
|
||||
{
|
||||
type: 'table-cell',
|
||||
children: [{ text: '4' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'table-row',
|
||||
children: [
|
||||
{
|
||||
type: 'table-cell',
|
||||
children: [{ text: '# of Lives', bold: true }],
|
||||
},
|
||||
{
|
||||
type: 'table-cell',
|
||||
children: [{ text: '1' }],
|
||||
},
|
||||
{
|
||||
type: 'table-cell',
|
||||
children: [{ text: '1' }],
|
||||
},
|
||||
{
|
||||
type: 'table-cell',
|
||||
children: [{ text: '9' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
text: "This table is just a basic example of rendering a table, and it doesn't have fancy functionality. But you could augment it to add support for navigating with arrow keys, displaying table headers, adding column and rows, or even formulas if you wanted to get really crazy!",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
export default TablesExample
|
4
site/examples/js/utils/environment.js
Normal file
4
site/examples/js/utils/environment.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export const IS_MAC =
|
||||
typeof navigator !== 'undefined' && /Mac OS X/.test(navigator.userAgent)
|
||||
export const IS_ANDROID =
|
||||
typeof navigator !== 'undefined' && /Android/.test(navigator.userAgent)
|
89
site/examples/js/utils/normalize-tokens.js
Normal file
89
site/examples/js/utils/normalize-tokens.js
Normal file
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Copied from prism-react-renderer repo
|
||||
* https://github.com/FormidableLabs/prism-react-renderer/blob/master/src/utils/normalizeTokens.js
|
||||
* */
|
||||
const newlineRe = /\r\n|\r|\n/
|
||||
// Empty lines need to contain a single empty token, denoted with { empty: true }
|
||||
const normalizeEmptyLines = line => {
|
||||
if (line.length === 0) {
|
||||
line.push({
|
||||
types: ['plain'],
|
||||
content: '\n',
|
||||
empty: true,
|
||||
})
|
||||
} else if (line.length === 1 && line[0].content === '') {
|
||||
line[0].content = '\n'
|
||||
line[0].empty = true
|
||||
}
|
||||
}
|
||||
const appendTypes = (types, add) => {
|
||||
const typesSize = types.length
|
||||
if (typesSize > 0 && types[typesSize - 1] === add) {
|
||||
return types
|
||||
}
|
||||
return types.concat(add)
|
||||
}
|
||||
// Takes an array of Prism's tokens and groups them by line, turning plain
|
||||
// strings into tokens as well. Tokens can become recursive in some cases,
|
||||
// which means that their types are concatenated. Plain-string tokens however
|
||||
// are always of type "plain".
|
||||
// This is not recursive to avoid exceeding the call-stack limit, since it's unclear
|
||||
// how nested Prism's tokens can become
|
||||
export const normalizeTokens = tokens => {
|
||||
const typeArrStack = [[]]
|
||||
const tokenArrStack = [tokens]
|
||||
const tokenArrIndexStack = [0]
|
||||
const tokenArrSizeStack = [tokens.length]
|
||||
let i = 0
|
||||
let stackIndex = 0
|
||||
let currentLine = []
|
||||
const acc = [currentLine]
|
||||
while (stackIndex > -1) {
|
||||
while (
|
||||
(i = tokenArrIndexStack[stackIndex]++) < tokenArrSizeStack[stackIndex]
|
||||
) {
|
||||
let content
|
||||
let types = typeArrStack[stackIndex]
|
||||
const tokenArr = tokenArrStack[stackIndex]
|
||||
const token = tokenArr[i]
|
||||
// Determine content and append type to types if necessary
|
||||
if (typeof token === 'string') {
|
||||
types = stackIndex > 0 ? types : ['plain']
|
||||
content = token
|
||||
} else {
|
||||
types = appendTypes(types, token.type)
|
||||
if (token.alias) {
|
||||
types = appendTypes(types, token.alias)
|
||||
}
|
||||
content = token.content
|
||||
}
|
||||
// If token.content is an array, increase the stack depth and repeat this while-loop
|
||||
if (typeof content !== 'string') {
|
||||
stackIndex++
|
||||
typeArrStack.push(types)
|
||||
tokenArrStack.push(content)
|
||||
tokenArrIndexStack.push(0)
|
||||
tokenArrSizeStack.push(content.length)
|
||||
continue
|
||||
}
|
||||
// Split by newlines
|
||||
const splitByNewlines = content.split(newlineRe)
|
||||
const newlineCount = splitByNewlines.length
|
||||
currentLine.push({ types, content: splitByNewlines[0] })
|
||||
// Create a new line for each string on a new line
|
||||
for (let i = 1; i < newlineCount; i++) {
|
||||
normalizeEmptyLines(currentLine)
|
||||
acc.push((currentLine = []))
|
||||
currentLine.push({ types, content: splitByNewlines[i] })
|
||||
}
|
||||
}
|
||||
// Decreate the stack depth
|
||||
stackIndex--
|
||||
typeArrStack.pop()
|
||||
tokenArrStack.pop()
|
||||
tokenArrIndexStack.pop()
|
||||
tokenArrSizeStack.pop()
|
||||
}
|
||||
normalizeEmptyLines(currentLine)
|
||||
return acc
|
||||
}
|
@@ -32,8 +32,8 @@ import { withHistory } from 'slate-history'
|
||||
import isHotkey from 'is-hotkey'
|
||||
import { css } from '@emotion/css'
|
||||
import { CodeBlockElement } from './custom-types.d'
|
||||
import { normalizeTokens } from '../utils/normalize-tokens'
|
||||
import { Button, Icon, Toolbar } from '../components'
|
||||
import { normalizeTokens } from './utils/normalize-tokens'
|
||||
import { Button, Icon, Toolbar } from './components'
|
||||
|
||||
const ParagraphType = 'paragraph'
|
||||
const CodeBlockType = 'code-block'
|
@@ -5,7 +5,7 @@ import { withHistory } from 'slate-history'
|
||||
import { css } from '@emotion/css'
|
||||
|
||||
import RichTextEditor from './richtext'
|
||||
import { Button, Icon, Toolbar } from '../components'
|
||||
import { Button, Icon, Toolbar } from './components'
|
||||
import { EditableVoidElement } from './custom-types.d'
|
||||
|
||||
const EditableVoidsExample = () => {
|
@@ -11,7 +11,7 @@ import {
|
||||
import { css } from '@emotion/css'
|
||||
import { withHistory } from 'slate-history'
|
||||
|
||||
import { Button, Icon, Menu, Portal } from '../components'
|
||||
import { Button, Icon, Menu, Portal } from './components'
|
||||
|
||||
const HoveringMenuExample = () => {
|
||||
const editor = useMemo(() => withHistory(withReact(createEditor())), [])
|
@@ -5,7 +5,7 @@ import { Editable, withReact, useSlate, Slate, ReactEditor } from 'slate-react'
|
||||
import { Editor, createEditor, Descendant } from 'slate'
|
||||
import { withHistory } from 'slate-history'
|
||||
|
||||
import { Button, Icon, Toolbar } from '../components'
|
||||
import { Button, Icon, Toolbar } from './components'
|
||||
|
||||
const HOTKEYS = {
|
||||
'mod+b': 'bold',
|
@@ -15,7 +15,7 @@ import {
|
||||
import { withHistory } from 'slate-history'
|
||||
import { css } from '@emotion/css'
|
||||
|
||||
import { Button, Icon, Toolbar } from '../components'
|
||||
import { Button, Icon, Toolbar } from './components'
|
||||
import { ImageElement } from './custom-types.d'
|
||||
|
||||
const ImagesExample = () => {
|
@@ -15,7 +15,7 @@ import {
|
||||
import { withHistory } from 'slate-history'
|
||||
import { LinkElement, ButtonElement } from './custom-types.d'
|
||||
|
||||
import { Button, Icon, Toolbar } from '../components'
|
||||
import { Button, Icon, Toolbar } from './components'
|
||||
|
||||
const initialValue: Descendant[] = [
|
||||
{
|
@@ -17,9 +17,9 @@ import {
|
||||
useFocused,
|
||||
} from 'slate-react'
|
||||
|
||||
import { Portal } from '../components'
|
||||
import { Portal } from './components'
|
||||
import { MentionElement } from './custom-types.d'
|
||||
import { IS_MAC } from '../utils/environment'
|
||||
import { IS_MAC } from './utils/environment'
|
||||
|
||||
const MentionExample = () => {
|
||||
const ref = useRef<HTMLDivElement | null>()
|
@@ -10,7 +10,7 @@ import {
|
||||
} from 'slate'
|
||||
import { withHistory } from 'slate-history'
|
||||
|
||||
import { Button, Icon, Toolbar } from '../components'
|
||||
import { Button, Icon, Toolbar } from './components'
|
||||
|
||||
const HOTKEYS = {
|
||||
'mod+b': 'bold',
|
@@ -4,7 +4,7 @@ import { Text, Descendant, createEditor } from 'slate'
|
||||
import { css } from '@emotion/css'
|
||||
import { withHistory } from 'slate-history'
|
||||
|
||||
import { Icon, Toolbar } from '../components'
|
||||
import { Icon, Toolbar } from './components'
|
||||
|
||||
const SearchHighlightingExample = () => {
|
||||
const [search, setSearch] = useState<string | undefined>()
|
@@ -1,7 +1,7 @@
|
||||
import { readdirSync } from 'fs'
|
||||
import { join } from 'path'
|
||||
|
||||
const examplePath = join(process.cwd(), 'examples')
|
||||
const examplePath = join(process.cwd(), 'examples/ts')
|
||||
|
||||
export function getAllExamples() {
|
||||
const slugs = readdirSync(examplePath)
|
||||
|
@@ -5,30 +5,30 @@ import Link from 'next/link'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { ErrorBoundary } from 'react-error-boundary'
|
||||
|
||||
import { Icon } from '../../components'
|
||||
import { Icon } from '../../examples/ts/components/index'
|
||||
|
||||
import CheckLists from '../../examples/check-lists'
|
||||
import CodeHighlighting from '../../examples/code-highlighting'
|
||||
import EditableVoids from '../../examples/editable-voids'
|
||||
import Embeds from '../../examples/embeds'
|
||||
import ForcedLayout from '../../examples/forced-layout'
|
||||
import HoveringToolbar from '../../examples/hovering-toolbar'
|
||||
import HugeDocument from '../../examples/huge-document'
|
||||
import Images from '../../examples/images'
|
||||
import Inlines from '../../examples/inlines'
|
||||
import MarkdownPreview from '../../examples/markdown-preview'
|
||||
import MarkdownShortcuts from '../../examples/markdown-shortcuts'
|
||||
import Mentions from '../../examples/mentions'
|
||||
import PasteHtml from '../../examples/paste-html'
|
||||
import PlainText from '../../examples/plaintext'
|
||||
import ReadOnly from '../../examples/read-only'
|
||||
import RichText from '../../examples/richtext'
|
||||
import SearchHighlighting from '../../examples/search-highlighting'
|
||||
import ShadowDOM from '../../examples/shadow-dom'
|
||||
import Styling from '../../examples/styling'
|
||||
import Tables from '../../examples/tables'
|
||||
import IFrames from '../../examples/iframe'
|
||||
import CustomPlaceholder from '../../examples/custom-placeholder'
|
||||
import CheckLists from '../../examples/ts/check-lists'
|
||||
import CodeHighlighting from '../../examples/ts/code-highlighting'
|
||||
import EditableVoids from '../../examples/ts/editable-voids'
|
||||
import Embeds from '../../examples/ts/embeds'
|
||||
import ForcedLayout from '../../examples/ts/forced-layout'
|
||||
import HoveringToolbar from '../../examples/ts/hovering-toolbar'
|
||||
import HugeDocument from '../../examples/ts/huge-document'
|
||||
import Images from '../../examples/ts/images'
|
||||
import Inlines from '../../examples/ts/inlines'
|
||||
import MarkdownPreview from '../../examples/ts/markdown-preview'
|
||||
import MarkdownShortcuts from '../../examples/ts/markdown-shortcuts'
|
||||
import Mentions from '../../examples/ts/mentions'
|
||||
import PasteHtml from '../../examples/ts/paste-html'
|
||||
import PlainText from '../../examples/ts/plaintext'
|
||||
import ReadOnly from '../../examples/ts/read-only'
|
||||
import RichText from '../../examples/ts/richtext'
|
||||
import SearchHighlighting from '../../examples/ts/search-highlighting'
|
||||
import ShadowDOM from '../../examples/ts/shadow-dom'
|
||||
import Styling from '../../examples/ts/styling'
|
||||
import Tables from '../../examples/ts/tables'
|
||||
import IFrames from '../../examples/ts/iframe'
|
||||
import CustomPlaceholder from '../../examples/ts/custom-placeholder'
|
||||
|
||||
// node
|
||||
import { getAllExamples } from '../api'
|
||||
@@ -108,6 +108,18 @@ const A = props => (
|
||||
/>
|
||||
)
|
||||
|
||||
const Pill = props => (
|
||||
<span
|
||||
{...props}
|
||||
className={css`
|
||||
background: #333;
|
||||
border-radius: 9999px;
|
||||
color: #aaa;
|
||||
padding: 0.2em 0.5em;
|
||||
`}
|
||||
/>
|
||||
)
|
||||
|
||||
const TabList = ({ isVisible, ...props }) => (
|
||||
<div
|
||||
{...props}
|
||||
@@ -316,9 +328,14 @@ const ExamplePage = ({ example }: { example: string }) => {
|
||||
<ExampleTitle>
|
||||
{name}
|
||||
<A
|
||||
href={`https://github.com/ianstormtaylor/slate/blob/main/site/examples/${path}.tsx`}
|
||||
href={`https://github.com/ianstormtaylor/slate/blob/main/site/examples/js/${path}.jsx`}
|
||||
>
|
||||
(View Source)
|
||||
<Pill>JS Code</Pill>
|
||||
</A>
|
||||
<A
|
||||
href={`https://github.com/ianstormtaylor/slate/blob/main/site/examples/ts/${path}.tsx`}
|
||||
>
|
||||
<Pill>TS Code</Pill>
|
||||
</A>
|
||||
</ExampleTitle>
|
||||
</ExampleHeader>
|
||||
|
21
site/tsconfig.example.json
Normal file
21
site/tsconfig.example.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"lib": ["dom"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
"downlevelIteration": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"rootDir": "./examples/ts",
|
||||
"outDir": "./examples/js"
|
||||
},
|
||||
"include": ["examples/ts/**/*.ts", "examples/ts/**/*.tsx"]
|
||||
}
|
Reference in New Issue
Block a user