Erez Rokah 2b41d8a838 feat: bundle assets with content (#2958)
* fix(media_folder_relative): use collection name in unpublished entry

* refactor: pass arguments as object to AssetProxy ctor

* feat: support media folders per collection

* feat: resolve media files path based on entry path

* fix: asset public path resolving

* refactor: introduce typescript for AssetProxy

* refactor: code cleanup

* refactor(asset-proxy): add tests,switch to typescript,extract arguments

* refactor: typescript for editorialWorkflow

* refactor: add typescript for media library actions

* refactor: fix type error on map set

* refactor: move locale selector into reducer

* refactor: add typescript for entries actions

* refactor: remove duplication between asset store and media lib

* feat: load assets from backend using API

* refactor(github): add typescript, cache media files

* fix: don't load media URL if already loaded

* feat: add media folder config to collection

* fix: load assets from API when not in UI state

* feat: load entry media files when opening media library

* fix: editorial workflow draft media files bug fixes

* test(unit): fix unit tests

* fix: editor control losing focus

* style: add eslint object-shorthand rule

* test(cypress): re-record mock data

* fix: fix non github backends, large media

* test: uncomment only in tests

* fix(backend-test): add missing displayURL property

* test(e2e): add media library tests

* test(e2e): enable visual testing

* test(e2e): add github backend media library tests

* test(e2e): add git-gateway large media tests

* chore: post rebase fixes

* test: fix tests

* test: fix tests

* test(cypress): fix tests

* docs: add media_folder docs

* test(e2e): add media library delete test

* test(e2e): try and fix image comparison on CI

* ci: reduce test machines from 9 to 8

* test: add reducers and selectors unit tests

* test(e2e): disable visual regression testing for now

* test: add getAsset unit tests

* refactor: use Asset class component instead of hooks

* build: don't inline source maps

* test: add more media path tests
2019-12-18 11:16:02 -05:00

143 lines
3.8 KiB
JavaScript

import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import styled from '@emotion/styled';
import { ClassNames } from '@emotion/core';
import { debounce } from 'lodash';
import { Editor as Slate, setEventTransfer } from 'slate-react';
import Plain from 'slate-plain-serializer';
import isHotkey from 'is-hotkey';
import { lengths, fonts } from 'netlify-cms-ui-default';
import { markdownToHtml } from '../serializers';
import { editorStyleVars, EditorControlBar } from '../styles';
import Toolbar from './Toolbar';
const styleStrings = {
slateRaw: `
position: relative;
overflow: hidden;
overflow-x: auto;
min-height: ${lengths.richTextEditorMinHeight};
font-family: ${fonts.mono};
border-top-left-radius: 0;
border-top-right-radius: 0;
border-top: 0;
margin-top: -${editorStyleVars.stickyDistanceBottom};
`,
};
const RawEditorContainer = styled.div`
position: relative;
`;
export default class RawEditor extends React.Component {
constructor(props) {
super(props);
this.state = {
value: Plain.deserialize(this.props.value || ''),
};
}
shouldComponentUpdate(nextProps, nextState) {
return !this.state.value.equals(nextState.value);
}
componentDidMount() {
if (this.props.pendingFocus) {
this.editor.focus();
this.props.pendingFocus();
}
}
handleCopy = async (event, editor) => {
event.persist();
const { getAsset, resolveWidget } = this.props;
const markdown = Plain.serialize(editor.value);
const html = await markdownToHtml(markdown, { getAsset, resolveWidget });
setEventTransfer(event, 'text', markdown);
setEventTransfer(event, 'html', html);
event.preventDefault();
};
handleCut = async (event, editor, next) => {
await this.handleCopy(event, editor, next);
editor.delete();
};
handlePaste = (event, editor, next) => {
const data = event.clipboardData;
if (isHotkey('shift', event)) {
return next();
}
const value = Plain.deserialize(data.getData('text/plain'));
return editor.insertFragment(value.document);
};
handleChange = editor => {
if (!this.state.value.document.equals(editor.value.document)) {
this.handleDocumentChange(editor);
}
this.setState({ value: editor.value });
};
/**
* When the document value changes, serialize from Slate's AST back to plain
* text (which is Markdown) and pass that up as the new value.
*/
handleDocumentChange = debounce(editor => {
const value = Plain.serialize(editor.value);
this.props.onChange(value);
}, 150);
handleToggleMode = () => {
this.props.onMode('visual');
};
processRef = ref => {
this.editor = ref;
};
render() {
const { className, field } = this.props;
return (
<RawEditorContainer>
<EditorControlBar>
<Toolbar
onToggleMode={this.handleToggleMode}
buttons={field.get('buttons')}
disabled
rawMode
/>
</EditorControlBar>
<ClassNames>
{({ css, cx }) => (
<Slate
className={cx(
className,
css`
${styleStrings.slateRaw}
`,
)}
value={this.state.value}
onChange={this.handleChange}
onPaste={this.handlePaste}
onCut={this.handleCut}
onCopy={this.handleCopy}
ref={this.processRef}
/>
)}
</ClassNames>
</RawEditorContainer>
);
}
}
RawEditor.propTypes = {
onChange: PropTypes.func.isRequired,
onMode: PropTypes.func.isRequired,
className: PropTypes.string.isRequired,
value: PropTypes.string,
field: ImmutablePropTypes.map.isRequired,
};