diff --git a/app/components/Editor/Editor.js b/app/components/Editor/Editor.js index ea8fd1b9..9de55c50 100644 --- a/app/components/Editor/Editor.js +++ b/app/components/Editor/Editor.js @@ -4,7 +4,7 @@ import { observable } from 'mobx'; import { observer } from 'mobx-react'; import { Value, Change } from 'slate'; import { Editor } from 'slate-react'; -import type { SlateNodeProps } from './types'; +import type { SlateNodeProps, Plugin } from './types'; import Plain from 'slate-plain-serializer'; import keydown from 'react-keydown'; import getDataTransferFiles from 'utils/getDataTransferFiles'; @@ -16,9 +16,10 @@ import Placeholder from './components/Placeholder'; import Contents from './components/Contents'; import Markdown from './serializer'; import createPlugins from './plugins'; -import insertImage from './insertImage'; +import { insertImageFile } from './changes'; import renderMark from './marks'; import createRenderNode from './nodes'; +import schema from './schema'; import styled from 'styled-components'; type Props = { @@ -37,7 +38,7 @@ class MarkdownEditor extends Component { props: Props; editor: Editor; renderNode: SlateNodeProps => *; - plugins: Object[]; + plugins: Plugin[]; @observable editorValue: Value; constructor(props: Props) { @@ -60,12 +61,11 @@ class MarkdownEditor extends Component { } componentDidMount() { - if (!this.props.readOnly) { - if (this.props.text) { - this.focusAtEnd(); - } else { - this.focusAtStart(); - } + if (this.props.readOnly) return; + if (this.props.text) { + this.focusAtEnd(); + } else { + this.focusAtStart(); } } @@ -78,8 +78,8 @@ class MarkdownEditor extends Component { onChange = (change: Change) => { if (this.editorValue !== change.value) { this.props.onChange(Markdown.serialize(change.value)); + this.editorValue = change.value; } - this.editorValue = change.value; }; handleDrop = async (ev: SyntheticEvent) => { @@ -100,15 +100,13 @@ class MarkdownEditor extends Component { }; insertImageFile = async (file: window.File) => { - this.editor.change( - async change => - await insertImage( - change, - file, - this.editor, - this.props.onImageUploadStart, - this.props.onImageUploadStop - ) + this.editor.change(change => + change.call( + insertImageFile, + file, + this.props.onImageUploadStart, + this.props.onImageUploadStop + ) ); }; @@ -206,10 +204,12 @@ class MarkdownEditor extends Component { value={this.editorValue} renderNode={this.renderNode} renderMark={renderMark} + schema={schema} onKeyDown={this.onKeyDown} onChange={this.onChange} onSave={onSave} readOnly={readOnly} + spellCheck={!readOnly} /> void, onImageUploadStop: () => void ) { onImageUploadStart(); - + console.log(file); try { // load the file as a data URL const id = uuid.v4(); @@ -27,6 +55,7 @@ export default async function insertImageFile( isVoid: true, data: { src, id, alt, loading: true }, }); + console.log('insertBlock', change); }); reader.readAsDataURL(file); @@ -37,12 +66,12 @@ export default async function insertImageFile( // we dont use the original change provided to the callback here // as the state may have changed significantly in the time it took to // upload the file. - const finalTransform = editor.value.change(); - const placeholder = editor.value.document.findDescendant( + const placeholder = change.value.document.findDescendant( node => node.data && node.data.get('id') === id ); + console.log('placeholder', placeholder); - return finalTransform.setNodeByKey(placeholder.key, { + return change.setNodeByKey(placeholder.key, { data: { src, alt, loading: false }, }); } catch (err) { diff --git a/app/components/Editor/components/TodoItem.js b/app/components/Editor/components/TodoItem.js index a1e7fed1..e6020228 100644 --- a/app/components/Editor/components/TodoItem.js +++ b/app/components/Editor/components/TodoItem.js @@ -10,12 +10,9 @@ export default class TodoItem extends Component { handleChange = (ev: SyntheticInputEvent) => { const checked = ev.target.checked; const { editor, node } = this.props; - const change = editor - .getState() - .change() - .setNodeByKey(node.key, { data: { checked } }); - - editor.onChange(change); + editor.change(change => + change.setNodeByKey(node.key, { data: { checked } }) + ); }; render() { diff --git a/app/components/Editor/components/Toolbar/BlockToolbar.js b/app/components/Editor/components/Toolbar/BlockToolbar.js index e1aa29e3..ded10b94 100644 --- a/app/components/Editor/components/Toolbar/BlockToolbar.js +++ b/app/components/Editor/components/Toolbar/BlockToolbar.js @@ -16,7 +16,7 @@ import ToolbarButton from './components/ToolbarButton'; import type { SlateNodeProps } from '../../types'; import { color } from 'shared/styles/constants'; import { fadeIn } from 'shared/styles/animations'; -import { splitAndInsertBlock } from '../../transforms'; +import { splitAndInsertBlock } from '../../changes'; type Props = SlateNodeProps & { onInsertImage: *, @@ -61,7 +61,7 @@ class BlockToolbar extends Component { editor.change(change => { splitAndInsertBlock(change, options); - change.value.document.nodes.forEach(node => { + editor.value.document.nodes.forEach(node => { if (node.type === 'block-toolbar') { change.removeNodeByKey(node.key); } diff --git a/app/components/Editor/components/Toolbar/components/LinkToolbar.js b/app/components/Editor/components/Toolbar/components/LinkToolbar.js index 72623c0b..d3209e8c 100644 --- a/app/components/Editor/components/Toolbar/components/LinkToolbar.js +++ b/app/components/Editor/components/Toolbar/components/LinkToolbar.js @@ -4,7 +4,6 @@ import ReactDOM from 'react-dom'; import { observable, action } from 'mobx'; import { observer, inject } from 'mobx-react'; import { withRouter } from 'react-router-dom'; -import { Change } from 'slate'; import { Editor } from 'slate-react'; import styled from 'styled-components'; import ArrowKeyNavigation from 'boundless-arrow-key-navigation'; @@ -28,7 +27,6 @@ class LinkToolbar extends Component { link: Object, documents: DocumentsStore, onBlur: () => void, - onChange: Change => *, }; @observable isEditing: boolean = false; diff --git a/app/components/Editor/nodes.js b/app/components/Editor/nodes.js index 62a0dd96..68ab6e2d 100644 --- a/app/components/Editor/nodes.js +++ b/app/components/Editor/nodes.js @@ -22,7 +22,7 @@ type Options = { onInsertImage: *, }; -export default function createRenderNode({ onChange, onInsertImage }: Options) { +export default function createRenderNode({ onInsertImage }: Options) { return function renderNode(props: SlateNodeProps) { const { attributes } = props; diff --git a/app/components/Editor/plugins.js b/app/components/Editor/plugins.js index b3130aac..46e59d89 100644 --- a/app/components/Editor/plugins.js +++ b/app/components/Editor/plugins.js @@ -1,5 +1,5 @@ // @flow -// import DropOrPasteImages from '@tommoor/slate-drop-or-paste-images'; +import InsertImages from 'slate-drop-or-paste-images'; import PasteLinkify from 'slate-paste-linkify'; import CollapseOnEscape from 'slate-collapse-on-escape'; import TrailingBlock from 'slate-trailing-block'; @@ -8,7 +8,7 @@ import Prism from 'slate-prism'; import EditList from './plugins/EditList'; import KeyboardShortcuts from './plugins/KeyboardShortcuts'; import MarkdownShortcuts from './plugins/MarkdownShortcuts'; -// import insertImage from './insertImage'; +import { insertImageFile } from './changes'; const onlyInCode = node => node.type === 'code'; @@ -23,18 +23,17 @@ const createPlugins = ({ onImageUploadStart, onImageUploadStop }: Options) => { type: 'link', collapseTo: 'end', }), - // DropOrPasteImages({ - // extensions: ['png', 'jpg', 'gif'], - // applyTransform: (transform, file, editor) => { - // return insertImage( - // transform, - // file, - // editor, - // onImageUploadStart, - // onImageUploadStop - // ); - // }, - // }), + InsertImages({ + extensions: ['png', 'jpg', 'gif'], + insertImage(change, file) { + return change.call( + insertImageFile, + file, + onImageUploadStart, + onImageUploadStop + ); + }, + }), EditList, EditCode({ onlyIn: onlyInCode, diff --git a/app/components/Editor/plugins/EditList.js b/app/components/Editor/plugins/EditList.js index 7bef46f4..9d3dea9a 100644 --- a/app/components/Editor/plugins/EditList.js +++ b/app/components/Editor/plugins/EditList.js @@ -4,4 +4,5 @@ import EditList from 'slate-edit-list'; export default EditList({ types: ['ordered-list', 'bulleted-list', 'todo-list'], typeItem: 'list-item', + typeDefault: 'paragraph', }); diff --git a/app/components/Editor/plugins/MarkdownShortcuts.js b/app/components/Editor/plugins/MarkdownShortcuts.js index 651ae207..8d4e7b66 100644 --- a/app/components/Editor/plugins/MarkdownShortcuts.js +++ b/app/components/Editor/plugins/MarkdownShortcuts.js @@ -182,8 +182,8 @@ export default function MarkdownShortcuts() { change.removeMarkByKey( textNode.key, - change.startOffset - charsInCodeBlock.size, - change.startOffset, + startOffset - charsInCodeBlock.size, + startOffset, 'code' ); } @@ -210,10 +210,13 @@ export default function MarkdownShortcuts() { onEnter(ev: SyntheticKeyboardEvent, change: Change) { const { value } = change; if (value.isExpanded) return; + const { startBlock, startOffset, endOffset } = value; if (startOffset === 0 && startBlock.length === 0) return this.onBackspace(ev, change); - if (endOffset !== startBlock.length) return; + + // Hitting enter at the end of the line reverts to standard behavior + if (endOffset === startBlock.length) return; // Hitting enter while an image is selected should jump caret below and // insert a new paragraph @@ -225,19 +228,12 @@ export default function MarkdownShortcuts() { // Hitting enter in a heading or blockquote will split the node at that // point and make the new node a paragraph if ( - startBlock.type !== 'heading1' && - startBlock.type !== 'heading2' && - startBlock.type !== 'heading3' && - startBlock.type !== 'heading4' && - startBlock.type !== 'heading5' && - startBlock.type !== 'heading6' && - startBlock.type !== 'block-quote' + startBlock.type.startsWith('heading') || + startBlock.type === 'block-quote' ) { - return; + ev.preventDefault(); + return change.splitBlock().setBlock('paragraph'); } - - ev.preventDefault(); - change.splitBlock().setBlock('paragraph'); }, /** diff --git a/app/components/Editor/schema.js b/app/components/Editor/schema.js index 1b59683a..14ae2baf 100644 --- a/app/components/Editor/schema.js +++ b/app/components/Editor/schema.js @@ -1,133 +1,82 @@ -// // @flow -// import React from 'react'; -// import Code from './components/Code'; -// import HorizontalRule from './components/HorizontalRule'; -// import InlineCode from './components/InlineCode'; -// import Image from './components/Image'; -// import Link from './components/Link'; -// import ListItem from './components/ListItem'; -// import TodoList from './components/TodoList'; -// import { -// Heading1, -// Heading2, -// Heading3, -// Heading4, -// Heading5, -// Heading6, -// } from './components/Heading'; -// import Paragraph from './components/Paragraph'; -// import BlockToolbar from './components/Toolbar/BlockToolbar'; -// import type { Props, Node, Transform } from './types'; -// -// type Options = { -// onInsertImage: Function, -// onChange: Function, -// }; -// -// const createSchema = ({ onInsertImage, onChange }: Options) => { -// return { -// marks: { -// bold: (props: Props) => {props.children}, -// code: (props: Props) => {props.children}, -// italic: (props: Props) => {props.children}, -// underlined: (props: Props) => {props.children}, -// deleted: (props: Props) => {props.children}, -// added: (props: Props) => {props.children}, -// }, -// -// nodes: { -// 'block-toolbar': (props: Props) => ( -// -// ), -// paragraph: (props: Props) => , -// 'block-quote': (props: Props) => ( -//
{props.children}
-// ), -// 'horizontal-rule': HorizontalRule, -// 'bulleted-list': (props: Props) => ( -//
    {props.children}
-// ), -// 'ordered-list': (props: Props) => ( -//
    {props.children}
-// ), -// 'todo-list': (props: Props) => ( -// {props.children} -// ), -// table: (props: Props) => ( -// {props.children}
-// ), -// 'table-row': (props: Props) => ( -// {props.children} -// ), -// 'table-head': (props: Props) => ( -// {props.children} -// ), -// 'table-cell': (props: Props) => ( -// {props.children} -// ), -// code: Code, -// image: Image, -// link: Link, -// 'list-item': ListItem, -// heading1: (props: Props) => , -// heading2: (props: Props) => , -// heading3: (props: Props) => , -// heading4: (props: Props) => , -// heading5: (props: Props) => , -// heading6: (props: Props) => , -// }, -// -// rules: [ -// // ensure first node is always a heading -// { -// match: (node: Node) => { -// return node.kind === 'document'; -// }, -// validate: (document: Node) => { -// const firstNode = document.nodes.first(); -// return firstNode && firstNode.type === 'heading1' ? null : firstNode; -// }, -// normalize: (transform: Transform, document: Node, firstNode: Node) => { -// transform.setBlock({ type: 'heading1' }); -// }, -// }, -// -// // automatically removes any marks in first heading -// { -// match: (node: Node) => { -// return node.kind === 'heading1'; -// }, -// validate: (heading: Node) => { -// const hasMarks = heading.getMarks().isEmpty(); -// const hasInlines = heading.getInlines().isEmpty(); -// -// return !(hasMarks && hasInlines); -// }, -// normalize: (transform: Transform, heading: Node) => { -// transform.unwrapInlineByKey(heading.key); -// -// heading.getMarks().forEach(mark => { -// heading.nodes.forEach(textNode => { -// if (textNode.kind === 'text') { -// transform.removeMarkByKey( -// textNode.key, -// 0, -// textNode.text.length, -// mark -// ); -// } -// }); -// }); -// -// return transform; -// }, -// }, -// ], -// }; -// }; -// -// export default createSchema; +// @flow +import { Block, Change, Node, Mark } from 'slate'; + +const schema = { + blocks: { + heading1: { marks: [''] }, + heading2: { marks: [''] }, + heading3: { marks: [''] }, + heading4: { marks: [''] }, + heading5: { marks: [''] }, + heading6: { marks: [''] }, + 'ordered-list': { + nodes: [{ types: ['list-item'] }], + }, + 'bulleted-list': { + nodes: [{ types: ['list-item'] }], + }, + table: { + nodes: [{ types: ['table-row', 'table-head', 'table-cell'] }], + }, + image: { + isVoid: true, + }, + 'horizontal-rule': { + isVoid: true, + }, + 'block-toolbar': { + isVoid: true, + }, + }, + document: { + nodes: [ + { types: ['heading1'], min: 1, max: 1 }, + { + types: [ + 'paragraph', + 'heading1', + 'heading2', + 'heading3', + 'heading4', + 'heading5', + 'heading6', + 'code', + 'horizontal-rule', + 'image', + 'bulleted-list', + 'ordered-list', + 'todo-list', + 'block-toolbar', + 'table', + ], + min: 1, + }, + ], + normalize: ( + change: Change, + reason: string, + { + node, + child, + mark, + index, + }: { node: Node, mark?: Mark, child: Node, index: number } + ) => { + switch (reason) { + case 'child_type_invalid': { + return change.setNodeByKey( + child.key, + index === 0 ? 'heading1' : 'paragraph' + ); + } + case 'child_required': { + const block = Block.create(index === 0 ? 'heading1' : 'paragraph'); + return change.insertNodeByKey(node.key, index, block); + } + default: + } + }, + }, +}; + +export default schema; diff --git a/app/components/Editor/transforms.js b/app/components/Editor/transforms.js deleted file mode 100644 index 9acd4787..00000000 --- a/app/components/Editor/transforms.js +++ /dev/null @@ -1,34 +0,0 @@ -// @flow -import { Change } from 'slate'; -import EditList from './plugins/EditList'; - -const { changes } = EditList; - -type Options = { - type: string | Object, - wrapper?: string | Object, - append?: string | Object, -}; - -export function splitAndInsertBlock(change: Change, options: Options) { - const { type, wrapper, append } = options; - const { value } = change; - const { document } = value; - const parent = document.getParent(value.startBlock.key); - - // lists get some special treatment - if (parent && parent.type === 'list-item') { - change = changes.unwrapList( - changes - .splitListItem(change.collapseToStart()) - .collapseToEndOfPreviousBlock() - ); - } - - change = change.insertBlock(type); - - if (wrapper) change = change.wrapBlock(wrapper); - if (append) change = change.insertBlock(append); - - return change; -} diff --git a/app/components/Editor/types.js b/app/components/Editor/types.js index 842ffd93..d39ed74e 100644 --- a/app/components/Editor/types.js +++ b/app/components/Editor/types.js @@ -1,5 +1,5 @@ // @flow -import { Value, Node } from 'slate'; +import { Value, Change, Node } from 'slate'; import { Editor } from 'slate-react'; export type SlateNodeProps = { @@ -11,3 +11,9 @@ export type SlateNodeProps = { node: Node, parent: Node, }; + +export type Plugin = { + validateNode?: Node => *, + onClick?: SyntheticEvent => *, + onKeyDown?: (SyntheticKeyboardEvent, Change) => *, +}; diff --git a/package.json b/package.json index 0dffb3c5..5e50210c 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,6 @@ "url": "git+ssh://git@github.com/outline/outline.git" }, "dependencies": { - "@tommoor/slate-drop-or-paste-images": "0.5.1", "aws-sdk": "^2.135.0", "babel-core": "^6.24.1", "babel-eslint": "^7.2.3", @@ -163,9 +162,10 @@ "sequelize-encrypted": "0.1.0", "slate": "^0.29.0", "slate-collapse-on-escape": "^0.6.0", + "slate-drop-or-paste-images": "^0.8.0", "slate-edit-code": "^0.13.2", "slate-edit-list": "^0.10.1", - "slate-md-serializer": "1.0.1", + "slate-md-serializer": "^1.0.4", "slate-paste-linkify": "^0.5.0", "slate-plain-serializer": "^0.4.12", "slate-prism": "^0.4.0", diff --git a/yarn.lock b/yarn.lock index 97467779..46907ed0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,18 +2,6 @@ # yarn lockfile v1 -"@tommoor/slate-drop-or-paste-images@0.5.1": - version "0.5.1" - resolved "https://registry.yarnpkg.com/@tommoor/slate-drop-or-paste-images/-/slate-drop-or-paste-images-0.5.1.tgz#67a8853bb59d3a449f2fe7c7071dc19fe3aff93d" - dependencies: - data-uri-to-blob "0.0.4" - es6-promise "^4.0.5" - image-to-data-uri "^1.0.0" - is-data-uri "^0.1.0" - is-image "^1.0.1" - is-url "^1.2.2" - mime-types "^2.1.11" - "@types/geojson@^1.0.0": version "1.0.3" resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-1.0.3.tgz#fbcf7fa5eb6dd108d51385cc6987ec1f24214523" @@ -2172,10 +2160,6 @@ data-uri-regex@^0.1.2: version "0.1.4" resolved "https://registry.yarnpkg.com/data-uri-regex/-/data-uri-regex-0.1.4.tgz#1e1db6c8397eca8a48ecdb55ad1b927ec0bbac2e" -data-uri-to-blob@0.0.4: - version "0.0.4" - resolved "https://registry.yarnpkg.com/data-uri-to-blob/-/data-uri-to-blob-0.0.4.tgz#087a7bff42f41a6cc0b2e2fb7312a7c29904fbaa" - date-fns@^1.27.2: version "1.28.5" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.28.5.tgz#257cfc45d322df45ef5658665967ee841cd73faf" @@ -8217,10 +8201,22 @@ slate-collapse-on-escape@^0.6.0: dependencies: to-pascal-case "^1.0.0" -slate-dev-logger@^0.1.25, slate-dev-logger@^0.1.36: +slate-dev-logger@^0.1.0, slate-dev-logger@^0.1.25, slate-dev-logger@^0.1.36: version "0.1.36" resolved "https://registry.npmjs.org/slate-dev-logger/-/slate-dev-logger-0.1.36.tgz#ecdb37dbf944dfc742bab23b6a20d5a0472db95e" +slate-drop-or-paste-images@^0.8.0: + version "0.8.0" + resolved "https://registry.npmjs.org/slate-drop-or-paste-images/-/slate-drop-or-paste-images-0.8.0.tgz#2c363a117688c1b57517ab9cd468c4060e09824e" + dependencies: + es6-promise "^4.0.5" + image-to-data-uri "^1.0.0" + is-data-uri "^0.1.0" + is-image "^1.0.1" + is-url "^1.2.2" + mime-types "^2.1.11" + slate-dev-logger "^0.1.0" + slate-edit-code@^0.13.2: version "0.13.2" resolved "https://registry.npmjs.org/slate-edit-code/-/slate-edit-code-0.13.2.tgz#682a7640da076906e5b4a4c73ec0e46d31d92c62" @@ -8234,9 +8230,9 @@ slate-edit-list@^0.10.1: version "0.10.1" resolved "https://registry.npmjs.org/slate-edit-list/-/slate-edit-list-0.10.1.tgz#9c6a142a314b0ff22a327f1b50c8f5c85468cb17" -slate-md-serializer@1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/slate-md-serializer/-/slate-md-serializer-1.0.1.tgz#10fb8118bf0b97addaf9d7fd77c1b19f3d767309" +slate-md-serializer@^1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/slate-md-serializer/-/slate-md-serializer-1.0.4.tgz#519b819b436706a31d93a8e787657694c0c75d35" slate-paste-linkify@^0.5.0: version "0.5.0"