p5.js-web-editor/client/modules/IDE/components/Editor.jsx

480 lines
16 KiB
React
Raw Normal View History

import PropTypes from 'prop-types';
import React from 'react';
2016-06-24 00:29:55 +02:00
import CodeMirror from 'codemirror';
2016-09-07 21:05:25 +02:00
import beautifyJS from 'js-beautify';
import { withTranslation } from 'react-i18next';
import 'codemirror/mode/css/css';
2016-06-24 00:29:55 +02:00
import 'codemirror/addon/selection/active-line';
2016-07-12 23:38:24 +02:00
import 'codemirror/addon/lint/lint';
import 'codemirror/addon/lint/javascript-lint';
import 'codemirror/addon/lint/css-lint';
import 'codemirror/addon/lint/html-lint';
2017-07-17 22:07:59 +02:00
import 'codemirror/addon/fold/brace-fold';
import 'codemirror/addon/fold/comment-fold';
import 'codemirror/addon/fold/foldcode';
import 'codemirror/addon/fold/foldgutter';
import 'codemirror/addon/fold/indent-fold';
import 'codemirror/addon/comment/comment';
import 'codemirror/keymap/sublime';
import 'codemirror/addon/search/searchcursor';
import 'codemirror/addon/search/matchesonscrollbar';
import 'codemirror/addon/search/match-highlighter';
2016-08-05 22:58:59 +02:00
import 'codemirror/addon/search/jump-to-line';
import 'codemirror/addon/edit/matchbrackets';
2016-07-12 23:38:24 +02:00
import { JSHINT } from 'jshint';
import { CSSLint } from 'csslint';
import { HTMLHint } from 'htmlhint';
2016-09-07 22:33:01 +02:00
import classNames from 'classnames';
2016-10-19 19:29:02 +02:00
import { debounce } from 'lodash';
2020-08-12 16:15:36 +02:00
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import '../../../utils/htmlmixed';
import '../../../utils/p5-javascript';
2018-05-31 00:23:32 +02:00
import '../../../utils/webGL-clike';
import Timer from '../components/Timer';
import EditorAccessibility from '../components/EditorAccessibility';
import { metaKey, } from '../../../utils/metaKey';
import search from '../../../utils/codemirror-search';
2020-04-30 00:34:37 +02:00
import beepUrl from '../../../sounds/audioAlert.mp3';
import UnsavedChangesDotIcon from '../../../images/unsaved-changes-dot.svg';
import RightArrowIcon from '../../../images/right-arrow.svg';
import LeftArrowIcon from '../../../images/left-arrow.svg';
2020-08-12 16:15:36 +02:00
import { getHTMLFile } from '../reducers/files';
import * as FileActions from '../actions/files';
import * as IDEActions from '../actions/ide';
import * as ProjectActions from '../actions/project';
import * as EditorAccessibilityActions from '../actions/editorAccessibility';
import * as PreferencesActions from '../actions/preferences';
import * as UserActions from '../../User/actions';
import * as ToastActions from '../actions/toast';
import * as ConsoleActions from '../actions/console';
2020-04-30 00:34:37 +02:00
search(CodeMirror);
const beautifyCSS = beautifyJS.css;
const beautifyHTML = beautifyJS.html;
window.JSHINT = JSHINT;
window.CSSLint = CSSLint;
window.HTMLHint = HTMLHint;
2016-06-24 00:29:55 +02:00
2019-03-24 01:53:07 +01:00
const IS_TAB_INDENT = false;
const INDENTATION_AMOUNT = 2;
2016-06-24 00:29:55 +02:00
class Editor extends React.Component {
2016-09-07 22:41:56 +02:00
constructor(props) {
super(props);
this.tidyCode = this.tidyCode.bind(this);
2017-07-26 21:17:05 +02:00
this.updateLintingMessageAccessibility = debounce((annotations) => {
this.props.clearLintMessage();
annotations.forEach((x) => {
if (x.from.line > -1) {
this.props.updateLintMessage(x.severity, (x.from.line + 1), x.message);
}
});
if (this.props.lintMessages.length > 0 && this.props.lintWarning) {
this.beep.play();
}
}, 2000);
2017-09-01 18:40:15 +02:00
this.showFind = this.showFind.bind(this);
this.findNext = this.findNext.bind(this);
this.findPrev = this.findPrev.bind(this);
this.getContent = this.getContent.bind(this);
2016-09-07 22:41:56 +02:00
}
2017-10-12 21:38:02 +02:00
2016-06-24 00:29:55 +02:00
componentDidMount() {
this.beep = new Audio(beepUrl);
this.widgets = [];
this._cm = CodeMirror(this.codemirrorContainer, { // eslint-disable-line
2016-09-20 17:51:09 +02:00
theme: `p5-${this.props.theme}`,
lineNumbers: this.props.lineNumbers,
2016-06-24 00:29:55 +02:00
styleActiveLine: true,
2016-07-13 17:59:47 +02:00
inputStyle: 'contenteditable',
lineWrapping: this.props.linewrap,
2016-10-05 18:26:49 +02:00
fixedGutter: false,
2017-07-17 22:07:59 +02:00
foldGutter: true,
foldOptions: { widget: '\u2026' },
2017-10-12 22:19:18 +02:00
gutters: ['CodeMirror-foldgutter', 'CodeMirror-lint-markers'],
2016-08-05 22:58:59 +02:00
keyMap: 'sublime',
highlightSelectionMatches: true, // highlight current search match
matchBrackets: true,
2016-08-05 22:58:59 +02:00
lint: {
2017-07-26 21:17:05 +02:00
onUpdateLinting: ((annotations) => {
this.props.hideRuntimeErrorWarning();
this.updateLintingMessageAccessibility(annotations);
}),
options: {
'asi': true,
'eqeqeq': false,
2017-09-14 20:32:43 +02:00
'-W041': false,
2019-02-25 22:52:07 +01:00
'esversion': 7
}
2016-08-05 22:58:59 +02:00
}
2016-06-24 00:29:55 +02:00
});
delete this._cm.options.lint.options.errors;
this._cm.setOption('extraKeys', {
Tab: (cm) => {
// might need to specify and indent more?
const selection = cm.doc.getSelection();
if (selection.length > 0) {
cm.execCommand('indentMore');
} else {
cm.replaceSelection(' '.repeat(INDENTATION_AMOUNT));
}
},
[`${metaKey}-Enter`]: () => null,
[`Shift-${metaKey}-Enter`]: () => null,
[`${metaKey}-F`]: 'findPersistent',
[`${metaKey}-G`]: 'findNext',
[`Shift-${metaKey}-G`]: 'findPrev',
});
2016-12-07 03:33:12 +01:00
this.initializeDocuments(this.props.files);
this._cm.swapDoc(this._docs[this.props.file.id]);
2016-10-19 19:29:02 +02:00
this._cm.on('change', debounce(() => {
this.props.setUnsavedChanges(true);
this.props.updateFileContent(this.props.file.id, this._cm.getValue());
if (this.props.autorefresh && this.props.isPlaying) {
this.props.clearConsole();
this.props.startRefreshSketch();
}
}, 1000));
2016-08-05 22:58:59 +02:00
this._cm.on('keyup', () => {
const temp = this.props.t('Editor.KeyUpLineNumber', { lineNumber: parseInt((this._cm.getCursor().line) + 1, 10) });
2016-08-25 18:32:06 +02:00
document.getElementById('current-line').innerHTML = temp;
2016-08-05 22:58:59 +02:00
});
this._cm.on('keydown', (_cm, e) => {
2016-10-20 00:35:59 +02:00
// 9 === Tab
if (e.keyCode === 9 && e.shiftKey) {
2016-09-07 22:41:56 +02:00
this.tidyCode();
}
});
this._cm.getWrapperElement().style['font-size'] = `${this.props.fontSize}px`;
2017-09-01 18:40:15 +02:00
this.props.provideController({
tidyCode: this.tidyCode,
showFind: this.showFind,
findNext: this.findNext,
findPrev: this.findPrev,
getContent: this.getContent
2017-09-01 18:40:15 +02:00
});
2016-06-24 00:29:55 +02:00
}
2016-12-07 03:33:12 +01:00
componentWillUpdate(nextProps) {
// check if files have changed
if (this.props.files[0].id !== nextProps.files[0].id) {
// then need to make CodeMirror documents
this.initializeDocuments(nextProps.files);
}
if (this.props.files.length !== nextProps.files.length) {
this.initializeDocuments(nextProps.files);
}
2016-12-07 03:33:12 +01:00
}
2016-06-24 00:29:55 +02:00
componentDidUpdate(prevProps) {
if (this.props.file.content !== prevProps.file.content &&
this.props.file.content !== this._cm.getValue()) {
2016-12-07 03:33:12 +01:00
const oldDoc = this._cm.swapDoc(this._docs[this.props.file.id]);
this._docs[prevProps.file.id] = oldDoc;
this._cm.focus();
2016-11-08 19:30:41 +01:00
if (!prevProps.unsavedChanges) {
setTimeout(() => this.props.setUnsavedChanges(false), 400);
}
2016-06-24 00:29:55 +02:00
}
if (this.props.fontSize !== prevProps.fontSize) {
this._cm.getWrapperElement().style['font-size'] = `${this.props.fontSize}px`;
}
if (this.props.linewrap !== prevProps.linewrap) {
this._cm.setOption('lineWrapping', this.props.linewrap);
}
2016-09-20 17:51:09 +02:00
if (this.props.theme !== prevProps.theme) {
this._cm.setOption('theme', `p5-${this.props.theme}`);
}
if (this.props.lineNumbers !== prevProps.lineNumbers) {
this._cm.setOption('lineNumbers', this.props.lineNumbers);
}
if (prevProps.consoleEvents !== this.props.consoleEvents) {
this.props.showRuntimeErrorWarning();
}
2018-02-16 17:56:44 +01:00
for (let i = 0; i < this._cm.lineCount(); i += 1) {
2017-07-17 23:27:21 +02:00
this._cm.removeLineClass(i, 'background', 'line-runtime-error');
}
if (this.props.runtimeErrorWarningVisible) {
this.props.consoleEvents.forEach((consoleEvent) => {
if (consoleEvent.method === 'error') {
if (consoleEvent.data &&
consoleEvent.data[0] &&
consoleEvent.data[0].indexOf &&
consoleEvent.data[0].indexOf(')') > -1) {
2018-11-21 00:00:54 +01:00
const n = consoleEvent.data[0].replace(')', '').split(' ');
2018-02-08 23:40:21 +01:00
const lineNumber = parseInt(n[n.length - 1], 10) - 1;
const { source } = consoleEvent;
const fileName = this.props.file.name;
const errorFromJavaScriptFile = (`${source}.js` === fileName);
const errorFromIndexHTML = ((source === fileName) && (fileName === 'index.html'));
if (!Number.isNaN(lineNumber) && (errorFromJavaScriptFile || errorFromIndexHTML)) {
this._cm.addLineClass(lineNumber, 'background', 'line-runtime-error');
}
2018-02-08 23:40:21 +01:00
}
}
});
}
2016-06-24 00:29:55 +02:00
}
componentWillUnmount() {
this._cm = null;
2017-09-01 18:40:15 +02:00
this.props.provideController(null);
2016-06-24 00:29:55 +02:00
}
2016-12-07 03:33:12 +01:00
getFileMode(fileName) {
let mode;
if (fileName.match(/.+\.js$/i)) {
mode = 'javascript';
2016-12-07 03:33:12 +01:00
} else if (fileName.match(/.+\.css$/i)) {
mode = 'css';
} else if (fileName.match(/.+\.html$/i)) {
mode = 'htmlmixed';
} else if (fileName.match(/.+\.json$/i)) {
mode = 'application/json';
2018-05-31 00:23:32 +02:00
} else if (fileName.match(/.+\.(frag|vert)$/i)) {
mode = 'clike';
2016-12-07 03:33:12 +01:00
} else {
mode = 'text/plain';
}
return mode;
}
getContent() {
const content = this._cm.getValue();
const updatedFile = Object.assign({}, this.props.file, { content });
return updatedFile;
}
findPrev() {
this._cm.focus();
this._cm.execCommand('findPrev');
}
findNext() {
this._cm.focus();
this._cm.execCommand('findNext');
}
showFind() {
this._cm.execCommand('findPersistent');
2016-12-07 03:33:12 +01:00
}
2016-09-07 22:41:56 +02:00
tidyCode() {
const beautifyOptions = {
2019-03-24 01:53:07 +01:00
indent_size: INDENTATION_AMOUNT,
indent_with_tabs: IS_TAB_INDENT
2016-09-07 22:41:56 +02:00
};
const mode = this._cm.getOption('mode');
2020-01-15 13:20:00 +01:00
const currentPosition = this._cm.doc.getCursor();
2016-09-07 22:41:56 +02:00
if (mode === 'javascript') {
this._cm.doc.setValue(beautifyJS(this._cm.doc.getValue(), beautifyOptions));
} else if (mode === 'css') {
this._cm.doc.setValue(beautifyCSS(this._cm.doc.getValue(), beautifyOptions));
} else if (mode === 'htmlmixed') {
this._cm.doc.setValue(beautifyHTML(this._cm.doc.getValue(), beautifyOptions));
}
2020-01-25 22:25:07 +01:00
setTimeout(() => {
2020-01-25 14:00:17 +01:00
this._cm.focus();
this._cm.doc.setCursor({ line: currentPosition.line, ch: currentPosition.ch + INDENTATION_AMOUNT });
2020-01-25 22:25:07 +01:00
}, 0);
2016-09-07 22:41:56 +02:00
}
initializeDocuments(files) {
this._docs = {};
files.forEach((file) => {
if (file.name !== 'root') {
this._docs[file.id] = CodeMirror.Doc(file.content, this.getFileMode(file.name)); // eslint-disable-line
}
});
2017-09-01 18:40:15 +02:00
}
toggleEditorOptions() {
if (this.props.editorOptionsVisible) {
this.props.closeEditorOptions();
} else {
this.optionsButton.focus();
this.props.showEditorOptions();
}
}
2016-06-24 00:29:55 +02:00
render() {
2016-09-07 22:33:01 +02:00
const editorSectionClass = classNames({
'editor': true,
'sidebar--contracted': !this.props.isExpanded,
2016-09-07 22:33:01 +02:00
'editor--options': this.props.editorOptionsVisible
});
console.log(this.props.file);
2019-06-12 23:11:35 +02:00
const editorHolderClass = classNames({
'editor-holder': true,
'editor-holder--hidden': this.props.file.fileType === 'folder' || this.props.file.url
});
let preview = '';
if (this.props.file.fileType === 'file' && this.props.file.url) {
// TODO check if it's an image
preview = (<div><img src={this.props.file.url} alt="preview" /></div>);
}
2016-08-05 22:58:59 +02:00
return (
<section className={editorSectionClass} >
2017-01-05 20:40:04 +01:00
<header className="editor__header">
<button
aria-label={this.props.t('Editor.OpenSketchARIA')}
2017-01-05 20:40:04 +01:00
className="sidebar__contract"
onClick={this.props.collapseSidebar}
>
<LeftArrowIcon focusable="false" aria-hidden="true" />
2017-01-05 20:40:04 +01:00
</button>
<button
aria-label={this.props.t('Editor.CloseSketchARIA')}
2017-01-05 20:40:04 +01:00
className="sidebar__expand"
onClick={this.props.expandSidebar}
>
<RightArrowIcon focusable="false" aria-hidden="true" />
2017-01-05 20:40:04 +01:00
</button>
<div className="editor__file-name">
<span>
{this.props.file.name}
2020-04-30 00:34:37 +02:00
<span className="editor__unsaved-changes">
{this.props.unsavedChanges ?
<UnsavedChangesDotIcon role="img" aria-label={this.props.t('Editor.UnsavedChangesARIA')} focusable="false" /> :
null}
2020-04-30 00:34:37 +02:00
</span>
</span>
2017-01-05 20:40:04 +01:00
<Timer
projectSavedTime={this.props.projectSavedTime}
isUserOwner={this.props.isUserOwner}
2017-01-05 20:40:04 +01:00
/>
</div>
</header>
<article ref={(element) => { this.codemirrorContainer = element; }} className={editorHolderClass} >
</article>
{preview}
<EditorAccessibility
lintMessages={this.props.lintMessages}
/>
2016-08-12 20:19:23 +02:00
</section>
2016-08-05 22:58:59 +02:00
);
2016-06-24 00:29:55 +02:00
}
}
2016-06-27 22:03:22 +02:00
Editor.propTypes = {
lineNumbers: PropTypes.bool.isRequired,
2016-08-11 20:09:59 +02:00
lintWarning: PropTypes.bool.isRequired,
linewrap: PropTypes.bool.isRequired,
lintMessages: PropTypes.arrayOf(PropTypes.shape({
severity: PropTypes.string.isRequired,
line: PropTypes.number.isRequired,
message: PropTypes.string.isRequired,
id: PropTypes.number.isRequired
})).isRequired,
2017-07-17 23:27:21 +02:00
consoleEvents: PropTypes.arrayOf(PropTypes.shape({
method: PropTypes.string.isRequired,
args: PropTypes.arrayOf(PropTypes.string)
})),
2016-08-11 19:24:02 +02:00
updateLintMessage: PropTypes.func.isRequired,
clearLintMessage: PropTypes.func.isRequired,
2016-07-12 05:40:30 +02:00
updateFileContent: PropTypes.func.isRequired,
fontSize: PropTypes.number.isRequired,
file: PropTypes.shape({
name: PropTypes.string.isRequired,
2016-12-07 03:33:12 +01:00
content: PropTypes.string.isRequired,
2019-06-12 23:11:35 +02:00
id: PropTypes.string.isRequired,
fileType: PropTypes.string.isRequired,
url: PropTypes.string
}).isRequired,
2016-09-07 22:33:01 +02:00
editorOptionsVisible: PropTypes.bool.isRequired,
showEditorOptions: PropTypes.func.isRequired,
2016-09-07 23:47:22 +02:00
closeEditorOptions: PropTypes.func.isRequired,
2016-09-20 17:51:09 +02:00
setUnsavedChanges: PropTypes.func.isRequired,
startRefreshSketch: PropTypes.func.isRequired,
autorefresh: PropTypes.bool.isRequired,
isPlaying: PropTypes.bool.isRequired,
theme: PropTypes.string.isRequired,
unsavedChanges: PropTypes.bool.isRequired,
2016-12-07 03:33:12 +01:00
projectSavedTime: PropTypes.string.isRequired,
files: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
content: PropTypes.string.isRequired
})).isRequired,
isExpanded: PropTypes.bool.isRequired,
collapseSidebar: PropTypes.func.isRequired,
expandSidebar: PropTypes.func.isRequired,
isUserOwner: PropTypes.bool,
clearConsole: PropTypes.func.isRequired,
showRuntimeErrorWarning: PropTypes.func.isRequired,
hideRuntimeErrorWarning: PropTypes.func.isRequired,
runtimeErrorWarningVisible: PropTypes.bool.isRequired,
provideController: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
2016-06-27 22:03:22 +02:00
};
Editor.defaultProps = {
2017-07-17 23:27:21 +02:00
isUserOwner: false,
consoleEvents: [],
};
2020-08-12 16:15:36 +02:00
function mapStateToProps(state) {
return {
files: state.files,
file:
state.files.find(file => file.isSelectedFile) ||
state.files.find(file => file.name === 'sketch.js') ||
state.files.find(file => file.name !== 'root'),
htmlFile: getHTMLFile(state.files),
ide: state.ide,
preferences: state.preferences,
editorAccessibility: state.editorAccessibility,
user: state.user,
project: state.project,
toast: state.toast,
console: state.console,
...state.preferences,
...state.ide,
...state.project,
...state.editorAccessibility,
isExpanded: state.ide.sidebarIsExpanded,
2020-08-12 16:15:36 +02:00
projectSavedTime: state.project.updatedAt
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators(
Object.assign(
{},
EditorAccessibilityActions,
FileActions,
ProjectActions,
IDEActions,
PreferencesActions,
UserActions,
ToastActions,
ConsoleActions
),
dispatch
);
}
export default withTranslation()(connect(mapStateToProps, mapDispatchToProps)(Editor));