Shawn Erquhart cfbf31b130
WIP - Global UI (#785)
* 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
2017-12-07 12:37:10 -05:00

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>
);
}
}