1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-01-18 22:08:18 +01:00

added start of richtext example, refactor range grouping logic

This commit is contained in:
Ian Storm Taylor 2016-06-18 23:54:08 -07:00
parent d9b4e58029
commit 7435c4019c
10 changed files with 298 additions and 84 deletions

View File

@ -35,6 +35,10 @@ example-basic: ./node_modules
example-plaintext: ./node_modules
@ $(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: ./node_modules
@ $(standard) ./lib
@ -67,6 +71,10 @@ watch-example-basic: ./node_modules
watch-example-plaintext: ./node_modules
@ $(MAKE) example-plaintext browserify=$(watchify)
# Watch the richtext example.
watch-example-richtext: ./node_modules
@ $(MAKE) example-richtext browserify=$(watchify)
# Phony targets.
.PHONY: examples
.PHONY: test

View File

@ -10,3 +10,15 @@ main {
max-width: 40em;
margin: 0 auto;
}
p {
margin: 0;
}
pre {
margin: 0;
}
main * + * {
margin-top: 1em;
}

View File

@ -49,45 +49,6 @@ const state = {
]
}
/**
* Renderers.
*/
function 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>
)
}
}
}
}
function renderMark(mark) {
switch (mark.type) {
case 'bold': {
return {
fontWeight: 'bold'
}
}
}
}
/**
* App.
*/
@ -101,8 +62,8 @@ class App extends React.Component {
render() {
return (
<Editor
renderNode={renderNode}
renderMark={renderMark}
renderNode={node => this.renderNode(node)}
renderMark={mark => this.renderMark(mark)}
state={this.state.state}
onChange={(state) => {
console.log('State:', state.toJS())
@ -113,6 +74,41 @@ class App extends React.Component {
)
}
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'
}
}
}
}
}
/**

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

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

View File

@ -10,7 +10,7 @@ import ReactDOM from 'react-dom'
class Leaf extends React.Component {
static propTypes = {
marks: React.PropTypes.array.isRequired,
marks: React.PropTypes.object.isRequired,
node: React.PropTypes.object.isRequired,
start: React.PropTypes.number.isRequired,
end: React.PropTypes.number.isRequired,
@ -94,17 +94,23 @@ class Leaf extends React.Component {
}
render() {
const { node, start, end, text, marks } = this.props
const styles = this.renderStyles()
const { node, text, marks, start, end, renderMark } = this.props
const offsetKey = OffsetKey.stringify({
key: node.key,
start,
end
})
const style = marks.reduce((style, mark) => {
return {
...style,
...renderMark(mark),
}
}, {})
return (
<span
style={styles}
style={style}
data-offset-key={offsetKey}
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),
}
}, {})
}
}
/**

View File

@ -1,8 +1,9 @@
import Leaf from './leaf'
import OffsetKey from '../utils/offset-key'
import Raw from '../serializers/raw'
import React from 'react'
import groupByMarks from '../utils/group-by-marks'
import { List } from 'immutable'
/**
* Text.
@ -32,15 +33,22 @@ class Text extends React.Component {
renderLeaves() {
const { node } = this.props
const { characters } = node
const ranges = Raw.serializeCharacters(characters)
return ranges.length
? ranges.map((range) => this.renderLeaf(range))
: this.renderSpacerLeaf()
const ranges = groupByMarks(characters)
return ranges.size == 0
? 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 { marks, offset, text } = range
const text = range.get('text')
const marks = range.get('marks')
const start = offset
const end = offset + text.length
const offsetKey = OffsetKey.stringify({
@ -65,9 +73,9 @@ class Text extends React.Component {
renderSpacerLeaf() {
return this.renderLeaf({
marks: new List(),
offset: 0,
text: '',
marks: []
text: ''
})
}

View File

@ -5,7 +5,7 @@ import Mark from '../models/mark'
import Node from '../models/node'
import Text from '../models/text'
import State from '../models/state'
import xor from 'lodash/xor'
import groupByMarks from '../utils/group-by-marks'
import { Map } from 'immutable'
/**
@ -59,27 +59,14 @@ function serializeNode(node) {
*/
function serializeCharacters(characters) {
return characters
return groupByMarks(characters)
.toArray()
.reduce((ranges, char, i) => {
const previous = i == 0 ? null : characters.get(i - 1)
const { text } = char
const marks = char.marks.toArray().map(mark => serializeMark(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
}
.map((range) => {
return {
text: range.text,
mark: serializeMark(range.mark)
}
const offset = ranges.map(range => range.text).join('').length
ranges.push({ text, marks, offset })
return ranges
}, [])
})
}
/**

View 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