318 lines
9.6 KiB
JavaScript
Raw Normal View History

feat: Code Widget + Markdown Widget Internal Overhaul (#2828) * wip - upgrade to slate 0.43 * wip * wip * wip * wip * wip * wip * wip * finish list handling logic * add plugins directory * tests wip * setup testing * wip * add selection commands * finish list testing * stuff * add codemirror * abstract codemirror from slate * wip * wip * wip * wip * wip * wip * wip * wip * wip * codemirror mostly working, some bugs * upgrade to slate 46 * upgrade to slate 47 * wip * wip * progress * wip * mostly working links with surrounding marks * wip * tests passing * add test * fix formatting * update snapshots * close self closing tag in markdown html output * wip - commonmark * hold on commonmark work * all tests passing * fix e2e specs * ignore tests in esm builds * break/backspace plugins wip * finish enter/backspace spec * fix soft break handling * wip - editor component deletion * add insertion points * make insertion points invisible * fix empty mark nodes output to markdown * fix pasting * improve insertion points * add static bottom insertion point * improve click handling at insertion points * restore current table functionality * add paste support for Slate fragments * support cut/copy markdown, paste between rich/raw editor * fix copy paste * wip - paste/select bug fixing * fixed known slate issues * split plugins * fix editor toggles * force text cursor in code widget * wip - reorg plugins * finish markdown control reorg * configure plugin types * quote block adjacent handling with tests * wip * finish quote logic and tests * fix copy paste plugin migration regressions * fix force insert before node * fix trailing insertion point * remove empty headers * codemirror working properly in markdown widget * return focus to codemirror on lang select enter * fix state issues for widgets with local state * wip - vim working, just need to work out distribution * add settings pane * wip - default modes * fix deps * add programming language data * implement linguist langs in code widget * everything built in * remove old registration code, fix focus styling * fix/update linting setup * fix js lint errors * remove stylelint from format script * fix remaining linting errors * fix reducer test failures * chore: update commitlint for worktree support * chore: fix remaining tests * chore: drop unused monaco plugin * chore: remove extraneous global styles rendering * chore: fix failing tests * fix: tests * fix: quote/list nesting (tests still broken) * fix: update quote tests * chore: bring back code widget test config * fix: autofocus * fix: code blocks without the code widget * fix: code editor component state issues * fix: error * fix: add code block test, few fixes * chore: remove notes * fix: [wip] update stateful shortcodes on undo/redo * fix: support code styled links, handle unknown langs * fix: few fixes * fix: autofocus on insert, focus on all clicks * fix: linting * fix: autofocus * fix: update code block fixture * fix: remove unused cypress snapshot plugin * fix: drop node 8 test, add node 12 * fix: use lodash.flatten instead of Array.flat * fix: remove console logs
2019-12-16 12:17:37 -05:00
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { ClassNames } from '@emotion/core';
import { Map } from 'immutable';
import { uniq, isEqual, isEmpty } from 'lodash';
import uuid from 'uuid/v4';
import { UnControlled as ReactCodeMirror } from 'react-codemirror2';
import CodeMirror from 'codemirror';
import 'codemirror/keymap/vim';
import 'codemirror/keymap/sublime';
import 'codemirror/keymap/emacs';
import codeMirrorStyles from 'codemirror/lib/codemirror.css';
import materialTheme from 'codemirror/theme/material.css';
import SettingsPane from './SettingsPane';
import SettingsButton from './SettingsButton';
import languageData from '../data/languages.json';
// TODO: relocate as a utility function
function getChangedProps(previous, next, keys) {
const propNames = keys || uniq(Object.keys(previous), Object.keys(next));
const changedProps = propNames.reduce((acc, prop) => {
if (previous[prop] !== next[prop]) {
acc[prop] = next[prop];
}
return acc;
}, {});
if (!isEmpty(changedProps)) {
return changedProps;
}
}
const languages = languageData.map(lang => ({
label: lang.label,
name: lang.identifiers[0],
mode: lang.codemirror_mode,
mimeType: lang.codemirror_mime_type,
}));
const styleString = `
padding: 0;
`;
const defaultLang = { name: '', mode: '', label: 'none' };
function valueToOption(val) {
if (typeof val === 'string') {
return { value: val, label: val };
}
return { value: val.name, label: val.label || val.name };
}
const modes = languages.map(valueToOption);
const themes = ['default', 'material'];
const settingsPersistKeys = {
theme: 'cms.codemirror.theme',
keyMap: 'cms.codemirror.keymap',
};
export default class CodeControl extends React.Component {
static propTypes = {
field: ImmutablePropTypes.map.isRequired,
onChange: PropTypes.func.isRequired,
value: PropTypes.node,
forID: PropTypes.string.isRequired,
classNameWrapper: PropTypes.string.isRequired,
widget: PropTypes.object.isRequired,
};
keys = this.getKeys(this.props.field);
state = {
isActive: false,
unknownLang: null,
lang: '',
keyMap: localStorage.getItem(settingsPersistKeys['keyMap']) || 'default',
settingsVisible: false,
codeMirrorKey: uuid(),
theme: localStorage.getItem(settingsPersistKeys['theme']) || themes[themes.length - 1],
lastKnownValue: this.valueIsMap() ? this.props.value?.get(this.keys.code) : this.props.value,
};
shouldComponentUpdate(nextProps, nextState) {
return (
!isEqual(this.state, nextState) || this.props.classNameWrapper !== nextProps.classNameWrapper
);
}
componentDidMount() {
this.setState({
lang: this.getInitialLang() || '',
});
}
componentDidUpdate(prevProps, prevState) {
this.updateCodeMirrorProps(prevState);
}
updateCodeMirrorProps(prevState) {
const keys = ['lang', 'theme', 'keyMap'];
const changedProps = getChangedProps(prevState, this.state, keys);
if (changedProps) {
this.handleChangeCodeMirrorProps(changedProps);
}
}
getLanguageByName = name => {
return languages.find(lang => lang.name === name);
};
getKeyMapOptions = () => {
return Object.keys(CodeMirror.keyMap)
.sort()
.filter(keyMap => ['emacs', 'vim', 'sublime', 'default'].includes(keyMap))
.map(keyMap => ({ value: keyMap, label: keyMap }));
};
// This widget is not fully controlled, it only takes a value through props
// upon initialization.
getInitialLang = () => {
const { value, field } = this.props;
const lang =
(this.valueIsMap() && value && value.get(this.keys.lang)) || field.get('default_language');
feat: Code Widget + Markdown Widget Internal Overhaul (#2828) * wip - upgrade to slate 0.43 * wip * wip * wip * wip * wip * wip * wip * finish list handling logic * add plugins directory * tests wip * setup testing * wip * add selection commands * finish list testing * stuff * add codemirror * abstract codemirror from slate * wip * wip * wip * wip * wip * wip * wip * wip * wip * codemirror mostly working, some bugs * upgrade to slate 46 * upgrade to slate 47 * wip * wip * progress * wip * mostly working links with surrounding marks * wip * tests passing * add test * fix formatting * update snapshots * close self closing tag in markdown html output * wip - commonmark * hold on commonmark work * all tests passing * fix e2e specs * ignore tests in esm builds * break/backspace plugins wip * finish enter/backspace spec * fix soft break handling * wip - editor component deletion * add insertion points * make insertion points invisible * fix empty mark nodes output to markdown * fix pasting * improve insertion points * add static bottom insertion point * improve click handling at insertion points * restore current table functionality * add paste support for Slate fragments * support cut/copy markdown, paste between rich/raw editor * fix copy paste * wip - paste/select bug fixing * fixed known slate issues * split plugins * fix editor toggles * force text cursor in code widget * wip - reorg plugins * finish markdown control reorg * configure plugin types * quote block adjacent handling with tests * wip * finish quote logic and tests * fix copy paste plugin migration regressions * fix force insert before node * fix trailing insertion point * remove empty headers * codemirror working properly in markdown widget * return focus to codemirror on lang select enter * fix state issues for widgets with local state * wip - vim working, just need to work out distribution * add settings pane * wip - default modes * fix deps * add programming language data * implement linguist langs in code widget * everything built in * remove old registration code, fix focus styling * fix/update linting setup * fix js lint errors * remove stylelint from format script * fix remaining linting errors * fix reducer test failures * chore: update commitlint for worktree support * chore: fix remaining tests * chore: drop unused monaco plugin * chore: remove extraneous global styles rendering * chore: fix failing tests * fix: tests * fix: quote/list nesting (tests still broken) * fix: update quote tests * chore: bring back code widget test config * fix: autofocus * fix: code blocks without the code widget * fix: code editor component state issues * fix: error * fix: add code block test, few fixes * chore: remove notes * fix: [wip] update stateful shortcodes on undo/redo * fix: support code styled links, handle unknown langs * fix: few fixes * fix: autofocus on insert, focus on all clicks * fix: linting * fix: autofocus * fix: update code block fixture * fix: remove unused cypress snapshot plugin * fix: drop node 8 test, add node 12 * fix: use lodash.flatten instead of Array.flat * fix: remove console logs
2019-12-16 12:17:37 -05:00
const langInfo = this.getLanguageByName(lang);
if (lang && !langInfo) {
this.setState({ unknownLang: lang });
}
return lang;
};
// If `allow_language_selection` is not set, default to true. Otherwise, use
// its value.
allowLanguageSelection =
!this.props.field.has('allow_language_selection') ||
!!this.props.field.get('allow_language_selection');
toValue = this.valueIsMap()
? (type, value) => (this.props.value || Map()).set(this.keys[type], value)
: (type, value) => (type === 'code' ? value : this.props.value);
// If the value is a map, keys can be customized via config.
getKeys(field) {
const defaults = {
code: 'code',
lang: 'lang',
};
// Force default keys if widget is an editor component code block.
if (this.props.isEditorComponent) {
return defaults;
}
const keys = field.get('keys', Map()).toJS();
return { ...defaults, ...keys };
}
// Determine if the persisted value is a map rather than a plain string. A map
// value allows both the code string and the language to be persisted.
valueIsMap() {
const { field, isEditorComponent } = this.props;
return !field.get('output_code_only') || isEditorComponent;
}
async handleChangeCodeMirrorProps(changedProps) {
const { onChange } = this.props;
if (changedProps.lang) {
const { mode } = this.getLanguageByName(changedProps.lang) || {};
if (mode) {
await import(`codemirror/mode/${mode}/${mode}.js`);
}
}
// Changing CodeMirror props requires re-initializing the
// detached/uncontrolled React CodeMirror component, so here we save and
// restore the selections and cursor position after the state change.
if (this.cm) {
const cursor = this.cm.doc.getCursor();
const selections = this.cm.doc.listSelections();
this.setState({ codeMirrorKey: uuid() }, () => {
this.cm.doc.setCursor(cursor);
this.cm.doc.setSelections(selections);
});
}
for (const key of ['theme', 'keyMap']) {
if (changedProps[key]) {
localStorage.setItem(settingsPersistKeys[key], changedProps[key]);
}
}
// Only persist the language change if supported - requires the value to be
// a map rather than just a code string.
if (changedProps.lang && this.valueIsMap()) {
onChange(this.toValue('lang', changedProps.lang));
}
}
handleChange(newValue) {
const cursor = this.cm.doc.getCursor();
const selections = this.cm.doc.listSelections();
this.setState({ lastKnownValue: newValue });
this.props.onChange(this.toValue('code', newValue), { cursor, selections });
}
showSettings = () => {
this.setState({ settingsVisible: true });
};
hideSettings = () => {
if (this.state.settingsVisible) {
this.setState({ settingsVisible: false });
}
this.cm.focus();
};
handleFocus = () => {
this.hideSettings();
this.props.setActiveStyle();
this.setActive();
};
handleBlur = () => {
this.setInactive();
this.props.setInactiveStyle();
};
setActive = () => this.setState({ isActive: true });
setInactive = () => this.setState({ isActive: false });
render() {
const { classNameWrapper, forID, widget, isNewEditorComponent } = this.props;
const { lang, settingsVisible, keyMap, codeMirrorKey, theme, lastKnownValue } = this.state;
const langInfo = this.getLanguageByName(lang);
const mode = langInfo?.mimeType || langInfo?.mode;
return (
<ClassNames>
{({ css, cx }) => (
<div
className={cx(
classNameWrapper,
css`
${codeMirrorStyles};
${materialTheme};
${styleString};
`,
)}
>
{!settingsVisible && <SettingsButton onClick={this.showSettings} />}
{settingsVisible && (
<SettingsPane
hideSettings={this.hideSettings}
forID={forID}
modes={modes}
mode={valueToOption(langInfo || defaultLang)}
theme={themes.find(t => t === theme)}
themes={themes}
keyMap={{ value: keyMap, label: keyMap }}
keyMaps={this.getKeyMapOptions()}
allowLanguageSelection={this.allowLanguageSelection}
onChangeLang={newLang => this.setState({ lang: newLang })}
onChangeTheme={newTheme => this.setState({ theme: newTheme })}
onChangeKeyMap={newKeyMap => this.setState({ keyMap: newKeyMap })}
/>
)}
<ReactCodeMirror
key={codeMirrorKey}
id={forID}
className={css`
height: 100%;
2019-12-19 10:47:43 -05:00
border-radius: 0 3px 3px 3px;
overflow: hidden;
feat: Code Widget + Markdown Widget Internal Overhaul (#2828) * wip - upgrade to slate 0.43 * wip * wip * wip * wip * wip * wip * wip * finish list handling logic * add plugins directory * tests wip * setup testing * wip * add selection commands * finish list testing * stuff * add codemirror * abstract codemirror from slate * wip * wip * wip * wip * wip * wip * wip * wip * wip * codemirror mostly working, some bugs * upgrade to slate 46 * upgrade to slate 47 * wip * wip * progress * wip * mostly working links with surrounding marks * wip * tests passing * add test * fix formatting * update snapshots * close self closing tag in markdown html output * wip - commonmark * hold on commonmark work * all tests passing * fix e2e specs * ignore tests in esm builds * break/backspace plugins wip * finish enter/backspace spec * fix soft break handling * wip - editor component deletion * add insertion points * make insertion points invisible * fix empty mark nodes output to markdown * fix pasting * improve insertion points * add static bottom insertion point * improve click handling at insertion points * restore current table functionality * add paste support for Slate fragments * support cut/copy markdown, paste between rich/raw editor * fix copy paste * wip - paste/select bug fixing * fixed known slate issues * split plugins * fix editor toggles * force text cursor in code widget * wip - reorg plugins * finish markdown control reorg * configure plugin types * quote block adjacent handling with tests * wip * finish quote logic and tests * fix copy paste plugin migration regressions * fix force insert before node * fix trailing insertion point * remove empty headers * codemirror working properly in markdown widget * return focus to codemirror on lang select enter * fix state issues for widgets with local state * wip - vim working, just need to work out distribution * add settings pane * wip - default modes * fix deps * add programming language data * implement linguist langs in code widget * everything built in * remove old registration code, fix focus styling * fix/update linting setup * fix js lint errors * remove stylelint from format script * fix remaining linting errors * fix reducer test failures * chore: update commitlint for worktree support * chore: fix remaining tests * chore: drop unused monaco plugin * chore: remove extraneous global styles rendering * chore: fix failing tests * fix: tests * fix: quote/list nesting (tests still broken) * fix: update quote tests * chore: bring back code widget test config * fix: autofocus * fix: code blocks without the code widget * fix: code editor component state issues * fix: error * fix: add code block test, few fixes * chore: remove notes * fix: [wip] update stateful shortcodes on undo/redo * fix: support code styled links, handle unknown langs * fix: few fixes * fix: autofocus on insert, focus on all clicks * fix: linting * fix: autofocus * fix: update code block fixture * fix: remove unused cypress snapshot plugin * fix: drop node 8 test, add node 12 * fix: use lodash.flatten instead of Array.flat * fix: remove console logs
2019-12-16 12:17:37 -05:00
.CodeMirror {
height: auto;
cursor: text;
min-height: 300px;
}
.CodeMirror-scroll {
min-height: 300px;
}
`}
options={{
lineNumbers: true,
...widget.codeMirrorConfig,
extraKeys: {
'Shift-Tab': 'indentLess',
Tab: 'indentMore',
...(widget.codeMirrorConfig.extraKeys || {}),
},
theme,
mode,
keyMap,
viewportMargin: Infinity,
}}
detach={true}
editorDidMount={cm => {
this.cm = cm;
if (isNewEditorComponent) {
this.handleFocus();
}
}}
value={lastKnownValue}
onChange={(editor, data, newValue) => this.handleChange(newValue)}
onFocus={this.handleFocus}
onBlur={this.handleBlur}
/>
</div>
)}
</ClassNames>
);
}
}