mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-09-02 11:42:53 +02:00
refactor decorations to use selections (#1221)
* refactor decorations to use selections * update docs * cleanup * add Selection.createList * fix tests * fix for nested blocks * fix lint * actually merge * revert small change * add state.decorations, with search example
This commit is contained in:
@@ -103,24 +103,25 @@ Slate schemas are built up of a set of rules. Each of the properties will add ce
|
|||||||
The `match` property is the only required property of a rule. It determines which objects the rule applies to.
|
The `match` property is the only required property of a rule. It determines which objects the rule applies to.
|
||||||
|
|
||||||
### `decorate`
|
### `decorate`
|
||||||
`Function decorate(text: Node, object: Node) => List<Characters>`
|
`Function decorate(node: Node) => List<Selection>|Array<Object>`
|
||||||
|
|
||||||
```js
|
```js
|
||||||
{
|
{
|
||||||
decorate: (text, node) => {
|
decorate: (node) => {
|
||||||
let { characters } = text
|
const text = node.getFirstText()
|
||||||
let first = characters.get(0)
|
|
||||||
let { marks } = first
|
return [{
|
||||||
let mark = Mark.create({ type: 'bold' })
|
anchorKey: text.key,
|
||||||
marks = marks.add(mark)
|
anchorOffset: 0,
|
||||||
first = first.merge({ marks })
|
focusKey: text.key,
|
||||||
characters = characters.set(0, first)
|
focusOffset: 1,
|
||||||
return characters
|
marks: [{ type: 'bold' }]
|
||||||
|
}]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The `decorate` property allows you define a function that will apply extra marks to all of the ranges of text inside a node. It is called with a [`Text`](./text.md) node and the matched node. It should return a list of characters with the desired marks, which will then be added to the text before rendering.
|
The `decorate` property allows you define a function that will apply extra marks to ranges of text inside a node. It is called with a [`Node`](./node.md). It should return a list of [`Selection`](./selection.md) objects with the desired marks, which will then be added to the text before rendering.
|
||||||
|
|
||||||
### `normalize`
|
### `normalize`
|
||||||
`Function normalize(change: Change, object: Node, failure: Any) => Change`
|
`Function normalize(change: Change, object: Node, failure: Any) => Change`
|
||||||
|
@@ -1,13 +1,13 @@
|
|||||||
|
|
||||||
import { Editor } from 'slate-react'
|
import { Editor } from 'slate-react'
|
||||||
import { Mark, State } from 'slate'
|
import { State } from 'slate'
|
||||||
|
|
||||||
import Prism from 'prismjs'
|
import Prism from 'prismjs'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import initialState from './state.json'
|
import initialState from './state.json'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define a code block component.
|
* Define our code components.
|
||||||
*
|
*
|
||||||
* @param {Object} props
|
* @param {Object} props
|
||||||
* @return {Element}
|
* @return {Element}
|
||||||
@@ -40,43 +40,69 @@ function CodeBlock(props) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function CodeBlockLine(props) {
|
||||||
|
return (
|
||||||
|
<div {...props.attributes}>{props.children}</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define a Prism.js decorator for code blocks.
|
* Define a Prism.js decorator for code blocks.
|
||||||
*
|
*
|
||||||
* @param {Text} text
|
|
||||||
* @param {Block} block
|
* @param {Block} block
|
||||||
|
* @return {Array}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function codeBlockDecorator(text, block) {
|
function codeBlockDecorator(block) {
|
||||||
const characters = text.characters.asMutable()
|
|
||||||
const language = block.data.get('language')
|
const language = block.data.get('language')
|
||||||
const string = text.text
|
const texts = block.getTexts().toArray()
|
||||||
|
const string = texts.map(t => t.text).join('\n')
|
||||||
const grammar = Prism.languages[language]
|
const grammar = Prism.languages[language]
|
||||||
const tokens = Prism.tokenize(string, grammar)
|
const tokens = Prism.tokenize(string, grammar)
|
||||||
let offset = 0
|
const decorations = []
|
||||||
|
let startText = texts.shift()
|
||||||
|
let endText = startText
|
||||||
|
let startOffset = 0
|
||||||
|
let endOffset = 0
|
||||||
|
let start = 0
|
||||||
|
|
||||||
for (const token of tokens) {
|
for (const token of tokens) {
|
||||||
if (typeof token == 'string') {
|
startText = endText
|
||||||
offset += token.length
|
startOffset = endOffset
|
||||||
continue
|
|
||||||
|
const content = typeof token == 'string' ? token : token.content
|
||||||
|
const newlines = content.split('\n').length - 1
|
||||||
|
const length = content.length - newlines
|
||||||
|
const end = start + length
|
||||||
|
|
||||||
|
let available = startText.text.length - startOffset
|
||||||
|
let remaining = length
|
||||||
|
|
||||||
|
endOffset = startOffset + remaining
|
||||||
|
|
||||||
|
while (available < remaining) {
|
||||||
|
endText = texts.shift()
|
||||||
|
remaining = length - available
|
||||||
|
available = endText.text.length
|
||||||
|
endOffset = remaining
|
||||||
}
|
}
|
||||||
|
|
||||||
const length = offset + token.content.length
|
if (typeof token != 'string') {
|
||||||
const type = `highlight-${token.type}`
|
const range = {
|
||||||
const mark = Mark.create({ type })
|
anchorKey: startText.key,
|
||||||
|
anchorOffset: startOffset,
|
||||||
for (let i = offset; i < length; i++) {
|
focusKey: endText.key,
|
||||||
let char = characters.get(i)
|
focusOffset: endOffset,
|
||||||
let { marks } = char
|
marks: [{ type: `highlight-${token.type}` }],
|
||||||
marks = marks.add(mark)
|
|
||||||
char = char.set('marks', marks)
|
|
||||||
characters.set(i, char)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
offset = length
|
decorations.push(range)
|
||||||
}
|
}
|
||||||
|
|
||||||
return characters.asImmutable()
|
start = end
|
||||||
|
}
|
||||||
|
|
||||||
|
return decorations
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -90,7 +116,10 @@ const schema = {
|
|||||||
code: {
|
code: {
|
||||||
render: CodeBlock,
|
render: CodeBlock,
|
||||||
decorate: codeBlockDecorator,
|
decorate: codeBlockDecorator,
|
||||||
}
|
},
|
||||||
|
code_line: {
|
||||||
|
render: CodeBlockLine,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
marks: {
|
marks: {
|
||||||
'highlight-comment': {
|
'highlight-comment': {
|
||||||
|
@@ -21,12 +21,172 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"language": "js"
|
"language": "js"
|
||||||
},
|
},
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"kind": "block",
|
||||||
|
"type": "code_line",
|
||||||
"nodes": [
|
"nodes": [
|
||||||
{
|
{
|
||||||
"kind": "text",
|
"kind": "text",
|
||||||
"ranges": [
|
"ranges": [
|
||||||
{
|
{
|
||||||
"text": "// A simple FizzBuzz implementation.\nfor (var i = 1; i <= 100; i++) {\n if (i % 15 == 0) {\n console.log('Fizz Buzz');\n } else if (i % 5 == 0) {\n console.log('Buzz');\n } else if (i % 3 == 0) {\n console.log('Fizz');\n } else {\n console.log(i);\n }\n}"
|
"text": "// A simple FizzBuzz implementation."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "block",
|
||||||
|
"type": "code_line",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"kind": "text",
|
||||||
|
"ranges": [
|
||||||
|
{
|
||||||
|
"text": "for (var i = 1; i <= 100; i++) {"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "block",
|
||||||
|
"type": "code_line",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"kind": "text",
|
||||||
|
"ranges": [
|
||||||
|
{
|
||||||
|
"text": " if (i % 15 == 0) {"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "block",
|
||||||
|
"type": "code_line",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"kind": "text",
|
||||||
|
"ranges": [
|
||||||
|
{
|
||||||
|
"text": " console.log('Fizz Buzz');"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "block",
|
||||||
|
"type": "code_line",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"kind": "text",
|
||||||
|
"ranges": [
|
||||||
|
{
|
||||||
|
"text": " } else if (i % 5 == 0) {"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "block",
|
||||||
|
"type": "code_line",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"kind": "text",
|
||||||
|
"ranges": [
|
||||||
|
{
|
||||||
|
"text": " console.log('Buzz');"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "block",
|
||||||
|
"type": "code_line",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"kind": "text",
|
||||||
|
"ranges": [
|
||||||
|
{
|
||||||
|
"text": " } else if (i % 3 == 0) {"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "block",
|
||||||
|
"type": "code_line",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"kind": "text",
|
||||||
|
"ranges": [
|
||||||
|
{
|
||||||
|
"text": " console.log('Fizz');"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "block",
|
||||||
|
"type": "code_line",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"kind": "text",
|
||||||
|
"ranges": [
|
||||||
|
{
|
||||||
|
"text": " } else {"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "block",
|
||||||
|
"type": "code_line",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"kind": "text",
|
||||||
|
"ranges": [
|
||||||
|
{
|
||||||
|
"text": " console.log(i);"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "block",
|
||||||
|
"type": "code_line",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"kind": "text",
|
||||||
|
"ranges": [
|
||||||
|
{
|
||||||
|
"text": " }"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "block",
|
||||||
|
"type": "code_line",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"kind": "text",
|
||||||
|
"ranges": [
|
||||||
|
{
|
||||||
|
"text": "}"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@@ -61,6 +61,7 @@ td {
|
|||||||
}
|
}
|
||||||
|
|
||||||
input {
|
input {
|
||||||
|
box-sizing: border-box;
|
||||||
font-size: .85em;
|
font-size: .85em;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: .5em;
|
padding: .5em;
|
||||||
@@ -173,12 +174,29 @@ input:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.toolbar-menu {
|
.toolbar-menu {
|
||||||
padding: 1px 0 17px 18px;
|
position: relative;
|
||||||
|
padding: 1px 18px 17px;
|
||||||
margin: 0 -20px;
|
margin: 0 -20px;
|
||||||
border-bottom: 2px solid #eee;
|
border-bottom: 2px solid #eee;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toolbar-menu .search {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-menu .search-icon {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.5em;
|
||||||
|
left: 0.5em;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-menu .search-box {
|
||||||
|
padding-left: 2em;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.hover-menu {
|
.hover-menu {
|
||||||
padding: 8px 7px 6px;
|
padding: 8px 7px 6px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@@ -20,6 +20,7 @@ import Plugins from './plugins'
|
|||||||
import RTL from './rtl'
|
import RTL from './rtl'
|
||||||
import ReadOnly from './read-only'
|
import ReadOnly from './read-only'
|
||||||
import RichText from './rich-text'
|
import RichText from './rich-text'
|
||||||
|
import SearchHighlighting from './search-highlighting'
|
||||||
import Tables from './tables'
|
import Tables from './tables'
|
||||||
|
|
||||||
import DevHugeDocument from './dev/huge-document'
|
import DevHugeDocument from './dev/huge-document'
|
||||||
@@ -54,6 +55,7 @@ const EXAMPLES = [
|
|||||||
['Code Highlighting', CodeHighlighting, '/code-highlighting'],
|
['Code Highlighting', CodeHighlighting, '/code-highlighting'],
|
||||||
['Tables', Tables, '/tables'],
|
['Tables', Tables, '/tables'],
|
||||||
['Paste HTML', PasteHtml, '/paste-html'],
|
['Paste HTML', PasteHtml, '/paste-html'],
|
||||||
|
['Search Highlighting', SearchHighlighting, '/search-highlighting'],
|
||||||
['Read-only', ReadOnly, '/read-only'],
|
['Read-only', ReadOnly, '/read-only'],
|
||||||
['RTL', RTL, '/rtl'],
|
['RTL', RTL, '/rtl'],
|
||||||
['Plugins', Plugins, '/plugins'],
|
['Plugins', Plugins, '/plugins'],
|
||||||
|
8
examples/search-highlighting/Readme.md
Normal file
8
examples/search-highlighting/Readme.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
# Rich Text Example
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
This example shows you can add a very different concepts together: key commands, toolbars, and custom formatting, to get the functionality you'd expect from a rich text editor. Of course this is just the beginning, you can layer in whatever other behaviors you want!
|
||||||
|
|
||||||
|
Check out the [Examples readme](..) to see how to run it!
|
149
examples/search-highlighting/index.js
Normal file
149
examples/search-highlighting/index.js
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
|
||||||
|
import { Editor } from 'slate-react'
|
||||||
|
import { State } from 'slate'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import initialState from './state.json'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define a schema.
|
||||||
|
*
|
||||||
|
* @type {Object}
|
||||||
|
*/
|
||||||
|
|
||||||
|
const schema = {
|
||||||
|
marks: {
|
||||||
|
highlight: {
|
||||||
|
backgroundColor: '#ffeeba'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The rich text example.
|
||||||
|
*
|
||||||
|
* @type {Component}
|
||||||
|
*/
|
||||||
|
|
||||||
|
class SearchHighlighting extends React.Component {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deserialize the initial editor state.
|
||||||
|
*
|
||||||
|
* @type {Object}
|
||||||
|
*/
|
||||||
|
|
||||||
|
state = {
|
||||||
|
state: State.fromJSON(initialState),
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On change, save the new `state`.
|
||||||
|
*
|
||||||
|
* @param {Change} change
|
||||||
|
*/
|
||||||
|
|
||||||
|
onChange = ({ state }) => {
|
||||||
|
this.setState({ state })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On input change, update the decorations.
|
||||||
|
*
|
||||||
|
* @param {Event} e
|
||||||
|
*/
|
||||||
|
|
||||||
|
onInputChange = (e) => {
|
||||||
|
const { state } = this.state
|
||||||
|
const string = e.target.value
|
||||||
|
const texts = state.document.getTexts()
|
||||||
|
const decorations = []
|
||||||
|
|
||||||
|
texts.forEach((node) => {
|
||||||
|
const { key, text } = node
|
||||||
|
const parts = text.split(string)
|
||||||
|
let offset = 0
|
||||||
|
|
||||||
|
parts.forEach((part, i) => {
|
||||||
|
if (i != 0) {
|
||||||
|
decorations.push({
|
||||||
|
anchorKey: key,
|
||||||
|
anchorOffset: offset - string.length,
|
||||||
|
focusKey: key,
|
||||||
|
focusOffset: offset,
|
||||||
|
marks: [{ type: 'highlight' }],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
offset = offset + part.length + string.length
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const change = state.change().setState({ decorations })
|
||||||
|
this.onChange(change)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render.
|
||||||
|
*
|
||||||
|
* @return {Element}
|
||||||
|
*/
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{this.renderToolbar()}
|
||||||
|
{this.renderEditor()}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the toolbar.
|
||||||
|
*
|
||||||
|
* @return {Element}
|
||||||
|
*/
|
||||||
|
|
||||||
|
renderToolbar = () => {
|
||||||
|
return (
|
||||||
|
<div className="menu toolbar-menu">
|
||||||
|
<div className="search">
|
||||||
|
<span className="search-icon material-icons">search</span>
|
||||||
|
<input
|
||||||
|
className="search-box"
|
||||||
|
type="search"
|
||||||
|
placeholder="Search the text..."
|
||||||
|
onChange={this.onInputChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the Slate editor.
|
||||||
|
*
|
||||||
|
* @return {Element}
|
||||||
|
*/
|
||||||
|
|
||||||
|
renderEditor = () => {
|
||||||
|
return (
|
||||||
|
<div className="editor">
|
||||||
|
<Editor
|
||||||
|
state={this.state.state}
|
||||||
|
onChange={this.onChange}
|
||||||
|
schema={schema}
|
||||||
|
placeholder={'Enter some rich text...'}
|
||||||
|
spellCheck
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default SearchHighlighting
|
34
examples/search-highlighting/state.json
Normal file
34
examples/search-highlighting/state.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"document": {
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"kind": "block",
|
||||||
|
"type": "paragraph",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"kind": "text",
|
||||||
|
"ranges": [
|
||||||
|
{
|
||||||
|
"text": "This is editable text that you can search. As you search, it looks for matching strings of text, and adds \"decoration\" marks to them in realtime."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "block",
|
||||||
|
"type": "paragraph",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"kind": "text",
|
||||||
|
"ranges": [
|
||||||
|
{
|
||||||
|
"text": "Try it out for yourself by typing in the search box above!"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
@@ -13,6 +13,7 @@
|
|||||||
"keycode": "^2.1.2",
|
"keycode": "^2.1.2",
|
||||||
"prop-types": "^15.5.8",
|
"prop-types": "^15.5.8",
|
||||||
"react-portal": "^3.1.0",
|
"react-portal": "^3.1.0",
|
||||||
|
"react-immutable-proptypes": "^2.1.0",
|
||||||
"selection-is-backward": "^1.0.0",
|
"selection-is-backward": "^1.0.0",
|
||||||
"slate-base64-serializer": "^0.1.11",
|
"slate-base64-serializer": "^0.1.11",
|
||||||
"slate-dev-logger": "^0.1.12",
|
"slate-dev-logger": "^0.1.12",
|
||||||
|
@@ -12,13 +12,13 @@ import TRANSFER_TYPES from '../constants/transfer-types'
|
|||||||
import Node from './node'
|
import Node from './node'
|
||||||
import extendSelection from '../utils/extend-selection'
|
import extendSelection from '../utils/extend-selection'
|
||||||
import findClosestNode from '../utils/find-closest-node'
|
import findClosestNode from '../utils/find-closest-node'
|
||||||
import getCaretPosition from '../utils/get-caret-position'
|
import findDropPoint from '../utils/find-drop-point'
|
||||||
|
import findNativePoint from '../utils/find-native-point'
|
||||||
|
import findPoint from '../utils/find-point'
|
||||||
import getHtmlFromNativePaste from '../utils/get-html-from-native-paste'
|
import getHtmlFromNativePaste from '../utils/get-html-from-native-paste'
|
||||||
import getPoint from '../utils/get-point'
|
|
||||||
import getDropPoint from '../utils/get-drop-point'
|
|
||||||
import getTransferData from '../utils/get-transfer-data'
|
import getTransferData from '../utils/get-transfer-data'
|
||||||
import setTransferData from '../utils/set-transfer-data'
|
|
||||||
import scrollToSelection from '../utils/scroll-to-selection'
|
import scrollToSelection from '../utils/scroll-to-selection'
|
||||||
|
import setTransferData from '../utils/set-transfer-data'
|
||||||
import { IS_FIREFOX, IS_MAC, IS_IE } from '../constants/environment'
|
import { IS_FIREFOX, IS_MAC, IS_IE } from '../constants/environment'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -121,7 +121,7 @@ class Content extends React.Component {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
updateSelection = () => {
|
updateSelection = () => {
|
||||||
const { editor, state } = this.props
|
const { state } = this.props
|
||||||
const { selection } = state
|
const { selection } = state
|
||||||
const window = getWindow(this.element)
|
const window = getWindow(this.element)
|
||||||
const native = window.getSelection()
|
const native = window.getSelection()
|
||||||
@@ -144,10 +144,8 @@ class Content extends React.Component {
|
|||||||
|
|
||||||
// Otherwise, figure out which DOM nodes should be selected...
|
// Otherwise, figure out which DOM nodes should be selected...
|
||||||
const { anchorKey, anchorOffset, focusKey, focusOffset, isCollapsed } = selection
|
const { anchorKey, anchorOffset, focusKey, focusOffset, isCollapsed } = selection
|
||||||
const anchor = getCaretPosition(anchorKey, anchorOffset, state, editor, this.element)
|
const anchor = findNativePoint(anchorKey, anchorOffset)
|
||||||
const focus = isCollapsed
|
const focus = isCollapsed ? anchor : findNativePoint(focusKey, focusOffset)
|
||||||
? anchor
|
|
||||||
: getCaretPosition(focusKey, focusOffset, state, editor, this.element)
|
|
||||||
|
|
||||||
// If they are already selected, do nothing.
|
// If they are already selected, do nothing.
|
||||||
if (
|
if (
|
||||||
@@ -432,12 +430,11 @@ class Content extends React.Component {
|
|||||||
|
|
||||||
if (this.props.readOnly) return
|
if (this.props.readOnly) return
|
||||||
|
|
||||||
const { editor, state } = this.props
|
const { state } = this.props
|
||||||
const { nativeEvent } = event
|
const { nativeEvent } = event
|
||||||
const { dataTransfer } = nativeEvent
|
const { dataTransfer } = nativeEvent
|
||||||
const data = getTransferData(dataTransfer)
|
const data = getTransferData(dataTransfer)
|
||||||
const point = getDropPoint(event, state, editor)
|
const point = findDropPoint(event, state)
|
||||||
|
|
||||||
if (!point) return
|
if (!point) return
|
||||||
|
|
||||||
// Add drop-specific information to the data.
|
// Add drop-specific information to the data.
|
||||||
@@ -484,26 +481,33 @@ class Content extends React.Component {
|
|||||||
// Get the selection point.
|
// Get the selection point.
|
||||||
const native = window.getSelection()
|
const native = window.getSelection()
|
||||||
const { anchorNode, anchorOffset } = native
|
const { anchorNode, anchorOffset } = native
|
||||||
const point = getPoint(anchorNode, anchorOffset, state, editor)
|
const point = findPoint(anchorNode, anchorOffset, state)
|
||||||
if (!point) return
|
if (!point) return
|
||||||
|
|
||||||
// Get the range in question.
|
// Get the text node and range in question.
|
||||||
const { key, index, start, end } = point
|
|
||||||
const { document, selection } = state
|
const { document, selection } = state
|
||||||
const schema = editor.getSchema()
|
const node = document.getDescendant(point.key)
|
||||||
const decorators = document.getDescendantDecorators(key, schema)
|
const ranges = node.getRanges()
|
||||||
const node = document.getDescendant(key)
|
let start = 0
|
||||||
const block = document.getClosestBlock(node.key)
|
let end = 0
|
||||||
const ranges = node.getRanges(decorators)
|
|
||||||
const lastText = block.getLastText()
|
const range = ranges.find((r) => {
|
||||||
|
end += r.text.length
|
||||||
|
if (end >= point.offset) return true
|
||||||
|
start = end
|
||||||
|
})
|
||||||
|
|
||||||
// Get the text information.
|
// Get the text information.
|
||||||
|
const { text } = range
|
||||||
let { textContent } = anchorNode
|
let { textContent } = anchorNode
|
||||||
|
const block = document.getClosestBlock(node.key)
|
||||||
|
const lastText = block.getLastText()
|
||||||
|
const lastRange = ranges.last()
|
||||||
const lastChar = textContent.charAt(textContent.length - 1)
|
const lastChar = textContent.charAt(textContent.length - 1)
|
||||||
const isLastText = node == lastText
|
const isLastText = node == lastText
|
||||||
const isLastRange = index == ranges.size - 1
|
const isLastRange = range == lastRange
|
||||||
|
|
||||||
// If we're dealing with the last leaf, and the DOM text ends in a new line,
|
// COMPAT: If this is the last range, and the DOM text ends in a new line,
|
||||||
// we will have added another new line in <Leaf>'s render method to account
|
// we will have added another new line in <Leaf>'s render method to account
|
||||||
// for browsers collapsing a single trailing new lines, so remove it.
|
// for browsers collapsing a single trailing new lines, so remove it.
|
||||||
if (isLastText && isLastRange && lastChar == '\n') {
|
if (isLastText && isLastRange && lastChar == '\n') {
|
||||||
@@ -511,26 +515,20 @@ class Content extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If the text is no different, abort.
|
// If the text is no different, abort.
|
||||||
const range = ranges.get(index)
|
|
||||||
const { text, marks } = range
|
|
||||||
if (textContent == text) return
|
if (textContent == text) return
|
||||||
|
|
||||||
// Determine what the selection should be after changing the text.
|
// Determine what the selection should be after changing the text.
|
||||||
const delta = textContent.length - text.length
|
const delta = textContent.length - text.length
|
||||||
const after = selection.collapseToEnd().move(delta)
|
const corrected = selection.collapseToEnd().move(delta)
|
||||||
|
const entire = selection.moveAnchorTo(point.key, start).moveFocusTo(point.key, end)
|
||||||
|
|
||||||
// Change the current state to have the text replaced.
|
// Change the current state to have the range's text replaced.
|
||||||
editor.change((change) => {
|
editor.change((change) => {
|
||||||
change
|
change
|
||||||
.select({
|
.select(entire)
|
||||||
anchorKey: key,
|
|
||||||
anchorOffset: start,
|
|
||||||
focusKey: key,
|
|
||||||
focusOffset: end
|
|
||||||
})
|
|
||||||
.delete()
|
.delete()
|
||||||
.insertText(textContent, marks)
|
.insertText(textContent, range.marks)
|
||||||
.select(after)
|
.select(corrected)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -677,7 +675,7 @@ class Content extends React.Component {
|
|||||||
if (!this.isInEditor(event.target)) return
|
if (!this.isInEditor(event.target)) return
|
||||||
|
|
||||||
const window = getWindow(event.target)
|
const window = getWindow(event.target)
|
||||||
const { state, editor } = this.props
|
const { state } = this.props
|
||||||
const { document, selection } = state
|
const { document, selection } = state
|
||||||
const native = window.getSelection()
|
const native = window.getSelection()
|
||||||
const data = {}
|
const data = {}
|
||||||
@@ -690,8 +688,8 @@ class Content extends React.Component {
|
|||||||
// Otherwise, determine the Slate selection from the native one.
|
// Otherwise, determine the Slate selection from the native one.
|
||||||
else {
|
else {
|
||||||
const { anchorNode, anchorOffset, focusNode, focusOffset } = native
|
const { anchorNode, anchorOffset, focusNode, focusOffset } = native
|
||||||
const anchor = getPoint(anchorNode, anchorOffset, state, editor)
|
const anchor = findPoint(anchorNode, anchorOffset, state)
|
||||||
const focus = getPoint(focusNode, focusOffset, state, editor)
|
const focus = findPoint(focusNode, focusOffset, state)
|
||||||
if (!anchor || !focus) return
|
if (!anchor || !focus) return
|
||||||
|
|
||||||
// There are situations where a select event will fire with a new native
|
// There are situations where a select event will fire with a new native
|
||||||
@@ -872,11 +870,14 @@ class Content extends React.Component {
|
|||||||
|
|
||||||
renderNode = (child, isSelected) => {
|
renderNode = (child, isSelected) => {
|
||||||
const { editor, readOnly, schema, state } = this.props
|
const { editor, readOnly, schema, state } = this.props
|
||||||
const { document } = state
|
const { document, decorations } = state
|
||||||
|
let decs = document.getDecorations(schema)
|
||||||
|
if (decorations) decs = decorations.concat(decs)
|
||||||
return (
|
return (
|
||||||
<Node
|
<Node
|
||||||
block={null}
|
block={null}
|
||||||
editor={editor}
|
editor={editor}
|
||||||
|
decorations={decs}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
key={child.key}
|
key={child.key}
|
||||||
node={child}
|
node={child}
|
||||||
|
@@ -100,6 +100,37 @@ class Leaf extends React.Component {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render all of the leaf's mark components.
|
||||||
|
*
|
||||||
|
* @param {Object} props
|
||||||
|
* @return {Element}
|
||||||
|
*/
|
||||||
|
|
||||||
|
renderMarks(props) {
|
||||||
|
const { marks, schema, node, offset, text, state, editor } = props
|
||||||
|
const children = this.renderText(props)
|
||||||
|
|
||||||
|
return marks.reduce((memo, mark) => {
|
||||||
|
const Component = mark.getComponent(schema)
|
||||||
|
if (!Component) return memo
|
||||||
|
return (
|
||||||
|
<Component
|
||||||
|
editor={editor}
|
||||||
|
mark={mark}
|
||||||
|
marks={marks}
|
||||||
|
node={node}
|
||||||
|
offset={offset}
|
||||||
|
schema={schema}
|
||||||
|
state={state}
|
||||||
|
text={text}
|
||||||
|
>
|
||||||
|
{memo}
|
||||||
|
</Component>
|
||||||
|
)
|
||||||
|
}, children)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render the text content of the leaf, accounting for browsers.
|
* Render the text content of the leaf, accounting for browsers.
|
||||||
*
|
*
|
||||||
@@ -136,37 +167,6 @@ class Leaf extends React.Component {
|
|||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Render all of the leaf's mark components.
|
|
||||||
*
|
|
||||||
* @param {Object} props
|
|
||||||
* @return {Element}
|
|
||||||
*/
|
|
||||||
|
|
||||||
renderMarks(props) {
|
|
||||||
const { marks, schema, node, offset, text, state, editor } = props
|
|
||||||
const children = this.renderText(props)
|
|
||||||
|
|
||||||
return marks.reduce((memo, mark) => {
|
|
||||||
const Component = mark.getComponent(schema)
|
|
||||||
if (!Component) return memo
|
|
||||||
return (
|
|
||||||
<Component
|
|
||||||
editor={editor}
|
|
||||||
mark={mark}
|
|
||||||
marks={marks}
|
|
||||||
node={node}
|
|
||||||
offset={offset}
|
|
||||||
schema={schema}
|
|
||||||
state={state}
|
|
||||||
text={text}
|
|
||||||
>
|
|
||||||
{memo}
|
|
||||||
</Component>
|
|
||||||
)
|
|
||||||
}, children)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
|
|
||||||
import Base64 from 'slate-base64-serializer'
|
import Base64 from 'slate-base64-serializer'
|
||||||
import Debug from 'debug'
|
import Debug from 'debug'
|
||||||
|
import ImmutableTypes from 'react-immutable-proptypes'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import SlateTypes from 'slate-prop-types'
|
import SlateTypes from 'slate-prop-types'
|
||||||
import logger from 'slate-dev-logger'
|
import logger from 'slate-dev-logger'
|
||||||
@@ -35,6 +36,7 @@ class Node extends React.Component {
|
|||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
block: SlateTypes.block,
|
block: SlateTypes.block,
|
||||||
|
decorations: ImmutableTypes.list.isRequired,
|
||||||
editor: Types.object.isRequired,
|
editor: Types.object.isRequired,
|
||||||
isSelected: Types.bool.isRequired,
|
isSelected: Types.bool.isRequired,
|
||||||
node: SlateTypes.node.isRequired,
|
node: SlateTypes.node.isRequired,
|
||||||
@@ -136,6 +138,9 @@ class Node extends React.Component {
|
|||||||
// need to be rendered again.
|
// need to be rendered again.
|
||||||
if (n.isSelected || p.isSelected) return true
|
if (n.isSelected || p.isSelected) return true
|
||||||
|
|
||||||
|
// If the decorations have changed, update.
|
||||||
|
if (!n.decorations.equals(p.decorations)) return true
|
||||||
|
|
||||||
// Otherwise, don't update.
|
// Otherwise, don't update.
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -225,11 +230,13 @@ class Node extends React.Component {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
renderNode = (child, isSelected) => {
|
renderNode = (child, isSelected) => {
|
||||||
const { block, editor, node, readOnly, schema, state } = this.props
|
const { block, decorations, editor, node, readOnly, schema, state } = this.props
|
||||||
const Component = child.kind === 'text' ? Text : Node
|
const Component = child.kind === 'text' ? Text : Node
|
||||||
|
const decs = decorations.concat(node.getDecorations(schema))
|
||||||
return (
|
return (
|
||||||
<Component
|
<Component
|
||||||
block={node.kind == 'block' ? node : block}
|
block={node.kind == 'block' ? node : block}
|
||||||
|
decorations={decs}
|
||||||
editor={editor}
|
editor={editor}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
key={child.key}
|
key={child.key}
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
|
|
||||||
import Debug from 'debug'
|
import Debug from 'debug'
|
||||||
|
import ImmutableTypes from 'react-immutable-proptypes'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import SlateTypes from 'slate-prop-types'
|
import SlateTypes from 'slate-prop-types'
|
||||||
import Types from 'prop-types'
|
import Types from 'prop-types'
|
||||||
@@ -24,6 +25,7 @@ class Text extends React.Component {
|
|||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
block: SlateTypes.block,
|
block: SlateTypes.block,
|
||||||
|
decorations: ImmutableTypes.list.isRequired,
|
||||||
editor: Types.object.isRequired,
|
editor: Types.object.isRequired,
|
||||||
node: SlateTypes.node.isRequired,
|
node: SlateTypes.node.isRequired,
|
||||||
parent: SlateTypes.node.isRequired,
|
parent: SlateTypes.node.isRequired,
|
||||||
@@ -63,16 +65,6 @@ class Text extends React.Component {
|
|||||||
// for simplicity we just let them through.
|
// for simplicity we just let them through.
|
||||||
if (n.node != p.node) return true
|
if (n.node != p.node) return true
|
||||||
|
|
||||||
// Re-render if the current decorations have changed, even if the content of
|
|
||||||
// the text node itself hasn't.
|
|
||||||
if (n.schema.hasDecorators) {
|
|
||||||
const nDecorators = n.state.document.getDescendantDecorators(n.node.key, n.schema)
|
|
||||||
const pDecorators = p.state.document.getDescendantDecorators(p.node.key, p.schema)
|
|
||||||
const nRanges = n.node.getRanges(nDecorators)
|
|
||||||
const pRanges = p.node.getRanges(pDecorators)
|
|
||||||
if (!nRanges.equals(pRanges)) return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the node parent is a block node, and it was the last child of the
|
// If the node parent is a block node, and it was the last child of the
|
||||||
// block, re-render to cleanup extra `<br/>` or `\n`.
|
// block, re-render to cleanup extra `<br/>` or `\n`.
|
||||||
if (n.parent.kind == 'block') {
|
if (n.parent.kind == 'block') {
|
||||||
@@ -81,6 +73,9 @@ class Text extends React.Component {
|
|||||||
if (p.node == pLast && n.node != nLast) return true
|
if (p.node == pLast && n.node != nLast) return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Re-render if the current decorations have changed.
|
||||||
|
if (!n.decorations.equals(p.decorations)) return true
|
||||||
|
|
||||||
// Otherwise, don't update.
|
// Otherwise, don't update.
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -95,10 +90,19 @@ class Text extends React.Component {
|
|||||||
const { props } = this
|
const { props } = this
|
||||||
this.debug('render', { props })
|
this.debug('render', { props })
|
||||||
|
|
||||||
const { node, schema, state } = props
|
const { decorations, node, state } = props
|
||||||
const { document } = state
|
const { document } = state
|
||||||
const decorators = schema.hasDecorators ? document.getDescendantDecorators(node.key, schema) : []
|
const { key } = node
|
||||||
const ranges = node.getRanges(decorators)
|
|
||||||
|
const decs = decorations.filter((d) => {
|
||||||
|
const { startKey, endKey } = d
|
||||||
|
if (startKey == key || endKey == key) return true
|
||||||
|
const startsBefore = document.areDescendantsSorted(startKey, key)
|
||||||
|
const endsAfter = document.areDescendantsSorted(key, endKey)
|
||||||
|
return startsBefore && endsAfter
|
||||||
|
})
|
||||||
|
|
||||||
|
const ranges = node.getRanges(decs)
|
||||||
let offset = 0
|
let offset = 0
|
||||||
|
|
||||||
const leaves = ranges.map((range, i) => {
|
const leaves = ranges.map((range, i) => {
|
||||||
@@ -108,7 +112,7 @@ class Text extends React.Component {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span data-key={node.key}>
|
<span data-key={key}>
|
||||||
{leaves}
|
{leaves}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
|
@@ -8,8 +8,8 @@ import { Block, Inline, coreSchema } from 'slate'
|
|||||||
|
|
||||||
import Content from '../components/content'
|
import Content from '../components/content'
|
||||||
import Placeholder from '../components/placeholder'
|
import Placeholder from '../components/placeholder'
|
||||||
import getPoint from '../utils/get-point'
|
|
||||||
import findDOMNode from '../utils/find-dom-node'
|
import findDOMNode from '../utils/find-dom-node'
|
||||||
|
import findPoint from '../utils/find-point'
|
||||||
import { IS_CHROME, IS_MAC, IS_SAFARI } from '../constants/environment'
|
import { IS_CHROME, IS_MAC, IS_SAFARI } from '../constants/environment'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -64,10 +64,9 @@ function Plugin(options = {}) {
|
|||||||
* @param {Event} e
|
* @param {Event} e
|
||||||
* @param {Object} data
|
* @param {Object} data
|
||||||
* @param {Change} change
|
* @param {Change} change
|
||||||
* @param {Editor} editor
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function onBeforeInput(e, data, change, editor) {
|
function onBeforeInput(e, data, change) {
|
||||||
debug('onBeforeInput', { data })
|
debug('onBeforeInput', { data })
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
@@ -82,8 +81,8 @@ function Plugin(options = {}) {
|
|||||||
// the selection has gotten out of sync, and adjust it if so. (03/18/2017)
|
// the selection has gotten out of sync, and adjust it if so. (03/18/2017)
|
||||||
const window = getWindow(e.target)
|
const window = getWindow(e.target)
|
||||||
const native = window.getSelection()
|
const native = window.getSelection()
|
||||||
const a = getPoint(native.anchorNode, native.anchorOffset, state, editor)
|
const a = findPoint(native.anchorNode, native.anchorOffset, state)
|
||||||
const f = getPoint(native.focusNode, native.focusOffset, state, editor)
|
const f = findPoint(native.focusNode, native.focusOffset, state)
|
||||||
const hasMismatch = a && f && (
|
const hasMismatch = a && f && (
|
||||||
anchorKey != a.key ||
|
anchorKey != a.key ||
|
||||||
anchorOffset != a.offset ||
|
anchorOffset != a.offset ||
|
||||||
|
@@ -1,16 +1,22 @@
|
|||||||
|
|
||||||
|
import { Node } from 'slate'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the DOM node for a `node`.
|
* Find the DOM node for a `key`.
|
||||||
*
|
*
|
||||||
* @param {Node} node
|
* @param {String|Node} key
|
||||||
* @return {Element}
|
* @return {Element}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function findDOMNode(node) {
|
function findDOMNode(key) {
|
||||||
const el = window.document.querySelector(`[data-key="${node.key}"]`)
|
if (Node.isNode(key)) {
|
||||||
|
key = key.key
|
||||||
|
}
|
||||||
|
|
||||||
|
const el = window.document.querySelector(`[data-key="${key}"]`)
|
||||||
|
|
||||||
if (!el) {
|
if (!el) {
|
||||||
throw new Error(`Unable to find a DOM node for "${node.key}". This is often because of forgetting to add \`props.attributes\` to a component returned from \`renderNode\`.`)
|
throw new Error(`Unable to find a DOM node for "${key}". This is often because of forgetting to add \`props.attributes\` to a component returned from \`renderNode\`.`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return el
|
return el
|
||||||
|
@@ -2,18 +2,17 @@
|
|||||||
import getWindow from 'get-window'
|
import getWindow from 'get-window'
|
||||||
|
|
||||||
import findClosestNode from './find-closest-node'
|
import findClosestNode from './find-closest-node'
|
||||||
import getPoint from './get-point'
|
import findPoint from './find-point'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the target point for a drop event.
|
* Find the target point for a drop `event`.
|
||||||
*
|
*
|
||||||
* @param {Event} event
|
* @param {Event} event
|
||||||
* @param {State} state
|
* @param {State} state
|
||||||
* @param {Editor} editor
|
|
||||||
* @return {Object}
|
* @return {Object}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function getDropPoint(event, state, editor) {
|
function findDropPoint(event, state) {
|
||||||
const { document } = state
|
const { document } = state
|
||||||
const { nativeEvent, target } = event
|
const { nativeEvent, target } = event
|
||||||
const { x, y } = nativeEvent
|
const { x, y } = nativeEvent
|
||||||
@@ -48,7 +47,6 @@ function getDropPoint(event, state, editor) {
|
|||||||
document.getNextSibling(nodeKey)
|
document.getNextSibling(nodeKey)
|
||||||
const key = text.key
|
const key = text.key
|
||||||
const offset = previous ? text.characters.size : 0
|
const offset = previous ? text.characters.size : 0
|
||||||
|
|
||||||
return { key, offset }
|
return { key, offset }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,12 +69,10 @@ function getDropPoint(event, state, editor) {
|
|||||||
const text = block.getLastText()
|
const text = block.getLastText()
|
||||||
const { key } = text
|
const { key } = text
|
||||||
const offset = 0
|
const offset = 0
|
||||||
|
|
||||||
return { key, offset }
|
return { key, offset }
|
||||||
}
|
}
|
||||||
|
|
||||||
const point = getPoint(n, o, state, editor)
|
const point = findPoint(n, o, state)
|
||||||
|
|
||||||
return point
|
return point
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,4 +82,4 @@ function getDropPoint(event, state, editor) {
|
|||||||
* @type {Function}
|
* @type {Function}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default getDropPoint
|
export default findDropPoint
|
45
packages/slate-react/src/utils/find-native-point.js
Normal file
45
packages/slate-react/src/utils/find-native-point.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
|
||||||
|
import getWindow from 'get-window'
|
||||||
|
|
||||||
|
import findDOMNode from './find-dom-node'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a native DOM selection point from a Slate `key` and `offset`.
|
||||||
|
*
|
||||||
|
* @param {Element} root
|
||||||
|
* @param {String} key
|
||||||
|
* @param {Number} offset
|
||||||
|
* @return {Object}
|
||||||
|
*/
|
||||||
|
|
||||||
|
function findNativePoint(key, offset) {
|
||||||
|
const el = findDOMNode(key)
|
||||||
|
if (!el) return null
|
||||||
|
|
||||||
|
const window = getWindow(el)
|
||||||
|
const iterator = window.document.createNodeIterator(el, NodeFilter.SHOW_TEXT)
|
||||||
|
let start = 0
|
||||||
|
let n
|
||||||
|
|
||||||
|
while (n = iterator.nextNode()) {
|
||||||
|
const { length } = n.textContent
|
||||||
|
const end = start + length
|
||||||
|
|
||||||
|
if (offset <= end) {
|
||||||
|
const o = offset - start
|
||||||
|
return { node: n, offset: o }
|
||||||
|
}
|
||||||
|
|
||||||
|
start = end
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export.
|
||||||
|
*
|
||||||
|
* @type {Function}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default findNativePoint
|
84
packages/slate-react/src/utils/find-point.js
Normal file
84
packages/slate-react/src/utils/find-point.js
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
|
||||||
|
import getWindow from 'get-window'
|
||||||
|
|
||||||
|
import OffsetKey from './offset-key'
|
||||||
|
import normalizeNodeAndOffset from './normalize-node-and-offset'
|
||||||
|
import findClosestNode from './find-closest-node'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constants.
|
||||||
|
*
|
||||||
|
* @type {String}
|
||||||
|
*/
|
||||||
|
|
||||||
|
const OFFSET_KEY_ATTRIBUTE = 'data-offset-key'
|
||||||
|
const RANGE_SELECTOR = `[${OFFSET_KEY_ATTRIBUTE}]`
|
||||||
|
const TEXT_SELECTOR = `[data-key]`
|
||||||
|
const VOID_SELECTOR = '[data-slate-void]'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a Slate point from a DOM selection's `nativeNode` and `nativeOffset`.
|
||||||
|
*
|
||||||
|
* @param {Element} nativeNode
|
||||||
|
* @param {Number} nativeOffset
|
||||||
|
* @param {State} state
|
||||||
|
* @return {Object}
|
||||||
|
*/
|
||||||
|
|
||||||
|
function findPoint(nativeNode, nativeOffset, state) {
|
||||||
|
const {
|
||||||
|
node: nearestNode,
|
||||||
|
offset: nearestOffset,
|
||||||
|
} = normalizeNodeAndOffset(nativeNode, nativeOffset)
|
||||||
|
|
||||||
|
const window = getWindow(nativeNode)
|
||||||
|
const { parentNode } = nearestNode
|
||||||
|
let rangeNode = findClosestNode(parentNode, RANGE_SELECTOR)
|
||||||
|
let offset
|
||||||
|
let node
|
||||||
|
|
||||||
|
// Calculate how far into the text node the `nearestNode` is, so that we can
|
||||||
|
// determine what the offset relative to the text node is.
|
||||||
|
if (rangeNode) {
|
||||||
|
const range = window.document.createRange()
|
||||||
|
const textNode = findClosestNode(rangeNode, TEXT_SELECTOR)
|
||||||
|
range.setStart(textNode, 0)
|
||||||
|
range.setEnd(nearestNode, nearestOffset)
|
||||||
|
node = textNode
|
||||||
|
offset = range.toString().length
|
||||||
|
}
|
||||||
|
|
||||||
|
// For void nodes, the element with the offset key will be a cousin, not an
|
||||||
|
// ancestor, so find it by going down from the nearest void parent.
|
||||||
|
else {
|
||||||
|
const voidNode = findClosestNode(parentNode, VOID_SELECTOR)
|
||||||
|
if (!voidNode) return null
|
||||||
|
rangeNode = voidNode.querySelector(RANGE_SELECTOR)
|
||||||
|
node = rangeNode
|
||||||
|
offset = node.textContent.length
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the string value of the offset key attribute.
|
||||||
|
const offsetKey = rangeNode.getAttribute(OFFSET_KEY_ATTRIBUTE)
|
||||||
|
if (!offsetKey) return null
|
||||||
|
|
||||||
|
const { key } = OffsetKey.parse(offsetKey)
|
||||||
|
|
||||||
|
// COMPAT: If someone is clicking from one Slate editor into another, the
|
||||||
|
// select event fires twice, once for the old editor's `element` first, and
|
||||||
|
// then afterwards for the correct `element`. (2017/03/03)
|
||||||
|
if (!state.document.hasDescendant(key)) return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
offset,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export.
|
||||||
|
*
|
||||||
|
* @type {Function}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default findPoint
|
@@ -1,46 +0,0 @@
|
|||||||
|
|
||||||
import findDeepestNode from './find-deepest-node'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get caret position from selection point.
|
|
||||||
*
|
|
||||||
* @param {String} key
|
|
||||||
* @param {Number} offset
|
|
||||||
* @param {State} state
|
|
||||||
* @param {Editor} editor
|
|
||||||
* @param {Element} el
|
|
||||||
* @return {Object}
|
|
||||||
*/
|
|
||||||
|
|
||||||
function getCaretPosition(key, offset, state, editor, el) {
|
|
||||||
const { document } = state
|
|
||||||
const text = document.getDescendant(key)
|
|
||||||
const schema = editor.getSchema()
|
|
||||||
const decorators = document.getDescendantDecorators(key, schema)
|
|
||||||
const ranges = text.getRanges(decorators)
|
|
||||||
|
|
||||||
let a = 0
|
|
||||||
let index
|
|
||||||
let off
|
|
||||||
|
|
||||||
ranges.forEach((range, i) => {
|
|
||||||
const { length } = range.text
|
|
||||||
a += length
|
|
||||||
if (a < offset) return
|
|
||||||
index = i
|
|
||||||
off = offset - (a - length)
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
const span = el.querySelector(`[data-offset-key="${key}-${index}"]`)
|
|
||||||
const node = findDeepestNode(span)
|
|
||||||
return { node, offset: off }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Export.
|
|
||||||
*
|
|
||||||
* @type {Function}
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default getCaretPosition
|
|
@@ -1,41 +0,0 @@
|
|||||||
|
|
||||||
import OffsetKey from './offset-key'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a point from a native selection's DOM `element` and `offset`.
|
|
||||||
*
|
|
||||||
* @param {Element} element
|
|
||||||
* @param {Number} offset
|
|
||||||
* @param {State} state
|
|
||||||
* @param {Editor} editor
|
|
||||||
* @return {Object}
|
|
||||||
*/
|
|
||||||
|
|
||||||
function getPoint(element, offset, state, editor) {
|
|
||||||
const { document } = state
|
|
||||||
const schema = editor.getSchema()
|
|
||||||
|
|
||||||
// If we can't find an offset key, we can't get a point.
|
|
||||||
const offsetKey = OffsetKey.findKey(element, offset)
|
|
||||||
if (!offsetKey) return null
|
|
||||||
|
|
||||||
// COMPAT: If someone is clicking from one Slate editor into another, the
|
|
||||||
// select event fires two, once for the old editor's `element` first, and
|
|
||||||
// then afterwards for the correct `element`. (2017/03/03)
|
|
||||||
const { key } = offsetKey
|
|
||||||
const node = document.getDescendant(key)
|
|
||||||
if (!node) return null
|
|
||||||
|
|
||||||
const decorators = document.getDescendantDecorators(key, schema)
|
|
||||||
const ranges = node.getRanges(decorators)
|
|
||||||
const point = OffsetKey.findPoint(offsetKey, ranges)
|
|
||||||
return point
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Export.
|
|
||||||
*
|
|
||||||
* @type {Function}
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default getPoint
|
|
@@ -1,7 +1,4 @@
|
|||||||
|
|
||||||
import normalizeNodeAndOffset from './normalize-node-and-offset'
|
|
||||||
import findClosestNode from './find-closest-node'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Offset key parser regex.
|
* Offset key parser regex.
|
||||||
*
|
*
|
||||||
@@ -10,117 +7,6 @@ import findClosestNode from './find-closest-node'
|
|||||||
|
|
||||||
const PARSER = /^(\w+)(?:-(\d+))?$/
|
const PARSER = /^(\w+)(?:-(\d+))?$/
|
||||||
|
|
||||||
/**
|
|
||||||
* Offset key attribute name.
|
|
||||||
*
|
|
||||||
* @type {String}
|
|
||||||
*/
|
|
||||||
|
|
||||||
const ATTRIBUTE = 'data-offset-key'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Offset key attribute selector.
|
|
||||||
*
|
|
||||||
* @type {String}
|
|
||||||
*/
|
|
||||||
|
|
||||||
const SELECTOR = `[${ATTRIBUTE}]`
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Void node selection.
|
|
||||||
*
|
|
||||||
* @type {String}
|
|
||||||
*/
|
|
||||||
|
|
||||||
const VOID_SELECTOR = '[data-slate-void]'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find the start and end bounds from an `offsetKey` and `ranges`.
|
|
||||||
*
|
|
||||||
* @param {Number} index
|
|
||||||
* @param {List<Range>} ranges
|
|
||||||
* @return {Object}
|
|
||||||
*/
|
|
||||||
|
|
||||||
function findBounds(index, ranges) {
|
|
||||||
const range = ranges.get(index)
|
|
||||||
const start = ranges
|
|
||||||
.slice(0, index)
|
|
||||||
.reduce((memo, r) => {
|
|
||||||
return memo += r.text.length
|
|
||||||
}, 0)
|
|
||||||
|
|
||||||
return {
|
|
||||||
start,
|
|
||||||
end: start + range.text.length
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* From a DOM node, find the closest parent's offset key.
|
|
||||||
*
|
|
||||||
* @param {Element} rawNode
|
|
||||||
* @param {Number} rawOffset
|
|
||||||
* @return {Object}
|
|
||||||
*/
|
|
||||||
|
|
||||||
function findKey(rawNode, rawOffset) {
|
|
||||||
let { node, offset } = normalizeNodeAndOffset(rawNode, rawOffset)
|
|
||||||
const { parentNode } = node
|
|
||||||
|
|
||||||
// Find the closest parent with an offset key attribute.
|
|
||||||
let closest = findClosestNode(parentNode, SELECTOR)
|
|
||||||
|
|
||||||
// For void nodes, the element with the offset key will be a cousin, not an
|
|
||||||
// ancestor, so find it by going down from the nearest void parent.
|
|
||||||
if (!closest) {
|
|
||||||
const closestVoid = findClosestNode(parentNode, VOID_SELECTOR)
|
|
||||||
if (!closestVoid) return null
|
|
||||||
closest = closestVoid.querySelector(SELECTOR)
|
|
||||||
offset = closest.textContent.length
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the string value of the offset key attribute.
|
|
||||||
const offsetKey = closest.getAttribute(ATTRIBUTE)
|
|
||||||
|
|
||||||
// If we still didn't find an offset key, abort.
|
|
||||||
if (!offsetKey) return null
|
|
||||||
|
|
||||||
// Return the parsed the offset key.
|
|
||||||
const parsed = parse(offsetKey)
|
|
||||||
return {
|
|
||||||
key: parsed.key,
|
|
||||||
index: parsed.index,
|
|
||||||
offset
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find the selection point from an `offsetKey` and `ranges`.
|
|
||||||
*
|
|
||||||
* @param {Object} offsetKey
|
|
||||||
* @param {List<Range>} ranges
|
|
||||||
* @return {Object}
|
|
||||||
*/
|
|
||||||
|
|
||||||
function findPoint(offsetKey, ranges) {
|
|
||||||
let { key, index, offset } = offsetKey
|
|
||||||
const { start, end } = findBounds(index, ranges)
|
|
||||||
|
|
||||||
// Don't let the offset be outside of the start and end bounds.
|
|
||||||
offset = start + offset
|
|
||||||
offset = Math.max(offset, start)
|
|
||||||
offset = Math.min(offset, end)
|
|
||||||
|
|
||||||
return {
|
|
||||||
key,
|
|
||||||
index,
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
offset
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse an offset key `string`.
|
* Parse an offset key `string`.
|
||||||
*
|
*
|
||||||
@@ -158,9 +44,6 @@ function stringify(object) {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
findBounds,
|
|
||||||
findKey,
|
|
||||||
findPoint,
|
|
||||||
parse,
|
parse,
|
||||||
stringify
|
stringify
|
||||||
}
|
}
|
||||||
|
@@ -1,19 +1,19 @@
|
|||||||
/** @jsx h */
|
/** @jsx h */
|
||||||
|
|
||||||
import h from '../../helpers/h'
|
import h from '../../helpers/h'
|
||||||
import { Mark } from 'slate'
|
|
||||||
|
|
||||||
export const schema = {
|
export const schema = {
|
||||||
nodes: {
|
nodes: {
|
||||||
paragraph: {
|
paragraph: {
|
||||||
decorate(text, block) {
|
decorate(block) {
|
||||||
let { characters } = text
|
const text = block.getFirstText()
|
||||||
let second = characters.get(1)
|
return [{
|
||||||
const mark = Mark.create({ type: 'bold' })
|
anchorKey: text.key,
|
||||||
const marks = second.marks.add(mark)
|
anchorOffset: 1,
|
||||||
second = second.merge({ marks })
|
focusKey: text.key,
|
||||||
characters = characters.set(1, second)
|
focusOffset: 2,
|
||||||
return characters
|
marks: [{ type: 'bold' }]
|
||||||
|
}]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -1,4 +1,6 @@
|
|||||||
|
|
||||||
|
import State from '../models/state'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Changes.
|
* Changes.
|
||||||
*
|
*
|
||||||
@@ -8,20 +10,20 @@
|
|||||||
const Changes = {}
|
const Changes = {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set `properties` on the top-level state's data.
|
* Set `properties` on the state.
|
||||||
*
|
*
|
||||||
* @param {Change} change
|
* @param {Change} change
|
||||||
* @param {Object} properties
|
* @param {Object|State} properties
|
||||||
*/
|
*/
|
||||||
|
|
||||||
Changes.setData = (change, properties) => {
|
Changes.setState = (change, properties) => {
|
||||||
|
properties = State.createProperties(properties)
|
||||||
const { state } = change
|
const { state } = change
|
||||||
const { data } = state
|
|
||||||
|
|
||||||
change.applyOperation({
|
change.applyOperation({
|
||||||
type: 'set_data',
|
type: 'set_state',
|
||||||
properties,
|
properties,
|
||||||
data,
|
state,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -165,19 +165,12 @@ class Node {
|
|||||||
first = normalizeKey(first)
|
first = normalizeKey(first)
|
||||||
second = normalizeKey(second)
|
second = normalizeKey(second)
|
||||||
|
|
||||||
let sorted
|
const keys = this.getKeysAsArray()
|
||||||
|
const firstIndex = keys.indexOf(first)
|
||||||
|
const secondIndex = keys.indexOf(second)
|
||||||
|
if (firstIndex == -1 || secondIndex == -1) return null
|
||||||
|
|
||||||
this.forEachDescendant((n) => {
|
return firstIndex < secondIndex
|
||||||
if (n.key === first) {
|
|
||||||
sorted = true
|
|
||||||
return false
|
|
||||||
} else if (n.key === second) {
|
|
||||||
sorted = false
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return sorted
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -609,8 +602,8 @@ class Node {
|
|||||||
* @return {Array}
|
* @return {Array}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
getDecorators(schema) {
|
getDecorations(schema) {
|
||||||
return schema.__getDecorators(this)
|
return schema.__getDecorations(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -674,32 +667,6 @@ class Node {
|
|||||||
return descendant
|
return descendant
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the decorators for a descendant by `key` given a `schema`.
|
|
||||||
*
|
|
||||||
* @param {String} key
|
|
||||||
* @param {Schema} schema
|
|
||||||
* @return {Array}
|
|
||||||
*/
|
|
||||||
|
|
||||||
getDescendantDecorators(key, schema) {
|
|
||||||
if (!schema.hasDecorators) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
const descendant = this.assertDescendant(key)
|
|
||||||
let child = this.getFurthestAncestor(key)
|
|
||||||
let decorators = []
|
|
||||||
|
|
||||||
while (child != descendant) {
|
|
||||||
decorators = decorators.concat(child.getDecorators(schema))
|
|
||||||
child = child.getFurthestAncestor(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
decorators = decorators.concat(descendant.getDecorators(schema))
|
|
||||||
return decorators
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the first child text node.
|
* Get the first child text node.
|
||||||
*
|
*
|
||||||
@@ -958,18 +925,29 @@ class Node {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a set of all keys in the node.
|
* Return a set of all keys in the node as an array.
|
||||||
*
|
*
|
||||||
* @return {Set<String>}
|
* @return {Array<String>}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
getKeys() {
|
getKeysAsArray() {
|
||||||
const keys = []
|
const keys = []
|
||||||
|
|
||||||
this.forEachDescendant((desc) => {
|
this.forEachDescendant((desc) => {
|
||||||
keys.push(desc.key)
|
keys.push(desc.key)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a set of all keys in the node.
|
||||||
|
*
|
||||||
|
* @return {Set<String>}
|
||||||
|
*/
|
||||||
|
|
||||||
|
getKeys() {
|
||||||
|
const keys = this.getKeysAsArray()
|
||||||
return new Set(keys)
|
return new Set(keys)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2102,6 +2080,7 @@ memoize(Node.prototype, [
|
|||||||
'getInlines',
|
'getInlines',
|
||||||
'getInlinesAsArray',
|
'getInlinesAsArray',
|
||||||
'getKeys',
|
'getKeys',
|
||||||
|
'getKeysAsArray',
|
||||||
'getLastText',
|
'getLastText',
|
||||||
'getMarks',
|
'getMarks',
|
||||||
'getOrderedMarks',
|
'getOrderedMarks',
|
||||||
@@ -2135,11 +2114,10 @@ memoize(Node.prototype, [
|
|||||||
'getClosestVoid',
|
'getClosestVoid',
|
||||||
'getCommonAncestor',
|
'getCommonAncestor',
|
||||||
'getComponent',
|
'getComponent',
|
||||||
'getDecorators',
|
'getDecorations',
|
||||||
'getDepth',
|
'getDepth',
|
||||||
'getDescendant',
|
'getDescendant',
|
||||||
'getDescendantAtPath',
|
'getDescendantAtPath',
|
||||||
'getDescendantDecorators',
|
|
||||||
'getFragmentAtRange',
|
'getFragmentAtRange',
|
||||||
'getFurthestBlock',
|
'getFurthestBlock',
|
||||||
'getFurthestInline',
|
'getFurthestInline',
|
||||||
|
@@ -7,6 +7,7 @@ import typeOf from 'type-of'
|
|||||||
import { Record } from 'immutable'
|
import { Record } from 'immutable'
|
||||||
|
|
||||||
import MODEL_TYPES from '../constants/model-types'
|
import MODEL_TYPES from '../constants/model-types'
|
||||||
|
import Selection from '../models/selection'
|
||||||
import isReactComponent from '../utils/is-react-component'
|
import isReactComponent from '../utils/is-react-component'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -126,24 +127,33 @@ class Schema extends Record(DEFAULTS) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the decorators for an `object`.
|
* Return the decorations for an `object`.
|
||||||
*
|
*
|
||||||
* This method is private, because it should always be called on one of the
|
* This method is private, because it should always be called on one of the
|
||||||
* often-changing immutable objects instead, since it will be memoized for
|
* often-changing immutable objects instead, since it will be memoized for
|
||||||
* much better performance.
|
* much better performance.
|
||||||
*
|
*
|
||||||
* @param {Mixed} object
|
* @param {Mixed} object
|
||||||
* @return {Array}
|
* @return {List<Selection>}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
__getDecorators(object) {
|
__getDecorations(object) {
|
||||||
return this.rules
|
const array = []
|
||||||
.filter(rule => rule.decorate && rule.match(object))
|
|
||||||
.map((rule) => {
|
this.rules.forEach((rule) => {
|
||||||
return (text) => {
|
if (!rule.decorate) return
|
||||||
return rule.decorate(text, object)
|
if (!rule.match(object)) return
|
||||||
}
|
|
||||||
|
const decorations = rule.decorate(object)
|
||||||
|
if (!decorations.length) return
|
||||||
|
|
||||||
|
decorations.forEach((dec) => {
|
||||||
|
array.push(dec)
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const list = Selection.createList(array)
|
||||||
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -1,9 +1,10 @@
|
|||||||
|
|
||||||
import isPlainObject from 'is-plain-object'
|
import isPlainObject from 'is-plain-object'
|
||||||
import logger from 'slate-dev-logger'
|
import logger from 'slate-dev-logger'
|
||||||
import { Record } from 'immutable'
|
import { List, Record, Set } from 'immutable'
|
||||||
|
|
||||||
import MODEL_TYPES from '../constants/model-types'
|
import MODEL_TYPES from '../constants/model-types'
|
||||||
|
import Mark from './mark'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default properties.
|
* Default properties.
|
||||||
@@ -48,6 +49,22 @@ class Selection extends Record(DEFAULTS) {
|
|||||||
throw new Error(`\`Selection.create\` only accepts objects or selections, but you passed it: ${attrs}`)
|
throw new Error(`\`Selection.create\` only accepts objects or selections, but you passed it: ${attrs}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a list of `Selections` from a `value`.
|
||||||
|
*
|
||||||
|
* @param {Array<Selection|Object>|List<Selection|Object>} value
|
||||||
|
* @return {List<Selection>}
|
||||||
|
*/
|
||||||
|
|
||||||
|
static createList(value = []) {
|
||||||
|
if (List.isList(value) || Array.isArray(value)) {
|
||||||
|
const list = new List(value.map(Selection.create))
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`\`Selection.createList\` only accepts arrays or lists, but you passed it: ${value}`)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a dictionary of settable selection properties from `attrs`.
|
* Create a dictionary of settable selection properties from `attrs`.
|
||||||
*
|
*
|
||||||
@@ -108,7 +125,7 @@ class Selection extends Record(DEFAULTS) {
|
|||||||
focusOffset,
|
focusOffset,
|
||||||
isBackward,
|
isBackward,
|
||||||
isFocused,
|
isFocused,
|
||||||
marks,
|
marks: marks == null ? null : new Set(marks.map(Mark.fromJSON)),
|
||||||
})
|
})
|
||||||
|
|
||||||
return selection
|
return selection
|
||||||
|
@@ -5,7 +5,7 @@ import { Record, Set, List, Map } from 'immutable'
|
|||||||
|
|
||||||
import MODEL_TYPES from '../constants/model-types'
|
import MODEL_TYPES from '../constants/model-types'
|
||||||
import SCHEMA from '../schemas/core'
|
import SCHEMA from '../schemas/core'
|
||||||
import Change from './change'
|
import Data from './data'
|
||||||
import Document from './document'
|
import Document from './document'
|
||||||
import History from './history'
|
import History from './history'
|
||||||
import Selection from './selection'
|
import Selection from './selection'
|
||||||
@@ -21,6 +21,7 @@ const DEFAULTS = {
|
|||||||
selection: Selection.create(),
|
selection: Selection.create(),
|
||||||
history: History.create(),
|
history: History.create(),
|
||||||
data: new Map(),
|
data: new Map(),
|
||||||
|
decorations: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -51,6 +52,31 @@ class State extends Record(DEFAULTS) {
|
|||||||
throw new Error(`\`State.create\` only accepts objects or states, but you passed it: ${attrs}`)
|
throw new Error(`\`State.create\` only accepts objects or states, but you passed it: ${attrs}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a dictionary of settable state properties from `attrs`.
|
||||||
|
*
|
||||||
|
* @param {Object|State} attrs
|
||||||
|
* @return {Object}
|
||||||
|
*/
|
||||||
|
|
||||||
|
static createProperties(attrs = {}) {
|
||||||
|
if (State.isState(attrs)) {
|
||||||
|
return {
|
||||||
|
data: attrs.data,
|
||||||
|
decorations: attrs.decorations,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPlainObject(attrs)) {
|
||||||
|
const props = {}
|
||||||
|
if ('data' in attrs) props.data = Data.create(attrs.data)
|
||||||
|
if ('decorations' in attrs) props.decorations = Selection.createList(attrs.decorations)
|
||||||
|
return props
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`\`State.createProperties\` only accepts objects or states, but you passed it: ${attrs}`)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a `State` from a JSON `object`.
|
* Create a `State` from a JSON `object`.
|
||||||
*
|
*
|
||||||
@@ -549,6 +575,7 @@ class State extends Record(DEFAULTS) {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
change(attrs = {}) {
|
change(attrs = {}) {
|
||||||
|
const Change = require('./change').default
|
||||||
return new Change({ ...attrs, state: this })
|
return new Change({ ...attrs, state: this })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -572,11 +599,20 @@ class State extends Record(DEFAULTS) {
|
|||||||
|
|
||||||
toJSON(options = {}) {
|
toJSON(options = {}) {
|
||||||
const object = {
|
const object = {
|
||||||
|
kind: this.kind,
|
||||||
data: this.data.toJSON(),
|
data: this.data.toJSON(),
|
||||||
document: this.document.toJSON(options),
|
document: this.document.toJSON(options),
|
||||||
kind: this.kind,
|
|
||||||
history: this.history.toJSON(),
|
|
||||||
selection: this.selection.toJSON(),
|
selection: this.selection.toJSON(),
|
||||||
|
decorations: this.decorations ? this.decorations.toArray().map(d => d.toJSON()) : null,
|
||||||
|
history: this.history.toJSON(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.preserveData) {
|
||||||
|
delete object.data
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.preserveDecorations) {
|
||||||
|
delete object.decorations
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!options.preserveHistory) {
|
if (!options.preserveHistory) {
|
||||||
@@ -587,10 +623,6 @@ class State extends Record(DEFAULTS) {
|
|||||||
delete object.selection
|
delete object.selection
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!options.preserveStateData) {
|
|
||||||
delete object.data
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.preserveSelection && !options.preserveKeys) {
|
if (options.preserveSelection && !options.preserveKeys) {
|
||||||
const { document, selection } = this
|
const { document, selection } = this
|
||||||
object.selection.anchorPath = selection.isSet ? document.getPath(selection.anchorKey) : null
|
object.selection.anchorPath = selection.isSet ? document.getPath(selection.anchorKey) : null
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
import isPlainObject from 'is-plain-object'
|
import isPlainObject from 'is-plain-object'
|
||||||
import logger from 'slate-dev-logger'
|
import logger from 'slate-dev-logger'
|
||||||
import { List, Record, OrderedSet, is } from 'immutable'
|
import { List, OrderedSet, Record, Set, is } from 'immutable'
|
||||||
|
|
||||||
import Character from './character'
|
import Character from './character'
|
||||||
import Mark from './mark'
|
import Mark from './mark'
|
||||||
@@ -193,11 +193,25 @@ class Text extends Record(DEFAULTS) {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
addMark(index, length, mark) {
|
addMark(index, length, mark) {
|
||||||
|
const marks = new Set([mark])
|
||||||
|
return this.addMarks(index, length, marks)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a `set` of marks at `index` and `length`.
|
||||||
|
*
|
||||||
|
* @param {Number} index
|
||||||
|
* @param {Number} length
|
||||||
|
* @param {Set<Mark>} set
|
||||||
|
* @return {Text}
|
||||||
|
*/
|
||||||
|
|
||||||
|
addMarks(index, length, set) {
|
||||||
const characters = this.characters.map((char, i) => {
|
const characters = this.characters.map((char, i) => {
|
||||||
if (i < index) return char
|
if (i < index) return char
|
||||||
if (i >= index + length) return char
|
if (i >= index + length) return char
|
||||||
let { marks } = char
|
let { marks } = char
|
||||||
marks = marks.add(mark)
|
marks = marks.union(set)
|
||||||
char = char.set('marks', marks)
|
char = char.set('marks', marks)
|
||||||
return char
|
return char
|
||||||
})
|
})
|
||||||
@@ -206,24 +220,29 @@ class Text extends Record(DEFAULTS) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Derive a set of decorated characters with `decorators`.
|
* Derive a set of decorated characters with `decorations`.
|
||||||
*
|
*
|
||||||
* @param {Array} decorators
|
* @param {List<Decoration>} decorations
|
||||||
* @return {List<Character>}
|
* @return {List<Character>}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
getDecorations(decorators) {
|
getDecoratedCharacters(decorations) {
|
||||||
const node = this
|
let node = this
|
||||||
let { characters } = node
|
const { key, characters } = node
|
||||||
|
|
||||||
|
// PERF: Exit early if there are no characters to be decorated.
|
||||||
if (characters.size == 0) return characters
|
if (characters.size == 0) return characters
|
||||||
|
|
||||||
for (let i = 0; i < decorators.length; i++) {
|
decorations.forEach((range) => {
|
||||||
const decorator = decorators[i]
|
const { startKey, endKey, startOffset, endOffset, marks } = range
|
||||||
const decorateds = decorator(node)
|
const hasStart = startKey == key
|
||||||
characters = characters.merge(decorateds)
|
const hasEnd = endKey == key
|
||||||
}
|
const index = hasStart ? startOffset : 0
|
||||||
|
const length = hasEnd ? endOffset - index : characters.size
|
||||||
|
node = node.addMarks(index, length, marks)
|
||||||
|
})
|
||||||
|
|
||||||
return characters
|
return node.characters
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -233,8 +252,8 @@ class Text extends Record(DEFAULTS) {
|
|||||||
* @return {Array}
|
* @return {Array}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
getDecorators(schema) {
|
getDecorations(schema) {
|
||||||
return schema.__getDecorators(this)
|
return schema.__getDecorations(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -291,12 +310,12 @@ class Text extends Record(DEFAULTS) {
|
|||||||
/**
|
/**
|
||||||
* Derive the ranges for a list of `characters`.
|
* Derive the ranges for a list of `characters`.
|
||||||
*
|
*
|
||||||
* @param {Array|Void} decorators (optional)
|
* @param {Array|Void} decorations (optional)
|
||||||
* @return {List<Range>}
|
* @return {List<Range>}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
getRanges(decorators = []) {
|
getRanges(decorations = []) {
|
||||||
const characters = this.getDecorations(decorators)
|
const characters = this.getDecoratedCharacters(decorations)
|
||||||
let ranges = []
|
let ranges = []
|
||||||
|
|
||||||
// PERF: cache previous values for faster lookup.
|
// PERF: cache previous values for faster lookup.
|
||||||
@@ -513,8 +532,8 @@ memoize(Text.prototype, [
|
|||||||
})
|
})
|
||||||
|
|
||||||
memoize(Text.prototype, [
|
memoize(Text.prototype, [
|
||||||
|
'getDecoratedCharacters',
|
||||||
'getDecorations',
|
'getDecorations',
|
||||||
'getDecorators',
|
|
||||||
'getMarksAtIndex',
|
'getMarksAtIndex',
|
||||||
'getRanges',
|
'getRanges',
|
||||||
'validate'
|
'validate'
|
||||||
|
@@ -310,23 +310,6 @@ const APPLIERS = {
|
|||||||
return state
|
return state
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Set `data` on `state`.
|
|
||||||
*
|
|
||||||
* @param {State} state
|
|
||||||
* @param {Object} operation
|
|
||||||
* @return {State}
|
|
||||||
*/
|
|
||||||
|
|
||||||
set_data(state, operation) {
|
|
||||||
const { properties } = operation
|
|
||||||
let { data } = state
|
|
||||||
|
|
||||||
data = data.merge(properties)
|
|
||||||
state = state.set('data', data)
|
|
||||||
return state
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set `properties` on mark on text at `offset` and `length` in node by `path`.
|
* Set `properties` on mark on text at `offset` and `length` in node by `path`.
|
||||||
*
|
*
|
||||||
@@ -359,15 +342,13 @@ const APPLIERS = {
|
|||||||
let { document } = state
|
let { document } = state
|
||||||
let node = document.assertPath(path)
|
let node = document.assertPath(path)
|
||||||
|
|
||||||
// Warn when trying to overwite a node's children.
|
if ('nodes' in properties) {
|
||||||
if (properties.nodes && properties.nodes != node.nodes) {
|
logger.warn('Updating a Node\'s `nodes` property via `setNode()` is not allowed. Use the appropriate insertion and removal methods instead. The operation in question was:', operation)
|
||||||
logger.warn('Updating a Node\'s `nodes` property via `setNode()` is not allowed. Use the appropriate insertion and removal operations instead. The opeartion in question was:', operation)
|
|
||||||
delete properties.nodes
|
delete properties.nodes
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warn when trying to change a node's key.
|
if ('key' in properties) {
|
||||||
if (properties.key && properties.key != node.key) {
|
logger.warn('Updating a Node\'s `key` property via `setNode()` is not allowed. There should be no reason to do this. The operation in question was:', operation)
|
||||||
logger.warn('Updating a Node\'s `key` property via `setNode()` is not allowed. There should be no reason to do this. The opeartion in question was:', operation)
|
|
||||||
delete properties.key
|
delete properties.key
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -413,6 +394,36 @@ const APPLIERS = {
|
|||||||
return state
|
return state
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set `properties` on `state`.
|
||||||
|
*
|
||||||
|
* @param {State} state
|
||||||
|
* @param {Object} operation
|
||||||
|
* @return {State}
|
||||||
|
*/
|
||||||
|
|
||||||
|
set_state(state, operation) {
|
||||||
|
const { properties } = operation
|
||||||
|
|
||||||
|
if ('document' in properties) {
|
||||||
|
logger.warn('Updating `state.document` property via `setState()` is not allowed. Use the appropriate document updating methods instead. The operation in question was:', operation)
|
||||||
|
delete properties.document
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('selection' in properties) {
|
||||||
|
logger.warn('Updating `state.selection` property via `setState()` is not allowed. Use the appropriate selection updating methods instead. The operation in question was:', operation)
|
||||||
|
delete properties.selection
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('history' in properties) {
|
||||||
|
logger.warn('Updating `state.history` property via `setState()` is not allowed. Use the appropriate history updating methods instead. The operation in question was:', operation)
|
||||||
|
delete properties.history
|
||||||
|
}
|
||||||
|
|
||||||
|
state = state.merge(properties)
|
||||||
|
return state
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Split a node by `path` at `offset`.
|
* Split a node by `path` at `offset`.
|
||||||
*
|
*
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
import h from '../../../helpers/h'
|
import h from '../../../helpers/h'
|
||||||
|
|
||||||
export default function (change) {
|
export default function (change) {
|
||||||
change.setData({ thing: 'value' })
|
change.setState({ data: { thing: 'value' }})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const input = (
|
export const input = (
|
||||||
|
@@ -42,5 +42,5 @@ export const output = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const options = {
|
export const options = {
|
||||||
preserveStateData: true,
|
preserveData: true,
|
||||||
}
|
}
|
||||||
|
@@ -5016,6 +5016,10 @@ react-frame-component@^1.1.1:
|
|||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-frame-component/-/react-frame-component-1.1.1.tgz#05b7f5689a2d373f25baf0c9adb0e59d78103388"
|
resolved "https://registry.yarnpkg.com/react-frame-component/-/react-frame-component-1.1.1.tgz#05b7f5689a2d373f25baf0c9adb0e59d78103388"
|
||||||
|
|
||||||
|
react-immutable-proptypes@^2.1.0:
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-immutable-proptypes/-/react-immutable-proptypes-2.1.0.tgz#023d6f39bb15c97c071e9e60d00d136eac5fa0b4"
|
||||||
|
|
||||||
react-portal@^3.1.0:
|
react-portal@^3.1.0:
|
||||||
version "3.1.0"
|
version "3.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-portal/-/react-portal-3.1.0.tgz#865c44fb72a1da106c649206936559ce891ee899"
|
resolved "https://registry.yarnpkg.com/react-portal/-/react-portal-3.1.0.tgz#865c44fb72a1da106c649206936559ce891ee899"
|
||||||
|
Reference in New Issue
Block a user