Paragraph with inline element
diff --git a/package.json b/package.json
index 667cb132a56f..9d02501908ba 100644
--- a/package.json
+++ b/package.json
@@ -51,38 +51,38 @@
"devDependencies": {
"babel": "^6.5.2",
"babel-cli": "^6.18.0",
- "babel-core": "^6.5.1",
+ "babel-core": "^6.23.1",
"babel-jest": "^20.0.3",
"babel-loader": "^7.0.0",
"babel-plugin-lodash": "^3.2.0",
- "babel-preset-es2015": "^6.5.0",
- "babel-preset-react": "^6.5.0",
- "babel-preset-stage-1": "^6.16.0",
- "babel-runtime": "^6.5.0",
+ "babel-preset-es2015": "^6.22.0",
+ "babel-preset-react": "^6.23.0",
+ "babel-preset-stage-1": "^6.22.0",
+ "babel-runtime": "^6.23.0",
"cross-env": "^5.0.2",
"css-loader": "^0.28.4",
"enzyme": "^2.4.1",
"eslint": "^3.7.1",
"eslint-config-netlify": "github:netlify/eslint-config-netlify",
"eslint-import-resolver-webpack": "^0.8.3",
- "exports-loader": "^0.6.3",
+ "exports-loader": "^0.6.4",
"extract-text-webpack-plugin": "^2.1.2",
"file-loader": "^0.11.2",
"identity-obj-proxy": "^3.0.0",
"imports-loader": "^0.7.1",
"jest": "^20.0.4",
"jest-cli": "^20.0.4",
- "lint-staged": "^3.1.0",
+ "lint-staged": "^3.3.1",
"node-sass": "^3.10.0",
"npm-check": "^5.2.3",
"postcss-cssnext": "^2.7.0",
"postcss-import": "^10.0.0",
"postcss-loader": "^2.0.5",
- "react-addons-test-utils": "^15.3.2",
+ "react-addons-test-utils": "^15.4.2",
"sass-loader": "^6.0.5",
"style-loader": "^0.18.2",
"stylefmt": "^4.3.1",
- "stylelint": "^7.3.1",
+ "stylelint": "^7.9.0",
"stylelint-config-css-modules": "^0.1.0",
"stylelint-config-standard": "^13.0.2",
"stylelint-declaration-block-order": "^0.1.0",
@@ -110,8 +110,9 @@
"jwt-decode": "^2.1.0",
"localforage": "^1.4.2",
"lodash": "^4.13.1",
- "markup-it": "^2.0.0",
"material-design-icons": "^3.0.1",
+ "mdast-util-definitions": "^1.2.2",
+ "mdast-util-to-string": "^1.0.4",
"moment": "^2.11.2",
"netlify-auth-js": "^0.5.5",
"normalize.css": "^4.2.0",
@@ -119,18 +120,6 @@
"preliminaries-parser-toml": "1.1.0",
"preliminaries-parser-yaml": "1.1.0",
"prismjs": "^1.5.1",
- "prosemirror-commands": "^0.16.0",
- "prosemirror-history": "^0.16.0",
- "prosemirror-inputrules": "^0.16.0",
- "prosemirror-keymap": "^0.16.0",
- "prosemirror-markdown": "^0.16.0",
- "prosemirror-model": "^0.16.0",
- "prosemirror-schema-basic": "^0.16.0",
- "prosemirror-schema-list": "^0.16.0",
- "prosemirror-schema-table": "^0.16.0",
- "prosemirror-state": "^0.16.0",
- "prosemirror-transform": "^0.16.0",
- "prosemirror-view": "^0.16.0",
"react": "^15.1.0",
"react-addons-css-transition-group": "^15.3.1",
"react-autosuggest": "^7.0.1",
@@ -157,12 +146,20 @@
"redux-notifications": "^2.1.1",
"redux-optimist": "^0.0.2",
"redux-thunk": "^1.0.3",
- "selection-position": "^1.0.0",
+ "rehype-parse": "^3.1.0",
+ "rehype-remark": "^2.0.0",
+ "rehype-stringify": "^3.0.0",
+ "remark-parse": "^3.0.1",
+ "remark-rehype": "^2.0.0",
+ "remark-stringify": "^3.0.1",
"semaphore": "^1.0.5",
- "slate": "^0.14.14",
- "slate-drop-or-paste-images": "^0.2.0",
+ "slate": "^0.21.0",
+ "slate-edit-list": "^0.7.1",
+ "slate-edit-table": "^0.10.1",
"slug": "^0.9.1",
- "textarea-caret-position": "^0.1.1",
+ "unified": "^6.1.4",
+ "unist-builder": "^1.0.2",
+ "unist-util-visit-parents": "^1.1.1",
"uuid": "^2.0.3",
"whatwg-fetch": "^1.0.0"
},
diff --git a/src/actions/editorialWorkflow.js b/src/actions/editorialWorkflow.js
index d04903a84b99..7c1afdd3c8c4 100644
--- a/src/actions/editorialWorkflow.js
+++ b/src/actions/editorialWorkflow.js
@@ -228,20 +228,28 @@ export function persistUnpublishedEntry(collection, existingUnpublishedEntry) {
if (!entryDraft.get('fieldsErrors').isEmpty()) return Promise.resolve();
const backend = currentBackend(state.config);
+ const transactionID = uuid.v4();
const assetProxies = entryDraft.get('mediaFiles').map(path => getAsset(state, path));
const entry = entryDraft.get('entry');
- const transactionID = uuid.v4();
- dispatch(unpublishedEntryPersisting(collection, entry, transactionID));
+ /**
+ * Serialize the values of any fields with registered serializers, and
+ * update the entry and entryDraft with the serialized values.
+ */
+ const serializedData = serializeValues(entryDraft.getIn(['entry', 'data']), collection.get('fields'));
+ const serializedEntry = entry.set('data', serializedData);
+ const serializedEntryDraft = entryDraft.set('entry', serializedEntry);
+
+ dispatch(unpublishedEntryPersisting(collection, serializedEntry, transactionID));
const persistAction = existingUnpublishedEntry ? backend.persistUnpublishedEntry : backend.persistEntry;
- return persistAction.call(backend, state.config, collection, entryDraft, assetProxies.toJS())
+ return persistAction.call(backend, state.config, collection, serializedEntryDraft, assetProxies.toJS())
.then(() => {
dispatch(notifSend({
message: 'Entry saved',
kind: 'success',
dismissAfter: 4000,
}));
- return dispatch(unpublishedEntryPersisted(collection, entry, transactionID));
+ return dispatch(unpublishedEntryPersisted(collection, serializedEntry, transactionID));
})
.catch((error) => {
dispatch(notifSend({
diff --git a/src/actions/entries.js b/src/actions/entries.js
index ad54d99a4ced..26235db25ba4 100644
--- a/src/actions/entries.js
+++ b/src/actions/entries.js
@@ -1,5 +1,6 @@
import { List } from 'immutable';
import { actions as notifActions } from 'redux-notifications';
+import { serializeValues } from '../lib/serializeEntryValues';
import { closeEntry } from './editor';
import { currentBackend } from '../backends/backend';
import { getIntegrationProvider } from '../integrations';
@@ -216,10 +217,11 @@ export function loadEntry(collection, slug) {
const backend = currentBackend(state.config);
dispatch(entryLoading(collection, slug));
return backend.getEntry(collection, slug)
- .then(loadedEntry => (
- dispatch(entryLoaded(collection, loadedEntry))
- ))
+ .then(loadedEntry => {
+ return dispatch(entryLoaded(collection, loadedEntry))
+ })
.catch((error) => {
+ console.error(error);
dispatch(notifSend({
message: `Failed to load entry: ${ error.message }`,
kind: 'danger',
@@ -265,28 +267,37 @@ export function persistEntry(collection) {
// Early return if draft contains validation errors
if (!entryDraft.get('fieldsErrors').isEmpty()) return Promise.reject();
-
+
const backend = currentBackend(state.config);
const assetProxies = entryDraft.get('mediaFiles').map(path => getAsset(state, path));
const entry = entryDraft.get('entry');
- dispatch(entryPersisting(collection, entry));
+
+ /**
+ * Serialize the values of any fields with registered serializers, and
+ * update the entry and entryDraft with the serialized values.
+ */
+ const serializedData = serializeValues(entryDraft.getIn(['entry', 'data']), collection.get('fields'));
+ const serializedEntry = entry.set('data', serializedData);
+ const serializedEntryDraft = entryDraft.set('entry', serializedEntry);
+ dispatch(entryPersisting(collection, serializedEntry));
return backend
- .persistEntry(state.config, collection, entryDraft, assetProxies.toJS())
+ .persistEntry(state.config, collection, serializedEntryDraft, assetProxies.toJS())
.then(() => {
dispatch(notifSend({
message: 'Entry saved',
kind: 'success',
dismissAfter: 4000,
}));
- return dispatch(entryPersisted(collection, entry));
+ return dispatch(entryPersisted(collection, serializedEntry));
})
.catch((error) => {
+ console.error(error);
dispatch(notifSend({
message: `Failed to persist entry: ${ error }`,
kind: 'danger',
dismissAfter: 8000,
}));
- return dispatch(entryPersistFail(collection, entry, error));
+ return dispatch(entryPersistFail(collection, serializedEntry, error));
});
};
}
diff --git a/src/components/ControlPanel/ControlPane.css b/src/components/ControlPanel/ControlPane.css
index ea8d67d32020..b3c953782220 100644
--- a/src/components/ControlPanel/ControlPane.css
+++ b/src/components/ControlPanel/ControlPane.css
@@ -8,8 +8,8 @@
& input,
& textarea,
- & select {
- font-family: 'SFMono-Regular', Consolas, "Liberation Mono", Menlo, Courier, monospace;
+ & select,
+ & div[contenteditable=true] {
display: block;
width: 100%;
padding: 12px;
@@ -28,6 +28,12 @@
border-color: var(--primaryColor);
}
}
+
+ & input,
+ & textarea,
+ & select {
+ font-family: var(--fontFamilyMono);
+ }
}
.label {
diff --git a/src/components/MarkupItReactRenderer/__tests__/MarkupItReactRenderer.spec.js b/src/components/MarkupItReactRenderer/__tests__/MarkupItReactRenderer.spec.js
deleted file mode 100644
index f9f2c158c8a1..000000000000
--- a/src/components/MarkupItReactRenderer/__tests__/MarkupItReactRenderer.spec.js
+++ /dev/null
@@ -1,264 +0,0 @@
-/* eslint max-len:0 */
-
-import React from 'react';
-import { shallow } from 'enzyme';
-import { padStart } from 'lodash';
-import { Map } from 'immutable';
-import MarkupIt from 'markup-it';
-import markdownSyntax from 'markup-it/syntaxes/markdown';
-import htmlSyntax from 'markup-it/syntaxes/html';
-import reInline from 'markup-it/syntaxes/markdown/re/inline';
-import MarkupItReactRenderer from '../';
-
-function getAsset(path) {
- return path;
-}
-
-describe('MarkitupReactRenderer', () => {
- describe('basics', () => {
- it('should re-render properly after a value and syntax update', () => {
- const component = shallow(
-
Paragraph with inline element
'; - const component = shallow( -Paragraph with inline element
Use the printf()
function.
There is a literal backtick (\`) here.
Text with bold & em elements
I get 10 times more traffic from Google than from Yahoo or MSN.
token.text).join('') }} />
;
- },
- [BLOCKS.BLOCKQUOTE]: 'blockquote',
- [BLOCKS.PARAGRAPH]: 'p',
- [BLOCKS.FOOTNOTE]: 'footnote',
- [BLOCKS.HTML]: ({ token }) => ,
- [BLOCKS.HR]: 'hr',
- [BLOCKS.HEADING_1]: 'h1',
- [BLOCKS.HEADING_2]: 'h2',
- [BLOCKS.HEADING_3]: 'h3',
- [BLOCKS.HEADING_4]: 'h4',
- [BLOCKS.HEADING_5]: 'h5',
- [BLOCKS.HEADING_6]: 'h6',
- [BLOCKS.TABLE]: 'table',
- [BLOCKS.TABLE_ROW]: 'tr',
- [BLOCKS.TABLE_CELL]: 'td',
- [BLOCKS.OL_LIST]: 'ol',
- [BLOCKS.UL_LIST]: 'ul',
- [BLOCKS.LIST_ITEM]: 'li',
-
- [STYLES.TEXT]: null,
- [STYLES.BOLD]: 'strong',
- [STYLES.ITALIC]: 'em',
- [STYLES.CODE]: 'code',
- [STYLES.STRIKETHROUGH]: 'del',
-
- [ENTITIES.LINK]: 'a',
- [ENTITIES.IMAGE]: 'img',
- [ENTITIES.FOOTNOTE_REF]: 'sup',
- [ENTITIES.HARD_BREAK]: 'br',
-};
-
-const notAllowedAttributes = ['loose', 'image'];
-
-export default class MarkupItReactRenderer extends React.Component {
-
- constructor(props) {
- super(props);
- const { syntax } = props;
- this.parser = new MarkupIt(syntax);
- this.plugins = {};
- registry.getEditorComponents().forEach((component) => {
- this.plugins[component.get('id')] = component;
- });
- }
-
- componentWillReceiveProps(nextProps) {
- if (nextProps.syntax != this.props.syntax) {
- this.parser = new MarkupIt(nextProps.syntax);
- }
- }
-
- sanitizeProps(props) {
- const { getAsset } = this.props;
-
- if (props.image) {
- props = Object.assign({}, props, { src: getAsset(props.image).toString() });
- }
-
- return omit(props, notAllowedAttributes);
- }
-
-
- renderToken(schema, token, index = 0, key = '0') {
- const type = token.get('type');
- const data = token.get('data');
- const text = token.get('text');
- const tokens = token.get('tokens');
- const nodeType = schema[type];
- key = `${ key }.${ index }`;
-
- // Only render if type is registered as renderer
- if (typeof nodeType !== 'undefined') {
- let children = null;
- if (tokens.size) {
- children = tokens.map((token, idx) => this.renderToken(schema, token, idx, key));
- } else if (type === 'text') {
- children = text;
- }
- if (nodeType !== null) {
- let props = { key, token };
- if (typeof nodeType !== 'function') {
- props = { key, ...this.sanitizeProps(data.toJS()) };
- }
- // If this is a react element
- return React.createElement(nodeType, props, children);
- } else {
- // If this is a text node
- return children;
- }
- }
-
- const plugin = this.plugins[token.get('type')];
- if (plugin) {
- const output = plugin.toPreview(token.get('data').toJS());
- return typeof output === 'string' ?
- :
- output;
- }
-
- return null;
- }
-
-
- render() {
- const { value, schema, getAsset } = this.props;
- const content = this.parser.toContent(value);
- return this.renderToken({ ...defaultSchema, ...schema }, content.get('token'));
- }
-}
-
-MarkupItReactRenderer.propTypes = {
- value: PropTypes.string,
- syntax: PropTypes.instanceOf(Syntax).isRequired,
- schema: PropTypes.objectOf(PropTypes.oneOfType([
- PropTypes.string,
- PropTypes.func,
- ])),
- getAsset: PropTypes.func.isRequired,
-};
diff --git a/src/components/PreviewPane/Preview.js b/src/components/PreviewPane/Preview.js
index 1875d1b15410..93ea02c32fc0 100644
--- a/src/components/PreviewPane/Preview.js
+++ b/src/components/PreviewPane/Preview.js
@@ -9,15 +9,22 @@ const style = {
fontFamily: 'Roboto, "Helvetica Neue", HelveticaNeue, Helvetica, Arial, sans-serif',
};
-export default function Preview({ collection, fields, widgetFor }) {
- if (!collection || !fields) {
- return null;
+/**
+ * Use a stateful component so that child components can effectively utilize
+ * `shouldComponentUpdate`.
+ */
+export default class Preview extends React.Component {
+ render() {
+ const { collection, fields, widgetFor } = this.props;
+ if (!collection || !fields) {
+ return null;
+ }
+ return (
+