-
- );
-}
-
-Block.propTypes = {
- children: PropTypes.node.isRequired,
- type: PropTypes.oneOf(AVAILABLE_TYPES).isRequired
-};
-
-export default Block;
diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/BlockTypesMenu.css b/src/components/Widgets/MarkdownControlElements/VisualEditor/BlockTypesMenu.css
deleted file mode 100644
index 9868af79..00000000
--- a/src/components/Widgets/MarkdownControlElements/VisualEditor/BlockTypesMenu.css
+++ /dev/null
@@ -1,32 +0,0 @@
-.root {
- position: absolute;
-}
-
-.button {
- margin-top: 2px;
- color: #ddd;
- transition: color 0.5s ease;
- cursor: pointer;
-}
-.button:hover {
- color: #aaa;
-}
-
-.menu {
- position: absolute;
- top: -5px;
- left: 20px;
- height: 32px;
- white-space: nowrap;
- background-color: rgba(126, 126, 126, 0.1);
-}
-
-.icon {
- margin: 8px;
- cursor: pointer;
- color: #555;
-}
-
-.input {
- display: none;
-}
diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/BlockTypesMenu.js b/src/components/Widgets/MarkdownControlElements/VisualEditor/BlockTypesMenu.js
deleted file mode 100644
index 36ab5166..00000000
--- a/src/components/Widgets/MarkdownControlElements/VisualEditor/BlockTypesMenu.js
+++ /dev/null
@@ -1,115 +0,0 @@
-import React, { Component, PropTypes } from 'react';
-import withPortalAtCursorPosition from './withPortalAtCursorPosition';
-import { Icon } from '../../../UI';
-import MediaProxy from '../../../../valueObjects/MediaProxy';
-import styles from './BlockTypesMenu.css';
-
-class BlockTypesMenu extends Component {
-
- static propTypes = {
- plugins: PropTypes.array.isRequired,
- onClickBlock: PropTypes.func.isRequired,
- onClickPlugin: PropTypes.func.isRequired,
- onClickImage: PropTypes.func.isRequired,
- };
-
- state = {
- expanded: false,
- };
-
- componentWillUpdate() {
- if (this.state.expanded) {
- this.setState({ expanded: false });
- }
- }
-
- toggleMenu = () => {
- this.setState({ expanded: !this.state.expanded });
- };
-
- handleBlockTypeClick = (e, type) => {
- this.props.onClickBlock(type);
- };
-
- handlePluginClick = (e, plugin) => {
- const data = {};
- plugin.fields.forEach((field) => {
- data[field.name] = window.prompt(field.label); // eslint-disable-line
- });
- this.props.onClickPlugin(plugin.id, data);
- };
-
- handleFileUploadClick = () => {
- this._fileInput.click();
- };
-
- handleFileUploadChange = (e) => {
- e.stopPropagation();
- e.preventDefault();
-
- const fileList = e.dataTransfer ? e.dataTransfer.files : e.target.files;
- const files = [...fileList];
- const imageType = /^image\//;
-
- // Iterate through the list of files and return the first image on the list
- const file = files.find((currentFile) => {
- if (imageType.test(currentFile.type)) {
- return currentFile;
- }
- });
-
- if (file) {
- const mediaProxy = new MediaProxy(file.name, file);
- this.props.onClickImage(mediaProxy);
- }
- };
-
- renderBlockTypeButton = (type, icon) => {
- const onClick = e => this.handleBlockTypeClick(e, type);
- return (
-
- );
- };
-
- renderPluginButton = (plugin) => {
- const onClick = e => this.handlePluginClick(e, plugin);
- return (
-
- );
- };
-
- renderMenu() {
- const { plugins } = this.props;
- if (this.state.expanded) {
- return (
-
- {this.renderBlockTypeButton('hr', 'dot-3')}
- {plugins.map(plugin => this.renderPluginButton(plugin))}
-
- {
- this._fileInput = el;
- }}
- />
-
- );
- } else {
- return null;
- }
- }
-
- render() {
- return (
-
-
- {this.renderMenu()}
-
- );
- }
-}
-
-export default withPortalAtCursorPosition(BlockTypesMenu);
diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/StylesMenu.css b/src/components/Widgets/MarkdownControlElements/VisualEditor/StylesMenu.css
deleted file mode 100644
index c87888af..00000000
--- a/src/components/Widgets/MarkdownControlElements/VisualEditor/StylesMenu.css
+++ /dev/null
@@ -1,39 +0,0 @@
-
-.button {
- color: #ccc;
- cursor: pointer;
-}
-
-.button[data-active="true"] {
- color: black;
-}
-
-
-.menu > * {
- display: inline-block;
-}
-
-.menu > * + * {
- margin-left: 10px;
-}
-
-.hoverMenu {
- padding: 8px 7px 6px;
- position: absolute;
- z-index: 1;
- top: -10000px;
- left: -10000px;
- margin-top: -6px;
- opacity: 0;
- background-color: #222;
- border-radius: 4px;
- transition: opacity .75s;
-}
-
-.hoverMenu .button {
- color: #aaa;
-}
-
-.hoverMenu .button[data-active="true"] {
- color: #fff;
-}
diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/StylesMenu.js b/src/components/Widgets/MarkdownControlElements/VisualEditor/StylesMenu.js
deleted file mode 100644
index 5d141098..00000000
--- a/src/components/Widgets/MarkdownControlElements/VisualEditor/StylesMenu.js
+++ /dev/null
@@ -1,99 +0,0 @@
-import React, { Component, PropTypes } from 'react';
-import withPortalAtCursorPosition from './withPortalAtCursorPosition';
-import { Icon } from '../../../UI';
-import styles from './StylesMenu.css';
-
-class StylesMenu extends Component {
-
- static propTypes = {
- marks: PropTypes.object.isRequired,
- blocks: PropTypes.object.isRequired,
- inlines: PropTypes.object.isRequired,
- onClickBlock: PropTypes.func.isRequired,
- onClickMark: PropTypes.func.isRequired,
- onClickInline: PropTypes.func.isRequired,
- };
-
- /**
- * Used to set toolbar buttons to active state
- */
- hasMark = (type) => {
- const { marks } = this.props;
- return marks.some(mark => mark.type == type);
- };
-
- hasBlock = (type) => {
- const { blocks } = this.props;
- return blocks.some(node => node.type == type);
- };
-
- hasLinks = (type) => {
- const { inlines } = this.props;
- return inlines.some(inline => inline.type == 'link');
- };
-
- handleMarkClick = (e, type) => {
- e.preventDefault();
- this.props.onClickMark(type);
- };
-
- renderMarkButton = (type, icon) => {
- const isActive = this.hasMark(type);
- const onMouseDown = e => this.handleMarkClick(e, type);
- return (
-
-
-
- );
- };
-
- handleInlineClick = (e, type, isActive) => {
- e.preventDefault();
- this.props.onClickInline(type, isActive);
- };
-
- renderLinkButton = () => {
- const isActive = this.hasLinks();
- const onMouseDown = e => this.handleInlineClick(e, 'link', isActive);
- return (
-
-
-
- );
- };
-
- handleBlockClick = (e, type) => {
- e.preventDefault();
- const isActive = this.hasBlock(type);
- const isList = this.hasBlock('list-item');
- this.props.onClickBlock(type, isActive, isList);
- };
-
- renderBlockButton = (type, icon, checkType) => {
- checkType = checkType || type;
- const isActive = this.hasBlock(checkType);
- const onMouseDown = e => this.handleBlockClick(e, type);
- return (
-
-
-
- );
- };
-
- render() {
- return (
-
- {this.renderMarkButton('BOLD', 'bold')}
- {this.renderMarkButton('ITALIC', 'italic')}
- {this.renderMarkButton('CODE', 'code')}
- {this.renderLinkButton()}
- {this.renderBlockButton('header_one', 'h1')}
- {this.renderBlockButton('header_two', 'h2')}
- {this.renderBlockButton('blockquote', 'quote-left')}
- {this.renderBlockButton('unordered_list', 'list-bullet', 'list_item')}
-
- );
- }
-}
-
-export default withPortalAtCursorPosition(StylesMenu);
diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/index.css b/src/components/Widgets/MarkdownControlElements/VisualEditor/index.css
index 6eb82211..54764f89 100644
--- a/src/components/Widgets/MarkdownControlElements/VisualEditor/index.css
+++ b/src/components/Widgets/MarkdownControlElements/VisualEditor/index.css
@@ -1,26 +1,91 @@
-.active {
- box-shadow: 0 0 0 2px blue;
+.editor {
+ position: relative;
+ & h1, & h2, & h3 {
+ padding: 0;
+ color: #7c8382;
+ text-decoration: none;
+ border-bottom: none;
+ margin-bottom: 20px;
+ line-height: 1.45;
+ }
+ & h1 {
+ font-size: 2.5rem;
+ }
+ & h2 {
+ font-size: 2rem;
+ }
+ & h3 {
+ font-size: 1.8rem;
+ }
+ & p {
+ margin-bottom: 20px;
+ }
+ & div[data-plugin] {
+ background: #fff;
+ border: 1px solid #aaa;
+ padding: 10px;
+ margin-bottom: 20px;
+ }
}
-:global .plugin {
- background-color: #ddd;
- color: #555;
- text-align: center;
- width: 200px;
- padding: 8px;
- border-radius: 2px;
-}
+:global {
+ & .ProseMirror {
+ position: relative;
+ }
-:global .plugin_icon {
- font-size: 50px;
- margin: 12px 0;
-}
+ & .ProseMirror-content {
+ white-space: pre-wrap;
+ }
-:global .plugin_fields {
- font-size: 11px;
- outline:none;
-}
+ & .ProseMirror-drop-target {
+ position: absolute;
+ width: 1px;
+ background: #666;
+ pointer-events: none;
+ }
-:global .active {
- box-shadow: 0 0 0 2px blue;
+ & .ProseMirror-content ul, & .ProseMirror-content ol {
+ padding-left: 30px;
+ cursor: default;
+ }
+
+ & .ProseMirror-content blockquote {
+ padding-left: 1em;
+ border-left: 3px solid #eee;
+ margin-left: 0; margin-right: 0;
+ }
+
+ & .ProseMirror-content pre {
+ white-space: pre-wrap;
+ }
+
+ & .ProseMirror-content li {
+ position: relative;
+ pointer-events: none; /* Don't do weird stuff with marker clicks */
+ }
+ & .ProseMirror-content li > * {
+ pointer-events: auto;
+ }
+
+ & .ProseMirror-nodeselection *::selection { background: transparent; }
+ & .ProseMirror-nodeselection *::-moz-selection { background: transparent; }
+
+ & .ProseMirror-selectednode {
+ outline: 2px solid #8cf;
+ }
+
+ /* Make sure li selections wrap around markers */
+
+ & li.ProseMirror-selectednode {
+ outline: none;
+ }
+
+ & li.ProseMirror-selectednode:after {
+ content: "";
+ position: absolute;
+ left: -32px;
+ right: -2px; top: -2px; bottom: -2px;
+ border: 2px solid #8cf;
+ pointer-events: none;
+ }
}
diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/index.js b/src/components/Widgets/MarkdownControlElements/VisualEditor/index.js
index 943948e0..f269e0c9 100644
--- a/src/components/Widgets/MarkdownControlElements/VisualEditor/index.js
+++ b/src/components/Widgets/MarkdownControlElements/VisualEditor/index.js
@@ -1,298 +1,256 @@
-import React, { PropTypes } from 'react';
-import _ from 'lodash';
-import { Editor, Raw } from 'slate';
-import PluginDropImages from 'slate-drop-or-paste-images';
-import MarkupIt, { SlateUtils } from 'markup-it';
-import MediaProxy from '../../../../valueObjects/MediaProxy';
-import { emptyParagraphBlock, mediaproxyBlock } from '../constants';
-import { DEFAULT_NODE, SCHEMA } from './schema';
-import { getNodes, getSyntaxes, getPlugins } from '../../richText';
-import StylesMenu from './StylesMenu';
-import BlockTypesMenu from './BlockTypesMenu';
+import React, { Component, PropTypes } from 'react';
+import { Schema } from 'prosemirror-model';
+import { EditorState } from 'prosemirror-state';
+import { EditorView } from 'prosemirror-view';
+import history from 'prosemirror-history';
+import {
+ blockQuoteRule, orderedListRule, bulletListRule, codeBlockRule, headingRule,
+ inputRules, allInputRules,
+} from 'prosemirror-inputrules';
+import { keymap } from 'prosemirror-keymap';
+import { schema, defaultMarkdownSerializer } from 'prosemirror-markdown';
+import { baseKeymap, setBlockType, toggleMark } from 'prosemirror-commands';
+import registry from '../../../../lib/registry';
+import { buildKeymap } from './keymap';
+import createMarkdownParser from './parser';
+import Toolbar from '../Toolbar';
+import BlockMenu from '../BlockMenu';
+import styles from './index.css';
-/**
- * Slate Render Configuration
- */
-export default class VisualEditor extends React.Component {
+function processUrl(url) {
+ if (url.match(/^(https?:\/\/|mailto:|\/)/)) {
+ return url;
+ }
+ if (url.match(/^[^\/]+\.[^\/]+/)) {
+ return `https://${ url }`;
+ }
+ return `/${ url }`;
+}
- static propTypes = {
- onChange: PropTypes.func.isRequired,
- onAddMedia: PropTypes.func.isRequired,
- getMedia: PropTypes.func.isRequired,
- value: PropTypes.string,
- };
+const ruleset = {
+ blockquote: [blockQuoteRule],
+ ordered_list: [orderedListRule],
+ bullet_list: [bulletListRule],
+ code_block: [codeBlockRule],
+ heading: [headingRule, 6],
+};
+function buildInputRules(schema) {
+ const result = [];
+ for (const rule in ruleset) {
+ const type = schema.nodes[rule];
+ if (type) {
+ const fn = ruleset[rule];
+ result.push(fn[0].apply(fn.slice(1)));
+ }
+ }
+ return result;
+}
+
+function markActive(state, type) {
+ const { from, to, empty } = state.selection;
+ if (empty) {
+ return type.isInSet(state.storedMarks || state.doc.marksAt(from));
+ }
+ return state.doc.rangeHasMark(from, to, type);
+}
+
+function schemaWithPlugins(schema, plugins) {
+ let nodeSpec = schema.nodeSpec;
+ plugins.forEach((plugin) => {
+ const attrs = {};
+ plugin.get('fields').forEach((field) => {
+ attrs[field.get('name')] = { default: null };
+ });
+ nodeSpec = nodeSpec.addToEnd(`plugin_${ plugin.get('id') }`, {
+ attrs,
+ group: 'block',
+ parseDOM: [{
+ tag: 'div[data-plugin]',
+ getAttrs(dom) {
+ return JSON.parse(dom.getAttribute('data-plugin'));
+ },
+ }],
+ toDOM(node) {
+ return ['div', { 'data-plugin': JSON.stringify(node.attrs) }, plugin.get('label')];
+ },
+ });
+ });
+
+ return new Schema({
+ nodes: nodeSpec,
+ marks: schema.markSpec,
+ });
+}
+
+function createSerializer(schema, plugins) {
+ const serializer = Object.create(defaultMarkdownSerializer);
+ plugins.forEach((plugin) => {
+ serializer.nodes[`plugin_${ plugin.get('id') }`] = (state, node) => {
+ const toBlock = plugin.get('toBlock');
+ state.write(toBlock.call(plugin, node.attrs));
+ };
+ });
+ return serializer;
+}
+
+export default class Editor extends Component {
constructor(props) {
super(props);
-
- const MarkdownSyntax = getSyntaxes(this.getMedia).markdown;
- this.markdown = new MarkupIt(MarkdownSyntax);
-
- SCHEMA.nodes = _.merge(SCHEMA.nodes, getNodes());
-
- this.blockEdit = false;
-
- let rawJson;
- if (props.value !== undefined) {
- const content = this.markdown.toContent(props.value);
- rawJson = SlateUtils.encode(content, null, ['mediaproxy'].concat(getPlugins().map(plugin => plugin.id)));
- } else {
- rawJson = emptyParagraphBlock;
- }
+ const plugins = registry.getEditorComponents();
+ const s = schemaWithPlugins(schema, plugins);
this.state = {
- state: Raw.deserialize(rawJson, { terse: true }),
+ plugins,
+ schema: s,
+ parser: createMarkdownParser(s, plugins),
+ serializer: createSerializer(s, plugins),
};
+ }
- this.plugins = [
- PluginDropImages({
- applyTransform: (transform, file) => {
- const mediaProxy = new MediaProxy(file.name, file);
- props.onAddMedia(mediaProxy);
- return transform
- .insertBlock(mediaproxyBlock(mediaProxy));
- },
+ componentDidMount() {
+ const { schema, parser } = this.state;
+ const doc = parser.parse(this.props.value || '');
+ this.view = new EditorView(this.ref, {
+ state: EditorState.create({
+ doc,
+ schema,
+ plugins: [
+ inputRules({
+ rules: allInputRules.concat(buildInputRules(schema)),
+ }),
+ keymap(buildKeymap(schema)),
+ keymap(baseKeymap),
+ history.history(),
+ keymap({
+ 'Mod-z': history.undo,
+ 'Mod-y': history.redo,
+ }),
+ ],
}),
- ];
+ onAction: this.handleAction,
+ });
}
- getMedia = (src) => {
- return this.props.getMedia(src);
- };
-
- /**
- * Slate keeps track of selections, scroll position etc.
- * So, onChange gets dispatched on every interaction (click, arrows, everything...)
- * It also have an onDocumentChange, that get's dispatched only when the actual
- * content changes
- */
- handleChange = (state) => {
- if (this.blockEdit) {
- this.blockEdit = false;
- } else {
- this.setState({ state });
+ handleAction = (action) => {
+ const { schema, serializer } = this.state;
+ const newState = this.view.state.applyAction(action);
+ const md = serializer.serialize(newState.doc);
+ this.props.onChange(md);
+ this.view.updateState(newState);
+ if (newState.selection !== this.state.selection) {
+ this.handleSelection(newState);
}
+ this.view.focus();
};
- handleDocumentChange = (document, state) => {
- const rawJson = Raw.serialize(state, { terse: true });
- const content = SlateUtils.decode(rawJson);
- this.props.onChange(this.markdown.toText(content));
- };
-
- /**
- * Toggle marks / blocks when button is clicked
- */
- handleMarkStyleClick = (type) => {
- let { state } = this.state;
-
- state = state
- .transform()
- .toggleMark(type)
- .apply();
-
- this.setState({ state });
- };
-
- handleBlockStyleClick = (type, isActive, isList) => {
- let { state } = this.state;
- let transform = state.transform();
- const { document } = state;
-
- // Handle everything but list buttons.
- if (type != 'unordered_list' && type != 'ordered_list') {
- if (isList) {
- transform = transform
- .setBlock(isActive ? DEFAULT_NODE : type)
- .unwrapBlock('unordered_list')
- .unwrapBlock('ordered_list');
- }
-
- else {
- transform = transform
- .setBlock(isActive ? DEFAULT_NODE : type);
- }
- }
-
- // Handle the extra wrapping required for list buttons.
- else {
- const isType = state.blocks.some((block) => {
- return !!document.getClosest(block, parent => parent.type == type);
- });
-
- if (isList && isType) {
- transform = transform
- .setBlock(DEFAULT_NODE)
- .unwrapBlock('unordered_list');
- } else if (isList) {
- transform = transform
- .unwrapBlock(type == 'unordered_list')
- .wrapBlock(type);
+ handleSelection = (state) => {
+ const { schema, selection } = state;
+ if (selection.from === selection.to) {
+ const { $from } = selection;
+ if ($from.parent && $from.parent.type === schema.nodes.paragraph && $from.parent.textContent === '') {
+ const pos = this.view.coordsAtPos(selection.from);
+ const editorPos = this.view.content.getBoundingClientRect();
+ const selectionPosition = { top: pos.top - editorPos.top, left: pos.left - editorPos.left };
+ this.setState({ showToolbar: false, showBlockMenu: true, selectionPosition });
} else {
- transform = transform
- .setBlock('list_item')
- .wrapBlock(type);
+ this.setState({ showToolbar: false, showBlockMenu: false });
}
- }
-
- state = transform.apply();
- this.setState({ state });
- };
-
- /**
- * When clicking a link, if the selection has a link in it, remove the link.
- * Otherwise, add a new link with an href and text.
- *
- * @param {Event} e
- */
-
- handleInlineClick = (type, isActive) => {
- let { state } = this.state;
-
- if (type === 'link') {
- if (!state.isExpanded) return;
-
- if (isActive) {
- state = state
- .transform()
- .unwrapInline('link')
- .apply();
- }
-
- else {
- const href = window.prompt('Enter the URL of the link:', 'http://www.'); // eslint-disable-line
- state = state
- .transform()
- .wrapInline({
- type: 'link',
- data: { href },
- })
- .collapseToEnd()
- .apply();
- }
- }
- this.setState({ state });
- };
-
- handleBlockTypeClick = (type) => {
- let { state } = this.state;
-
- state = state
- .transform()
- .insertBlock({
- type,
- isVoid: true,
- })
- .apply();
-
- this.setState({ state }, this.focusAndAddParagraph);
- };
-
- handlePluginClick = (type, data) => {
- let { state } = this.state;
-
- state = state
- .transform()
- .insertInline({
- type,
- data,
- isVoid: true,
- })
- .collapseToEnd()
- .insertBlock(DEFAULT_NODE)
- .focus()
- .apply();
-
- this.setState({ state });
- };
-
- handleImageClick = (mediaProxy) => {
- let { state } = this.state;
- this.props.onAddMedia(mediaProxy);
-
- state = state
- .transform()
- .insertBlock(mediaproxyBlock(mediaProxy))
- .apply();
-
- this.setState({ state });
- };
-
- focusAndAddParagraph = () => {
- const { state } = this.state;
- const blocks = state.document.getBlocks();
- const last = blocks.last();
- const normalized = state
- .transform()
- .focus()
- .collapseToEndOf(last)
- .splitBlock()
- .setBlock(DEFAULT_NODE)
- .apply({
- snapshot: false,
- });
- this.setState({ state: normalized });
- };
-
- handleKeyDown = (evt) => {
- if (evt.shiftKey && evt.key === 'Enter') {
- this.blockEdit = true;
- let { state } = this.state;
- state = state
- .transform()
- .insertText('\n')
- .apply();
-
- this.setState({ state });
+ } else {
+ const pos = this.view.coordsAtPos(selection.from);
+ const editorPos = this.view.content.getBoundingClientRect();
+ const selectionPosition = { top: pos.top - editorPos.top, left: pos.left - editorPos.left };
+ this.setState({ showToolbar: true, showBlockMenu: false, selectionPosition });
}
};
- renderBlockTypesMenu = () => {
- const currentBlock = this.state.state.blocks.get(0);
- const isOpen = (this.props.value !== undefined && currentBlock.isEmpty && currentBlock.type !== 'horizontal-rule');
-
- return (
-
- );
+ handleRef = (ref) => {
+ this.ref = ref;
};
- renderStylesMenu() {
- const { state } = this.state;
- const isOpen = !(state.isBlurred || state.isCollapsed);
+ handleHeader = level => (
+ () => {
+ const { schema } = this.state;
+ const state = this.view.state;
+ const { $from, to, node } = state.selection;
+ let nodeType = schema.nodes.heading;
+ let attrs = { level };
+ let inHeader = node && node.hasMarkup(nodeType, attrs);
+ if (!inHeader) {
+ inHeader = to <= $from.end() && $from.parent.hasMarkup(nodeType, attrs);
+ }
+ if (inHeader) {
+ nodeType = schema.nodes.paragraph;
+ attrs = {};
+ }
- return (
-
- );
- }
+ const command = setBlockType(nodeType, { level });
+ command(state, this.handleAction);
+ }
+ );
+
+ handleBold = () => {
+ const command = toggleMark(this.state.schema.marks.strong);
+ command(this.view.state, this.handleAction);
+ };
+
+ handleItalic = () => {
+ const command = toggleMark(this.state.schema.marks.em);
+ command(this.view.state, this.handleAction);
+ };
+
+ handleLink = () => {
+ let url = null;
+ if (!markActive(this.view.state, this.state.schema.marks.link)) {
+ url = prompt('Link URL:');
+ }
+ const command = toggleMark(this.state.schema.marks.link, { href: url ? processUrl(url) : null });
+ command(this.view.state, this.handleAction);
+ };
+
+ handleBlock = (plugin, data) => {
+ const { schema } = this.state;
+ const nodeType = schema.nodes[`plugin_${ plugin.get('id') }`];
+ this.view.props.onAction(this.view.state.tr.replaceSelection(nodeType.create(data.toJS())).action());
+ };
+
+ handleToggle = () => {
+ this.props.onMode('raw');
+ };
render() {
- return (
-
- {this.renderStylesMenu()}
- {this.renderBlockTypesMenu()}
-
-
- );
+ const { onAddMedia, onRemoveMedia, getMedia } = this.props;
+ const { plugins, showToolbar, showBlockMenu, selectionPosition } = this.state;
+
+ return (
);
}
}
+
+Editor.propTypes = {
+ onAddMedia: PropTypes.func.isRequired,
+ onRemoveMedia: PropTypes.func.isRequired,
+ getMedia: PropTypes.func.isRequired,
+ onChange: PropTypes.func.isRequired,
+ onMode: PropTypes.func.isRequired,
+ value: PropTypes.node,
+};
diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/keymap.js b/src/components/Widgets/MarkdownControlElements/VisualEditor/keymap.js
new file mode 100644
index 00000000..bc0e1a22
--- /dev/null
+++ b/src/components/Widgets/MarkdownControlElements/VisualEditor/keymap.js
@@ -0,0 +1,92 @@
+const { wrapIn, setBlockType, chainCommands, newlineInCode, toggleMark } = require('prosemirror-commands');
+const { selectNextCell, selectPreviousCell } = require('prosemirror-schema-table');
+const { wrapInList, splitListItem, liftListItem, sinkListItem } = require('prosemirror-schema-list');
+const { undo, redo } = require('prosemirror-history');
+
+const mac = typeof navigator != 'undefined' ? /Mac/.test(navigator.platform) : false;
+
+// :: (Schema, ?Object) → Object
+// Inspect the given schema looking for marks and nodes from the
+// basic schema, and if found, add key bindings related to them.
+// This will add:
+//
+// * **Mod-b** for toggling [strong](#schema-basic.StrongMark)
+// * **Mod-i** for toggling [emphasis](#schema-basic.EmMark)
+// * **Mod-`** for toggling [code font](#schema-basic.CodeMark)
+// * **Ctrl-Shift-0** for making the current textblock a paragraph
+// * **Ctrl-Shift-1** to **Ctrl-Shift-Digit6** for making the current
+// textblock a heading of the corresponding level
+// * **Ctrl-Shift-Backslash** to make the current textblock a code block
+// * **Ctrl-Shift-8** to wrap the selection in an ordered list
+// * **Ctrl-Shift-9** to wrap the selection in a bullet list
+// * **Ctrl->** to wrap the selection in a block quote
+// * **Enter** to split a non-empty textblock in a list item while at
+// the same time splitting the list item
+// * **Mod-Enter** to insert a hard break
+// * **Mod-_** to insert a horizontal rule
+//
+// You can suppress or map these bindings by passing a `mapKeys`
+// argument, which maps key names (say `"Mod-B"` to either `false`, to
+// remove the binding, or a new key name string.
+function buildKeymap(schema, mapKeys) {
+ let keys = {}, type;
+ function bind(key, cmd) {
+ if (mapKeys) {
+ const mapped = mapKeys[key];
+ if (mapped === false) return;
+ if (mapped) key = mapped;
+ }
+ keys[key] = cmd;
+ }
+
+ bind('Mod-z', undo);
+ bind('Mod-y', redo);
+
+ if (type = schema.marks.strong)
+ bind('Mod-b', toggleMark(type));
+ if (type = schema.marks.em)
+ bind('Mod-i', toggleMark(type));
+ if (type = schema.marks.code)
+ bind('Mod-`', toggleMark(type));
+
+ if (type = schema.nodes.bullet_list)
+ bind('Shift-Ctrl-8', wrapInList(type));
+ if (type = schema.nodes.ordered_list)
+ bind('Shift-Ctrl-9', wrapInList(type));
+ if (type = schema.nodes.blockquote)
+ bind('Ctrl->', wrapIn(type));
+ if (type = schema.nodes.hard_break) {
+ let br = type, cmd = chainCommands(newlineInCode, (state, onAction) => {
+ onAction(state.tr.replaceSelection(br.create()).scrollAction());
+ return true;
+ });
+ bind('Mod-Enter', cmd);
+ bind('Shift-Enter', cmd);
+ if (mac) bind('Ctrl-Enter', cmd);
+ }
+ if (type = schema.nodes.list_item) {
+ bind('Enter', splitListItem(type));
+ bind('Mod-[', liftListItem(type));
+ bind('Mod-]', sinkListItem(type));
+ }
+ if (type = schema.nodes.paragraph)
+ bind('Shift-Ctrl-0', setBlockType(type));
+ if (type = schema.nodes.code_block)
+ bind('Shift-Ctrl-\\', setBlockType(type));
+ if (type = schema.nodes.heading)
+ for (let i = 1; i <= 6; i++) bind(`Shift-Ctrl-${ i }`, setBlockType(type, { level: i }));
+ if (type = schema.nodes.horizontal_rule) {
+ const hr = type;
+ bind('Mod-_', (state, onAction) => {
+ onAction(state.tr.replaceSelection(hr.create()).scrollAction());
+ return true;
+ });
+ }
+
+ if (schema.nodes.table_row) {
+ bind('Tab', selectNextCell);
+ bind('Shift-Tab', selectPreviousCell);
+ }
+ return keys;
+}
+exports.buildKeymap = buildKeymap;
diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/parser.js b/src/components/Widgets/MarkdownControlElements/VisualEditor/parser.js
new file mode 100644
index 00000000..a3d438cc
--- /dev/null
+++ b/src/components/Widgets/MarkdownControlElements/VisualEditor/parser.js
@@ -0,0 +1,254 @@
+/* eslint-disable */
+/*
+ Based closely on
+ https://github.com/ProseMirror/prosemirror-markdown/blob/master/src/from_markdown.js
+
+ Adds a bit of logic allowing editor plugins to hook into the parsing.
+*/
+
+const markdownit = require("markdown-it")
+const {Mark} = require("prosemirror-model")
+
+function maybeMerge(a, b) {
+ if (a.isText && b.isText && Mark.sameSet(a.marks, b.marks))
+ return a.copy(a.text + b.text)
+}
+
+function pluginHandler(schema, plugins) {
+ return (type, attrs, content) => {
+ if (type.name === 'paragraph' && content.length === 1 && content[0].type.name === 'text') {
+ const text = content[0].text;
+ const plugin = plugins.find(plugin => plugin.get('pattern').test(text));
+ if (plugin) {
+ const nodeType = schema.nodes[`plugin_${ plugin.get('id') }`];
+ const data = plugin.get('fromBlock').call(plugin, text.match(plugin.get('pattern')));
+ return nodeType.create(data);
+ }
+ }
+ return null;
+ };
+}
+
+// Object used to track the context of a running parse.
+class MarkdownParseState {
+ constructor(schema, plugins, tokenHandlers) {
+ this.schema = schema
+ this.stack = [{type: schema.nodes.doc, content: []}]
+ this.marks = Mark.none
+ this.tokenHandlers = tokenHandlers
+ this.pluginHandler = pluginHandler(schema, plugins);
+ }
+
+ top() {
+ return this.stack[this.stack.length - 1]
+ }
+
+ push(elt) {
+ if (this.stack.length) this.top().content.push(elt)
+ }
+
+ // : (string)
+ // Adds the given text to the current position in the document,
+ // using the current marks as styling.
+ addText(text) {
+ if (!text) return
+ let nodes = this.top().content, last = nodes[nodes.length - 1]
+ let node = this.schema.text(text, this.marks), merged
+ if (last && (merged = maybeMerge(last, node))) nodes[nodes.length - 1] = merged
+ else nodes.push(node)
+ }
+
+ // : (Mark)
+ // Adds the given mark to the set of active marks.
+ openMark(mark) {
+ this.marks = mark.addToSet(this.marks)
+ }
+
+ // : (Mark)
+ // Removes the given mark from the set of active marks.
+ closeMark(mark) {
+ this.marks = mark.removeFromSet(this.marks)
+ }
+
+ parseTokens(toks) {
+ for (let i = 0; i < toks.length; i++) {
+ let tok = toks[i]
+ let handler = this.tokenHandlers[tok.type]
+ if (!handler)
+ throw new Error("Token type `" + tok.type + "` not supported by Markdown parser")
+ handler(this, tok)
+ }
+ }
+
+ // : (NodeType, ?Object, ?[Node]) → ?Node
+ // Add a node at the current position.
+ addNode(type, attrs, content) {
+ const node = this.pluginHandler(type, attrs, content) || type.createAndFill(attrs, content, this.marks);
+ if (!node) return null
+ this.push(node)
+ return node
+ }
+
+ // : (NodeType, ?Object)
+ // Wrap subsequent content in a node of the given type.
+ openNode(type, attrs) {
+ this.stack.push({type: type, attrs: attrs, content: []})
+ }
+
+ // : () → ?Node
+ // Close and return the node that is currently on top of the stack.
+ closeNode() {
+ if (this.marks.length) this.marks = Mark.none
+ let info = this.stack.pop()
+ return this.addNode(info.type, info.attrs, info.content)
+ }
+}
+
+function attrs(given, token) {
+ return given instanceof Function ? given(token) : given
+}
+
+// Code content is represented as a single token with a `content`
+// property in Markdown-it.
+function noOpenClose(type) {
+ return type == "code_inline" || type == "code_block" || type == "fence"
+}
+
+function withoutTrailingNewline(str) {
+ return str[str.length - 1] == "\n" ? str.slice(0, str.length - 1) : str
+}
+
+function tokenHandlers(schema, tokens) {
+ let handlers = Object.create(null)
+ for (let type in tokens) {
+ let spec = tokens[type]
+ if (spec.block) {
+ let nodeType =schema.nodeType(spec.block);
+ if (noOpenClose(type)) {
+ handlers[type] = (state, tok) => {
+ state.openNode(nodeType, attrs(spec.attrs, tok))
+ state.addText(withoutTrailingNewline(tok.content))
+ state.closeNode()
+ }
+ } else {
+ handlers[type + "_open"] = (state, tok) => state.openNode(nodeType, attrs(spec.attrs, tok))
+ handlers[type + "_close"] = state => state.closeNode()
+ }
+ } else if (spec.node) {
+ let nodeType = schema.nodeType(spec.node)
+ handlers[type] = (state, tok) => state.addNode(nodeType, attrs(spec.attrs, tok))
+ } else if (spec.mark) {
+ let markType = schema.marks[spec.mark]
+ if (noOpenClose(type)) {
+ handlers[type] = (state, tok) => {
+ state.openMark(markType.create(attrs(spec.attrs, tok)))
+ state.addText(withoutTrailingNewline(tok.content))
+ state.closeMark(markType)
+ }
+ } else {
+ handlers[type + "_open"] = (state, tok) => state.openMark(markType.create(attrs(spec.attrs, tok)))
+ handlers[type + "_close"] = state => state.closeMark(markType)
+ }
+ } else {
+ throw new RangeError("Unrecognized parsing spec " + JSON.stringify(spec))
+ }
+ }
+
+ handlers.text = (state, tok) => state.addText(tok.content)
+ handlers.inline = (state, tok) => state.parseTokens(tok.children)
+ handlers.softbreak = state => state.addText("\n")
+
+ return handlers
+}
+
+// ;; A configuration of a Markdown parser. Such a parser uses
+// [markdown-it](https://github.com/markdown-it/markdown-it) to
+// tokenize a file, and then runs the custom rules it is given over
+// the tokens to create a ProseMirror document tree.
+class MarkdownParser {
+ // :: (Schema, MarkdownIt, Object)
+ // Create a parser with the given configuration. You can configure
+ // the markdown-it parser to parse the dialect you want, and provide
+ // a description of the ProseMirror entities those tokens map to in
+ // the `tokens` object, which maps token names to descriptions of
+ // what to do with them. Such a description is an object, and may
+ // have the following properties:
+ //
+ // **`node`**`: ?string`
+ // : This token maps to a single node, whose type can be looked up
+ // in the schema under the given name. Exactly one of `node`,
+ // `block`, or `mark` must be set.
+ //
+ // **`block`**`: ?string`
+ // : This token comes in `_open` and `_close` variants (which are
+ // appended to the base token name provides a the object
+ // property), and wraps a block of content. The block should be
+ // wrapped in a node of the type named to by the property's
+ // value.
+ //
+ // **`mark`**`: ?string`
+ // : This token also comes in `_open` and `_close` variants, but
+ // should add a mark (named by the value) to its content, rather
+ // than wrapping it in a node.
+ //
+ // **`attrs`**`: ?union
`
+ // : If the mark or node to be created needs attributes, they can
+ // be either given directly, or as a function that takes a
+ // [markdown-it
+ // token](https://markdown-it.github.io/markdown-it/#Token) and
+ // returns an attribute object.
+ constructor(schema, plugins, tokenizer, tokens) {
+ // :: Object The value of the `tokens` object used to construct
+ // this parser. Can be useful to copy and modify to base other
+ // parsers on.
+ this.tokens = tokens
+ this.schema = schema
+ this.tokenizer = tokenizer
+ this.plugins = plugins
+ this.tokenHandlers = tokenHandlers(schema, tokens)
+ }
+
+ // :: (string) → Node
+ // Parse a string as [CommonMark](http://commonmark.org/) markup,
+ // and create a ProseMirror document as prescribed by this parser's
+ // rules.
+ parse(text) {
+ let state = new MarkdownParseState(this.schema, this.plugins, this.tokenHandlers), doc
+ state.parseTokens(this.tokenizer.parse(text, {}))
+ do { doc = state.closeNode() } while (state.stack.length)
+ return doc
+ }
+}
+
+// :: MarkdownParser
+// A parser parsing unextended [CommonMark](http://commonmark.org/),
+// without inline HTML, and producing a document in the basic schema.
+export default function createMarkdownParser(schema, plugins) {
+ const tokens = {
+ blockquote: {block: "blockquote"},
+ paragraph: {block: "paragraph"},
+ list_item: {block: "list_item"},
+ bullet_list: {block: "bullet_list"},
+ ordered_list: {block: "ordered_list", attrs: tok => ({order: +tok.attrGet("order") || 1})},
+ heading: {block: "heading", attrs: tok => ({level: +tok.tag.slice(1)})},
+ code_block: {block: "code_block"},
+ fence: {block: "code_block"},
+ hr: {node: "horizontal_rule"},
+ image: {node: "image", attrs: tok => ({
+ src: tok.attrGet("src"),
+ title: tok.attrGet("title") || null,
+ alt: tok.children[0] && tok.children[0].content || null
+ })},
+ hardbreak: {node: "hard_break"},
+
+ em: {mark: "em"},
+ strong: {mark: "strong"},
+ link: {mark: "link", attrs: tok => ({
+ href: tok.attrGet("href"),
+ title: tok.attrGet("title") || null
+ })},
+ code_inline: {mark: "code"}
+ };
+
+ return new MarkdownParser(schema, plugins, markdownit("commonmark", {html: false}), tokens);
+}
diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/schema.js b/src/components/Widgets/MarkdownControlElements/VisualEditor/schema.js
deleted file mode 100644
index 412dae5e..00000000
--- a/src/components/Widgets/MarkdownControlElements/VisualEditor/schema.js
+++ /dev/null
@@ -1,65 +0,0 @@
-import React from 'react';
-import Block from './Block';
-import styles from './index.css';
-
-/* eslint react/prop-types: 0, react/no-multi-comp: 0 */
-
-// Define the default node type.
-export const DEFAULT_NODE = 'paragraph';
-
-/**
- * Define a schema.
- *
- * @type {Object}
- */
-
-export const SCHEMA = {
- nodes: {
- 'blockquote': (props) => {props.children} ,
- 'unordered_list': props => ,
- 'header_one': props => {props.children} ,
- 'header_two': props => {props.children} ,
- 'header_three': props => {props.children} ,
- 'header_four': props => {props.children} ,
- 'header_five': props => {props.children} ,
- 'header_six': props => {props.children} ,
- 'list_item': props => {props.children} ,
- 'paragraph': props => {props.children} ,
- 'hr': props => {
- const { node, state } = props;
- const isFocused = state.selection.hasEdgeIn(node);
- const className = isFocused ? styles.active : null;
- return (
-
- );
- },
- 'link': (props) => {
- const { data } = props.node;
- const href = data.get('href');
- return {props.children} ;
- },
- 'image': (props) => {
- const { node, state } = props;
- const isFocused = state.selection.hasEdgeIn(node);
- const className = isFocused ? styles.active : null;
- const src = node.data.get('src');
- return (
-
- );
- }
- },
- marks: {
- BOLD: {
- fontWeight: 'bold'
- },
- ITALIC: {
- fontStyle: 'italic'
- },
- CODE: {
- fontFamily: 'monospace',
- backgroundColor: '#eee',
- padding: '3px',
- borderRadius: '4px'
- }
- }
-};
diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/serializer.js b/src/components/Widgets/MarkdownControlElements/VisualEditor/serializer.js
new file mode 100644
index 00000000..e69de29b
diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/withPortalAtCursorPosition.js b/src/components/Widgets/MarkdownControlElements/VisualEditor/withPortalAtCursorPosition.js
deleted file mode 100644
index 4ff68cb0..00000000
--- a/src/components/Widgets/MarkdownControlElements/VisualEditor/withPortalAtCursorPosition.js
+++ /dev/null
@@ -1,59 +0,0 @@
-import React from 'react';
-import Portal from 'react-portal';
-import position from 'selection-position';
-
-export default function withPortalAtCursorPosition(WrappedComponent) {
- return class extends React.Component {
-
- static propTypes = {
- isOpen: React.PropTypes.bool.isRequired,
- };
-
- state = {
- menu: null,
- cursorPosition: null,
- };
-
- componentDidMount() {
- this.adjustPosition();
- }
-
- componentDidUpdate() {
- this.adjustPosition();
- }
-
- adjustPosition = () => {
- const { menu } = this.state;
-
- if (!menu) return;
-
- const cursorPosition = position(); // TODO: Results aren't determenistic
- const centerX = Math.ceil(
- cursorPosition.left
- + cursorPosition.width / 2
- + window.scrollX
- - menu.offsetWidth / 2
- );
- const centerY = cursorPosition.top + window.scrollY;
- menu.style.opacity = 1;
- menu.style.top = `${ centerY }px`;
- menu.style.left = `${ centerX }px`;
- };
-
- /**
- * When the portal opens, cache the menu element.
- */
- handleOpen = (portal) => {
- this.setState({ menu: portal.firstChild });
- };
-
- render() {
- const { isOpen, ...rest } = this.props;
- return (
-
-
-
- );
- }
- };
-}
diff --git a/yarn.lock b/yarn.lock
index b2db4520..2782d87c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2872,6 +2872,10 @@ extend@^3.0.0, extend@~3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.0.tgz#5a474353b9f3353ddd8176dfd37b91c83a46f1d4"
+extending-char@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/extending-char/-/extending-char-1.0.1.tgz#4c6c0eee3658a49df1600b32fc73876f418c7c6c"
+
extglob@^0.3.1:
version "0.3.2"
resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1"
@@ -4733,6 +4737,12 @@ liftoff@^2.2.0:
rechoir "^0.6.2"
resolve "^1.1.7"
+linkify-it@~1.2.2:
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-1.2.4.tgz#0773526c317c8fd13bd534ee1d180ff88abf881a"
+ dependencies:
+ uc.micro "^1.0.1"
+
lint-staged@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-3.1.0.tgz#4bb3da3b98135b0a076606c5e4f129af034bfe48"
@@ -5271,6 +5281,16 @@ map-obj@^1.0.0, map-obj@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
+markdown-it@^6.0.4:
+ version "6.1.1"
+ resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-6.1.1.tgz#ced037f4473ee9f5153ac414f77dc83c91ba927c"
+ dependencies:
+ argparse "^1.0.7"
+ entities "~1.1.1"
+ linkify-it "~1.2.2"
+ mdurl "~1.0.1"
+ uc.micro "^1.0.1"
+
marked-terminal@^1.6.2:
version "1.6.2"
resolved "https://registry.yarnpkg.com/marked-terminal/-/marked-terminal-1.6.2.tgz#44c128d69b5d9776c848314cdf69d4ec96322973"
@@ -5310,6 +5330,10 @@ math-expression-evaluator@^1.2.14:
dependencies:
lodash.indexof "^4.0.5"
+mdurl@~1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e"
+
media-typer@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
@@ -6711,6 +6735,89 @@ proper-lockfile@^1.1.2:
graceful-fs "^4.1.2"
retry "^0.10.0"
+prosemirror-commands@^0.12.0:
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/prosemirror-commands/-/prosemirror-commands-0.12.0.tgz#d790fe3dbabb5221e4d87e82834835e0f65881b2"
+ dependencies:
+ extending-char "^1.0.0"
+ prosemirror-model "^0.12.0"
+ prosemirror-state "^0.12.0"
+ prosemirror-transform "^0.12.0"
+
+prosemirror-history@^0.12.0:
+ version "0.12.1"
+ resolved "https://registry.yarnpkg.com/prosemirror-history/-/prosemirror-history-0.12.1.tgz#cbcdb536455b6af36bd2ba3ccced5387e5cfbfe1"
+ dependencies:
+ prosemirror-state "^0.12.0"
+ prosemirror-transform "^0.12.0"
+ rope-sequence "^1.2.0"
+
+prosemirror-inputrules@^0.12.0:
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/prosemirror-inputrules/-/prosemirror-inputrules-0.12.0.tgz#2e07b5cb1bfc7007c2b51ea5394303204b4b34df"
+ dependencies:
+ prosemirror-state "^0.12.0"
+ prosemirror-transform "^0.12.0"
+
+prosemirror-keymap@^0.12.0:
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/prosemirror-keymap/-/prosemirror-keymap-0.12.0.tgz#b70645b5d3f5ff4843bc6d26a74fa0022b504221"
+ dependencies:
+ prosemirror-state "^0.12.0"
+ w3c-keyname "^1.1.0"
+
+prosemirror-markdown@^0.12.0:
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/prosemirror-markdown/-/prosemirror-markdown-0.12.0.tgz#7ff8557c159168dcb532833c0b23b5b2866715c8"
+ dependencies:
+ markdown-it "^6.0.4"
+ prosemirror-model "~0.12.0"
+
+prosemirror-model@^0.12.0, prosemirror-model@~0.12.0:
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-0.12.0.tgz#5430c4056f2d3fe87d36de3f73aa9d9d07b0e8a7"
+
+prosemirror-schema-basic@^0.12.0:
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/prosemirror-schema-basic/-/prosemirror-schema-basic-0.12.0.tgz#9af876f8a915e75ba65847c794eebfc0df9f274e"
+ dependencies:
+ prosemirror-model "^0.12.0"
+
+prosemirror-schema-list@^0.12.0:
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/prosemirror-schema-list/-/prosemirror-schema-list-0.12.0.tgz#d93ba425ed202fc113d7b3388e5d9be1f698c276"
+ dependencies:
+ prosemirror-model "^0.12.0"
+ prosemirror-transform "^0.12.0"
+
+prosemirror-schema-table@^0.12.0:
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/prosemirror-schema-table/-/prosemirror-schema-table-0.12.0.tgz#a665dcb66bbd4c0ff2eac492d82991c6c410b5f3"
+ dependencies:
+ prosemirror-model "^0.12.0"
+ prosemirror-state "^0.12.0"
+ prosemirror-transform "^0.12.0"
+
+prosemirror-state@^0.12.0:
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/prosemirror-state/-/prosemirror-state-0.12.0.tgz#16e13d57d91840d0c3c340d47694efabeb77e987"
+ dependencies:
+ prosemirror-model "^0.12.0"
+ prosemirror-transform "^0.12.0"
+
+prosemirror-transform@^0.12.0, prosemirror-transform@^0.12.1:
+ version "0.12.1"
+ resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-0.12.1.tgz#69bca7e55976815e59281fbd8af4518f5ab90844"
+ dependencies:
+ prosemirror-model "^0.12.0"
+
+prosemirror-view@^0.12.0:
+ version "0.12.2"
+ resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-0.12.2.tgz#4a48bfe2ae3119b8c0c79166d7cd73e82284c99d"
+ dependencies:
+ prosemirror-model "^0.12.0"
+ prosemirror-state "^0.12.0"
+
proxy-addr@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.2.tgz#b4cc5f22610d9535824c123aef9d3cf73c40ba37"
@@ -7391,6 +7498,10 @@ rollup@^0.36.0:
dependencies:
source-map-support "^0.4.0"
+rope-sequence@^1.2.0:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/rope-sequence/-/rope-sequence-1.2.1.tgz#7da14c04fdc06f60bacdb9d26936c56265ffee2e"
+
rsvp@^3.0.13, rsvp@^3.0.18:
version "3.3.3"
resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.3.3.tgz#34633caaf8bc66ceff4be3c2e1dffd032538a813"
@@ -8297,6 +8408,10 @@ ua-parser-js@^0.7.10, ua-parser-js@^0.7.9:
version "0.7.10"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.10.tgz#917559ddcce07cbc09ece7d80495e4c268f4ef9f"
+uc.micro@^1.0.1:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.3.tgz#7ed50d5e0f9a9fb0a573379259f2a77458d50192"
+
uglify-js@^2.6, uglify-js@^2.6.1:
version "2.7.3"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.7.3.tgz#39b3a7329b89f5ec507e344c6e22568698ef4868"
@@ -8468,6 +8583,10 @@ vm-browserify@0.0.4:
dependencies:
indexof "0.0.1"
+w3c-keyname@^1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-1.1.1.tgz#0bb8566fbba0e414c2b798b696a71e1726967661"
+
walkdir@0.0.11:
version "0.0.11"
resolved "https://registry.yarnpkg.com/walkdir/-/walkdir-0.0.11.tgz#a16d025eb931bd03b52f308caed0f40fcebe9532"