diff --git a/examples/code-highlighting/index.js b/examples/code-highlighting/index.js index 6ec28a58a..57d616903 100644 --- a/examples/code-highlighting/index.js +++ b/examples/code-highlighting/index.js @@ -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 diff --git a/examples/index.js b/examples/index.js index 4a5ae5c62..3d839cfa3 100644 --- a/examples/index.js +++ b/examples/index.js @@ -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 {
{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')}
) } @@ -154,7 +156,6 @@ const router = ( - @@ -165,6 +166,8 @@ const router = ( + + diff --git a/examples/auto-markdown/Readme.md b/examples/markdown-preview/Readme.md similarity index 100% rename from examples/auto-markdown/Readme.md rename to examples/markdown-preview/Readme.md diff --git a/examples/markdown-preview/index.js b/examples/markdown-preview/index.js new file mode 100644 index 000000000..f7a18db62 --- /dev/null +++ b/examples/markdown-preview/index.js @@ -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 ( +
+ +
+ ) + } + + /** + * On change. + * + * @param {State} state + */ + + onChange = (state) => { + this.setState({ state }) + } + +} + +/** + * Export. + */ + +export default MarkdownPreview diff --git a/examples/auto-markdown/state.json b/examples/markdown-preview/state.json similarity index 100% rename from examples/auto-markdown/state.json rename to examples/markdown-preview/state.json diff --git a/examples/markdown-shortcuts/Readme.md b/examples/markdown-shortcuts/Readme.md new file mode 100644 index 000000000..16cee1cec --- /dev/null +++ b/examples/markdown-shortcuts/Readme.md @@ -0,0 +1,8 @@ + +# Auto-markdown Example + +![](../../docs/images/auto-markdown-example.png) + +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! diff --git a/examples/auto-markdown/index.js b/examples/markdown-shortcuts/index.js similarity index 98% rename from examples/auto-markdown/index.js rename to examples/markdown-shortcuts/index.js index 5c10686ee..961325be5 100644 --- a/examples/auto-markdown/index.js +++ b/examples/markdown-shortcuts/index.js @@ -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 diff --git a/examples/markdown-shortcuts/state.json b/examples/markdown-shortcuts/state.json new file mode 100644 index 000000000..83b5b286b --- /dev/null +++ b/examples/markdown-shortcuts/state.json @@ -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." + } + ] + } + ] +} diff --git a/src/components/content.js b/src/components/content.js index 4331370c7..105066329 100644 --- a/src/components/content.js +++ b/src/components/content.js @@ -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 ( diff --git a/src/components/leaf.js b/src/components/leaf.js index 181a406ab..b99d29f1b 100644 --- a/src/components/leaf.js +++ b/src/components/leaf.js @@ -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. * diff --git a/src/utils/find-deepest-node.js b/src/utils/find-deepest-node.js new file mode 100644 index 000000000..53e2a863b --- /dev/null +++ b/src/utils/find-deepest-node.js @@ -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