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:
parent
d9b4e58029
commit
7435c4019c
8
Makefile
8
Makefile
@ -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
|
||||
|
@ -10,3 +10,15 @@ main {
|
||||
max-width: 40em;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
main * + * {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
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 {
|
||||
|
||||
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),
|
||||
}
|
||||
}, {})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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: ''
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
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
|
Loading…
x
Reference in New Issue
Block a user