1
0
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:
Ian Storm Taylor
2016-06-21 14:49:08 -07:00
parent d742d109a5
commit dbdf3760e9
12 changed files with 252 additions and 358 deletions

View File

@@ -31,10 +31,6 @@ dist: $(shell find ./lib)
example-auto-markdown: example-auto-markdown:
@ $(browserify) --debug --transform babelify --outfile ./examples/auto-markdown/build.js ./examples/auto-markdown/index.js @ $(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. # Build the plain-text example.
example-plain-text: example-plain-text:
@ $(browserify) --debug --transform babelify --outfile ./examples/plain-text/build.js ./examples/plain-text/index.js @ $(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: watch-example-auto-markdown:
@ $(MAKE) example-auto-markdown browserify=$(watchify) @ $(MAKE) example-auto-markdown browserify=$(watchify)
# Watch the basic example.
watch-example-basic:
@ $(MAKE) example-basic browserify=$(watchify)
# Watch the plain-text example. # Watch the plain-text example.
watch-example-plain-text: watch-example-plain-text:
@ $(MAKE) example-plain-text browserify=$(watchify) @ $(MAKE) example-plain-text browserify=$(watchify)

View File

@@ -1,84 +1,9 @@
import Editor, { Mark, Raw } from '../..' import Editor, { Raw } from '../..'
import React from 'react' import React from 'react'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import keycode from 'keycode' import keycode from 'keycode'
import state from './state.json'
/**
* 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.'
}
]
}
]
}
]
}
/** /**
* Define our example app. * Define our example app.

View 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."
}
]
}
]
}
]
}

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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)

View File

@@ -2,14 +2,7 @@
import Editor, { Character, Document, Element, State, Text } from '../..' import Editor, { Character, Document, Element, State, Text } from '../..'
import React from 'react' import React from 'react'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import state from './state.json'
/**
* The initial editor state.
*
* @type {String}
*/
const state = 'This is editable plain text, just like a <textarea>!'
/** /**
* A helper to deserialize a string into an editor state. * 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 * @type {Component} App
*/ */
class App extends React.Component { class App extends React.Component {
/**
* Deserialize the initial editor state.
*
* @type {Object}
*/
state = { state = {
state: deserialize(state) state: deserialize(state)
}; };
/**
* Render the editor.
*
* @return {Component} component
*/
render() { render() {
return ( return (
<Editor <Editor
@@ -82,7 +87,7 @@ class App extends React.Component {
} }
/** /**
* Attach. * Mount the example app.
*/ */
const app = <App /> const app = <App />

View File

@@ -0,0 +1 @@
"This is editable plain text, just like a <textarea>!"

View File

@@ -2,110 +2,7 @@
import Editor, { Mark, Raw } from '../..' import Editor, { Mark, Raw } from '../..'
import React from 'react' import React from 'react'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import state from './state.json'
/**
* 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!'
}
]
}
]
}
]
}
/** /**
* App. * App.

View 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!"
}
]
}
]
}
]
}

View File

@@ -34,6 +34,7 @@ const Node = {
deleteAtRange(range) { deleteAtRange(range) {
let node = this let node = this
range = range.normalize(node)
// If the range is collapsed, there's nothing to do. // If the range is collapsed, there's nothing to do.
if (range.isCollapsed) return node if (range.isCollapsed) return node
@@ -123,6 +124,7 @@ const Node = {
deleteBackwardAtRange(range, n = 1) { deleteBackwardAtRange(range, n = 1) {
let node = this let node = this
range = range.normalize(node)
// When collapsed at the start of the node, there's nothing to do. // When collapsed at the start of the node, there's nothing to do.
if (range.isCollapsed && range.isAtStartOf(node)) return node if (range.isCollapsed && range.isAtStartOf(node)) return node
@@ -160,6 +162,7 @@ const Node = {
deleteForwardAtRange(range, n = 1) { deleteForwardAtRange(range, n = 1) {
let node = this let node = this
range = range.normalize(node)
// When collapsed at the end of the node, there's nothing to do. // When collapsed at the end of the node, there's nothing to do.
if (range.isCollapsed && range.isAtEndOf(node)) return node if (range.isCollapsed && range.isAtEndOf(node)) return node
@@ -231,6 +234,7 @@ const Node = {
*/ */
getCharactersAtRange(range) { getCharactersAtRange(range) {
range = range.normalize(this)
const texts = this.getTextNodesAtRange(range) const texts = this.getTextNodesAtRange(range)
let list = new List() let list = new List()
@@ -272,6 +276,7 @@ const Node = {
*/ */
getMarksAtRange(range) { getMarksAtRange(range) {
range = range.normalize(this)
const { startKey, startOffset, endKey } = range const { startKey, startOffset, endKey } = range
// If the selection isn't set, return nothing. // If the selection isn't set, return nothing.
@@ -470,6 +475,7 @@ const Node = {
*/ */
getTextNodesAtRange(range) { getTextNodesAtRange(range) {
range = range.normalize(this)
const { startKey, endKey } = range const { startKey, endKey } = range
if (startKey == null || endKey == null) return new OrderedMap() if (startKey == null || endKey == null) return new OrderedMap()
@@ -494,6 +500,8 @@ const Node = {
getWrappingNodesAtRange(range) { getWrappingNodesAtRange(range) {
const node = this const node = this
range = range.normalize(node)
const texts = node.getTextNodesAtRange(range) const texts = node.getTextNodesAtRange(range)
const parents = texts.map((text) => { const parents = texts.map((text) => {
return node.nodes.includes(text) ? node : node.getParentNode(text) return node.nodes.includes(text) ? node : node.getParentNode(text)
@@ -533,6 +541,7 @@ const Node = {
insertTextAtRange(range, text) { insertTextAtRange(range, text) {
let node = this let node = this
range = range.normalize(node)
// When still expanded, remove the current range first. // When still expanded, remove the current range first.
if (range.isExpanded) { if (range.isExpanded) {
@@ -581,6 +590,7 @@ const Node = {
markAtRange(range, mark) { markAtRange(range, mark) {
let node = this let node = this
range = range.normalize(node)
// Allow for just passing a type for convenience. // Allow for just passing a type for convenience.
if (typeof mark == 'string') { if (typeof mark == 'string') {
@@ -694,6 +704,8 @@ const Node = {
setTypeAtRange(range, type) { setTypeAtRange(range, type) {
let node = this let node = this
range = range.normalize(node)
const texts = node.getTextNodesAtRange(range) const texts = node.getTextNodesAtRange(range)
let parents = new OrderedSet() let parents = new OrderedSet()
@@ -725,6 +737,7 @@ const Node = {
splitAtRange(range) { splitAtRange(range) {
let node = this let node = this
range = range.normalize(node)
// If the range is expanded, remove it first. // If the range is expanded, remove it first.
if (range.isExpanded) { if (range.isExpanded) {
@@ -786,6 +799,7 @@ const Node = {
unmarkAtRange(range, mark) { unmarkAtRange(range, mark) {
let node = this let node = this
range = range.normalize(node)
// Allow for just passing a type for convenience. // Allow for just passing a type for convenience.
if (typeof mark == 'string') { if (typeof mark == 'string') {
@@ -853,6 +867,8 @@ const Node = {
*/ */
wrapAtRange(range, parent) { wrapAtRange(range, parent) {
let node = this
range = range.normalize(node)
// Allow for the parent to by just a type. // Allow for the parent to by just a type.
if (typeof parent == 'string') { if (typeof parent == 'string') {
@@ -860,7 +876,7 @@ const Node = {
} }
// Add the child to the parent's nodes. // 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) parent = node.nodes.set(child.key, child)
// Remove the child from this node. // Remove the child from this node.

View File

@@ -126,6 +126,54 @@ class Selection extends SelectionRecord {
return endKey == last.key && endOffset == last.length 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`. * Move the selection to a set of `properties`.
* *