mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-16 20:24:01 +02:00
Add format_text
command, and editor.marks
state (#3308)
* add format_text command, refactor command extensions * update onChange to not receive selection * update docs * fix tests
This commit is contained in:
@@ -4,15 +4,16 @@ All of the behaviors, content and state of a Slate editor is rollup up into a si
|
||||
|
||||
```ts
|
||||
interface Editor {
|
||||
children: Node[]
|
||||
operations: Operation[]
|
||||
selection: Range | null
|
||||
marks: Record<string, any> | null
|
||||
apply: (operation: Operation) => void
|
||||
exec: (command: Command) => void
|
||||
isInline: (element: Element) => boolean
|
||||
isVoid: (element: Element) => boolean
|
||||
normalizeNode: (entry: NodeEntry) => void
|
||||
onChange: () => void
|
||||
children: Node[]
|
||||
operations: Operation[]
|
||||
selection: Range | null
|
||||
[key: string]: any
|
||||
}
|
||||
```
|
||||
|
@@ -45,15 +45,14 @@ const App = () => {
|
||||
|
||||
Of course we haven't rendered anything, so you won't see any changes.
|
||||
|
||||
Next we want to create state for `value` and `selection`:
|
||||
Next we want to create state for `value`:
|
||||
|
||||
```jsx
|
||||
const App = () => {
|
||||
const editor = useMemo(() => withReact(createEditor()), [])
|
||||
|
||||
// Keep track of state for the value and selection of the editor.
|
||||
// Keep track of state for the value of the editor.
|
||||
const [value, setValue] = useState([])
|
||||
const [selection, setSelection] = useState(null)
|
||||
return null
|
||||
}
|
||||
```
|
||||
@@ -66,18 +65,9 @@ The provider component keeps track of your Slate editor, its plugins, its value,
|
||||
const App = () => {
|
||||
const editor = useMemo(() => withReact(createEditor()), [])
|
||||
const [value, setValue] = useState([])
|
||||
const [selection, setSelection] = useState(null)
|
||||
// Render the Slate context.
|
||||
return (
|
||||
<Slate
|
||||
editor={editor}
|
||||
value={value}
|
||||
selection={selection}
|
||||
onChange={(value, selection) => {
|
||||
setValue(value)
|
||||
setSelection(selection)
|
||||
}}
|
||||
/>
|
||||
<Slate editor={editor} value={value} onChange={value => setValue(value)} />
|
||||
)
|
||||
}
|
||||
```
|
||||
@@ -94,18 +84,9 @@ Okay, so the next step is to render the `<Editable>` component itself:
|
||||
const App = () => {
|
||||
const editor = useMemo(() => withReact(createEditor()), [])
|
||||
const [value, setValue] = useState([])
|
||||
const [selection, setSelection] = useState(null)
|
||||
return (
|
||||
// Add the editable component inside the context.
|
||||
<Slate
|
||||
editor={editor}
|
||||
value={value}
|
||||
selection={selection}
|
||||
onChange={(value, selection) => {
|
||||
setValue(value)
|
||||
setSelection(selection)
|
||||
}}
|
||||
>
|
||||
<Slate editor={editor} value={value} onChange={value => setValue(value)}>
|
||||
<Editable />
|
||||
</Slate>
|
||||
)
|
||||
@@ -121,7 +102,6 @@ The value is just plain JSON. Here's one containing a single paragraph block wit
|
||||
```js
|
||||
const App = () => {
|
||||
const editor = useMemo(() => withReact(createEditor()), [])
|
||||
const [selection, setSelection] = useState(null)
|
||||
// Add the initial value when setting up our state.
|
||||
const [value, setValue] = useState([
|
||||
{
|
||||
@@ -131,15 +111,7 @@ const App = () => {
|
||||
])
|
||||
|
||||
return (
|
||||
<Slate
|
||||
editor={editor}
|
||||
value={value}
|
||||
selection={selection}
|
||||
onChange={(value, selection) => {
|
||||
setValue(value)
|
||||
setSelection(selection)
|
||||
}}
|
||||
>
|
||||
<Slate editor={editor} value={value} onChange={value => setValue(value)}>
|
||||
<Editable />
|
||||
</Slate>
|
||||
)
|
||||
|
@@ -11,7 +11,6 @@ Here's our app from earlier:
|
||||
```js
|
||||
const App = () => {
|
||||
const editor = useMemo(() => withReact(createEditor()), [])
|
||||
const [selection, setSelection] = useState(null)
|
||||
const [value, setValue] = useState([
|
||||
{
|
||||
type: 'paragraph',
|
||||
@@ -20,15 +19,7 @@ const App = () => {
|
||||
])
|
||||
|
||||
return (
|
||||
<Slate
|
||||
editor={editor}
|
||||
value={value}
|
||||
selection={selection}
|
||||
onChange={(value, selection) => {
|
||||
setValue(value)
|
||||
setSelection(selection)
|
||||
}}
|
||||
>
|
||||
<Slate editor={editor} value={value} onChange={value => setValue(value)}>
|
||||
<Editable />
|
||||
</Slate>
|
||||
)
|
||||
@@ -40,7 +31,6 @@ Now we add an `onKeyDown` handler:
|
||||
```js
|
||||
const App = () => {
|
||||
const editor = useMemo(() => withReact(createEditor()), [])
|
||||
const [selection, setSelection] = useState(null)
|
||||
const [value, setValue] = useState([
|
||||
{
|
||||
type: 'paragraph',
|
||||
@@ -49,15 +39,7 @@ const App = () => {
|
||||
])
|
||||
|
||||
return (
|
||||
<Slate
|
||||
editor={editor}
|
||||
value={value}
|
||||
selection={selection}
|
||||
onChange={(value, selection) => {
|
||||
setValue(value)
|
||||
setSelection(selection)
|
||||
}}
|
||||
>
|
||||
<Slate editor={editor} value={value} onChange={value => setValue(value)}>
|
||||
<Editable
|
||||
// Define a new handler which prints the key that was pressed.
|
||||
onKeyDown={event => {
|
||||
@@ -78,7 +60,6 @@ Our `onKeyDown` handler might look like this:
|
||||
```js
|
||||
const App = () => {
|
||||
const editor = useMemo(() => withReact(createEditor()), [])
|
||||
const [selection, setSelection] = useState(null)
|
||||
const [value, setValue] = useState([
|
||||
{
|
||||
type: 'paragraph',
|
||||
@@ -87,15 +68,7 @@ const App = () => {
|
||||
])
|
||||
|
||||
return (
|
||||
<Slate
|
||||
editor={editor}
|
||||
value={value}
|
||||
selection={selection}
|
||||
onChange={(value, selection) => {
|
||||
setValue(value)
|
||||
setSelection(selection)
|
||||
}}
|
||||
>
|
||||
<Slate editor={editor} value={value} onChange={value => setValue(value)}>
|
||||
<Editable
|
||||
onKeyDown={event => {
|
||||
if (event.key === '&') {
|
||||
|
@@ -9,7 +9,6 @@ We'll show you how. Let's start with our app from earlier:
|
||||
```js
|
||||
const App = () => {
|
||||
const editor = useMemo(() => withReact(createEditor()), [])
|
||||
const [selection, setSelection] = useState(null)
|
||||
const [value, setValue] = useState([
|
||||
{
|
||||
type: 'paragraph',
|
||||
@@ -18,15 +17,7 @@ const App = () => {
|
||||
])
|
||||
|
||||
return (
|
||||
<Slate
|
||||
editor={editor}
|
||||
value={value}
|
||||
selection={selection}
|
||||
onChange={(value, selection) => {
|
||||
setValue(value)
|
||||
setSelection(selection)
|
||||
}}
|
||||
>
|
||||
<Slate editor={editor} value={value} onChange={value => setValue(value)}>
|
||||
<Editable
|
||||
onKeyDown={event => {
|
||||
if (event.key === '&') {
|
||||
@@ -76,7 +67,6 @@ Now, let's add that renderer to our `Editor`:
|
||||
```js
|
||||
const App = () => {
|
||||
const editor = useMemo(() => withReact(createEditor()), [])
|
||||
const [selection, setSelection] = useState(null)
|
||||
const [value, setValue] = useState([
|
||||
{
|
||||
type: 'paragraph',
|
||||
@@ -96,15 +86,7 @@ const App = () => {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Slate
|
||||
editor={editor}
|
||||
value={value}
|
||||
selection={selection}
|
||||
onChange={(value, selection) => {
|
||||
setValue(value)
|
||||
setSelection(selection)
|
||||
}}
|
||||
>
|
||||
<Slate editor={editor} value={value} onChange={value => setValue(value)}>
|
||||
<Editable
|
||||
// Pass in the `renderElement` function.
|
||||
renderElement={renderElement}
|
||||
@@ -140,7 +122,6 @@ import { Editor } from 'slate'
|
||||
|
||||
const App = () => {
|
||||
const editor = useMemo(() => withReact(createEditor()), [])
|
||||
const [selection, setSelection] = useState(null)
|
||||
const [value, setValue] = useState([
|
||||
{
|
||||
type: 'paragraph',
|
||||
@@ -158,15 +139,7 @@ const App = () => {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Slate
|
||||
editor={editor}
|
||||
value={value}
|
||||
selection={selection}
|
||||
onChange={(value, selection) => {
|
||||
setValue(value)
|
||||
setSelection(selection)
|
||||
}}
|
||||
>
|
||||
<Slate editor={editor} value={value} onChange={value => setValue(value)}>
|
||||
<Editable
|
||||
renderElement={renderElement}
|
||||
onKeyDown={event => {
|
||||
@@ -220,15 +193,7 @@ const App = () => {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Slate
|
||||
editor={editor}
|
||||
value={value}
|
||||
selection={selection}
|
||||
onChange={(value, selection) => {
|
||||
setValue(value)
|
||||
setSelection(selection)
|
||||
}}
|
||||
>
|
||||
<Slate editor={editor} value={value} onChange={value => setValue(value)}>
|
||||
<Editable
|
||||
renderElement={renderElement}
|
||||
onKeyDown={event => {
|
||||
|
@@ -9,7 +9,6 @@ So we start with our app from earlier:
|
||||
```js
|
||||
const App = () => {
|
||||
const editor = useMemo(() => withReact(createEditor()), [])
|
||||
const [selection, setSelection] = useState(null)
|
||||
const [value, setValue] = useState([
|
||||
{
|
||||
type: 'paragraph',
|
||||
@@ -27,15 +26,7 @@ const App = () => {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Slate
|
||||
editor={editor}
|
||||
value={value}
|
||||
selection={selection}
|
||||
onChange={(value, selection) => {
|
||||
setValue(value)
|
||||
setSelection(selection)
|
||||
}}
|
||||
>
|
||||
<Slate editor={editor} value={value} onChange={value => setValue(value)}>
|
||||
<Editable
|
||||
renderElement={renderElement}
|
||||
onKeyDown={event => {
|
||||
@@ -61,7 +52,6 @@ And now, we'll edit the `onKeyDown` handler to make it so that when you press `c
|
||||
```js
|
||||
const App = () => {
|
||||
const editor = useMemo(() => withReact(createEditor()), [])
|
||||
const [selection, setSelection] = useState(null)
|
||||
const [value, setValue] = useState([
|
||||
{
|
||||
type: 'paragraph',
|
||||
@@ -79,15 +69,7 @@ const App = () => {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Slate
|
||||
editor={editor}
|
||||
value={value}
|
||||
selection={selection}
|
||||
onChange={(value, selection) => {
|
||||
setValue(value)
|
||||
setSelection(selection)
|
||||
}}
|
||||
>
|
||||
<Slate editor={editor} value={value} onChange={value => setValue(value)}>
|
||||
<Editable
|
||||
renderElement={renderElement}
|
||||
onKeyDown={event => {
|
||||
@@ -153,7 +135,6 @@ And now, let's tell Slate about that leaf. To do that, we'll pass in the `render
|
||||
```js
|
||||
const App = () => {
|
||||
const editor = useMemo(() => withReact(createEditor()), [])
|
||||
const [selection, setSelection] = useState(null)
|
||||
const [value, setValue] = useState([
|
||||
{
|
||||
type: 'paragraph',
|
||||
@@ -176,15 +157,7 @@ const App = () => {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Slate
|
||||
editor={editor}
|
||||
value={value}
|
||||
selection={selection}
|
||||
onChange={(value, selection) => {
|
||||
setValue(value)
|
||||
setSelection(selection)
|
||||
}}
|
||||
>
|
||||
<Slate editor={editor} value={value} onChange={value => setValue(value)}>
|
||||
<Editable
|
||||
renderElement={renderElement}
|
||||
// Pass in the `renderLeaf` function.
|
||||
|
@@ -13,7 +13,6 @@ We'll start with our app from earlier:
|
||||
```js
|
||||
const App = () => {
|
||||
const editor = useMemo(() => withReact(createEditor()), [])
|
||||
const [selection, setSelection] = useState(null)
|
||||
const [value, setValue] = useState([
|
||||
{
|
||||
type: 'paragraph',
|
||||
@@ -35,15 +34,7 @@ const App = () => {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Slate
|
||||
editor={editor}
|
||||
value={value}
|
||||
selection={selection}
|
||||
onChange={(value, selection) => {
|
||||
setValue(value)
|
||||
setSelection(selection)
|
||||
}}
|
||||
>
|
||||
<Slate editor={editor} value={value} onChange={value => setValue(value)}>
|
||||
<Editable
|
||||
renderElement={renderElement}
|
||||
renderLeaf={renderLeaf}
|
||||
@@ -94,7 +85,6 @@ const withCustom = editor => {
|
||||
const App = () => {
|
||||
// Wrap the editor with our new `withCustom` plugin.
|
||||
const editor = useMemo(() => withCustom(withReact(createEditor())), [])
|
||||
const [selection, setSelection] = useState(null)
|
||||
const [value, setValue] = useState([
|
||||
{
|
||||
type: 'paragraph',
|
||||
@@ -116,15 +106,7 @@ const App = () => {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Slate
|
||||
editor={editor}
|
||||
value={value}
|
||||
selection={selection}
|
||||
onChange={(value, selection) => {
|
||||
setValue(value)
|
||||
setSelection(selection)
|
||||
}}
|
||||
>
|
||||
<Slate editor={editor} value={value} onChange={value => setValue(value)}>
|
||||
<Editable
|
||||
renderElement={renderElement}
|
||||
renderLeaf={renderLeaf}
|
||||
@@ -224,7 +206,6 @@ const CustomEditor = {
|
||||
|
||||
const App = () => {
|
||||
const editor = useMemo(() => withCustom(withReact(createEditor())), [])
|
||||
const [selection, setSelection] = useState(null)
|
||||
const [value, setValue] = useState([
|
||||
{
|
||||
type: 'paragraph',
|
||||
@@ -246,15 +227,7 @@ const App = () => {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Slate
|
||||
editor={editor}
|
||||
value={value}
|
||||
selection={selection}
|
||||
onChange={(value, selection) => {
|
||||
setValue(value)
|
||||
setSelection(selection)
|
||||
}}
|
||||
>
|
||||
<Slate editor={editor} value={value} onChange={value => setValue(value)}>
|
||||
<Editable
|
||||
renderElement={renderElement}
|
||||
renderLeaf={renderLeaf}
|
||||
@@ -289,7 +262,6 @@ Now our commands are clearly defined and you can invoke them from anywhere we ha
|
||||
```js
|
||||
const App = () => {
|
||||
const editor = useMemo(() => withCustom(withReact(createEditor())), [])
|
||||
const [selection, setSelection] = useState(null)
|
||||
const [value, setValue] = useState([
|
||||
{
|
||||
type: 'paragraph',
|
||||
@@ -312,15 +284,7 @@ const App = () => {
|
||||
|
||||
return (
|
||||
// Add a toolbar with buttons that call the same methods.
|
||||
<Slate
|
||||
editor={editor}
|
||||
value={value}
|
||||
selection={selection}
|
||||
onChange={(value, selection) => {
|
||||
setValue(value)
|
||||
setSelection(selection)
|
||||
}}
|
||||
>
|
||||
<Slate editor={editor} value={value} onChange={value => setValue(value)}>
|
||||
<div>
|
||||
<button
|
||||
onMouseDown={event => {
|
||||
|
@@ -9,7 +9,6 @@ Let's start with a basic editor:
|
||||
```js
|
||||
const App = () => {
|
||||
const editor = useMemo(() => withReact(createEditor()), [])
|
||||
const [selection, setSelection] = useState(null)
|
||||
const [value, setValue] = useState([
|
||||
{
|
||||
type: 'paragraph',
|
||||
@@ -18,15 +17,7 @@ const App = () => {
|
||||
])
|
||||
|
||||
return (
|
||||
<Slate
|
||||
editor={editor}
|
||||
value={value}
|
||||
selection={selection}
|
||||
onChange={(value, selection) => {
|
||||
setValue(value)
|
||||
setSelection(selection)
|
||||
}}
|
||||
>
|
||||
<Slate editor={editor} value={value} onChange={value => setValue(value)}>
|
||||
<Editable />
|
||||
</Slate>
|
||||
)
|
||||
@@ -52,7 +43,6 @@ const defaultValue = [
|
||||
|
||||
const App = () => {
|
||||
const editor = useMemo(() => withReact(createEditor()), [])
|
||||
const [selection, setSelection] = useState(null)
|
||||
const [value, setValue] = useState([
|
||||
{
|
||||
type: 'paragraph',
|
||||
@@ -65,9 +55,8 @@ const App = () => {
|
||||
editor={editor}
|
||||
value={value}
|
||||
selection={selection}
|
||||
onChange={(value, selection) => {
|
||||
onChange={value => {
|
||||
setValue(value)
|
||||
setSelection(selection)
|
||||
|
||||
// Save the value to Local Storage.
|
||||
const content = JSON.stringify(value)
|
||||
@@ -87,7 +76,6 @@ But... if you refresh the page, everything is still reset. That's because we nee
|
||||
```js
|
||||
const App = () => {
|
||||
const editor = useMemo(() => withReact(createEditor()), [])
|
||||
const [selection, setSelection] = useState(null)
|
||||
// Update the initial content to be pulled from Local Storage if it exists.
|
||||
const [value, setValue] = useState(
|
||||
JSON.parse(localStorage.getItem('content')) || [
|
||||
@@ -102,10 +90,8 @@ const App = () => {
|
||||
<Slate
|
||||
editor={editor}
|
||||
value={value}
|
||||
selection={selection}
|
||||
onChange={(value, selection) => {
|
||||
onChange={value => {
|
||||
setValue(value)
|
||||
setSelection(selection)
|
||||
const content = JSON.stringify(value)
|
||||
localStorage.setItem('content', content)
|
||||
}}
|
||||
@@ -149,7 +135,6 @@ const deserialize = string => {
|
||||
|
||||
const App = () => {
|
||||
const editor = useMemo(() => withReact(createEditor()), [])
|
||||
const [selection, setSelection] = useState(null)
|
||||
// Use our deserializing function to read the data from Local Storage.
|
||||
const [value, setValue] = useState(
|
||||
deserialize(localStorage.getItem('content')) || ''
|
||||
@@ -159,10 +144,8 @@ const App = () => {
|
||||
<Slate
|
||||
editor={editor}
|
||||
value={value}
|
||||
selection={selection}
|
||||
onChange={(value, selection) => {
|
||||
onChange={value => {
|
||||
setValue(value)
|
||||
setSelection(selection)
|
||||
// Serialize the value and save the string value to Local Storage.
|
||||
localStorage.setItem('content', serialize(value))
|
||||
}}
|
||||
|
@@ -16,24 +16,14 @@ export const HistoryCommand = {
|
||||
*/
|
||||
|
||||
isHistoryCommand(value: any): value is HistoryCommand {
|
||||
return (
|
||||
HistoryCommand.isRedoCommand(value) || HistoryCommand.isUndoCommand(value)
|
||||
)
|
||||
},
|
||||
if (Command.isCommand(value)) {
|
||||
switch (value.type) {
|
||||
case 'redo':
|
||||
case 'undo':
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a value is a `RedoCommand` object.
|
||||
*/
|
||||
|
||||
isRedoCommand(value: any): value is RedoCommand {
|
||||
return Command.isCommand(value) && value.type === 'redo'
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a value is an `UndoCommand` object.
|
||||
*/
|
||||
|
||||
isUndoCommand(value: any): value is UndoCommand {
|
||||
return Command.isCommand(value) && value.type === 'undo'
|
||||
return false
|
||||
},
|
||||
}
|
||||
|
@@ -13,11 +13,14 @@ export const withHistory = (editor: Editor): HistoryEditor => {
|
||||
editor.history = { undos: [], redos: [] }
|
||||
|
||||
editor.exec = (command: Command) => {
|
||||
if (HistoryEditor.isHistoryEditor(editor)) {
|
||||
if (
|
||||
HistoryEditor.isHistoryEditor(editor) &&
|
||||
HistoryCommand.isHistoryCommand(command)
|
||||
) {
|
||||
const { history } = editor
|
||||
const { undos, redos } = history
|
||||
|
||||
if (redos.length > 0 && HistoryCommand.isRedoCommand(command)) {
|
||||
if (command.type === 'redo' && redos.length > 0) {
|
||||
const batch = redos[redos.length - 1]
|
||||
|
||||
HistoryEditor.withoutSaving(editor, () => {
|
||||
@@ -33,7 +36,7 @@ export const withHistory = (editor: Editor): HistoryEditor => {
|
||||
return
|
||||
}
|
||||
|
||||
if (undos.length > 0 && HistoryCommand.isUndoCommand(command)) {
|
||||
if (command.type === 'undo' && undos.length > 0) {
|
||||
const batch = undos[undos.length - 1]
|
||||
|
||||
HistoryEditor.withoutSaving(editor, () => {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import React, { useMemo, useState, useCallback } from 'react'
|
||||
import { Editor, Node, Range } from 'slate'
|
||||
|
||||
import { ReactEditor } from '../plugin/react-editor'
|
||||
@@ -15,19 +15,24 @@ import { EDITOR_TO_ON_CHANGE } from '../utils/weak-maps'
|
||||
export const Slate = (props: {
|
||||
editor: Editor
|
||||
value: Node[]
|
||||
selection: Range | null
|
||||
children: React.ReactNode
|
||||
onChange: (children: Node[], selection: Range | null) => void
|
||||
onChange: (value: Node[]) => void
|
||||
[key: string]: any
|
||||
}) => {
|
||||
const { editor, children, onChange, value, selection, ...rest } = props
|
||||
const { editor, children, onChange, value, ...rest } = props
|
||||
const [key, setKey] = useState(0)
|
||||
const context: [Editor] = useMemo(() => {
|
||||
editor.children = value
|
||||
editor.selection = selection
|
||||
Object.assign(editor, rest)
|
||||
return [editor]
|
||||
}, [value, selection, ...Object.values(rest)])
|
||||
}, [key, value, ...Object.values(rest)])
|
||||
|
||||
EDITOR_TO_ON_CHANGE.set(editor, onChange)
|
||||
const onContextChange = useCallback(() => {
|
||||
onChange(editor.children)
|
||||
setKey(key + 1)
|
||||
}, [key, onChange])
|
||||
|
||||
EDITOR_TO_ON_CHANGE.set(editor, onContextChange)
|
||||
|
||||
return (
|
||||
<SlateContext.Provider value={context}>
|
||||
|
@@ -16,23 +16,18 @@ export interface InsertDataCommand {
|
||||
export type ReactCommand = InsertDataCommand
|
||||
|
||||
export const ReactCommand = {
|
||||
/**
|
||||
* Check if a value is an `InsertDataCommand` object.
|
||||
*/
|
||||
|
||||
isInsertDataCommand(value: any): value is InsertDataCommand {
|
||||
return (
|
||||
Command.isCommand(value) &&
|
||||
value.type === 'insert_data' &&
|
||||
value.data instanceof DataTransfer
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a value is a `ReactCommand` object.
|
||||
*/
|
||||
|
||||
isReactCommand(value: any): value is InsertDataCommand {
|
||||
return ReactCommand.isInsertDataCommand(value)
|
||||
if (Command.isCommand(value)) {
|
||||
switch (value.type) {
|
||||
case 'insert_data':
|
||||
return value.data instanceof DataTransfer
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
}
|
||||
|
@@ -57,7 +57,10 @@ export const withReact = (editor: Editor): Editor => {
|
||||
}
|
||||
|
||||
editor.exec = (command: Command) => {
|
||||
if (ReactCommand.isInsertDataCommand(command)) {
|
||||
if (
|
||||
ReactCommand.isReactCommand(command) &&
|
||||
command.type === 'insert_data'
|
||||
) {
|
||||
const { data } = command
|
||||
const fragment = data.getData('application/x-slate-fragment')
|
||||
|
||||
@@ -94,11 +97,10 @@ export const withReact = (editor: Editor): Editor => {
|
||||
// have to use this unstable API to ensure it batches them. (2019/12/03)
|
||||
// https://github.com/facebook/react/issues/14259#issuecomment-439702367
|
||||
ReactDOM.unstable_batchedUpdates(() => {
|
||||
const contextOnChange = EDITOR_TO_ON_CHANGE.get(editor)
|
||||
const onContextChange = EDITOR_TO_ON_CHANGE.get(editor)
|
||||
|
||||
if (contextOnChange) {
|
||||
const { children, selection } = editor
|
||||
contextOnChange(children, selection)
|
||||
if (onContextChange) {
|
||||
onContextChange()
|
||||
}
|
||||
|
||||
onChange()
|
||||
|
@@ -32,13 +32,10 @@ export const IS_DRAGGING: WeakMap<Editor, boolean> = new WeakMap()
|
||||
export const IS_CLICKING: WeakMap<Editor, boolean> = new WeakMap()
|
||||
|
||||
/**
|
||||
* Weak map for associating the context `onChange` prop with the plugin.
|
||||
* Weak map for associating the context `onChange` context with the plugin.
|
||||
*/
|
||||
|
||||
export const EDITOR_TO_ON_CHANGE = new WeakMap<
|
||||
Editor,
|
||||
(children: Node[], selection: Range | null) => void
|
||||
>()
|
||||
export const EDITOR_TO_ON_CHANGE = new WeakMap<Editor, () => void>()
|
||||
|
||||
/**
|
||||
* Symbols.
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
Command,
|
||||
CoreCommand,
|
||||
Descendant,
|
||||
Editor,
|
||||
Element,
|
||||
@@ -26,6 +27,7 @@ export const createEditor = (): Editor => {
|
||||
children: [],
|
||||
operations: [],
|
||||
selection: null,
|
||||
marks: null,
|
||||
isInline: () => false,
|
||||
isVoid: () => false,
|
||||
onChange: () => {},
|
||||
@@ -73,6 +75,11 @@ export const createEditor = (): Editor => {
|
||||
editor.operations.push(op)
|
||||
Editor.normalize(editor)
|
||||
|
||||
// Clear any formats applied to the cursor if the selection changes.
|
||||
if (op.type === 'set_selection') {
|
||||
editor.marks = null
|
||||
}
|
||||
|
||||
if (!FLUSHING.get(editor)) {
|
||||
FLUSHING.set(editor, true)
|
||||
|
||||
@@ -84,9 +91,9 @@ export const createEditor = (): Editor => {
|
||||
}
|
||||
},
|
||||
exec: (command: Command) => {
|
||||
if (CoreCommand.isCoreCommand(command)) {
|
||||
const { selection } = editor
|
||||
|
||||
if (Command.isCoreCommand(command)) {
|
||||
switch (command.type) {
|
||||
case 'delete_backward': {
|
||||
if (selection && Range.isCollapsed(selection)) {
|
||||
@@ -112,6 +119,56 @@ export const createEditor = (): Editor => {
|
||||
break
|
||||
}
|
||||
|
||||
case 'format_text': {
|
||||
if (selection) {
|
||||
const { properties } = command
|
||||
|
||||
if (Range.isExpanded(selection)) {
|
||||
const [match] = Editor.nodes(editor, {
|
||||
at: selection,
|
||||
match: n => Text.isText(n) && Text.matches(n, properties),
|
||||
// TODO: should be `mode: 'universal'`
|
||||
})
|
||||
|
||||
if (match) {
|
||||
const keys = Object.keys(properties)
|
||||
Editor.unsetNodes(editor, keys, {
|
||||
match: 'text',
|
||||
split: true,
|
||||
})
|
||||
} else {
|
||||
Editor.setNodes(editor, properties, {
|
||||
match: 'text',
|
||||
split: true,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
const marks = { ...(Editor.marks(editor) || {}) }
|
||||
let match = true
|
||||
|
||||
for (const key in properties) {
|
||||
if (marks[key] !== properties[key]) {
|
||||
match = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (match) {
|
||||
for (const key in properties) {
|
||||
delete marks[key]
|
||||
}
|
||||
} else {
|
||||
Object.assign(marks, properties)
|
||||
}
|
||||
|
||||
editor.marks = marks
|
||||
editor.onChange()
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'insert_break': {
|
||||
Editor.splitNodes(editor, { always: true })
|
||||
break
|
||||
@@ -123,7 +180,7 @@ export const createEditor = (): Editor => {
|
||||
}
|
||||
|
||||
case 'insert_node': {
|
||||
Editor.insertNodes(editor, [command.node])
|
||||
Editor.insertNodes(editor, command.node)
|
||||
break
|
||||
}
|
||||
|
||||
@@ -131,8 +188,8 @@ export const createEditor = (): Editor => {
|
||||
if (selection) {
|
||||
const { anchor } = selection
|
||||
|
||||
// If the cursor is at the end of an inline, move it outside
|
||||
// of the inline before inserting
|
||||
// If the cursor is at the end of an inline, move it outside of
|
||||
// the inline before inserting
|
||||
if (Range.isCollapsed(selection)) {
|
||||
const inline = Editor.match(editor, anchor, 'inline')
|
||||
|
||||
@@ -146,7 +203,17 @@ export const createEditor = (): Editor => {
|
||||
}
|
||||
}
|
||||
|
||||
Editor.insertText(editor, command.text)
|
||||
const { marks } = editor
|
||||
const { text } = command
|
||||
|
||||
if (marks) {
|
||||
const node = { text, ...marks }
|
||||
Editor.insertNodes(editor, node)
|
||||
} else {
|
||||
Editor.insertText(editor, text)
|
||||
}
|
||||
|
||||
editor.marks = null
|
||||
}
|
||||
break
|
||||
}
|
||||
|
@@ -19,97 +19,6 @@ export const Command = {
|
||||
isCommand(value: any): value is Command {
|
||||
return isPlainObject(value) && typeof value.type === 'string'
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a value is a `CoreCommand` object.
|
||||
*/
|
||||
|
||||
isCoreCommand(value: any): value is CoreCommand {
|
||||
return (
|
||||
Command.isDeleteBackwardCommand(value) ||
|
||||
Command.isDeleteForwardCommand(value) ||
|
||||
Command.isDeleteFragmentCommand(value) ||
|
||||
Command.isInsertTextCommand(value) ||
|
||||
Command.isInsertFragmentCommand(value) ||
|
||||
Command.isInsertBreakCommand(value)
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a value is a `DeleteBackwardCommand` object.
|
||||
*/
|
||||
|
||||
isDeleteBackwardCommand(value: any): value is DeleteBackwardCommand {
|
||||
return (
|
||||
Command.isCommand(value) &&
|
||||
value.type === 'delete_backward' &&
|
||||
typeof value.unit === 'string'
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a value is a `DeleteForwardCommand` object.
|
||||
*/
|
||||
|
||||
isDeleteForwardCommand(value: any): value is DeleteForwardCommand {
|
||||
return (
|
||||
Command.isCommand(value) &&
|
||||
value.type === 'delete_forward' &&
|
||||
typeof value.unit === 'string'
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a value is a `DeleteFragmentCommand` object.
|
||||
*/
|
||||
|
||||
isDeleteFragmentCommand(value: any): value is DeleteFragmentCommand {
|
||||
return Command.isCommand(value) && value.type === 'delete_fragment'
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a value is an `InsertBreakCommand` object.
|
||||
*/
|
||||
|
||||
isInsertBreakCommand(value: any): value is InsertBreakCommand {
|
||||
return Command.isCommand(value) && value.type === 'insert_break'
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a value is an `InsertFragmentCommand` object.
|
||||
*/
|
||||
|
||||
isInsertFragmentCommand(value: any): value is InsertFragmentCommand {
|
||||
return (
|
||||
Command.isCommand(value) &&
|
||||
value.type === 'insert_fragment' &&
|
||||
Node.isNodeList(value.fragment)
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a value is an `InsertNodeCommand` object.
|
||||
*/
|
||||
|
||||
isInsertNodeCommand(value: any): value is InsertNodeCommand {
|
||||
return (
|
||||
Command.isCommand(value) &&
|
||||
value.type === 'insert_node' &&
|
||||
Node.isNode(value.node)
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a value is a `InsertTextCommand` object.
|
||||
*/
|
||||
|
||||
isInsertTextCommand(value: any): value is InsertTextCommand {
|
||||
return (
|
||||
Command.isCommand(value) &&
|
||||
value.type === 'insert_text' &&
|
||||
typeof value.text === 'string'
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -140,6 +49,15 @@ export interface DeleteFragmentCommand {
|
||||
type: 'delete_fragment'
|
||||
}
|
||||
|
||||
/**
|
||||
* The `FormatTextCommand` adds properties to the text nodes in the selection.
|
||||
*/
|
||||
|
||||
export interface FormatTextCommand {
|
||||
type: 'format_text'
|
||||
properties: Record<string, any>
|
||||
}
|
||||
|
||||
/**
|
||||
* The `InsertBreakCommand` breaks a block in two at the current selection.
|
||||
*/
|
||||
@@ -184,7 +102,39 @@ export type CoreCommand =
|
||||
| DeleteBackwardCommand
|
||||
| DeleteForwardCommand
|
||||
| DeleteFragmentCommand
|
||||
| FormatTextCommand
|
||||
| InsertBreakCommand
|
||||
| InsertFragmentCommand
|
||||
| InsertNodeCommand
|
||||
| InsertTextCommand
|
||||
|
||||
export const CoreCommand = {
|
||||
/**
|
||||
* Check if a value is a `CoreCommand` object.
|
||||
*/
|
||||
|
||||
isCoreCommand(value: any): value is CoreCommand {
|
||||
if (Command.isCommand(value)) {
|
||||
switch (value.type) {
|
||||
case 'delete_backward':
|
||||
return typeof value.unit === 'string'
|
||||
case 'delete_forward':
|
||||
return typeof value.unit === 'string'
|
||||
case 'delete_fragment':
|
||||
return true
|
||||
case 'format_text':
|
||||
return isPlainObject(value.properties)
|
||||
case 'insert_break':
|
||||
return true
|
||||
case 'insert_fragment':
|
||||
return Node.isNodeList(value.fragment)
|
||||
case 'insert_node':
|
||||
return Node.isNode(value.node)
|
||||
case 'insert_text':
|
||||
return typeof value.text === 'string'
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
}
|
||||
|
@@ -16,15 +16,16 @@ import { TextTransforms } from './transforms/text'
|
||||
*/
|
||||
|
||||
export interface Editor {
|
||||
apply: (operation: Operation) => void
|
||||
children: Node[]
|
||||
selection: Range | null
|
||||
operations: Operation[]
|
||||
marks: Record<string, any> | null
|
||||
apply: (operation: Operation) => void
|
||||
exec: (command: Command) => void
|
||||
isInline: (element: Element) => boolean
|
||||
isVoid: (element: Element) => boolean
|
||||
normalizeNode: (entry: NodeEntry) => void
|
||||
onChange: () => void
|
||||
operations: Operation[]
|
||||
selection: Range | null
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
|
@@ -4,12 +4,14 @@ import {
|
||||
Operation,
|
||||
Path,
|
||||
Point,
|
||||
Text,
|
||||
PathRef,
|
||||
PointRef,
|
||||
Range,
|
||||
RangeRef,
|
||||
Node,
|
||||
} from '../../..'
|
||||
import { TextEntry } from '../../text'
|
||||
|
||||
export const NORMALIZING: WeakMap<Editor, boolean> = new WeakMap()
|
||||
export const PATH_REFS: WeakMap<Editor, Set<PathRef>> = new WeakMap()
|
||||
@@ -30,6 +32,7 @@ export const GeneralQueries = {
|
||||
typeof value.isVoid === 'function' &&
|
||||
typeof value.normalizeNode === 'function' &&
|
||||
typeof value.onChange === 'function' &&
|
||||
(value.marks === null || isPlainObject(value.marks)) &&
|
||||
(value.selection === null || Range.isRange(value.selection)) &&
|
||||
Node.isNodeList(value.children) &&
|
||||
Operation.isOperationList(value.operations)
|
||||
@@ -45,6 +48,58 @@ export const GeneralQueries = {
|
||||
return isNormalizing === undefined ? true : isNormalizing
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the marks that would be added to text at the current selection.
|
||||
*/
|
||||
|
||||
marks(editor: Editor): Record<string, any> | null {
|
||||
const { marks, selection } = editor
|
||||
|
||||
if (!selection) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (marks) {
|
||||
return marks
|
||||
}
|
||||
|
||||
if (Range.isExpanded(selection)) {
|
||||
const [match] = Editor.nodes(editor, {
|
||||
match: 'text',
|
||||
mode: 'all',
|
||||
})
|
||||
|
||||
if (match) {
|
||||
const [node] = match as TextEntry
|
||||
const { text, ...rest } = node
|
||||
return rest
|
||||
} else {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
const { anchor } = selection
|
||||
const { path } = anchor
|
||||
let [node] = Editor.leaf(editor, path)
|
||||
|
||||
if (anchor.offset === 0) {
|
||||
const prev = Editor.previous(editor, path, 'text')
|
||||
const block = Editor.match(editor, path, 'block')
|
||||
|
||||
if (prev && block) {
|
||||
const [prevNode, prevPath] = prev
|
||||
const [, blockPath] = block
|
||||
|
||||
if (Path.isAncestor(blockPath, prevPath)) {
|
||||
node = prevNode as Text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { text, ...rest } = node
|
||||
return rest
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a mutable ref for a `Path` object, which will stay in sync as new
|
||||
* operations are applied to the editor.
|
||||
|
@@ -1,14 +1,14 @@
|
||||
import { Node, Path, Range } from '..'
|
||||
import isPlainObject from 'is-plain-object'
|
||||
|
||||
type InsertNodeOperation = {
|
||||
export type InsertNodeOperation = {
|
||||
type: 'insert_node'
|
||||
path: Path
|
||||
node: Node
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
type InsertTextOperation = {
|
||||
export type InsertTextOperation = {
|
||||
type: 'insert_text'
|
||||
path: Path
|
||||
offset: number
|
||||
@@ -16,7 +16,7 @@ type InsertTextOperation = {
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
type MergeNodeOperation = {
|
||||
export type MergeNodeOperation = {
|
||||
type: 'merge_node'
|
||||
path: Path
|
||||
position: number
|
||||
@@ -25,21 +25,21 @@ type MergeNodeOperation = {
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
type MoveNodeOperation = {
|
||||
export type MoveNodeOperation = {
|
||||
type: 'move_node'
|
||||
path: Path
|
||||
newPath: Path
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
type RemoveNodeOperation = {
|
||||
export type RemoveNodeOperation = {
|
||||
type: 'remove_node'
|
||||
path: Path
|
||||
node: Node
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
type RemoveTextOperation = {
|
||||
export type RemoveTextOperation = {
|
||||
type: 'remove_text'
|
||||
path: Path
|
||||
offset: number
|
||||
@@ -47,7 +47,7 @@ type RemoveTextOperation = {
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
type SetNodeOperation = {
|
||||
export type SetNodeOperation = {
|
||||
type: 'set_node'
|
||||
path: Path
|
||||
properties: Partial<Node>
|
||||
@@ -55,7 +55,7 @@ type SetNodeOperation = {
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
type SetSelectionOperation =
|
||||
export type SetSelectionOperation =
|
||||
| {
|
||||
type: 'set_selection'
|
||||
[key: string]: any
|
||||
@@ -75,7 +75,7 @@ type SetSelectionOperation =
|
||||
newProperties: null
|
||||
}
|
||||
|
||||
type SplitNodeOperation = {
|
||||
export type SplitNodeOperation = {
|
||||
type: 'split_node'
|
||||
path: Path
|
||||
position: number
|
||||
@@ -84,16 +84,7 @@ type SplitNodeOperation = {
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/**
|
||||
* `Operation` objects define the low-level instructions that Slate editors use
|
||||
* to apply changes to their internal state. Representing all changes as
|
||||
* operations is what allows Slate editors to easily implement history,
|
||||
* collaboration, and other features.
|
||||
*/
|
||||
|
||||
type Operation = NodeOperation | SelectionOperation | TextOperation
|
||||
|
||||
type NodeOperation =
|
||||
export type NodeOperation =
|
||||
| InsertNodeOperation
|
||||
| MergeNodeOperation
|
||||
| MoveNodeOperation
|
||||
@@ -101,11 +92,20 @@ type NodeOperation =
|
||||
| SetNodeOperation
|
||||
| SplitNodeOperation
|
||||
|
||||
type SelectionOperation = SetSelectionOperation
|
||||
export type SelectionOperation = SetSelectionOperation
|
||||
|
||||
type TextOperation = InsertTextOperation | RemoveTextOperation
|
||||
export type TextOperation = InsertTextOperation | RemoveTextOperation
|
||||
|
||||
const Operation = {
|
||||
/**
|
||||
* `Operation` objects define the low-level instructions that Slate editors use
|
||||
* to apply changes to their internal state. Representing all changes as
|
||||
* operations is what allows Slate editors to easily implement history,
|
||||
* collaboration, and other features.
|
||||
*/
|
||||
|
||||
export type Operation = NodeOperation | SelectionOperation | TextOperation
|
||||
|
||||
export const Operation = {
|
||||
/**
|
||||
* Check of a value is a `NodeOperation` object.
|
||||
*/
|
||||
@@ -124,79 +124,54 @@ const Operation = {
|
||||
}
|
||||
|
||||
switch (value.type) {
|
||||
case 'insert_node': {
|
||||
case 'insert_node':
|
||||
return Path.isPath(value.path) && Node.isNode(value.node)
|
||||
}
|
||||
|
||||
case 'insert_text': {
|
||||
case 'insert_text':
|
||||
return (
|
||||
typeof value.offset === 'number' &&
|
||||
typeof value.text === 'string' &&
|
||||
Path.isPath(value.path)
|
||||
)
|
||||
}
|
||||
|
||||
case 'merge_node': {
|
||||
case 'merge_node':
|
||||
return (
|
||||
typeof value.position === 'number' &&
|
||||
(typeof value.target === 'number' || value.target === null) &&
|
||||
Path.isPath(value.path) &&
|
||||
isPlainObject(value.properties)
|
||||
)
|
||||
}
|
||||
|
||||
case 'move_node': {
|
||||
case 'move_node':
|
||||
return Path.isPath(value.path) && Path.isPath(value.newPath)
|
||||
}
|
||||
|
||||
case 'remove_node': {
|
||||
case 'remove_node':
|
||||
return Path.isPath(value.path) && Node.isNode(value.node)
|
||||
}
|
||||
|
||||
case 'remove_text': {
|
||||
case 'remove_text':
|
||||
return (
|
||||
typeof value.offset === 'number' &&
|
||||
typeof value.text === 'string' &&
|
||||
Path.isPath(value.path)
|
||||
)
|
||||
}
|
||||
|
||||
case 'set_node': {
|
||||
case 'set_node':
|
||||
return (
|
||||
Path.isPath(value.path) &&
|
||||
isPlainObject(value.properties) &&
|
||||
isPlainObject(value.newProperties)
|
||||
)
|
||||
}
|
||||
|
||||
case 'set_selection': {
|
||||
case 'set_selection':
|
||||
return (
|
||||
(value.properties === null && Range.isRange(value.newProperties)) ||
|
||||
(value.newProperties === null && Range.isRange(value.properties)) ||
|
||||
(isPlainObject(value.properties) &&
|
||||
isPlainObject(value.newProperties))
|
||||
)
|
||||
}
|
||||
|
||||
case 'set_value': {
|
||||
return (
|
||||
isPlainObject(value.properties) && isPlainObject(value.newProperties)
|
||||
)
|
||||
}
|
||||
|
||||
case 'split_node': {
|
||||
case 'split_node':
|
||||
return (
|
||||
Path.isPath(value.path) &&
|
||||
typeof value.position === 'number' &&
|
||||
(typeof value.target === 'number' || value.target === null) &&
|
||||
isPlainObject(value.properties)
|
||||
)
|
||||
}
|
||||
|
||||
default: {
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -300,16 +275,3 @@ const Operation = {
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export {
|
||||
InsertNodeOperation,
|
||||
InsertTextOperation,
|
||||
MergeNodeOperation,
|
||||
MoveNodeOperation,
|
||||
RemoveNodeOperation,
|
||||
RemoveTextOperation,
|
||||
SetNodeOperation,
|
||||
SetSelectionOperation,
|
||||
SplitNodeOperation,
|
||||
Operation,
|
||||
}
|
||||
|
@@ -2,8 +2,9 @@ import { Element } from 'slate'
|
||||
|
||||
export const input = {
|
||||
children: [],
|
||||
selection: null,
|
||||
operations: [],
|
||||
selection: null,
|
||||
marks: null,
|
||||
apply() {},
|
||||
exec() {},
|
||||
isInline() {},
|
||||
|
@@ -3,8 +3,9 @@ import { Element } from 'slate'
|
||||
export const input = [
|
||||
{
|
||||
children: [],
|
||||
selection: null,
|
||||
operations: [],
|
||||
selection: null,
|
||||
marks: null,
|
||||
apply() {},
|
||||
exec() {},
|
||||
isInline() {},
|
||||
|
@@ -1,13 +0,0 @@
|
||||
import { Operation } from 'slate'
|
||||
|
||||
export const input = {
|
||||
type: 'set_value',
|
||||
properties: {},
|
||||
newProperties: {},
|
||||
}
|
||||
|
||||
export const test = value => {
|
||||
return Operation.isOperation(value)
|
||||
}
|
||||
|
||||
export const output = true
|
@@ -13,7 +13,6 @@ import { withHistory } from 'slate-history'
|
||||
|
||||
const CheckListsExample = () => {
|
||||
const [value, setValue] = useState(initialValue)
|
||||
const [selection, setSelection] = useState(null)
|
||||
const renderElement = useCallback(props => <Element {...props} />, [])
|
||||
const editor = useMemo(
|
||||
() => withChecklists(withHistory(withReact(createEditor()))),
|
||||
@@ -21,15 +20,7 @@ const CheckListsExample = () => {
|
||||
)
|
||||
|
||||
return (
|
||||
<Slate
|
||||
editor={editor}
|
||||
value={value}
|
||||
selection={selection}
|
||||
onChange={(value, selection) => {
|
||||
setValue(value)
|
||||
setSelection(selection)
|
||||
}}
|
||||
>
|
||||
<Slate editor={editor} value={value} onChange={value => setValue(value)}>
|
||||
<Editable
|
||||
renderElement={renderElement}
|
||||
placeholder="Get to work…"
|
||||
|
@@ -11,18 +11,9 @@ import {
|
||||
|
||||
const EmbedsExample = () => {
|
||||
const [value, setValue] = useState(initialValue)
|
||||
const [selection, setSelection] = useState(null)
|
||||
const editor = useMemo(() => withEmbeds(withReact(createEditor())), [])
|
||||
return (
|
||||
<Slate
|
||||
editor={editor}
|
||||
value={value}
|
||||
selection={selection}
|
||||
onChange={(value, selection) => {
|
||||
setValue(value)
|
||||
setSelection(selection)
|
||||
}}
|
||||
>
|
||||
<Slate editor={editor} value={value} onChange={value => setValue(value)}>
|
||||
<Editable
|
||||
renderElement={props => <Element {...props} />}
|
||||
placeholder="Enter some text..."
|
||||
|
@@ -35,22 +35,13 @@ const withLayout = editor => {
|
||||
|
||||
const ForcedLayoutExample = () => {
|
||||
const [value, setValue] = useState(initialValue)
|
||||
const [selection, setSelection] = useState(null)
|
||||
const renderElement = useCallback(props => <Element {...props} />, [])
|
||||
const editor = useMemo(
|
||||
() => withLayout(withHistory(withReact(createEditor()))),
|
||||
[]
|
||||
)
|
||||
return (
|
||||
<Slate
|
||||
editor={editor}
|
||||
value={value}
|
||||
selection={selection}
|
||||
onChange={(value, selection) => {
|
||||
setValue(value)
|
||||
setSelection(selection)
|
||||
}}
|
||||
>
|
||||
<Slate editor={editor} value={value} onChange={value => setValue(value)}>
|
||||
<Editable
|
||||
renderElement={renderElement}
|
||||
placeholder="Enter a title…"
|
||||
|
@@ -9,22 +9,13 @@ import { Range } from 'slate'
|
||||
|
||||
const HoveringMenuExample = () => {
|
||||
const [value, setValue] = useState(initialValue)
|
||||
const [selection, setSelection] = useState(null)
|
||||
const editor = useMemo(
|
||||
() => withFormatting(withHistory(withReact(createEditor()))),
|
||||
[]
|
||||
)
|
||||
|
||||
return (
|
||||
<Slate
|
||||
editor={editor}
|
||||
value={value}
|
||||
selection={selection}
|
||||
onChange={(value, selection) => {
|
||||
setValue(value)
|
||||
setSelection(selection)
|
||||
}}
|
||||
>
|
||||
<Slate editor={editor} value={value} onChange={value => setValue(value)}>
|
||||
<HoveringToolbar />
|
||||
<Editable
|
||||
renderLeaf={props => <Leaf {...props} />}
|
||||
|
@@ -22,19 +22,10 @@ for (let h = 0; h < HEADINGS; h++) {
|
||||
|
||||
const HugeDocumentExample = () => {
|
||||
const [value, setValue] = useState(initialValue)
|
||||
const [selection, setSelection] = useState(null)
|
||||
const renderElement = useCallback(props => <Element {...props} />, [])
|
||||
const editor = useMemo(() => withReact(createEditor()), [])
|
||||
return (
|
||||
<Slate
|
||||
editor={editor}
|
||||
value={value}
|
||||
selection={selection}
|
||||
onChange={(value, selection) => {
|
||||
setValue(value)
|
||||
setSelection(selection)
|
||||
}}
|
||||
>
|
||||
<Slate editor={editor} value={value} onChange={value => setValue(value)}>
|
||||
<Editable renderElement={renderElement} spellCheck autoFocus />
|
||||
</Slate>
|
||||
)
|
||||
|
@@ -17,22 +17,13 @@ import { Button, Icon, Toolbar } from '../components'
|
||||
|
||||
const ImagesExample = () => {
|
||||
const [value, setValue] = useState(initialValue)
|
||||
const [selection, setSelection] = useState(null)
|
||||
const editor = useMemo(
|
||||
() => withImages(withHistory(withReact(createEditor()))),
|
||||
[]
|
||||
)
|
||||
|
||||
return (
|
||||
<Slate
|
||||
editor={editor}
|
||||
value={value}
|
||||
selection={selection}
|
||||
onChange={(value, selection) => {
|
||||
setValue(value)
|
||||
setSelection(selection)
|
||||
}}
|
||||
>
|
||||
<Slate editor={editor} value={value} onChange={value => setValue(value)}>
|
||||
<Toolbar>
|
||||
<InsertImageButton />
|
||||
</Toolbar>
|
||||
|
@@ -8,22 +8,13 @@ import { Button, Icon, Toolbar } from '../components'
|
||||
|
||||
const LinkExample = () => {
|
||||
const [value, setValue] = useState(initialValue)
|
||||
const [selection, setSelection] = useState(null)
|
||||
const editor = useMemo(
|
||||
() => withLinks(withHistory(withReact(createEditor()))),
|
||||
[]
|
||||
)
|
||||
|
||||
return (
|
||||
<Slate
|
||||
editor={editor}
|
||||
value={value}
|
||||
selection={selection}
|
||||
onChange={(value, selection) => {
|
||||
setValue(value)
|
||||
setSelection(selection)
|
||||
}}
|
||||
>
|
||||
<Slate editor={editor} value={value} onChange={value => setValue(value)}>
|
||||
<Toolbar>
|
||||
<LinkButton />
|
||||
</Toolbar>
|
||||
|
@@ -10,7 +10,6 @@ import { css } from 'emotion'
|
||||
|
||||
const MarkdownPreviewExample = () => {
|
||||
const [value, setValue] = useState(initialValue)
|
||||
const [selection, setSelection] = useState(null)
|
||||
const renderLeaf = useCallback(props => <Leaf {...props} />, [])
|
||||
const editor = useMemo(() => withHistory(withReact(createEditor())), [])
|
||||
const decorate = useCallback(([node, path]) => {
|
||||
@@ -52,15 +51,7 @@ const MarkdownPreviewExample = () => {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Slate
|
||||
editor={editor}
|
||||
value={value}
|
||||
selection={selection}
|
||||
onChange={(value, selection) => {
|
||||
setValue(value)
|
||||
setSelection(selection)
|
||||
}}
|
||||
>
|
||||
<Slate editor={editor} value={value} onChange={value => setValue(value)}>
|
||||
<Editable
|
||||
decorate={decorate}
|
||||
renderLeaf={renderLeaf}
|
||||
|
@@ -18,22 +18,13 @@ const SHORTCUTS = {
|
||||
|
||||
const MarkdownShortcutsExample = () => {
|
||||
const [value, setValue] = useState(initialValue)
|
||||
const [selection, setSelection] = useState(null)
|
||||
const renderElement = useCallback(props => <Element {...props} />, [])
|
||||
const editor = useMemo(
|
||||
() => withShortcuts(withReact(withHistory(createEditor()))),
|
||||
[]
|
||||
)
|
||||
return (
|
||||
<Slate
|
||||
editor={editor}
|
||||
value={value}
|
||||
selection={selection}
|
||||
onChange={(value, selection) => {
|
||||
setValue(value)
|
||||
setSelection(selection)
|
||||
}}
|
||||
>
|
||||
<Slate editor={editor} value={value} onChange={value => setValue(value)}>
|
||||
<Editable
|
||||
renderElement={renderElement}
|
||||
placeholder="Write some markdown..."
|
||||
|
@@ -15,7 +15,6 @@ import { Portal } from '../components'
|
||||
const MentionExample = () => {
|
||||
const ref = useRef()
|
||||
const [value, setValue] = useState(initialValue)
|
||||
const [selection, setSelection] = useState(null)
|
||||
const [target, setTarget] = useState()
|
||||
const [index, setIndex] = useState(0)
|
||||
const [search, setSearch] = useState('')
|
||||
@@ -74,10 +73,8 @@ const MentionExample = () => {
|
||||
<Slate
|
||||
editor={editor}
|
||||
value={value}
|
||||
selection={selection}
|
||||
onChange={(value, selection) => {
|
||||
onChange={value => {
|
||||
setValue(value)
|
||||
setSelection(selection)
|
||||
|
||||
if (selection && Range.isCollapsed(selection)) {
|
||||
const [start] = Range.edges(selection)
|
||||
|
@@ -82,7 +82,6 @@ export const deserialize = el => {
|
||||
|
||||
const PasteHtmlExample = () => {
|
||||
const [value, setValue] = useState(initialValue)
|
||||
const [selection, setSelection] = useState(null)
|
||||
const renderElement = useCallback(props => <Element {...props} />, [])
|
||||
const renderLeaf = useCallback(props => <Leaf {...props} />, [])
|
||||
const editor = useMemo(
|
||||
@@ -90,15 +89,7 @@ const PasteHtmlExample = () => {
|
||||
[]
|
||||
)
|
||||
return (
|
||||
<Slate
|
||||
editor={editor}
|
||||
value={value}
|
||||
selection={selection}
|
||||
onChange={(value, selection) => {
|
||||
setValue(value)
|
||||
setSelection(selection)
|
||||
}}
|
||||
>
|
||||
<Slate editor={editor} value={value} onChange={value => setValue(value)}>
|
||||
<Editable
|
||||
renderElement={renderElement}
|
||||
renderLeaf={renderLeaf}
|
||||
|
@@ -5,18 +5,9 @@ import { withHistory } from 'slate-history'
|
||||
|
||||
const PlainTextExample = () => {
|
||||
const [value, setValue] = useState(initialValue)
|
||||
const [selection, setSelection] = useState(null)
|
||||
const editor = useMemo(() => withHistory(withReact(createEditor())), [])
|
||||
return (
|
||||
<Slate
|
||||
editor={editor}
|
||||
value={value}
|
||||
selection={selection}
|
||||
onChange={(value, selection) => {
|
||||
setValue(value)
|
||||
setSelection(selection)
|
||||
}}
|
||||
>
|
||||
<Slate editor={editor} value={value} onChange={value => setValue(value)}>
|
||||
<Editable placeholder="Enter some plain text..." />
|
||||
</Slate>
|
||||
)
|
||||
|
@@ -4,18 +4,9 @@ import { Slate, Editable, withReact } from 'slate-react'
|
||||
|
||||
const ReadOnlyExample = () => {
|
||||
const [value, setValue] = useState(initialValue)
|
||||
const [selection, setSelection] = useState(null)
|
||||
const editor = useMemo(() => withReact(createEditor()), [])
|
||||
return (
|
||||
<Slate
|
||||
editor={editor}
|
||||
value={value}
|
||||
selection={selection}
|
||||
onChange={(value, selection) => {
|
||||
setValue(value)
|
||||
setSelection(selection)
|
||||
}}
|
||||
>
|
||||
<Slate editor={editor} value={value} onChange={value => setValue(value)}>
|
||||
<Editable readOnly placeholder="Enter some plain text..." />
|
||||
</Slate>
|
||||
)
|
||||
|
@@ -9,22 +9,14 @@ import { Button, Icon, Toolbar } from '../components'
|
||||
const HOTKEYS = {
|
||||
'mod+b': 'bold',
|
||||
'mod+i': 'italic',
|
||||
'mod+u': 'underlined',
|
||||
'mod+u': 'underline',
|
||||
'mod+`': 'code',
|
||||
}
|
||||
|
||||
const TEXT_FORMATS = ['bold', 'italic', 'underlined', 'code']
|
||||
const LIST_FORMATS = ['numbered-list', 'bulleted-list']
|
||||
const BLOCK_FORMATS = [
|
||||
...LIST_FORMATS,
|
||||
'heading-one',
|
||||
'heading-two',
|
||||
'block-quote',
|
||||
]
|
||||
const LIST_TYPES = ['numbered-list', 'bulleted-list']
|
||||
|
||||
const RichTextExample = () => {
|
||||
const [value, setValue] = useState(initialValue)
|
||||
const [selection, setSelection] = useState(null)
|
||||
const renderElement = useCallback(props => <Element {...props} />, [])
|
||||
const renderLeaf = useCallback(props => <Leaf {...props} />, [])
|
||||
const editor = useMemo(
|
||||
@@ -33,25 +25,17 @@ const RichTextExample = () => {
|
||||
)
|
||||
|
||||
return (
|
||||
<Slate
|
||||
editor={editor}
|
||||
value={value}
|
||||
selection={selection}
|
||||
onChange={(value, selection) => {
|
||||
setValue(value)
|
||||
setSelection(selection)
|
||||
}}
|
||||
>
|
||||
<Slate editor={editor} value={value} onChange={value => setValue(value)}>
|
||||
<Toolbar>
|
||||
<FormatButton format="bold" icon="format_bold" />
|
||||
<FormatButton format="italic" icon="format_italic" />
|
||||
<FormatButton format="underlined" icon="format_underlined" />
|
||||
<FormatButton format="code" icon="code" />
|
||||
<FormatButton format="heading-one" icon="looks_one" />
|
||||
<FormatButton format="heading-two" icon="looks_two" />
|
||||
<FormatButton format="block-quote" icon="format_quote" />
|
||||
<FormatButton format="numbered-list" icon="format_list_numbered" />
|
||||
<FormatButton format="bulleted-list" icon="format_list_bulleted" />
|
||||
<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" />
|
||||
</Toolbar>
|
||||
<Editable
|
||||
renderElement={renderElement}
|
||||
@@ -63,7 +47,11 @@ const RichTextExample = () => {
|
||||
for (const hotkey in HOTKEYS) {
|
||||
if (isHotkey(hotkey, event)) {
|
||||
event.preventDefault()
|
||||
editor.exec({ type: 'toggle_format', format: HOTKEYS[hotkey] })
|
||||
const mark = HOTKEYS[hotkey]
|
||||
editor.exec({
|
||||
type: 'format_text',
|
||||
properties: { [mark]: true },
|
||||
})
|
||||
}
|
||||
}
|
||||
}}
|
||||
@@ -76,21 +64,12 @@ const withRichText = editor => {
|
||||
const { exec } = editor
|
||||
|
||||
editor.exec = command => {
|
||||
if (command.type === 'toggle_format') {
|
||||
if (command.type === 'format_block') {
|
||||
const { format } = command
|
||||
const isActive = isFormatActive(editor, format)
|
||||
const isList = LIST_FORMATS.includes(format)
|
||||
const isActive = isBlockActive(editor, format)
|
||||
const isList = LIST_TYPES.includes(format)
|
||||
|
||||
if (TEXT_FORMATS.includes(format)) {
|
||||
Editor.setNodes(
|
||||
editor,
|
||||
{ [format]: isActive ? null : true },
|
||||
{ match: 'text', split: true }
|
||||
)
|
||||
}
|
||||
|
||||
if (BLOCK_FORMATS.includes(format)) {
|
||||
for (const f of LIST_FORMATS) {
|
||||
for (const f of LIST_TYPES) {
|
||||
Editor.unwrapNodes(editor, { match: { type: f }, split: true })
|
||||
}
|
||||
|
||||
@@ -101,7 +80,6 @@ const withRichText = editor => {
|
||||
if (!isActive && isList) {
|
||||
Editor.wrapNodes(editor, { type: format, children: [] })
|
||||
}
|
||||
}
|
||||
} else {
|
||||
exec(command)
|
||||
}
|
||||
@@ -110,17 +88,7 @@ const withRichText = editor => {
|
||||
return editor
|
||||
}
|
||||
|
||||
const isFormatActive = (editor, format) => {
|
||||
if (TEXT_FORMATS.includes(format)) {
|
||||
const [match] = Editor.nodes(editor, {
|
||||
match: { [format]: true },
|
||||
mode: 'all',
|
||||
})
|
||||
|
||||
return !!match
|
||||
}
|
||||
|
||||
if (BLOCK_FORMATS.includes(format)) {
|
||||
const isBlockActive = (editor, format) => {
|
||||
const [match] = Editor.nodes(editor, {
|
||||
match: { type: format },
|
||||
mode: 'all',
|
||||
@@ -129,7 +97,9 @@ const isFormatActive = (editor, format) => {
|
||||
return !!match
|
||||
}
|
||||
|
||||
return false
|
||||
const isMarkActive = (editor, format) => {
|
||||
const marks = Editor.marks(editor)
|
||||
return marks ? marks[format] === true : false
|
||||
}
|
||||
|
||||
const Element = ({ attributes, children, element }) => {
|
||||
@@ -164,21 +134,36 @@ const Leaf = ({ attributes, children, leaf }) => {
|
||||
children = <em>{children}</em>
|
||||
}
|
||||
|
||||
if (leaf.underlined) {
|
||||
if (leaf.underline) {
|
||||
children = <u>{children}</u>
|
||||
}
|
||||
|
||||
return <span {...attributes}>{children}</span>
|
||||
}
|
||||
|
||||
const FormatButton = ({ format, icon }) => {
|
||||
const BlockButton = ({ format, icon }) => {
|
||||
const editor = useSlate()
|
||||
return (
|
||||
<Button
|
||||
active={isFormatActive(editor, format)}
|
||||
active={isBlockActive(editor, format)}
|
||||
onMouseDown={event => {
|
||||
event.preventDefault()
|
||||
editor.exec({ type: 'toggle_format', format })
|
||||
editor.exec({ type: 'format_block', format })
|
||||
}}
|
||||
>
|
||||
<Icon>{icon}</Icon>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
const MarkButton = ({ format, icon }) => {
|
||||
const editor = useSlate()
|
||||
return (
|
||||
<Button
|
||||
active={isMarkActive(editor, format)}
|
||||
onMouseDown={event => {
|
||||
event.preventDefault()
|
||||
editor.exec({ type: 'format_text', properties: { [format]: true } })
|
||||
}}
|
||||
>
|
||||
<Icon>{icon}</Icon>
|
||||
|
@@ -8,7 +8,6 @@ import { Icon, Toolbar } from '../components'
|
||||
|
||||
const SearchHighlightingExample = () => {
|
||||
const [value, setValue] = useState(initialValue)
|
||||
const [selection, setSelection] = useState(null)
|
||||
const [search, setSearch] = useState()
|
||||
const editor = useMemo(() => withHistory(withReact(createEditor())), [])
|
||||
const decorate = useCallback(
|
||||
@@ -39,15 +38,7 @@ const SearchHighlightingExample = () => {
|
||||
)
|
||||
|
||||
return (
|
||||
<Slate
|
||||
editor={editor}
|
||||
value={value}
|
||||
selection={selection}
|
||||
onChange={(value, selection) => {
|
||||
setValue(value)
|
||||
setSelection(selection)
|
||||
}}
|
||||
>
|
||||
<Slate editor={editor} value={value} onChange={value => setValue(value)}>
|
||||
<Toolbar>
|
||||
<div
|
||||
className={css`
|
||||
|
@@ -5,7 +5,6 @@ import { withHistory } from 'slate-history'
|
||||
|
||||
const TablesExample = () => {
|
||||
const [value, setValue] = useState(initialValue)
|
||||
const [selection, setSelection] = useState(null)
|
||||
const renderElement = useCallback(props => <Element {...props} />, [])
|
||||
const renderLeaf = useCallback(props => <Leaf {...props} />, [])
|
||||
const editor = useMemo(
|
||||
@@ -13,15 +12,7 @@ const TablesExample = () => {
|
||||
[]
|
||||
)
|
||||
return (
|
||||
<Slate
|
||||
editor={editor}
|
||||
value={value}
|
||||
selection={selection}
|
||||
onChange={(value, selection) => {
|
||||
setValue(value)
|
||||
setSelection(selection)
|
||||
}}
|
||||
>
|
||||
<Slate editor={editor} value={value} onChange={value => setValue(value)}>
|
||||
<Editable renderElement={renderElement} renderLeaf={renderLeaf} />
|
||||
</Slate>
|
||||
)
|
||||
|
Reference in New Issue
Block a user