diff --git a/app/components/elements/SlateEditor.jsx b/app/components/elements/SlateEditor.jsx index b2cf2a6d603f0b96327fc09b949693cc829f440b..f2e97f6afa90bc4bf88374e1381b6b1715e9239f 100644 --- a/app/components/elements/SlateEditor.jsx +++ b/app/components/elements/SlateEditor.jsx @@ -1,109 +1,20 @@ -/* -import EditBlockquote from 'slate-edit-blockquote' -import TrailingBlock from 'slate-trailing-block' - -const plugins = [ - TrailingBlock({ type: 'paragraph' }), - EditBlockquote() -] -*/ -const plugins = [] - +import React from 'react' import { Editor, Mark, Raw, Html } from 'slate' import Portal from 'react-portal' -import React from 'react' import position from 'selection-position' -const serializer = new Html({rules: [ - { - deserialize: (el, next) => null, - serialize: (object, children) => { - if(object.kind == 'string') return; - if(object.kind == 'block') { - switch(object.type) { - case 'paragraph': return <p>{children}</p> - case 'block-quote': return <blockquote>{children}</blockquote> - case 'bulleted-list': return <ul>{children}</ul> - case 'numbered-list': return <ol>{children}</ol> - case 'heading-one': return <h1>{children}</h1> - case 'heading-two': return <h2>{children}</h2> - case 'heading-three': return <h3>{children}</h3> - case 'heading-four': return <h4>{children}</h4> - case 'bulleted-list-item': return <li>{children}</li> - case 'numbered-list-item': return <li>{children}</li> - } - } - if(object.kind == 'mark') { - switch(object.type) { - case 'bold': return <strong>{children}</strong> - case 'italic': return <i>{children}</i> - case 'underline': return <u>{children}</u> - case 'strike': return <s>{children}</s> - case 'code': return <code>{children}</code> - } - } - - console.log("No serializer: ", object.kind, JSON.stringify(object, null, 2), children) - } - }, -]}) - -const schema = { - defaultNode: 'paragraph', - //blockTypes: { - // ...Blocks, - //}, - toolbarMarks: [ - { type: 'bold', icon: 'bold' }, - { type: 'italic', icon: 'italic' }, - { type: 'underline', icon: 'underline' }, - { type: 'code', icon: 'code' }, - ], - toolbarTypes: [ - { type: 'heading-one', icon: 'header' }, - { type: 'heading-two', icon: 'header' }, - { type: 'block-quote', icon: 'quote-left' }, - { type: 'numbered-list', icon: 'list-ol' }, - { type: 'bulleted-list', icon: 'list-ul' }, - ], - sidebarTypes: [], - nodes: { - 'block': ({ children }) => <p style={{background: 'red'}}>{children}</p>, - 'paragraph': ({ children }) => <p style={{color: 'blue'}}>{children}</p>, - 'block-quote': ({ children }) => <blockquote>{children}</blockquote>, - 'bulleted-list': ({ children }) => <ul>{children}</ul>, - 'numbered-list': ({ children, attributes }) => <ol {...attributes}>{children}</ol>, - 'heading-one': ({ children }) => <h1>{children}</h1>, - 'heading-two': ({ children }) => <h2>{children}</h2>, - 'heading-three': ({ children }) => <h3>{children}</h3>, - 'heading-four': ({ children }) => <h4>{children}</h4>, - 'bulleted-list-item': ({ children }) => <li>{children}</li>, - 'numbered-list-item': ({ children }) => <li>{children}</li>, - }, - marks: { - bold: props => <strong>{props.children}</strong>, - code: props => <code>{props.children}</code>, - italic: props => <em>{props.children}</em>, - underline: props => <u>{props.children}</u>, - strike: props => <s>{props.children}</s>, - }, - getMarkdownType: (chars) => { - switch (chars) { - case '*': - case '-': return 'bulleted-list-item'; - case '>': return 'block-quote'; - case '#': return 'heading-one'; - case '##': return 'heading-two'; - case '###': return 'heading-three'; - case '####': return 'heading-four'; - case '1.': return 'numbered-list-item'; - default: return null; - } - }, -} +import demoState from 'app/utils/SlateEditor/DemoState' +import {HtmlRules, schema, getMarkdownType} from 'app/utils/SlateEditor/Schema' + +const serializer = new Html({rules: HtmlRules}) +export const serializeHtml = (state) => serializer.serialize(state) +export const deserializeHtml = (html) => serializer.deserialize(html) +export const getDemoState = () => Raw.deserialize(demoState, { terse: true }) + +const plugins = [] -class SlateEditor extends React.Component { +export default class SlateEditor extends React.Component { constructor(props) { super(props) @@ -126,6 +37,7 @@ class SlateEditor extends React.Component { onChange = (state) => { this.setState({ state }) + this.props.onChange(state) } // When a mark button is clicked, toggle the current mark. @@ -146,7 +58,6 @@ class SlateEditor extends React.Component { this.setState({ menu: portal.firstChild }) } - // Markdown-style quick formatting onKeyDown = (e, data, state) => { switch (data.key) { @@ -162,7 +73,7 @@ class SlateEditor extends React.Component { let { selection } = state const { startText, startBlock, startOffset } = state const chars = startBlock.text.slice(0, startOffset).replace(/\s*/g, '') - const type = schema.getMarkdownType(chars) + const type = getMarkdownType(chars) if (!type) return if (type == 'bulleted-list-item' && startBlock.type == 'bulleted-list-item') return @@ -230,7 +141,6 @@ class SlateEditor extends React.Component { render = () => { const { state } = this.state - console.log(serializer.serialize(state)); return ( <div> {this.renderMenu()} @@ -248,7 +158,7 @@ class SlateEditor extends React.Component { {this.renderMarkButton('bold', <strong>B</strong>)} {this.renderMarkButton('italic', <i>I</i>)} {this.renderMarkButton('underline', <u>U</u>)} - {this.renderMarkButton('strike', <s>S</s>)} + {this.renderMarkButton('strike', <del>S</del>)} {this.renderMarkButton('code', <code>{'{}'}</code>)} </div> </Portal> @@ -268,7 +178,7 @@ class SlateEditor extends React.Component { renderEditor = () => { return ( - <div className="SlateEditor"> + <div className="SlateEditor Markdown"> <Editor schema={schema} plugins={plugins} @@ -295,5 +205,3 @@ class SlateEditor extends React.Component { menu.style.left = `${rect.left + window.scrollX - menu.offsetWidth / 2 + rect.width / 2}px` } } - -export default SlateEditor diff --git a/app/components/elements/SlateEditor.scss b/app/components/elements/SlateEditor.scss index cfadd14f6436ac9ff20f940effd59d661c01edbe..3d3cc2dbc4219f0bb309c3440e90b178baeeba54 100644 --- a/app/components/elements/SlateEditor.scss +++ b/app/components/elements/SlateEditor.scss @@ -17,18 +17,18 @@ } .SlateEditor__menu > * + * { - margin-left: 10px; + margin-left: 4px; } .SlateEditor__menu-button { font-family: 'Georgia', serif; - color: #777; + color: #FFF; cursor: pointer; > span { display: inline-block; - width: 2rem; + width: 1.5rem; text-align: center; - background: #333; + background: #555; border-radius: 2px; code { border: none; @@ -36,16 +36,18 @@ color: inherit; } } +} + +.SlateEditor__menu-button[data-active="false"] { > span:hover { - background: #555; - color: white; + color: #333; + background: #CCC; } } - .SlateEditor__menu-button[data-active="true"] { - color: white; > span { - background: #555; + color: #333; + background: #EEE; } } @@ -59,7 +61,8 @@ */ .SlateEditor__hover-menu { - padding: 8px 7px 6px; + font-size: 90%; + padding: 5px; position: absolute; z-index: 1; top: -10000px; @@ -70,11 +73,3 @@ border-radius: 4px; transition: opacity .75s; } - -.SlateEditor__hover-menu .button { - color: #aaa; -} - -.SlateEditor__hover-menu .button[data-active="true"] { - color: #fff; -} diff --git a/app/utils/SlateEditor/DemoState.js b/app/utils/SlateEditor/DemoState.js new file mode 100644 index 0000000000000000000000000000000000000000..82813d3b8ad08a3111fbca76461a6c4b3c329164 --- /dev/null +++ b/app/utils/SlateEditor/DemoState.js @@ -0,0 +1,96 @@ +export default { + "nodes": [ + { + "kind": "block", + "type": "paragraph", + "nodes": [ + { + "kind": "text", + "ranges": [ + { + "text": "This is editable " + }, + { + "text": "rich", + "marks": [ + { + "type": "bold" + } + ] + }, + { + "text": " text, " + }, + { + "text": "much", + "marks": [ + { + "type": "italic" + } + ] + }, + { + "text": " better than a " + }, + { + "text": "<textarea>", + "marks": [ + { + "type": "code" + } + ] + }, + { + "text": "!" + } + ] + } + ] + }, + { + "kind": "block", + "type": "paragraph", + "nodes": [ + { + "kind": "text", + "ranges": [ + { + "text": "Since it's rich text, you can do things like turn a selection of text " + }, + { + "text": "bold", + "marks": [ + { + "type": "bold" + } + ] + },{ + "text": ", or add a semantically rendered block quote in the middle of the page, like this:" + } + ] + } + ] + }, + { + "kind": "block", + "type": "block-quote", + "nodes": [ + { + "kind": "text", + "text": "A wise quote." + } + ] + }, + { + "kind": "block", + "type": "paragraph", + "nodes": [ + { + "kind": "text", + "text": "Try it out for yourself!" + } + ] + } + ] +} + diff --git a/app/utils/SlateEditor/Schema.js b/app/utils/SlateEditor/Schema.js new file mode 100644 index 0000000000000000000000000000000000000000..162a9ae56b1ff23eecd861d993e4249e3dc61147 --- /dev/null +++ b/app/utils/SlateEditor/Schema.js @@ -0,0 +1,224 @@ +import React from 'react' + +/* + +--deprecate +s, strike? +q +h5, h6 + +--unsupported +hr +div ['pull-right', 'pull-left', 'text-justify', 'text-rtl'] +iframe +br +center +table, thead, tbody, tr, th, td + +--inline +a +img + +*/ + +const BLOCK_TAGS = { + blockquote: 'block-quote', + p: 'paragraph', + pre: 'code', + h1: 'heading-one', + h2: 'heading-two', + h3: 'heading-three', + h4: 'heading-four', + ul: 'bulleted-list', + ol: 'numbered-list', + li: 'bulleted-list-item', +} + +const MARK_TAGS = { + em: 'italic', + i: 'italic', + strong: 'bold', + b: 'bold', + u: 'underline', + del: 'strike', + strike: 'strike', + code: 'code', +/* + sup: 'sup', + sub: 'sub', + +*/ +} + + +export const HtmlRules = [ + + // Block rules + { + deserialize: (el, next) => { + let type = BLOCK_TAGS[el.tagName] + if (!type) return + if(type == 'bulleted-list-item' && el.parent.name == 'ol') type = 'numbered-list-item' + return { + kind: 'block', + type: type, + nodes: next(el.children) + } + }, + serialize: (object, children) => { + if(object.kind !== 'block') return + switch(object.type) { + case 'code': return <pre><code>{children}</code></pre> + case 'paragraph': return <p>{children}</p> + case 'block-quote': return <blockquote>{children}</blockquote> + case 'bulleted-list': return <ul>{children}</ul> + case 'numbered-list': return <ol>{children}</ol> + case 'heading-one': return <h1>{children}</h1> + case 'heading-two': return <h2>{children}</h2> + case 'heading-three': return <h3>{children}</h3> + case 'heading-four': return <h4>{children}</h4> + case 'bulleted-list-item': return <li>{children}</li> + case 'numbered-list-item': return <li>{children}</li> + } + } + }, + + // Mark rules + { + deserialize: (el, next) => { + const type = MARK_TAGS[el.tagName] + if (!type) return + return { + kind: 'mark', + type: type, + nodes: next(el.children) + } + }, + serialize: (object, children) => { + if(object.kind !== 'mark') return; + switch(object.type) { + case 'bold': return <strong>{children}</strong> + case 'italic': return <i>{children}</i> + case 'underline': return <u>{children}</u> + case 'strike': return <del>{children}</del> + case 'code': return <code>{children}</code> + } + } + }, + + + // Custom + { + deserialize: (el, next) => { + if (el.tagName == 'iframe') { + return { + kind: 'block', + type: 'paragraph', + nodes: next(el.children) + } + } + if (el.tagName == 'img') { + return { + kind: 'block', + type: 'image', + data: {src: el.attribs.src}, + nodes: next(el.children) + } + } + if (el.tagName == 'a') { + console.log("deserialized <a>, the href is", el.attribs.href) + return { + kind: 'block', + type: 'link', + data: {href: el.attribs.href}, + nodes: next(el.children) + } + } + if(el.type == 'text') return + if(BLOCK_TAGS[el.tagName] || MARK_TAGS[el.tagName]) return + console.log("No deserializer for: ", el.tagName, el) + }, + serialize: (object, children) => { + if(object.kind == 'string') return; + if(object.kind == 'block' && object.type == 'link') { + console.log("Serialized <a>, the href is", object.data.get('href'), JSON.stringify(object.data, null, 2)) + return <a href={object.data.get('href')}>{children}</a> + } + if(object.kind == 'block' && object.type == 'image') { + const data = object.data + const src = data.get('src') + return <img src={src} /> + } + console.log("No serializer for: ", object.kind, JSON.stringify(object, null, 2), children) + } + } +] + +export const schema = { + defaultNode: 'paragraph', +/* + blockTypes: { + ...Blocks, + }, + toolbarMarks: [ + { type: 'bold', icon: 'bold' }, + { type: 'italic', icon: 'italic' }, + { type: 'underline', icon: 'underline' }, + { type: 'code', icon: 'code' }, + ], + toolbarTypes: [ + { type: 'heading-one', icon: 'header' }, + { type: 'heading-two', icon: 'header' }, + { type: 'block-quote', icon: 'quote-left' }, + { type: 'numbered-list', icon: 'list-ol' }, + { type: 'bulleted-list', icon: 'list-ul' }, + ], + sidebarTypes: [], +*/ + nodes: { + 'block': ({ children }) => <p style={{background: 'red'}}>{children}</p>, + 'paragraph': ({ children }) => <p>{children}</p>, + 'block-quote': ({ children }) => <blockquote>{children}</blockquote>, + 'bulleted-list': ({ children }) => <ul>{children}</ul>, + 'numbered-list': ({ children, attributes }) => <ol {...attributes}>{children}</ol>, + 'heading-one': ({ children }) => <h1>{children}</h1>, + 'heading-two': ({ children }) => <h2>{children}</h2>, + 'heading-three': ({ children }) => <h3>{children}</h3>, + 'heading-four': ({ children }) => <h4>{children}</h4>, + 'bulleted-list-item': ({ children }) => <li>{children}</li>, + 'numbered-list-item': ({ children }) => <li>{children}</li>, + 'image': (props) => { + const { data } = props.node + const src = data.get('src') + return <img {...props.attributes} src={'https://img1.steemit.com/0x0/' + src} /> + }, + 'link': (props) => { + const { data } = props.node + const href = data.get('href') +console.log("rendering link...href=",href) + return <a {...props.attributes} href={href}>{props.children}</a> + }, + }, + marks: { + bold: props => <strong>{props.children}</strong>, + code: props => <code>{props.children}</code>, + italic: props => <em>{props.children}</em>, + underline: props => <u>{props.children}</u>, + strike: props => <del>{props.children}</del>, + }, +} + +export const getMarkdownType = (chars) => { + switch (chars) { + case '*': + case '-': return 'bulleted-list-item'; + case '>': return 'block-quote'; + case '#': return 'heading-one'; + case '##': return 'heading-two'; + case '###': return 'heading-three'; + case '####': return 'heading-four'; + case '1.': return 'numbered-list-item'; + default: return null; + } +} +