diff --git a/package.json b/package.json index fa53cd9d..64b5b4c9 100644 --- a/package.json +++ b/package.json @@ -176,12 +176,12 @@ "remark-stringify": "^3.0.1", "sanitize-filename": "^1.6.1", "semaphore": "^1.0.5", - "slate": "^0.27.0", + "slate": "^0.28.0", "slate-edit-list": "^0.9.0", "slate-edit-table": "^0.12.0", "slate-plain-serializer": "^0.2.0", - "slate-react": "^0.4.0", - "slate-soft-break": "^0.4.0", + "slate-react": "^0.8.0", + "slate-soft-break": "^0.5.0", "slug": "^0.9.1", "toml-j0.4": "^1.1.1", "tomlify-j0.4": "^3.0.0-alpha.0", diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/components.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/components.js deleted file mode 100644 index cdae5ee2..00000000 --- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/components.js +++ /dev/null @@ -1,67 +0,0 @@ -import React from 'react'; -import { List } from 'immutable'; -import cn from 'classnames'; - -/** - * Slate uses React components to render each type of node that it receives. - * This is the closest thing Slate has to a schema definition. The types are set - * by us when we manually deserialize from Remark's MDAST to Slate's AST. - */ - -export const MARK_COMPONENTS = { - bold: props => {props.children}, - italic: props => {props.children}, - strikethrough: props => {props.children}, - code: props => {props.children}, -}; - -export const NODE_COMPONENTS = { - 'paragraph': props =>

{props.children}

, - 'list-item': props =>
  • {props.children}
  • , - 'quote': props =>
    {props.children}
    , - 'code': props =>
    {props.children}
    , - 'heading-one': props =>

    {props.children}

    , - 'heading-two': props =>

    {props.children}

    , - 'heading-three': props =>

    {props.children}

    , - 'heading-four': props =>

    {props.children}

    , - 'heading-five': props =>
    {props.children}
    , - 'heading-six': props =>
    {props.children}
    , - 'table': props => {props.children}
    , - 'table-row': props => {props.children}, - 'table-cell': props => {props.children}, - 'thematic-break': props =>
    , - 'bulleted-list': props => , - 'numbered-list': props => -
      {props.children}
    , - 'link': props => { - const data = props.node.get('data'); - const marks = data.get('marks'); - const url = data.get('url'); - const title = data.get('title'); - const link = {props.children}; - const result = !marks ? link : marks.reduce((acc, mark) => { - const MarkComponent = MARK_COMPONENTS[mark.type]; - return {acc}; - }, link); - return result; - }, - 'image': props => { - const data = props.node.get('data'); - const marks = data.get('marks'); - const url = data.get('url'); - const title = data.get('title'); - const alt = data.get('alt'); - const image = {alt}; - const result = !marks ? image : marks.reduce((acc, mark) => { - const MarkComponent = MARK_COMPONENTS[mark.type]; - return {acc}; - }, image); - return result; - }, - 'shortcode': props => { - const { attributes, node, state: editorState } = props; - const isSelected = editorState.selection.hasFocusIn(node); - const className = cn('nc-visualEditor-shortcode', { ['nc-visualEditor-shortcodeSelected']: isSelected }); - return
    {node.data.get('shortcode')}
    ; - }, -}; diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js index 22177e1c..9a8cfe6d 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js @@ -7,8 +7,8 @@ import { slateToMarkdown, markdownToSlate, htmlToSlate } from '../../serializers import registry from '../../../../../lib/registry'; import Toolbar from '../Toolbar/Toolbar'; import { Sticky } from '../../../../UI/Sticky/Sticky'; -import { MARK_COMPONENTS, NODE_COMPONENTS } from './components'; -import RULES from './rules'; +import { renderNode, renderMark } from './renderers'; +import { validateNode } from './validators'; import plugins, { EditListConfigured } from './plugins'; import onKeyDown from './keys'; @@ -24,11 +24,6 @@ export default class Editor extends Component { const editorState = State.create({ document }); this.state = { editorState, - schema: { - nodes: NODE_COMPONENTS, - marks: MARK_COMPONENTS, - rules: RULES, - }, shortcodePlugins: registry.getEditorComponents(), }; } @@ -208,7 +203,9 @@ export default class Editor extends Component { { + switch (props.mark.type) { + case bold: return props => {props.children}; + case italic: return props => {props.children}; + case strikethrough: return props => {props.children}; + case code: return props => {props.children}; + } +}; + +export const renderNode = props => { + switch (props.node.type) { + case 'paragraph': return props =>

    {props.children}

    ; + case 'list-item': return props =>
  • {props.children}
  • ; + case 'quote': return props =>
    {props.children}
    ; + case 'code': return props =>
    {props.children}
    ; + case 'heading-one': return props =>

    {props.children}

    ; + case 'heading-two': return props =>

    {props.children}

    ; + case 'heading-three': return props =>

    {props.children}

    ; + case 'heading-four': return props =>

    {props.children}

    ; + case 'heading-five': return props =>
    {props.children}
    ; + case 'heading-six': return props =>
    {props.children}
    ; + case 'table': return props => {props.children}
    ; + case 'table-row': return props => {props.children}; + case 'table-cell': return props => {props.children}; + case 'thematic-break': return props =>
    ; + case 'bulleted-list': return props => ; + case 'numbered-list': return props => ( +
      {props.children}
    + ); + case 'link': return props => { + const data = props.node.get('data'); + const marks = data.get('marks'); + const url = data.get('url'); + const title = data.get('title'); + const link = {props.children}; + const result = !marks ? link : marks.reduce((acc, mark) => { + const MarkComponent = MARK_COMPONENTS[mark.type]; + return {acc}; + }, link); + return result; + }; + case 'image': props => { + const data = props.node.get('data'); + const marks = data.get('marks'); + const url = data.get('url'); + const title = data.get('title'); + const alt = data.get('alt'); + const image = {alt}; + const result = !marks ? image : marks.reduce((acc, mark) => { + const MarkComponent = MARK_COMPONENTS[mark.type]; + return {acc}; + }, image); + return result; + }; + case 'shortcode': props => { + const { attributes, node, state: editorState } = props; + const isSelected = editorState.selection.hasFocusIn(node); + const className = cn('nc-visualEditor-shortcode', { ['nc-visualEditor-shortcodeSelected']: isSelected }); + return
    {node.data.get('shortcode')}
    ; + }; + } +}; diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/rules.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/rules.js deleted file mode 100644 index c898aff1..00000000 --- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/rules.js +++ /dev/null @@ -1,78 +0,0 @@ -import { Block, Text } from 'slate'; - -/** - * Rules are used to validate the editor state each time it changes, to ensure - * it is never rendered in an undesirable state. - */ - -/** - * If the editor is ever in an empty state, insert an empty - * paragraph block. - */ -const enforceNeverEmpty = { - match: object => object.kind === 'document', - validate: doc => { - const hasBlocks = !doc.getBlocks().isEmpty(); - return hasBlocks ? null : {}; - }, - normalize: change => { - const block = Block.create({ - type: 'paragraph', - nodes: [Text.create('')], - }); - const { key } = change.state.document; - return change.insertNodeByKey(key, 0, block).focus(); - }, -}; - -/** - * Ensure that shortcodes are children of the root node. - */ -const shortcodesAtRoot = { - match: object => object.kind === 'document', - validate: doc => { - return doc.findDescendant(node => { - return node.type === 'shortcode' && doc.getParent(node.key).key !== doc.key; - }); - }, - normalize: (change, doc, node) => { - return change.unwrapNodeByKey(node.key); - }, -}; - -/** - * Ensure that trailing shortcodes are followed by an empty paragraph. - */ -const noTrailingShortcodes = { - match: object => object.kind === 'document', - validate: doc => { - return doc.findDescendant(node => { - return node.type === 'shortcode' && doc.getBlocks().last().key === node.key; - }); - }, - normalize: (change, doc, node) => { - const text = Text.create(''); - const block = Block.create({ type: 'paragraph', nodes: [ text ] }); - return change.insertNodeByKey(doc.key, doc.get('nodes').size, block); - }, -}; - -/** - * Ensure that code blocks contain no marks. - */ -const codeBlocksContainPlainText = { - match: node => node.type === 'code', - validate: node => { - const invalidChild = node.getTexts().find(text => !text.getMarks().isEmpty()); - return invalidChild || null; - }, - normalize: (change, node, invalidChild) => { - invalidChild.getMarks().forEach(mark => { - change.removeMarkByKey(invalidChild.key, 0, invalidChild.get('characters').size, mark); - }); - }, -}; - -const rules = [ enforceNeverEmpty, shortcodesAtRoot, noTrailingShortcodes, codeBlocksContainPlainText ]; - -export default rules; diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/validators.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/validators.js new file mode 100644 index 00000000..4c7be246 --- /dev/null +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/validators.js @@ -0,0 +1,69 @@ +import { Block, Text } from 'slate'; + +/** + * Validation functions are used to validate the editor state each time it + * changes, to ensure it is never rendered in an undesirable state. + */ +export function validateNode(node) { + /** + * Validation of the document itself. + */ + if (node.kind === 'document') { + /** + * If the editor is ever in an empty state, insert an empty + * paragraph block. + */ + const hasBlocks = !doc.getBlocks().isEmpty(); + if (!hasBlocks) { + return change => { + const block = Block.create({ + type: 'paragraph', + nodes: [Text.create('')], + }); + const { key } = change.state.document; + return change.insertNodeByKey(key, 0, block).focus(); + }; + } + + /** + * Ensure that shortcodes are children of the root node. + */ + const nestedShortcode = node.findDescendant(descendant => { + const { type, key } = descendant; + return type === 'shortcode' && node.getParent(key).key !== node.key; + }); + if (nestedShortcode) { + return change => change.unwrapNodeByKey(node.key); + } + + /** + * Ensure that trailing shortcodes are followed by an empty paragraph. + */ + const trailingShortcode = node.findDescendant(descendant => { + const { type, key } = descendant; + return type === 'shortcode' && node.getBlocks().last().key === key; + }); + if (trailingShortcode) { + return change => { + const text = Text.create(''); + const block = Block.create({ type: 'paragraph', nodes: [ text ] }); + return change.insertNodeByKey(node.key, node.get('nodes').size, block); + }; + } + } + + + /** + * Ensure that code blocks contain no marks. + */ + if (node.type === 'code') { + const invalidChild = node.getTexts().find(text => !text.getMarks().isEmpty()); + if (invalidChild) { + return change => ( + invalidChild.getMarks().forEach(mark => ( + change.removeMarkByKey(invalidChild.key, 0, invalidChild.get('characters').size, mark) + )) + ); + } + } +}; diff --git a/yarn.lock b/yarn.lock index 89fa8656..ae58dca3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2827,7 +2827,7 @@ es6-iterator@2, es6-iterator@^2.0.1, es6-iterator@~2.0.1: es5-ext "^0.10.14" es6-symbol "^3.1" -es6-map@^0.1.3, es6-map@^0.1.4: +es6-map@^0.1.3: version "0.1.5" resolved "https://registry.yarnpkg.com/es6-map/-/es6-map-0.1.5.tgz#9136e0503dcc06a301690f0bb14ff4e364e949f0" dependencies: @@ -4472,6 +4472,10 @@ is-hexadecimal@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.1.tgz#6e084bbc92061fbb0971ec58b6ce6d404e24da69" +is-hotkey@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-hotkey/-/is-hotkey-0.1.1.tgz#b279a2fd108391be9aa93c6cb317f50357da549a" + is-in-browser@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/is-in-browser/-/is-in-browser-1.1.3.tgz#56ff4db683a078c6082eb95dad7dc62e1d04f835" @@ -8521,13 +8525,13 @@ slash@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" -slate-base64-serializer@^0.1.14: +slate-base64-serializer@^0.1.22: version "0.1.22" resolved "https://registry.yarnpkg.com/slate-base64-serializer/-/slate-base64-serializer-0.1.22.tgz#548e589178c75653004168004aad152f1976dd35" dependencies: isomorphic-base64 "^1.0.2" -slate-dev-logger@^0.1.15, slate-dev-logger@^0.1.20, slate-dev-logger@^0.1.23: +slate-dev-logger@^0.1.23: version "0.1.33" resolved "https://registry.yarnpkg.com/slate-dev-logger/-/slate-dev-logger-0.1.33.tgz#b4a4272255c2d598e5f26db5d85c58435357755f" @@ -8539,24 +8543,25 @@ slate-edit-table@^0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/slate-edit-table/-/slate-edit-table-0.12.0.tgz#9163e67b8025c3c09d6037eb76cb5e652b65dd47" -slate-plain-serializer@^0.2.0: +slate-plain-serializer@^0.2.0, slate-plain-serializer@^0.2.8: version "0.2.8" resolved "https://registry.yarnpkg.com/slate-plain-serializer/-/slate-plain-serializer-0.2.8.tgz#9bff5fafa09ab2ad47d961820f09d7d2abcb20a9" dependencies: slate-dev-logger "^0.1.23" -slate-prop-types@^0.2.0: +slate-prop-types@^0.2.8: version "0.2.8" resolved "https://registry.yarnpkg.com/slate-prop-types/-/slate-prop-types-0.2.8.tgz#2d0e1df0a372c635068c6f74a52b567b996f51c2" dependencies: slate-dev-logger "^0.1.23" -slate-react@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/slate-react/-/slate-react-0.4.0.tgz#e15c9034df5ea58fcb8a0c49c1cd159702296e0c" +slate-react@^0.8.0: + version "0.8.2" + resolved "https://registry.yarnpkg.com/slate-react/-/slate-react-0.8.2.tgz#035452c7aa90d7ec37f097e2430b2ce4198cfb78" dependencies: debug "^2.3.2" get-window "^1.1.1" + is-hotkey "^0.1.1" is-in-browser "^1.1.3" is-window "^1.0.2" keycode "^2.1.2" @@ -8564,27 +8569,26 @@ slate-react@^0.4.0: react-immutable-proptypes "^2.1.0" react-portal "^3.1.0" selection-is-backward "^1.0.0" - slate-base64-serializer "^0.1.14" - slate-dev-logger "^0.1.15" - slate-plain-serializer "^0.2.0" - slate-prop-types "^0.2.0" + slate-base64-serializer "^0.1.22" + slate-dev-logger "^0.1.23" + slate-plain-serializer "^0.2.8" + slate-prop-types "^0.2.8" -slate-soft-break@^0.4.0: - version "0.4.3" - resolved "https://registry.yarnpkg.com/slate-soft-break/-/slate-soft-break-0.4.3.tgz#e3a9279a9b92ca173915467f5fbd359f739a7e96" +slate-soft-break@^0.5.0: + version "0.5.1" + resolved "https://registry.yarnpkg.com/slate-soft-break/-/slate-soft-break-0.5.1.tgz#817348c6c38c5c4983f58de3bc497234b27378eb" -slate@^0.27.0: - version "0.27.5" - resolved "https://registry.yarnpkg.com/slate/-/slate-0.27.5.tgz#ab9d9f35e03f1910c59016ad66ee685255b5a645" +slate@^0.28.0: + version "0.28.2" + resolved "https://registry.yarnpkg.com/slate/-/slate-0.28.2.tgz#e740976ae494c9a2952e925b00f2694416b5e84d" dependencies: debug "^2.3.2" direction "^0.1.5" - es6-map "^0.1.4" esrever "^0.2.0" is-empty "^1.0.0" is-plain-object "^2.0.4" lodash "^4.17.4" - slate-dev-logger "^0.1.20" + slate-dev-logger "^0.1.23" type-of "^2.0.1" slice-ansi@0.0.4: