mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-28 09:29:49 +02:00
add markdown example, fix selection handling with decorators
This commit is contained in:
@@ -55,7 +55,7 @@ function CodeBlock(props) {
|
||||
*/
|
||||
|
||||
function codeBlockDecorator(text, block) {
|
||||
let characters = text.characters.asMutable()
|
||||
const characters = text.characters.asMutable()
|
||||
const language = block.data.get('language')
|
||||
const string = text.text
|
||||
const grammar = Prism.languages[language]
|
||||
@@ -76,7 +76,7 @@ function codeBlockDecorator(text, block) {
|
||||
let { marks } = char
|
||||
marks = marks.add(Mark.create({ type }))
|
||||
char = char.merge({ marks })
|
||||
characters = characters.set(i, char)
|
||||
characters.set(i, char)
|
||||
}
|
||||
|
||||
offset = length
|
||||
|
@@ -7,7 +7,6 @@ import { Router, Route, Link, IndexRedirect, hashHistory } from 'react-router'
|
||||
* Examples.
|
||||
*/
|
||||
|
||||
import AutoMarkdown from './auto-markdown'
|
||||
import CheckLists from './check-lists'
|
||||
import CodeHighlighting from './code-highlighting'
|
||||
import Embeds from './embeds'
|
||||
@@ -18,6 +17,8 @@ import Iframes from './iframes'
|
||||
import Images from './images'
|
||||
import LargeDocument from './large-document'
|
||||
import Links from './links'
|
||||
import MarkdownPreview from './markdown-preview'
|
||||
import MarkdownShortcuts from './markdown-shortcuts'
|
||||
import PasteHtml from './paste-html'
|
||||
import PlainText from './plain-text'
|
||||
import Plugins from './plugins'
|
||||
@@ -94,22 +95,23 @@ class App extends React.Component {
|
||||
<div className="tabs">
|
||||
{this.renderTab('Rich Text', 'rich-text')}
|
||||
{this.renderTab('Plain Text', 'plain-text')}
|
||||
{this.renderTab('Auto-markdown', 'auto-markdown')}
|
||||
{this.renderTab('Hovering Menu', 'hovering-menu')}
|
||||
{this.renderTab('Large Document', 'large')}
|
||||
{this.renderTab('Links', 'links')}
|
||||
{this.renderTab('Images', 'images')}
|
||||
{this.renderTab('Embeds', 'embeds')}
|
||||
{this.renderTab('Emojis', 'emojis')}
|
||||
{this.renderTab('Tables', 'tables')}
|
||||
{this.renderTab('Markdown Preview', 'markdown-preview')}
|
||||
{this.renderTab('Markdown Shortcuts', 'markdown-shortcuts')}
|
||||
{this.renderTab('Check Lists', 'check-lists')}
|
||||
{this.renderTab('Code Highlighting', 'code-highlighting')}
|
||||
{this.renderTab('Tables', 'tables')}
|
||||
{this.renderTab('Paste HTML', 'paste-html')}
|
||||
{this.renderTab('Read-only', 'read-only')}
|
||||
{this.renderTab('RTL', 'rtl')}
|
||||
{this.renderTab('Plugins', 'plugins')}
|
||||
{this.renderTab('Iframes', 'iframes')}
|
||||
{this.renderTab('Focus & Blur', 'focus-blur')}
|
||||
{this.renderTab('Large Document', 'large')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -154,7 +156,6 @@ const router = (
|
||||
<Route path="/" component={App}>
|
||||
<IndexRedirect to="rich-text" />
|
||||
|
||||
<Route path="auto-markdown" component={AutoMarkdown} />
|
||||
<Route path="check-lists" component={CheckLists} />
|
||||
<Route path="code-highlighting" component={CodeHighlighting} />
|
||||
<Route path="embeds" component={Embeds} />
|
||||
@@ -165,6 +166,8 @@ const router = (
|
||||
<Route path="images" component={Images} />
|
||||
<Route path="large" component={LargeDocument} />
|
||||
<Route path="links" component={Links} />
|
||||
<Route path="markdown-preview" component={MarkdownPreview} />
|
||||
<Route path="markdown-shortcuts" component={MarkdownShortcuts} />
|
||||
<Route path="paste-html" component={PasteHtml} />
|
||||
<Route path="plain-text" component={PlainText} />
|
||||
<Route path="plugins" component={Plugins} />
|
||||
|
156
examples/markdown-preview/index.js
Normal file
156
examples/markdown-preview/index.js
Normal file
@@ -0,0 +1,156 @@
|
||||
|
||||
import { Editor, Mark, Plain } from '../..'
|
||||
import Prism from 'prismjs'
|
||||
import React from 'react'
|
||||
|
||||
/**
|
||||
* Add the markdown syntax to Prism.
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line
|
||||
Prism.languages.markdown=Prism.languages.extend("markup",{}),Prism.languages.insertBefore("markdown","prolog",{blockquote:{pattern:/^>(?:[\t ]*>)*/m,alias:"punctuation"},code:[{pattern:/^(?: {4}|\t).+/m,alias:"keyword"},{pattern:/``.+?``|`[^`\n]+`/,alias:"keyword"}],title:[{pattern:/\w+.*(?:\r?\n|\r)(?:==+|--+)/,alias:"important",inside:{punctuation:/==+$|--+$/}},{pattern:/(^\s*)#+.+/m,lookbehind:!0,alias:"important",inside:{punctuation:/^#+|#+$/}}],hr:{pattern:/(^\s*)([*-])([\t ]*\2){2,}(?=\s*$)/m,lookbehind:!0,alias:"punctuation"},list:{pattern:/(^\s*)(?:[*+-]|\d+\.)(?=[\t ].)/m,lookbehind:!0,alias:"punctuation"},"url-reference":{pattern:/!?\[[^\]]+\]:[\t ]+(?:\S+|<(?:\\.|[^>\\])+>)(?:[\t ]+(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\((?:\\.|[^)\\])*\)))?/,inside:{variable:{pattern:/^(!?\[)[^\]]+/,lookbehind:!0},string:/(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\((?:\\.|[^)\\])*\))$/,punctuation:/^[\[\]!:]|[<>]/},alias:"url"},bold:{pattern:/(^|[^\\])(\*\*|__)(?:(?:\r?\n|\r)(?!\r?\n|\r)|.)+?\2/,lookbehind:!0,inside:{punctuation:/^\*\*|^__|\*\*$|__$/}},italic:{pattern:/(^|[^\\])([*_])(?:(?:\r?\n|\r)(?!\r?\n|\r)|.)+?\2/,lookbehind:!0,inside:{punctuation:/^[*_]|[*_]$/}},url:{pattern:/!?\[[^\]]+\](?:\([^\s)]+(?:[\t ]+"(?:\\.|[^"\\])*")?\)| ?\[[^\]\n]*\])/,inside:{variable:{pattern:/(!?\[)[^\]]+(?=\]$)/,lookbehind:!0},string:{pattern:/"(?:\\.|[^"\\])*"(?=\)$)/}}}}),Prism.languages.markdown.bold.inside.url=Prism.util.clone(Prism.languages.markdown.url),Prism.languages.markdown.italic.inside.url=Prism.util.clone(Prism.languages.markdown.url),Prism.languages.markdown.bold.inside.italic=Prism.util.clone(Prism.languages.markdown.italic),Prism.languages.markdown.italic.inside.bold=Prism.util.clone(Prism.languages.markdown.bold);
|
||||
|
||||
/**
|
||||
* Define a decorator for markdown styles.
|
||||
*
|
||||
* @param {Text} text
|
||||
* @param {Block} block
|
||||
*/
|
||||
|
||||
function markdownDecorator(text, block) {
|
||||
const characters = text.characters.asMutable()
|
||||
const language = 'markdown'
|
||||
const string = text.text
|
||||
const grammar = Prism.languages[language]
|
||||
const tokens = Prism.tokenize(string, grammar)
|
||||
addMarks(characters, tokens, 0)
|
||||
return characters.asImmutable()
|
||||
}
|
||||
|
||||
function addMarks(characters, tokens, offset) {
|
||||
for (const token of tokens) {
|
||||
if (typeof token == 'string') {
|
||||
offset += token.length
|
||||
continue
|
||||
}
|
||||
|
||||
const { content, length, type } = token
|
||||
const mark = Mark.create({ type })
|
||||
|
||||
for (let i = offset; i < offset + length; i++) {
|
||||
let char = characters.get(i)
|
||||
let { marks } = char
|
||||
marks = marks.add(mark)
|
||||
char = char.merge({ marks })
|
||||
characters.set(i, char)
|
||||
}
|
||||
|
||||
if (Array.isArray(content)) {
|
||||
addMarks(characters, content, offset)
|
||||
}
|
||||
|
||||
offset += length
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Define a schema.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
|
||||
const schema = {
|
||||
marks: {
|
||||
'title': {
|
||||
fontWeight: 'bold',
|
||||
fontSize: '20px',
|
||||
margin: '20px 0 10px 0',
|
||||
display: 'inline-block'
|
||||
},
|
||||
'bold': {
|
||||
fontWeight: 'bold'
|
||||
},
|
||||
'italic': {
|
||||
fontStyle: 'italic'
|
||||
},
|
||||
'punctuation': {
|
||||
opacity: 0.2
|
||||
},
|
||||
'code': {
|
||||
fontFamily: 'monospace',
|
||||
display: 'inline-block',
|
||||
padding: '2px 1px',
|
||||
},
|
||||
'list': {
|
||||
paddingLeft: '10px',
|
||||
lineHeight: '10px',
|
||||
fontSize: '20px'
|
||||
},
|
||||
'hr': {
|
||||
borderBottom: '2px solid #000',
|
||||
display: 'block',
|
||||
opacity: 0.2
|
||||
}
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
match: () => true,
|
||||
decorate: markdownDecorator,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* The markdown preview example.
|
||||
*
|
||||
* @type {Component}
|
||||
*/
|
||||
|
||||
class MarkdownPreview extends React.Component {
|
||||
|
||||
/**
|
||||
* Deserialize the initial editor state.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
|
||||
state = {
|
||||
state: Plain.deserialize('Slate is flexible enough to add **decorators** that can format text based on its content. For example, this editor has **Markdown** preview decorators on it, to make it _dead_ simple to make an editor with built-in Markdown previewing.\n## Try it out!\nTry it out for yourself!')
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Render the example.
|
||||
*
|
||||
* @return {Component} component
|
||||
*/
|
||||
|
||||
render = () => {
|
||||
return (
|
||||
<div className="editor">
|
||||
<Editor
|
||||
schema={schema}
|
||||
state={this.state.state}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* On change.
|
||||
*
|
||||
* @param {State} state
|
||||
*/
|
||||
|
||||
onChange = (state) => {
|
||||
this.setState({ state })
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Export.
|
||||
*/
|
||||
|
||||
export default MarkdownPreview
|
8
examples/markdown-shortcuts/Readme.md
Normal file
8
examples/markdown-shortcuts/Readme.md
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
# Auto-markdown Example
|
||||
|
||||

|
||||
|
||||
This example shows you can add a few key command handlers to get Markdown-like shortcuts in the editor. Such that once you press `> ` at the start of a line it turns it into a block quote!
|
||||
|
||||
Check out the [Examples readme](..) to see how to run it!
|
@@ -29,7 +29,7 @@ const schema = {
|
||||
* @type {Component}
|
||||
*/
|
||||
|
||||
class AutoMarkdown extends React.Component {
|
||||
class MarkdownShortcuts extends React.Component {
|
||||
|
||||
/**
|
||||
* Deserialize the raw initial state.
|
||||
@@ -212,4 +212,4 @@ class AutoMarkdown extends React.Component {
|
||||
* Export.
|
||||
*/
|
||||
|
||||
export default AutoMarkdown
|
||||
export default MarkdownShortcuts
|
54
examples/markdown-shortcuts/state.json
Normal file
54
examples/markdown-shortcuts/state.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"kind": "block",
|
||||
"type": "paragraph",
|
||||
"nodes": [
|
||||
{
|
||||
"kind": "text",
|
||||
"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:"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "block",
|
||||
"type": "block-quote",
|
||||
"nodes": [
|
||||
{
|
||||
"kind": "text",
|
||||
"text": "A wise quote."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "block",
|
||||
"type": "paragraph",
|
||||
"nodes": [
|
||||
{
|
||||
"kind": "text",
|
||||
"text": "Order when you start a line with \"## \" you get a level-two heading, like this:"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "block",
|
||||
"type": "heading-two",
|
||||
"nodes": [
|
||||
{
|
||||
"kind": "text",
|
||||
"text": "Try it out!"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "block",
|
||||
"type": "paragraph",
|
||||
"nodes": [
|
||||
{
|
||||
"kind": "text",
|
||||
"text": "Try it out for yourself! Try starting a new line with \">\", \"-\", or \"#\"s."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@@ -9,6 +9,7 @@ import Selection from '../models/selection'
|
||||
import getTransferData from '../utils/get-transfer-data'
|
||||
import TYPES from '../constants/types'
|
||||
import getWindow from 'get-window'
|
||||
import findDeepestNode from '../utils/find-deepest-node'
|
||||
import keycode from 'keycode'
|
||||
import { IS_FIREFOX, IS_MAC } from '../constants/environment'
|
||||
|
||||
@@ -135,8 +136,8 @@ class Content extends React.Component {
|
||||
*/
|
||||
|
||||
updateSelection = () => {
|
||||
const { state } = this.props
|
||||
const { selection } = state
|
||||
const { editor, state } = this.props
|
||||
const { document, selection } = state
|
||||
const el = ReactDOM.findDOMNode(this)
|
||||
const window = getWindow(el)
|
||||
const native = window.getSelection()
|
||||
@@ -157,8 +158,11 @@ class Content extends React.Component {
|
||||
// Otherwise, figure out which DOM nodes should be selected...
|
||||
const { anchorText, focusText } = state
|
||||
const { anchorKey, anchorOffset, focusKey, focusOffset } = selection
|
||||
const anchorRanges = anchorText.getRanges()
|
||||
const focusRanges = focusText.getRanges()
|
||||
const schema = editor.getSchema()
|
||||
const anchorDecorators = document.getDescendantDecorators(anchorKey, schema)
|
||||
const focusDecorators = document.getDescendantDecorators(focusKey, schema)
|
||||
const anchorRanges = anchorText.getRanges(anchorDecorators)
|
||||
const focusRanges = focusText.getRanges(focusDecorators)
|
||||
let a = 0
|
||||
let f = 0
|
||||
let anchorIndex
|
||||
@@ -186,8 +190,8 @@ class Content extends React.Component {
|
||||
|
||||
const anchorSpan = el.querySelector(`[data-offset-key="${anchorKey}-${anchorIndex}"]`)
|
||||
const focusSpan = el.querySelector(`[data-offset-key="${focusKey}-${focusIndex}"]`)
|
||||
const anchorEl = anchorSpan.firstChild
|
||||
const focusEl = focusSpan.firstChild
|
||||
const anchorEl = findDeepestNode(anchorSpan)
|
||||
const focusEl = findDeepestNode(focusSpan)
|
||||
|
||||
// If they are already selected, do nothing.
|
||||
if (
|
||||
|
@@ -3,6 +3,7 @@ import Debug from 'debug'
|
||||
import OffsetKey from '../utils/offset-key'
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import findDeepestNode from '../utils/find-deepest-node'
|
||||
import { IS_FIREFOX } from '../constants/environment'
|
||||
|
||||
/**
|
||||
@@ -190,19 +191,6 @@ class Leaf extends React.Component {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the deepest descendant of a DOM `element`.
|
||||
*
|
||||
* @param {Element} node
|
||||
* @return {Element}
|
||||
*/
|
||||
|
||||
function findDeepestNode(element) {
|
||||
return element.firstChild
|
||||
? findDeepestNode(element.firstChild)
|
||||
: element
|
||||
}
|
||||
|
||||
/**
|
||||
* Export.
|
||||
*
|
||||
|
21
src/utils/find-deepest-node.js
Normal file
21
src/utils/find-deepest-node.js
Normal file
@@ -0,0 +1,21 @@
|
||||
|
||||
/**
|
||||
* Find the deepest descendant of a DOM `element`.
|
||||
*
|
||||
* @param {Element} node
|
||||
* @return {Element}
|
||||
*/
|
||||
|
||||
function findDeepestNode(element) {
|
||||
return element.firstChild
|
||||
? findDeepestNode(element.firstChild)
|
||||
: element
|
||||
}
|
||||
|
||||
/**
|
||||
* Export.
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
|
||||
export default findDeepestNode
|
Reference in New Issue
Block a user