* update top bar and collections sidebar UI * update collection entries UI * improve global layout * merge search page into collection page * enable new entry button * search fixup * wip -initial editor update * update editor scrolling and markdown toolbar position * wip * editor toolbar progress * editor toolbar wip * finished basic editor toolbar * add standalone toggle component * improve markdown toolbar spacing * add user avatar placeholder * finish markdown toggle styling * refactor icon setup, add new icons * add new icons to markdown editor toolbar * remove extra app container * add markdown active mark style * relation and text widget styling * widget design updates, basic list/object design update * widget style updates, image widget improvements * refactor widget directory, fix file removal * widget focus styles * finish editor widget focus styles * migrate media library modal to react-modal * wip - migrate editor component form to modal * wip - move editor component form to modal * wip - embed plugin forms in the editor * inline shortcode forms working * disable react hot loading, its breaking things * improve shortcode form styles * make shortcode form collapsible, improve styling * add close functionality to shortcode blocks * improve base media library styling * fix shortcode label * migrate unstyled workflow to new UI * wip - reorganizing everything * more work moving everything * finish more moving and eliminating stuff * restructure, remove react-toolbox * wip - removing old stuff, more restructure * finish restructure * wip - css arch * switch back to test repo * update react-datetime to ^2.11.0 * remove leftover react-toolbox button * more restructuring clean-up * fix UI component directory case * wip -css editor control style * wip - consolidate widget styles * wip - use a single control renderer * fixed object values breaking * wip - editor control active styles * pass control wrapper to widgets * ensure branch name is trimmed * wip - improve widget authoring support * import Map to Widget component * refactor toolbar buttons * wip - more widget active styles * break out editor toggle component * add local scroll sync back * update editor toggle icons * limit editor control pane content width * fix editor control spacing * migrate markdown toolbar stickiness to css * fix markdown toolbar border radius * temporarily use test backend * stop markdown toolbar from going to bottom * restore disabled markdown toolbar buttons for raw * test markdown widget without focus styles * more widget updates * remove card visuals from editor * disable dragging editor split off screen * use editorControl component for shortcode fields * make header site link configurable * add configurable collection descriptions * temporarily add example assets * add basic list view * remove outdated css mixins * add and implement search icon * activate quick add menu * visualize usable space in editor view * fix entry close, other improvements * wip - editorial workflow updates * some dropshadow and other CSS tweaks * workflow ui updates * add worfklow card buttons * fix workflow card button handlers * some dropshadow and other CSS tweaks * make workflow board wider * center workflow and collection views * add basic responsiveness * a bunch of fun UI fixes! a BUNCH! (#875) * give `.nc-entryEditor-toolbar-mainSection` left and right child divs * a bunch of fun UI fixes! a BUNCH! * remove obscure --buttonShadow * revert to test repo * fix not found page styling * allow workflow publishing from any column * disallow publishing from all columns, with feedback * fix new entry button * fix markdown state persisting across entries * enable simple workflow save and new from editor * update slug in address bar when saving new entry * wip - workflow updates, deletion working * add status change functionality to editor * wip - improving status change from editor * editor toolbar back button improvements, loading improvements, cleanup * progress on the media library UI cleanup * remove font smothing css * a quick fix for these buttons * tweaks * progress on media library modal— broken FYI * fix media library functionality, finish migrating footer * remove media library footer files * remove leftover css import * fix media library * editor publishing functionality complete (unstyled) * remove leftover loader var from media library * wip - editor publishing styles * add status dropdown styling * editor toolbar style updates * editor toolbar state improvements * progress on the media library UI cleanup, style improvements * finish editorial workflow editor styling * finish media library styling * fix config * add what-input to optimize focus styling * fix button * fix navigation blocking for simple workflow * improve simple workflow publishing * add avatar dropdown to editor top bar * style github and test-repo auth pages * add git gateway auth page styles * improve editor error styling
238 lines
7.2 KiB
JavaScript
238 lines
7.2 KiB
JavaScript
import PropTypes from 'prop-types';
|
|
import React, { Component } from 'react';
|
|
import { get, isEmpty, debounce } from 'lodash';
|
|
import { Map } from 'immutable';
|
|
import { Value, Document, Block, Text } from 'slate';
|
|
import { Editor as Slate } from 'slate-react';
|
|
import { slateToMarkdown, markdownToSlate, htmlToSlate } from 'EditorWidgets/Markdown/serializers';
|
|
import { getEditorComponents } from 'Lib/registry';
|
|
import Toolbar from 'EditorWidgets/Markdown/MarkdownControl/Toolbar/Toolbar';
|
|
import { renderNode, renderMark } from './renderers';
|
|
import { validateNode } from './validators';
|
|
import plugins, { EditListConfigured } from './plugins';
|
|
import onKeyDown from './keys';
|
|
|
|
const createEmptyRawDoc = () => {
|
|
const emptyText = Text.create('');
|
|
const emptyBlock = Block.create({ kind: 'block', type: 'paragraph', nodes: [ emptyText ] });
|
|
return { nodes: [emptyBlock] };
|
|
};
|
|
|
|
const createSlateValue = (rawValue) => {
|
|
const rawDoc = rawValue && markdownToSlate(rawValue);
|
|
const rawDocHasNodes = !isEmpty(get(rawDoc, 'nodes'))
|
|
const document = Document.fromJSON(rawDocHasNodes ? rawDoc : createEmptyRawDoc());
|
|
return Value.create({ document });
|
|
}
|
|
|
|
export default class Editor extends Component {
|
|
static propTypes = {
|
|
onAddAsset: PropTypes.func.isRequired,
|
|
getAsset: PropTypes.func.isRequired,
|
|
onChange: PropTypes.func.isRequired,
|
|
onMode: PropTypes.func.isRequired,
|
|
className: PropTypes.string.isRequired,
|
|
value: PropTypes.string,
|
|
};
|
|
|
|
constructor(props) {
|
|
super(props);
|
|
this.state = {
|
|
value: createSlateValue(props.value),
|
|
shortcodePlugins: getEditorComponents(),
|
|
};
|
|
}
|
|
|
|
shouldComponentUpdate(nextProps, nextState) {
|
|
return (this.props.value !== null && nextProps.value === null)
|
|
|| (this.props.value === null && nextProps.value !== null)
|
|
|| !this.state.value.equals(nextState.value);
|
|
}
|
|
|
|
componentWillUpdate(nextProps) {
|
|
const shouldResetState = (this.props.value !== null && nextProps.value === null)
|
|
|| (this.props.value === null && nextProps.value !== null)
|
|
if (shouldResetState) {
|
|
this.setState({ value: createSlateValue(nextProps.value) });
|
|
}
|
|
}
|
|
|
|
handlePaste = (e, data, change) => {
|
|
if (data.type !== 'html' || data.isShift) {
|
|
return;
|
|
}
|
|
const ast = htmlToSlate(data.html);
|
|
const doc = Document.fromJSON(ast);
|
|
return change.insertFragment(doc);
|
|
}
|
|
|
|
selectionHasMark = type => this.state.value.activeMarks.some(mark => mark.type === type);
|
|
selectionHasBlock = type => this.state.value.blocks.some(node => node.type === type);
|
|
|
|
handleMarkClick = (event, type) => {
|
|
event.preventDefault();
|
|
const resolvedChange = this.state.value.change().focus().toggleMark(type);
|
|
this.ref.onChange(resolvedChange);
|
|
this.setState({ value: resolvedChange.value });
|
|
};
|
|
|
|
handleBlockClick = (event, type) => {
|
|
event.preventDefault();
|
|
let { value } = this.state;
|
|
const { document: doc, selection } = value;
|
|
const { unwrapList, wrapInList } = EditListConfigured.changes;
|
|
let change = value.change();
|
|
|
|
// Handle everything except list buttons.
|
|
if (!['bulleted-list', 'numbered-list'].includes(type)) {
|
|
const isActive = this.selectionHasBlock(type);
|
|
change = change.setBlock(isActive ? 'paragraph' : type);
|
|
}
|
|
|
|
// Handle the extra wrapping required for list buttons.
|
|
else {
|
|
const isSameListType = value.blocks.some(block => {
|
|
return !!doc.getClosest(block.key, parent => parent.type === type);
|
|
});
|
|
const isInList = EditListConfigured.utils.isSelectionInList(value);
|
|
|
|
if (isInList && isSameListType) {
|
|
change = change.call(unwrapList, type);
|
|
} else if (isInList) {
|
|
const currentListType = type === 'bulleted-list' ? 'numbered-list' : 'bulleted-list';
|
|
change = change.call(unwrapList, currentListType).call(wrapInList, type);
|
|
} else {
|
|
change = change.call(wrapInList, type);
|
|
}
|
|
}
|
|
|
|
const resolvedChange = change.focus();
|
|
this.ref.onChange(resolvedChange);
|
|
this.setState({ value: resolvedChange.value });
|
|
};
|
|
|
|
hasLinks = () => {
|
|
return this.state.value.inlines.some(inline => inline.type === 'link');
|
|
};
|
|
|
|
handleLink = () => {
|
|
let change = this.state.value.change();
|
|
|
|
// If the current selection contains links, clicking the "link" button
|
|
// should simply unlink them.
|
|
if (this.hasLinks()) {
|
|
change = change.unwrapInline('link');
|
|
}
|
|
|
|
else {
|
|
const url = window.prompt('Enter the URL of the link');
|
|
|
|
// If nothing is entered in the URL prompt, do nothing.
|
|
if (!url) return;
|
|
|
|
// If no text is selected, use the entered URL as text.
|
|
if (change.value.isCollapsed) {
|
|
change = change
|
|
.insertText(url)
|
|
.extend(0 - url.length);
|
|
}
|
|
|
|
change = change
|
|
.wrapInline({ type: 'link', data: { url } })
|
|
.collapseToEnd();
|
|
}
|
|
|
|
this.ref.onChange(change);
|
|
this.setState({ value: change.value });
|
|
};
|
|
|
|
handlePluginAdd = pluginId => {
|
|
const { value } = this.state;
|
|
const nodes = [Text.create('')];
|
|
const block = {
|
|
kind: 'block',
|
|
type: 'shortcode',
|
|
data: {
|
|
shortcode: pluginId,
|
|
shortcodeNew: true,
|
|
shortcodeData: Map(),
|
|
},
|
|
isVoid: true,
|
|
nodes
|
|
};
|
|
let change = value.change();
|
|
const { focusBlock } = change.value;
|
|
|
|
if (focusBlock.text === '') {
|
|
change = change.setNodeByKey(focusBlock.key, block);
|
|
} else {
|
|
change = change.insertBlock(block);
|
|
}
|
|
|
|
change = change.focus();
|
|
|
|
this.ref.onChange(change);
|
|
this.setState({ value: change.value });
|
|
};
|
|
|
|
handleToggle = () => {
|
|
this.props.onMode('raw');
|
|
};
|
|
|
|
|
|
handleDocumentChange = debounce(change => {
|
|
const raw = change.value.document.toJSON();
|
|
const plugins = this.state.shortcodePlugins;
|
|
const markdown = slateToMarkdown(raw, plugins);
|
|
this.props.onChange(markdown);
|
|
}, 150);
|
|
|
|
handleChange = change => {
|
|
if (!this.state.value.document.equals(change.value.document)) {
|
|
this.handleDocumentChange(change);
|
|
}
|
|
this.setState({ value: change.value });
|
|
};
|
|
|
|
processRef = ref => {
|
|
this.ref = ref;
|
|
}
|
|
|
|
render() {
|
|
const { onAddAsset, getAsset, className } = this.props;
|
|
|
|
return (
|
|
<div className="nc-visualEditor-wrapper">
|
|
<div className="nc-visualEditor-editorControlBar">
|
|
<Toolbar
|
|
onMarkClick={this.handleMarkClick}
|
|
onBlockClick={this.handleBlockClick}
|
|
onLinkClick={this.handleLink}
|
|
selectionHasMark={this.selectionHasMark}
|
|
selectionHasBlock={this.selectionHasBlock}
|
|
selectionHasLink={this.hasLinks}
|
|
onToggleMode={this.handleToggle}
|
|
plugins={this.state.shortcodePlugins}
|
|
onSubmit={this.handlePluginAdd}
|
|
onAddAsset={onAddAsset}
|
|
getAsset={getAsset}
|
|
/>
|
|
</div>
|
|
<Slate
|
|
className={`${className} nc-visualEditor-editor`}
|
|
value={this.state.value}
|
|
renderNode={renderNode}
|
|
renderMark={renderMark}
|
|
validateNode={validateNode}
|
|
plugins={plugins}
|
|
onChange={this.handleChange}
|
|
onKeyDown={onKeyDown}
|
|
onPaste={this.handlePaste}
|
|
ref={this.processRef}
|
|
spellCheck
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
}
|