mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-11 09:43:58 +02:00
added start of richtext example, refactor range grouping logic
This commit is contained in:
8
Makefile
8
Makefile
@@ -35,6 +35,10 @@ example-basic: ./node_modules
|
|||||||
example-plaintext: ./node_modules
|
example-plaintext: ./node_modules
|
||||||
@ $(browserify) --debug --transform babelify --outfile ./examples/plaintext/build.js ./examples/plaintext/index.js
|
@ $(browserify) --debug --transform babelify --outfile ./examples/plaintext/build.js ./examples/plaintext/index.js
|
||||||
|
|
||||||
|
# Build the richtext example.
|
||||||
|
example-richtext: ./node_modules
|
||||||
|
@ $(browserify) --debug --transform babelify --outfile ./examples/richtext/build.js ./examples/richtext/index.js
|
||||||
|
|
||||||
# Lint the sources files with Standard JS.
|
# Lint the sources files with Standard JS.
|
||||||
lint: ./node_modules
|
lint: ./node_modules
|
||||||
@ $(standard) ./lib
|
@ $(standard) ./lib
|
||||||
@@ -67,6 +71,10 @@ watch-example-basic: ./node_modules
|
|||||||
watch-example-plaintext: ./node_modules
|
watch-example-plaintext: ./node_modules
|
||||||
@ $(MAKE) example-plaintext browserify=$(watchify)
|
@ $(MAKE) example-plaintext browserify=$(watchify)
|
||||||
|
|
||||||
|
# Watch the richtext example.
|
||||||
|
watch-example-richtext: ./node_modules
|
||||||
|
@ $(MAKE) example-richtext browserify=$(watchify)
|
||||||
|
|
||||||
# Phony targets.
|
# Phony targets.
|
||||||
.PHONY: examples
|
.PHONY: examples
|
||||||
.PHONY: test
|
.PHONY: test
|
||||||
|
@@ -10,3 +10,15 @@ main {
|
|||||||
max-width: 40em;
|
max-width: 40em;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
main * + * {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
@@ -50,10 +50,31 @@ const state = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renderers.
|
* App.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function renderNode(node) {
|
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.log('State:', state.toJS())
|
||||||
|
console.log('Content:', Raw.serialize(state))
|
||||||
|
this.setState({ state })
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
renderNode(node) {
|
||||||
switch (node.type) {
|
switch (node.type) {
|
||||||
case 'code': {
|
case 'code': {
|
||||||
return (props) => {
|
return (props) => {
|
||||||
@@ -78,7 +99,7 @@ function renderNode(node) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderMark(mark) {
|
renderMark(mark) {
|
||||||
switch (mark.type) {
|
switch (mark.type) {
|
||||||
case 'bold': {
|
case 'bold': {
|
||||||
return {
|
return {
|
||||||
@@ -88,31 +109,6 @@ function renderMark(mark) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* App.
|
|
||||||
*/
|
|
||||||
|
|
||||||
class App extends React.Component {
|
|
||||||
|
|
||||||
state = {
|
|
||||||
state: Raw.deserialize(state)
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<Editor
|
|
||||||
renderNode={renderNode}
|
|
||||||
renderMark={renderMark}
|
|
||||||
state={this.state.state}
|
|
||||||
onChange={(state) => {
|
|
||||||
console.log('State:', state.toJS())
|
|
||||||
console.log('Content:', Raw.serialize(state))
|
|
||||||
this.setState({ state })
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
20
examples/richtext/index.css
Normal file
20
examples/richtext/index.css
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
|
||||||
|
html {
|
||||||
|
background: #eee;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
background: #fff;
|
||||||
|
padding: 10px;
|
||||||
|
max-width: 40em;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
p + p {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
11
examples/richtext/index.html
Normal file
11
examples/richtext/index.html
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>Editor | Richtext Example</title>
|
||||||
|
<link rel="stylesheet" href="index.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main></main>
|
||||||
|
<script src="build.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
128
examples/richtext/index.js
Normal file
128
examples/richtext/index.js
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
|
||||||
|
import Editor from '../..'
|
||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom'
|
||||||
|
import { Raw } from '../..'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* State.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
ranges: [
|
||||||
|
{
|
||||||
|
text: 'This is '
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'editable',
|
||||||
|
marks: [
|
||||||
|
{
|
||||||
|
type: 'italic'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: ' '
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'rich',
|
||||||
|
marks: [
|
||||||
|
{
|
||||||
|
type: 'bold'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: ' text, much better than a '
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '<textarea>',
|
||||||
|
marks: [
|
||||||
|
{
|
||||||
|
type: 'code'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '!'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* App.
|
||||||
|
*/
|
||||||
|
|
||||||
|
class App extends React.Component {
|
||||||
|
|
||||||
|
state = {
|
||||||
|
state: Raw.deserialize(state)
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Editor
|
||||||
|
state={this.state.state}
|
||||||
|
renderNode={node => this.renderNode(node)}
|
||||||
|
renderMark={mark => this.renderMark(mark)}
|
||||||
|
onChange={(state) => {
|
||||||
|
console.log('Document:', state.document.toJS())
|
||||||
|
console.log('Content:', Raw.serialize(state))
|
||||||
|
this.setState({ state })
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
renderNode(node) {
|
||||||
|
switch (node.type) {
|
||||||
|
case 'paragraph': {
|
||||||
|
return (props) => {
|
||||||
|
return <p>{props.children}</p>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderMark(mark) {
|
||||||
|
switch (mark.type) {
|
||||||
|
case 'bold': {
|
||||||
|
return {
|
||||||
|
fontWeight: 'bold'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'italic': {
|
||||||
|
return {
|
||||||
|
fontStyle: 'italic'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'code': {
|
||||||
|
return {
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
backgroundColor: '#eee',
|
||||||
|
padding: '3px',
|
||||||
|
borderRadius: '4px'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attach.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const app = <App />
|
||||||
|
const root = document.body.querySelector('main')
|
||||||
|
ReactDOM.render(app, root)
|
@@ -10,7 +10,7 @@ import ReactDOM from 'react-dom'
|
|||||||
class Leaf extends React.Component {
|
class Leaf extends React.Component {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
marks: React.PropTypes.array.isRequired,
|
marks: React.PropTypes.object.isRequired,
|
||||||
node: React.PropTypes.object.isRequired,
|
node: React.PropTypes.object.isRequired,
|
||||||
start: React.PropTypes.number.isRequired,
|
start: React.PropTypes.number.isRequired,
|
||||||
end: React.PropTypes.number.isRequired,
|
end: React.PropTypes.number.isRequired,
|
||||||
@@ -94,17 +94,23 @@ class Leaf extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { node, start, end, text, marks } = this.props
|
const { node, text, marks, start, end, renderMark } = this.props
|
||||||
const styles = this.renderStyles()
|
|
||||||
const offsetKey = OffsetKey.stringify({
|
const offsetKey = OffsetKey.stringify({
|
||||||
key: node.key,
|
key: node.key,
|
||||||
start,
|
start,
|
||||||
end
|
end
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const style = marks.reduce((style, mark) => {
|
||||||
|
return {
|
||||||
|
...style,
|
||||||
|
...renderMark(mark),
|
||||||
|
}
|
||||||
|
}, {})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
style={styles}
|
style={style}
|
||||||
data-offset-key={offsetKey}
|
data-offset-key={offsetKey}
|
||||||
data-type='leaf'
|
data-type='leaf'
|
||||||
>
|
>
|
||||||
@@ -113,16 +119,6 @@ class Leaf extends React.Component {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
renderStyles() {
|
|
||||||
const { marks, renderMark } = this.props
|
|
||||||
return marks.reduce((styles, mark) => {
|
|
||||||
return {
|
|
||||||
...styles,
|
|
||||||
...renderMark(mark),
|
|
||||||
}
|
|
||||||
}, {})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -1,8 +1,9 @@
|
|||||||
|
|
||||||
import Leaf from './leaf'
|
import Leaf from './leaf'
|
||||||
import OffsetKey from '../utils/offset-key'
|
import OffsetKey from '../utils/offset-key'
|
||||||
import Raw from '../serializers/raw'
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import groupByMarks from '../utils/group-by-marks'
|
||||||
|
import { List } from 'immutable'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Text.
|
* Text.
|
||||||
@@ -32,15 +33,22 @@ class Text extends React.Component {
|
|||||||
renderLeaves() {
|
renderLeaves() {
|
||||||
const { node } = this.props
|
const { node } = this.props
|
||||||
const { characters } = node
|
const { characters } = node
|
||||||
const ranges = Raw.serializeCharacters(characters)
|
const ranges = groupByMarks(characters)
|
||||||
return ranges.length
|
return ranges.size == 0
|
||||||
? ranges.map((range) => this.renderLeaf(range))
|
? this.renderSpacerLeaf()
|
||||||
: this.renderSpacerLeaf()
|
: ranges.map((range, i, ranges) => {
|
||||||
|
const previous = ranges.slice(0, i)
|
||||||
|
const offset = previous.size
|
||||||
|
? previous.map(range => range.get('text')).join('').length
|
||||||
|
: 0
|
||||||
|
return this.renderLeaf(range, offset)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
renderLeaf(range) {
|
renderLeaf(range, offset) {
|
||||||
const { node, renderMark, state } = this.props
|
const { node, renderMark, state } = this.props
|
||||||
const { marks, offset, text } = range
|
const text = range.get('text')
|
||||||
|
const marks = range.get('marks')
|
||||||
const start = offset
|
const start = offset
|
||||||
const end = offset + text.length
|
const end = offset + text.length
|
||||||
const offsetKey = OffsetKey.stringify({
|
const offsetKey = OffsetKey.stringify({
|
||||||
@@ -65,9 +73,9 @@ class Text extends React.Component {
|
|||||||
|
|
||||||
renderSpacerLeaf() {
|
renderSpacerLeaf() {
|
||||||
return this.renderLeaf({
|
return this.renderLeaf({
|
||||||
|
marks: new List(),
|
||||||
offset: 0,
|
offset: 0,
|
||||||
text: '',
|
text: ''
|
||||||
marks: []
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -5,7 +5,7 @@ import Mark from '../models/mark'
|
|||||||
import Node from '../models/node'
|
import Node from '../models/node'
|
||||||
import Text from '../models/text'
|
import Text from '../models/text'
|
||||||
import State from '../models/state'
|
import State from '../models/state'
|
||||||
import xor from 'lodash/xor'
|
import groupByMarks from '../utils/group-by-marks'
|
||||||
import { Map } from 'immutable'
|
import { Map } from 'immutable'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -59,27 +59,14 @@ function serializeNode(node) {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
function serializeCharacters(characters) {
|
function serializeCharacters(characters) {
|
||||||
return characters
|
return groupByMarks(characters)
|
||||||
.toArray()
|
.toArray()
|
||||||
.reduce((ranges, char, i) => {
|
.map((range) => {
|
||||||
const previous = i == 0 ? null : characters.get(i - 1)
|
return {
|
||||||
const { text } = char
|
text: range.text,
|
||||||
const marks = char.marks.toArray().map(mark => serializeMark(mark))
|
mark: serializeMark(range.mark)
|
||||||
|
|
||||||
if (previous) {
|
|
||||||
const previousMarks = previous.marks.toArray()
|
|
||||||
const diff = xor(marks, previousMarks)
|
|
||||||
if (!diff.length) {
|
|
||||||
const previousRange = ranges[ranges.length - 1]
|
|
||||||
previousRange.text += text
|
|
||||||
return ranges
|
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
const offset = ranges.map(range => range.text).join('').length
|
|
||||||
ranges.push({ text, marks, offset })
|
|
||||||
return ranges
|
|
||||||
}, [])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
48
lib/utils/group-by-marks.js
Normal file
48
lib/utils/group-by-marks.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
|
||||||
|
import { List, Map } from 'immutable'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Group a list of `characters` into ranges by the marks they have.
|
||||||
|
*
|
||||||
|
* @param {List} characters
|
||||||
|
* @return {List} ranges
|
||||||
|
*/
|
||||||
|
|
||||||
|
function groupByMarks(characters) {
|
||||||
|
return characters
|
||||||
|
.toList()
|
||||||
|
.reduce((ranges, char, i) => {
|
||||||
|
const { marks, text } = char
|
||||||
|
|
||||||
|
// The first one can always just be created.
|
||||||
|
if (i == 0) {
|
||||||
|
return ranges.push(new Map({ text, marks }))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, compare to the previous and see if a new range should be
|
||||||
|
// created, or whether the text should be added to the previous range.
|
||||||
|
const previous = characters.get(i - 1)
|
||||||
|
const prevMarks = previous.marks
|
||||||
|
const added = marks.filterNot(mark => prevMarks.includes(mark))
|
||||||
|
const removed = prevMarks.filterNot(mark => marks.includes(mark))
|
||||||
|
const isSame = !added.size && !removed.size
|
||||||
|
|
||||||
|
// If the marks are the same, add the text to the previous range.
|
||||||
|
if (isSame) {
|
||||||
|
const index = ranges.size - 1
|
||||||
|
let prevRange = ranges.get(index)
|
||||||
|
let prevText = prevRange.get('text')
|
||||||
|
prevRange = prevRange.set('text', prevText += text)
|
||||||
|
return ranges.set(index, prevRange)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, create a new range.
|
||||||
|
return ranges.push(new Map({ text, marks }))
|
||||||
|
}, new List())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default groupByMarks
|
Reference in New Issue
Block a user