mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-06 15:26:34 +02:00
refactor examples, normalize selections
This commit is contained in:
8
Makefile
8
Makefile
@@ -31,10 +31,6 @@ dist: $(shell find ./lib)
|
||||
example-auto-markdown:
|
||||
@ $(browserify) --debug --transform babelify --outfile ./examples/auto-markdown/build.js ./examples/auto-markdown/index.js
|
||||
|
||||
# Build the basic example.
|
||||
example-basic:
|
||||
@ $(browserify) --debug --transform babelify --outfile ./examples/basic/build.js ./examples/basic/index.js
|
||||
|
||||
# Build the plain-text example.
|
||||
example-plain-text:
|
||||
@ $(browserify) --debug --transform babelify --outfile ./examples/plain-text/build.js ./examples/plain-text/index.js
|
||||
@@ -70,10 +66,6 @@ test-server:
|
||||
watch-example-auto-markdown:
|
||||
@ $(MAKE) example-auto-markdown browserify=$(watchify)
|
||||
|
||||
# Watch the basic example.
|
||||
watch-example-basic:
|
||||
@ $(MAKE) example-basic browserify=$(watchify)
|
||||
|
||||
# Watch the plain-text example.
|
||||
watch-example-plain-text:
|
||||
@ $(MAKE) example-plain-text browserify=$(watchify)
|
||||
|
@@ -1,84 +1,9 @@
|
||||
|
||||
import Editor, { Mark, Raw } from '../..'
|
||||
import Editor, { Raw } from '../..'
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import keycode from 'keycode'
|
||||
|
||||
/**
|
||||
* Define the initial state.
|
||||
*
|
||||
* @type {Object} state
|
||||
*/
|
||||
|
||||
const state = {
|
||||
nodes: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
nodes: [
|
||||
{
|
||||
type: 'text',
|
||||
ranges: [
|
||||
{
|
||||
text: 'The editor gives you full control over the logic you can add. For example, it\'s fairly common to want to add markdown-like shortcuts to editors. So that, when you start a line with "> " you get a blockquote that looks like this:',
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'block-quote',
|
||||
nodes: [
|
||||
{
|
||||
type: 'text',
|
||||
ranges: [
|
||||
{
|
||||
text: 'A wise quote.'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
nodes: [
|
||||
{
|
||||
type: 'text',
|
||||
ranges: [
|
||||
{
|
||||
text: 'Order when you start a line with "## " you get a level-two heading, like this:',
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'heading-two',
|
||||
nodes: [
|
||||
{
|
||||
type: 'text',
|
||||
ranges: [
|
||||
{
|
||||
text: 'Try it out!'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
nodes: [
|
||||
{
|
||||
type: 'text',
|
||||
ranges: [
|
||||
{
|
||||
text: 'Try it out for yourself! Try starting a new line with ">", "-", or "#"s.'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
import state from './state.json'
|
||||
|
||||
/**
|
||||
* Define our example app.
|
||||
|
69
examples/auto-markdown/state.json
Normal file
69
examples/auto-markdown/state.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"type": "paragraph",
|
||||
"nodes": [
|
||||
{
|
||||
"type": "text",
|
||||
"ranges": [
|
||||
{
|
||||
"text": "The editor gives you full control over the logic you can add. For example, it's fairly common to want to add markdown-like shortcuts to editors. So that, when you start a line with \"> \" you get a blockquote that looks like this:"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "block-quote",
|
||||
"nodes": [
|
||||
{
|
||||
"type": "text",
|
||||
"ranges": [
|
||||
{
|
||||
"text": "A wise quote."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "paragraph",
|
||||
"nodes": [
|
||||
{
|
||||
"type": "text",
|
||||
"ranges": [
|
||||
{
|
||||
"text": "Order when you start a line with \"## \" you get a level-two heading, like this:"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "heading-two",
|
||||
"nodes": [
|
||||
{
|
||||
"type": "text",
|
||||
"ranges": [
|
||||
{
|
||||
"text": "Try it out!"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "paragraph",
|
||||
"nodes": [
|
||||
{
|
||||
"type": "text",
|
||||
"ranges": [
|
||||
{
|
||||
"text": "Try it out for yourself! Try starting a new line with \">\", \"-\", or \"#\"s."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@@ -1,24 +0,0 @@
|
||||
|
||||
html {
|
||||
background: #eee;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
main {
|
||||
background: #fff;
|
||||
padding: 10px;
|
||||
max-width: 40em;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
main * + * {
|
||||
margin-top: 1em;
|
||||
}
|
@@ -1,11 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Editor | Basic Example</title>
|
||||
<link rel="stylesheet" href="index.css">
|
||||
</head>
|
||||
<body>
|
||||
<main></main>
|
||||
<script src="build.js"></script>
|
||||
</body>
|
||||
</html>
|
@@ -1,123 +0,0 @@
|
||||
|
||||
import Editor, { State, Raw } from '../..'
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
|
||||
/**
|
||||
* State.
|
||||
*/
|
||||
|
||||
const state = {
|
||||
nodes: [
|
||||
{
|
||||
type: 'code',
|
||||
nodes: [
|
||||
{
|
||||
type: 'text',
|
||||
ranges: [
|
||||
{
|
||||
text: 'A\nfew\nlines\nof\ncode.'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
nodes: [
|
||||
{
|
||||
type: 'text',
|
||||
ranges: [
|
||||
{
|
||||
text: 'A '
|
||||
},
|
||||
{
|
||||
text: 'simple',
|
||||
marks: [
|
||||
{
|
||||
type: 'bold'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
text: ' paragraph of text.'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* App.
|
||||
*/
|
||||
|
||||
class App extends React.Component {
|
||||
|
||||
state = {
|
||||
state: Raw.deserialize(state)
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Editor
|
||||
renderNode={node => this.renderNode(node)}
|
||||
renderMark={mark => this.renderMark(mark)}
|
||||
state={this.state.state}
|
||||
onChange={(state) => {
|
||||
console.groupCollapsed('Change!')
|
||||
console.log('Document:', state.document.toJS())
|
||||
console.log('Selection:', state.selection.toJS())
|
||||
console.log('Content:', Raw.serialize(state))
|
||||
console.groupEnd()
|
||||
this.setState({ state })
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
renderNode(node) {
|
||||
switch (node.type) {
|
||||
case 'code': {
|
||||
return (props) => {
|
||||
return (
|
||||
<pre>
|
||||
<code>
|
||||
{props.children}
|
||||
</code>
|
||||
</pre>
|
||||
)
|
||||
}
|
||||
}
|
||||
case 'paragraph': {
|
||||
return (props) => {
|
||||
return (
|
||||
<p>
|
||||
{props.children}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderMark(mark) {
|
||||
switch (mark.type) {
|
||||
case 'bold': {
|
||||
return {
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach.
|
||||
*/
|
||||
|
||||
const app = <App />
|
||||
const root = document.body.querySelector('main')
|
||||
ReactDOM.render(app, root)
|
@@ -2,14 +2,7 @@
|
||||
import Editor, { Character, Document, Element, State, Text } from '../..'
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
|
||||
/**
|
||||
* The initial editor state.
|
||||
*
|
||||
* @type {String}
|
||||
*/
|
||||
|
||||
const state = 'This is editable plain text, just like a <textarea>!'
|
||||
import state from './state.json'
|
||||
|
||||
/**
|
||||
* A helper to deserialize a string into an editor state.
|
||||
@@ -52,17 +45,29 @@ function serialize(state) {
|
||||
}
|
||||
|
||||
/**
|
||||
* The example's app.
|
||||
* The example app.
|
||||
*
|
||||
* @type {Component} App
|
||||
*/
|
||||
|
||||
class App extends React.Component {
|
||||
|
||||
/**
|
||||
* Deserialize the initial editor state.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
|
||||
state = {
|
||||
state: deserialize(state)
|
||||
};
|
||||
|
||||
/**
|
||||
* Render the editor.
|
||||
*
|
||||
* @return {Component} component
|
||||
*/
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Editor
|
||||
@@ -82,7 +87,7 @@ class App extends React.Component {
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach.
|
||||
* Mount the example app.
|
||||
*/
|
||||
|
||||
const app = <App />
|
||||
|
1
examples/plain-text/state.json
Normal file
1
examples/plain-text/state.json
Normal file
@@ -0,0 +1 @@
|
||||
"This is editable plain text, just like a <textarea>!"
|
@@ -2,110 +2,7 @@
|
||||
import Editor, { Mark, Raw } from '../..'
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
|
||||
/**
|
||||
* State.
|
||||
*/
|
||||
|
||||
const state = {
|
||||
nodes: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
nodes: [
|
||||
{
|
||||
type: 'text',
|
||||
ranges: [
|
||||
{
|
||||
text: 'This is editable '
|
||||
},
|
||||
{
|
||||
text: 'rich',
|
||||
marks: [
|
||||
{
|
||||
type: 'bold'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
text: ' text, '
|
||||
},
|
||||
{
|
||||
text: 'much',
|
||||
marks: [
|
||||
{
|
||||
type: 'italic'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
text: ' better than a '
|
||||
},
|
||||
{
|
||||
text: '<textarea>',
|
||||
marks: [
|
||||
{
|
||||
type: 'code'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '!'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
nodes: [
|
||||
{
|
||||
type: 'text',
|
||||
ranges: [
|
||||
{
|
||||
text: 'Since it\'s rich text, you can do things like turn a selection of text ',
|
||||
},
|
||||
{
|
||||
text: 'bold',
|
||||
marks: [
|
||||
{
|
||||
type: 'bold'
|
||||
}
|
||||
]
|
||||
},{
|
||||
text: ', or add a semanticlly rendered block quote in the middle of the page, like this:'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'block-quote',
|
||||
nodes: [
|
||||
{
|
||||
type: 'text',
|
||||
ranges: [
|
||||
{
|
||||
text: 'A wise quote.'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
nodes: [
|
||||
{
|
||||
type: 'text',
|
||||
ranges: [
|
||||
{
|
||||
text: 'Try it out for yourself!'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
import state from './state.json'
|
||||
|
||||
/**
|
||||
* App.
|
||||
|
99
examples/rich-text/state.json
Normal file
99
examples/rich-text/state.json
Normal file
@@ -0,0 +1,99 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"type": "paragraph",
|
||||
"nodes": [
|
||||
{
|
||||
"type": "text",
|
||||
"ranges": [
|
||||
{
|
||||
"text": "This is editable "
|
||||
},
|
||||
{
|
||||
"text": "rich",
|
||||
"marks": [
|
||||
{
|
||||
"type": "bold"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"text": " text, "
|
||||
},
|
||||
{
|
||||
"text": "much",
|
||||
"marks": [
|
||||
{
|
||||
"type": "italic"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"text": " better than a "
|
||||
},
|
||||
{
|
||||
"text": "<textarea>",
|
||||
"marks": [
|
||||
{
|
||||
"type": "code"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"text": "!"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "paragraph",
|
||||
"nodes": [
|
||||
{
|
||||
"type": "text",
|
||||
"ranges": [
|
||||
{
|
||||
"text": "Since it's rich text, you can do things like turn a selection of text ",
|
||||
},
|
||||
{
|
||||
"text": "bold",
|
||||
"marks": [
|
||||
{
|
||||
"type": "bold"
|
||||
}
|
||||
]
|
||||
},{
|
||||
"text": ", or add a semanticlly rendered block quote in the middle of the page, like this:"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "block-quote",
|
||||
"nodes": [
|
||||
{
|
||||
"type": "text",
|
||||
"ranges": [
|
||||
{
|
||||
"text": "A wise quote."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "paragraph",
|
||||
"nodes": [
|
||||
{
|
||||
"type": "text",
|
||||
"ranges": [
|
||||
{
|
||||
"text": "Try it out for yourself!"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@@ -34,6 +34,7 @@ const Node = {
|
||||
|
||||
deleteAtRange(range) {
|
||||
let node = this
|
||||
range = range.normalize(node)
|
||||
|
||||
// If the range is collapsed, there's nothing to do.
|
||||
if (range.isCollapsed) return node
|
||||
@@ -123,6 +124,7 @@ const Node = {
|
||||
|
||||
deleteBackwardAtRange(range, n = 1) {
|
||||
let node = this
|
||||
range = range.normalize(node)
|
||||
|
||||
// When collapsed at the start of the node, there's nothing to do.
|
||||
if (range.isCollapsed && range.isAtStartOf(node)) return node
|
||||
@@ -160,6 +162,7 @@ const Node = {
|
||||
|
||||
deleteForwardAtRange(range, n = 1) {
|
||||
let node = this
|
||||
range = range.normalize(node)
|
||||
|
||||
// When collapsed at the end of the node, there's nothing to do.
|
||||
if (range.isCollapsed && range.isAtEndOf(node)) return node
|
||||
@@ -231,6 +234,7 @@ const Node = {
|
||||
*/
|
||||
|
||||
getCharactersAtRange(range) {
|
||||
range = range.normalize(this)
|
||||
const texts = this.getTextNodesAtRange(range)
|
||||
let list = new List()
|
||||
|
||||
@@ -272,6 +276,7 @@ const Node = {
|
||||
*/
|
||||
|
||||
getMarksAtRange(range) {
|
||||
range = range.normalize(this)
|
||||
const { startKey, startOffset, endKey } = range
|
||||
|
||||
// If the selection isn't set, return nothing.
|
||||
@@ -470,6 +475,7 @@ const Node = {
|
||||
*/
|
||||
|
||||
getTextNodesAtRange(range) {
|
||||
range = range.normalize(this)
|
||||
const { startKey, endKey } = range
|
||||
if (startKey == null || endKey == null) return new OrderedMap()
|
||||
|
||||
@@ -494,6 +500,8 @@ const Node = {
|
||||
|
||||
getWrappingNodesAtRange(range) {
|
||||
const node = this
|
||||
range = range.normalize(node)
|
||||
|
||||
const texts = node.getTextNodesAtRange(range)
|
||||
const parents = texts.map((text) => {
|
||||
return node.nodes.includes(text) ? node : node.getParentNode(text)
|
||||
@@ -533,6 +541,7 @@ const Node = {
|
||||
|
||||
insertTextAtRange(range, text) {
|
||||
let node = this
|
||||
range = range.normalize(node)
|
||||
|
||||
// When still expanded, remove the current range first.
|
||||
if (range.isExpanded) {
|
||||
@@ -581,6 +590,7 @@ const Node = {
|
||||
|
||||
markAtRange(range, mark) {
|
||||
let node = this
|
||||
range = range.normalize(node)
|
||||
|
||||
// Allow for just passing a type for convenience.
|
||||
if (typeof mark == 'string') {
|
||||
@@ -694,6 +704,8 @@ const Node = {
|
||||
|
||||
setTypeAtRange(range, type) {
|
||||
let node = this
|
||||
range = range.normalize(node)
|
||||
|
||||
const texts = node.getTextNodesAtRange(range)
|
||||
let parents = new OrderedSet()
|
||||
|
||||
@@ -725,6 +737,7 @@ const Node = {
|
||||
|
||||
splitAtRange(range) {
|
||||
let node = this
|
||||
range = range.normalize(node)
|
||||
|
||||
// If the range is expanded, remove it first.
|
||||
if (range.isExpanded) {
|
||||
@@ -786,6 +799,7 @@ const Node = {
|
||||
|
||||
unmarkAtRange(range, mark) {
|
||||
let node = this
|
||||
range = range.normalize(node)
|
||||
|
||||
// Allow for just passing a type for convenience.
|
||||
if (typeof mark == 'string') {
|
||||
@@ -853,6 +867,8 @@ const Node = {
|
||||
*/
|
||||
|
||||
wrapAtRange(range, parent) {
|
||||
let node = this
|
||||
range = range.normalize(node)
|
||||
|
||||
// Allow for the parent to by just a type.
|
||||
if (typeof parent == 'string') {
|
||||
@@ -860,7 +876,7 @@ const Node = {
|
||||
}
|
||||
|
||||
// Add the child to the parent's nodes.
|
||||
const child = this.findNode(key)
|
||||
const child = node.findNode(key)
|
||||
parent = node.nodes.set(child.key, child)
|
||||
|
||||
// Remove the child from this node.
|
||||
|
@@ -126,6 +126,54 @@ class Selection extends SelectionRecord {
|
||||
return endKey == last.key && endOffset == last.length
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize the selection, relative to a `node`, ensuring that the anchor
|
||||
* and focus nodes of the selection always refer to leaf text nodes.
|
||||
*
|
||||
* @param {Node} node
|
||||
* @return {Selection} selection
|
||||
*/
|
||||
|
||||
normalize(node) {
|
||||
let selection = this
|
||||
let { anchorKey, anchorOffset, focusKey, focusOffset } = selection
|
||||
|
||||
// If the selection isn't formed yet, abort.
|
||||
if (anchorKey == null || focusKey == null) return selection
|
||||
|
||||
// Asset that the anchor and focus nodes exist in the node tree.
|
||||
node.assertHasNode(anchorKey)
|
||||
node.assertHasNode(focusKey)
|
||||
let anchorNode = node.getNode(anchorKey)
|
||||
let focusNode = node.getNode(focusKey)
|
||||
|
||||
// If the anchor node isn't a text node, match it to one.
|
||||
if (anchorNode.type != 'text') {
|
||||
anchorNode = node.getNodeAtOffset(anchorOffset)
|
||||
let parent = node.getParentNode(anchorNode)
|
||||
let offset = parent.getNodeOffset(anchorNode)
|
||||
anchorOffset = anchorOffset - offset
|
||||
anchorKey = anchorNode.key
|
||||
}
|
||||
|
||||
// If the focus node isn't a text node, match it to one.
|
||||
if (focusNode.type != 'text') {
|
||||
focusNode = node.getNodeAtOffset(focusOffset)
|
||||
let parent = node.getParentNode(focusNode)
|
||||
let offset = parent.getNodeOffset(focusNode)
|
||||
focusOffset = focusOffset - offset
|
||||
focusKey = focusNode.key
|
||||
}
|
||||
|
||||
// Merge in any updated properties.
|
||||
return selection.merge({
|
||||
anchorKey,
|
||||
anchorOffset,
|
||||
focusKey,
|
||||
focusOffset
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the selection to a set of `properties`.
|
||||
*
|
||||
|
Reference in New Issue
Block a user