diff --git a/.babelrc b/.babelrc index aae12b97..3f488527 100644 --- a/.babelrc +++ b/.babelrc @@ -45,6 +45,11 @@ "@babel/preset-env", "@babel/preset-react" ] + }, + "development": { + "plugins": [ + "react-hot-loader/babel" + ] } }, "plugins": [ diff --git a/README.md b/README.md index 1a2ba3ca..3eb1e492 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # [p5.js Web Editor](https://editor.p5js.org) -Hello! The p5.js Web Editor is an in-browser editor for creative coding, specifically for writing [p5.js](https://p5js.org/) sketches. p5.js, a separate [open source project](https://github.com/processing/p5.js), is a JavaScript library with the goal of making coding accessible for artists, designers, educators, and beginners. The web editor shares the same spirit as p5.js–it is designed with the beginner in mind. When using the web editor, you don't need to download or configure anything, you can simply open the website, and start writing code. You can also host your work online and share it with others. +The p5.js Web Editor is a platform for creative coding, with a focus on making coding accessible for as many people as possible, including artists, designers, educators, beginners, and anyone who wants to learn. Simply by opening the website you can get started writing p5.js sketches without downloading or configuring anything. The editor is designed with simplicity in mind by limiting features and frills. We strive to listen to the community to drive the editor’s development, and to be intentional with every change. The editor is free and open-source. + +We also strive to give the community as much ownership and control as possible. You can download your sketches so that you can edit them locally or host them elsewhere. You can also host your own version of the editor, giving you control over its data. The p5.js Web Editor is currently in active development, and looking for contributions of any type! Please check out the [contribution guide](https://github.com/processing/p5.js-web-editor/blob/master/.github/CONTRIBUTING.md) for more details. diff --git a/client/components/Nav.jsx b/client/components/Nav.jsx index d47f8eb4..ab06441a 100644 --- a/client/components/Nav.jsx +++ b/client/components/Nav.jsx @@ -6,6 +6,7 @@ import { Link } from 'react-router'; import InlineSVG from 'react-inlinesvg'; import classNames from 'classnames'; import * as IDEActions from '../modules/IDE/actions/ide'; +import * as toastActions from '../modules/IDE/actions/toast'; import * as projectActions from '../modules/IDE/actions/project'; import { setAllAccessibleOutput } from '../modules/IDE/actions/preferences'; import { logoutUser } from '../modules/User/actions'; @@ -93,8 +94,12 @@ class Nav extends React.PureComponent { handleNew() { if (!this.props.unsavedChanges) { + this.props.showToast(1500); + this.props.setToastText('Opened new sketch.'); this.props.newProject(); } else if (this.props.warnIfUnsavedChanges()) { + this.props.showToast(1500); + this.props.setToastText('Opened new sketch.'); this.props.newProject(); } this.setDropdown('none'); @@ -682,6 +687,8 @@ class Nav extends React.PureComponent { Nav.propTypes = { newProject: PropTypes.func.isRequired, + showToast: PropTypes.func.isRequired, + setToastText: PropTypes.func.isRequired, saveProject: PropTypes.func.isRequired, autosaveProject: PropTypes.func.isRequired, exportProjectAsZip: PropTypes.func.isRequired, @@ -738,6 +745,7 @@ function mapStateToProps(state) { const mapDispatchToProps = { ...IDEActions, ...projectActions, + ...toastActions, logoutUser, setAllAccessibleOutput }; diff --git a/client/constants.js b/client/constants.js index 4dd57213..75c81b2f 100644 --- a/client/constants.js +++ b/client/constants.js @@ -12,6 +12,7 @@ export const STOP_ACCESSIBLE_OUTPUT = 'STOP_ACCESSIBLE_OUTPUT'; export const OPEN_PREFERENCES = 'OPEN_PREFERENCES'; export const CLOSE_PREFERENCES = 'CLOSE_PREFERENCES'; export const SET_FONT_SIZE = 'SET_FONT_SIZE'; +export const SET_LINE_NUMBERS = 'SET_LINE_NUMBERS'; export const AUTH_USER = 'AUTH_USER'; export const UNAUTH_USER = 'UNAUTH_USER'; @@ -124,6 +125,9 @@ export const SET_ASSETS = 'SET_ASSETS'; export const TOGGLE_DIRECTION = 'TOGGLE_DIRECTION'; export const SET_SORTING = 'SET_SORTING'; +export const SET_SORT_PARAMS = 'SET_SORT_PARAMS'; +export const SET_SEARCH_TERM = 'SET_SEARCH_TERM'; +export const CLOSE_SKETCHLIST_MODAL = 'CLOSE_SKETCHLIST_MODAL'; export const START_LOADING = 'START_LOADING'; export const STOP_LOADING = 'STOP_LOADING'; diff --git a/client/images/magnifyingglass.svg b/client/images/magnifyingglass.svg new file mode 100644 index 00000000..38f54010 --- /dev/null +++ b/client/images/magnifyingglass.svg @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/client/index.jsx b/client/index.jsx index 980b4ec1..09f6eba0 100644 --- a/client/index.jsx +++ b/client/index.jsx @@ -1,6 +1,6 @@ import React from 'react'; import { render } from 'react-dom'; -import { hot } from 'react-hot-loader'; +import { hot } from 'react-hot-loader/root'; import { Provider } from 'react-redux'; import { Router, browserHistory } from 'react-router'; import configureStore from './store'; @@ -22,7 +22,7 @@ const App = () => ( ); -const HotApp = hot(module)(App); +const HotApp = hot(App); render( , diff --git a/client/modules/IDE/actions/assets.js b/client/modules/IDE/actions/assets.js index 5b72791e..e2b49cf6 100644 --- a/client/modules/IDE/actions/assets.js +++ b/client/modules/IDE/actions/assets.js @@ -1,26 +1,32 @@ import axios from 'axios'; - import * as ActionTypes from '../../../constants'; +import { startLoader, stopLoader } from './loader'; const __process = (typeof global !== 'undefined' ? global : window).process; const ROOT_URL = __process.env.API_URL; -function setAssets(assets) { +function setAssets(assets, totalSize) { return { type: ActionTypes.SET_ASSETS, - assets + assets, + totalSize }; } export function getAssets() { - return (dispatch, getState) => { + return (dispatch) => { + dispatch(startLoader()); axios.get(`${ROOT_URL}/S3/objects`, { withCredentials: true }) .then((response) => { - dispatch(setAssets(response.data.assets)); + dispatch(setAssets(response.data.assets, response.data.totalSize)); + dispatch(stopLoader()); }) - .catch(response => dispatch({ - type: ActionTypes.ERROR - })); + .catch(() => { + dispatch({ + type: ActionTypes.ERROR + }); + dispatch(stopLoader()); + }); }; } diff --git a/client/modules/IDE/actions/preferences.js b/client/modules/IDE/actions/preferences.js index 567425cc..ae8e2438 100644 --- a/client/modules/IDE/actions/preferences.js +++ b/client/modules/IDE/actions/preferences.js @@ -32,6 +32,24 @@ export function setFontSize(value) { }; } +export function setLineNumbers(value) { + return (dispatch, getState) => { + dispatch({ + type: ActionTypes.SET_LINE_NUMBERS, + value + }); + const state = getState(); + if (state.user.authenticated) { + const formParams = { + preferences: { + lineNumbers: value + } + }; + updatePreferences(formParams, dispatch); + } + }; +} + export function setAutosave(value) { return (dispatch, getState) => { dispatch({ diff --git a/client/modules/IDE/actions/sorting.js b/client/modules/IDE/actions/sorting.js index 0e306b46..8feb9b12 100644 --- a/client/modules/IDE/actions/sorting.js +++ b/client/modules/IDE/actions/sorting.js @@ -25,3 +25,14 @@ export function toggleDirectionForField(field) { field }; } + +export function setSearchTerm(searchTerm) { + return { + type: ActionTypes.SET_SEARCH_TERM, + query: searchTerm + }; +} + +export function resetSearchTerm() { + return setSearchTerm(''); +} diff --git a/client/modules/IDE/components/AssetList.jsx b/client/modules/IDE/components/AssetList.jsx index ab548322..a658fedf 100644 --- a/client/modules/IDE/components/AssetList.jsx +++ b/client/modules/IDE/components/AssetList.jsx @@ -4,12 +4,11 @@ import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import { Link } from 'react-router'; import { Helmet } from 'react-helmet'; - import prettyBytes from 'pretty-bytes'; +import Loader from '../../App/components/loader'; import * as AssetActions from '../actions/assets'; - class AssetList extends React.Component { constructor(props) { super(props); @@ -23,17 +22,37 @@ class AssetList extends React.Component { return `p5.js Web Editor | ${this.props.username}'s assets`; } + hasAssets() { + return !this.props.loading && this.props.assetList.length > 0; + } + + renderLoader() { + if (this.props.loading) return ; + return null; + } + + renderEmptyTable() { + if (!this.props.loading && this.props.assetList.length === 0) { + return (

No uploaded assets.

); + } + return null; + } + render() { const username = this.props.username !== undefined ? this.props.username : this.props.user.username; + const { assetList, totalSize } = this.props; return (
+ {/* Eventually, this copy should be Total / 250 MB Used */} + {this.hasAssets() && +

{`${prettyBytes(totalSize)} Total`}

+ } {this.getAssetsTitle()} - {this.props.assets.length === 0 && -

No uploaded assets.

- } - {this.props.assets.length > 0 && + {this.renderLoader()} + {this.renderEmptyTable()} + {this.hasAssets() && @@ -44,7 +63,7 @@ class AssetList extends React.Component { - {this.props.assets.map(asset => + {assetList.map(asset => ( @@ -65,20 +84,24 @@ AssetList.propTypes = { username: PropTypes.string }).isRequired, username: PropTypes.string.isRequired, - assets: PropTypes.arrayOf(PropTypes.shape({ + assetList: PropTypes.arrayOf(PropTypes.shape({ key: PropTypes.string.isRequired, name: PropTypes.string.isRequired, url: PropTypes.string.isRequired, sketchName: PropTypes.string.isRequired, sketchId: PropTypes.string.isRequired })).isRequired, + totalSize: PropTypes.number.isRequired, getAssets: PropTypes.func.isRequired, + loading: PropTypes.bool.isRequired }; function mapStateToProps(state) { return { user: state.user, - assets: state.assets + assetList: state.assets.list, + totalSize: state.assets.totalSize, + loading: state.loading }; } diff --git a/client/modules/IDE/components/Editor.jsx b/client/modules/IDE/components/Editor.jsx index 892028c4..cb330b32 100644 --- a/client/modules/IDE/components/Editor.jsx +++ b/client/modules/IDE/components/Editor.jsx @@ -80,7 +80,7 @@ class Editor extends React.Component { this.widgets = []; this._cm = CodeMirror(this.codemirrorContainer, { // eslint-disable-line theme: `p5-${this.props.theme}`, - lineNumbers: true, + lineNumbers: this.props.lineNumbers, styleActiveLine: true, inputStyle: 'contenteditable', lineWrapping: this.props.linewrap, @@ -181,6 +181,9 @@ class Editor extends React.Component { 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(); @@ -188,7 +191,7 @@ class Editor extends React.Component { for (let i = 0; i < this._cm.lineCount(); i += 1) { this._cm.removeLineClass(i, 'background', 'line-runtime-error'); } - if (this.props.runtimeErrorWarningVisible && this._cm.getDoc().modeOption === 'javascript') { + if (this.props.runtimeErrorWarningVisible) { this.props.consoleEvents.forEach((consoleEvent) => { if (consoleEvent.method === 'error') { if (consoleEvent.data && @@ -197,7 +200,11 @@ class Editor extends React.Component { consoleEvent.data[0].indexOf(')') > -1) { const n = consoleEvent.data[0].replace(')', '').split(' '); const lineNumber = parseInt(n[n.length - 1], 10) - 1; - if (!Number.isNaN(lineNumber)) { + 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'); } } @@ -338,6 +345,7 @@ class Editor extends React.Component { } Editor.propTypes = { + lineNumbers: PropTypes.bool.isRequired, lintWarning: PropTypes.bool.isRequired, linewrap: PropTypes.bool.isRequired, lintMessages: PropTypes.arrayOf(PropTypes.shape({ @@ -359,7 +367,7 @@ Editor.propTypes = { content: PropTypes.string.isRequired, id: PropTypes.string.isRequired, fileType: PropTypes.string.isRequired, - url: PropTypes.string.isRequired + url: PropTypes.string }).isRequired, editorOptionsVisible: PropTypes.bool.isRequired, showEditorOptions: PropTypes.func.isRequired, diff --git a/client/modules/IDE/components/NewFolderForm.jsx b/client/modules/IDE/components/NewFolderForm.jsx index 2795197e..3490de64 100644 --- a/client/modules/IDE/components/NewFolderForm.jsx +++ b/client/modules/IDE/components/NewFolderForm.jsx @@ -14,7 +14,7 @@ class NewFolderForm extends React.Component { render() { const { - fields: { name }, handleSubmit, submitting, pristine + fields: { name }, handleSubmit } = this.props; return ( { this.fileName = element; }} {...domOnlyProps(name)} /> - + {name.touched && name.error && {name.error}} ); diff --git a/client/modules/IDE/components/Preferences.jsx b/client/modules/IDE/components/Preferences.jsx index 2143cd16..0e78fbfa 100644 --- a/client/modules/IDE/components/Preferences.jsx +++ b/client/modules/IDE/components/Preferences.jsx @@ -17,6 +17,7 @@ class Preferences extends React.Component { this.handleUpdateAutosave = this.handleUpdateAutosave.bind(this); this.handleUpdateLinewrap = this.handleUpdateLinewrap.bind(this); this.handleLintWarning = this.handleLintWarning.bind(this); + this.handleLineNumbers = this.handleLineNumbers.bind(this); this.onFontInputChange = this.onFontInputChange.bind(this); this.onFontInputSubmit = this.onFontInputSubmit.bind(this); this.increaseFontSize = this.increaseFontSize.bind(this); @@ -29,9 +30,12 @@ class Preferences extends React.Component { } onFontInputChange(event) { - this.setState({ - fontSize: event.target.value - }); + const INTEGER_REGEX = /^[0-9\b]+$/; + if (event.target.value === '' || INTEGER_REGEX.test(event.target.value)) { + this.setState({ + fontSize: event.target.value + }); + } } onFontInputSubmit(event) { @@ -79,6 +83,11 @@ class Preferences extends React.Component { this.props.setLintWarning(value); } + handleLineNumbers(event) { + const value = event.target.value === 'true'; + this.props.setLineNumbers(value); + } + render() { const beep = new Audio(beepUrl); @@ -151,10 +160,9 @@ class Preferences extends React.Component { aria-atomic="true" value={this.state.fontSize} onChange={this.onFontInputChange} + type="text" ref={(element) => { this.fontSizeInput = element; }} - onClick={() => { - this.fontSizeInput.select(); - }} + onClick={() => { this.fontSizeInput.select(); }} /> + + + + ); + } +} + +Searchbar.propTypes = { + searchTerm: PropTypes.string.isRequired, + setSearchTerm: PropTypes.func.isRequired, + resetSearchTerm: PropTypes.func.isRequired +}; + +function mapStateToProps(state) { + return { + searchTerm: state.search.searchTerm + }; +} + +function mapDispatchToProps(dispatch) { + return bindActionCreators(Object.assign({}, SortingActions), dispatch); +} + +export default connect(mapStateToProps, mapDispatchToProps)(Searchbar); diff --git a/client/modules/IDE/components/SketchList.jsx b/client/modules/IDE/components/SketchList.jsx index 3b9b958e..b430df14 100644 --- a/client/modules/IDE/components/SketchList.jsx +++ b/client/modules/IDE/components/SketchList.jsx @@ -7,6 +7,7 @@ import { connect } from 'react-redux'; import { Link } from 'react-router'; import { bindActionCreators } from 'redux'; import classNames from 'classnames'; +import slugify from 'slugify'; import * as ProjectActions from '../actions/project'; import * as ProjectsActions from '../actions/projects'; import * as ToastActions from '../actions/toast'; @@ -137,13 +138,17 @@ class SketchListRowBase extends React.Component { const { sketch, username } = this.props; const { renameOpen, optionsOpen, renameValue } = this.state; const userIsOwner = this.props.user.username === this.props.username; + let url = `/${username}/sketches/${sketch.id}`; + if (username === 'p5') { + url = `/${username}/sketches/${slugify(sketch.name, '_')}`; + } return (
{asset.name}
- + {renameOpen ? '' : sketch.name} {renameOpen @@ -359,10 +364,20 @@ SketchList.propTypes = { sorting: PropTypes.shape({ field: PropTypes.string.isRequired, direction: PropTypes.string.isRequired - }).isRequired + }).isRequired, + project: PropTypes.shape({ + id: PropTypes.string, + owner: PropTypes.shape({ + id: PropTypes.string + }) + }) }; SketchList.defaultProps = { + project: { + id: undefined, + owner: undefined + }, username: undefined }; @@ -371,7 +386,8 @@ function mapStateToProps(state) { user: state.user, sketches: getSortedSketches(state), sorting: state.sorting, - loading: state.loading + loading: state.loading, + project: state.project }; } diff --git a/client/modules/IDE/pages/IDEView.jsx b/client/modules/IDE/pages/IDEView.jsx index b0be087e..072360ae 100644 --- a/client/modules/IDE/pages/IDEView.jsx +++ b/client/modules/IDE/pages/IDEView.jsx @@ -29,6 +29,9 @@ import * as ToastActions from '../actions/toast'; import * as ConsoleActions from '../actions/console'; import { getHTMLFile } from '../reducers/files'; import Overlay from '../../App/components/Overlay'; +import SketchList from '../components/SketchList'; +import Searchbar from '../components/Searchbar'; +import AssetList from '../components/AssetList'; import About from '../components/About'; import Feedback from '../components/Feedback'; @@ -203,6 +206,8 @@ class IDEView extends React.Component { setFontSize={this.props.setFontSize} autosave={this.props.preferences.autosave} linewrap={this.props.preferences.linewrap} + lineNumbers={this.props.preferences.lineNumbers} + setLineNumbers={this.props.setLineNumbers} setAutosave={this.props.setAutosave} setLinewrap={this.props.setLinewrap} lintWarning={this.props.preferences.lintWarning} @@ -268,6 +273,7 @@ class IDEView extends React.Component { file={this.props.selectedFile} updateFileContent={this.props.updateFileContent} fontSize={this.props.preferences.fontSize} + lineNumbers={this.props.preferences.lineNumbers} files={this.props.files} editorOptionsVisible={this.props.ide.editorOptionsVisible} showEditorOptions={this.props.showEditorOptions} @@ -363,6 +369,31 @@ class IDEView extends React.Component { createFolder={this.props.createFolder} /> } + { this.props.location.pathname.match(/sketches$/) && + + + + + } + { this.props.location.pathname.match(/assets$/) && + + + + } { this.props.location.pathname === '/about' && { +// 1,000,000 bytes in a MB. can't upload if totalSize is bigger than this. +const initialState = { + list: [], + totalSize: 0 +}; + +const assets = (state = initialState, action) => { switch (action.type) { case ActionTypes.SET_ASSETS: - return action.assets; + return { list: action.assets, totalSize: action.totalSize }; default: return state; } diff --git a/client/modules/IDE/reducers/files.js b/client/modules/IDE/reducers/files.js index a95c4c9d..46c0dd30 100644 --- a/client/modules/IDE/reducers/files.js +++ b/client/modules/IDE/reducers/files.js @@ -13,9 +13,9 @@ const defaultHTML = ` - - - + + + diff --git a/client/modules/IDE/reducers/ide.js b/client/modules/IDE/reducers/ide.js index f9559234..5e7f106f 100644 --- a/client/modules/IDE/reducers/ide.js +++ b/client/modules/IDE/reducers/ide.js @@ -10,9 +10,9 @@ const initialState = { projectOptionsVisible: false, newFolderModalVisible: false, shareModalVisible: false, - shareModalProjectId: null, - shareModalProjectName: null, - shareModalProjectUsername: null, + shareModalProjectId: 'abcd', + shareModalProjectName: 'My Cute Sketch', + shareModalProjectUsername: 'p5_user', editorOptionsVisible: false, keyboardShortcutVisible: false, unsavedChanges: false, diff --git a/client/modules/IDE/reducers/preferences.js b/client/modules/IDE/reducers/preferences.js index 738effa4..080a7343 100644 --- a/client/modules/IDE/reducers/preferences.js +++ b/client/modules/IDE/reducers/preferences.js @@ -4,6 +4,7 @@ const initialState = { fontSize: 18, autosave: true, linewrap: true, + lineNumbers: true, lintWarning: false, textOutput: false, gridOutput: false, @@ -34,6 +35,8 @@ const preferences = (state = initialState, action) => { return Object.assign({}, state, { theme: action.value }); case ActionTypes.SET_AUTOREFRESH: return Object.assign({}, state, { autorefresh: action.value }); + case ActionTypes.SET_LINE_NUMBERS: + return Object.assign({}, state, { lineNumbers: action.value }); default: return state; } diff --git a/client/modules/IDE/reducers/search.js b/client/modules/IDE/reducers/search.js new file mode 100644 index 00000000..1e2ff8d4 --- /dev/null +++ b/client/modules/IDE/reducers/search.js @@ -0,0 +1,14 @@ +import * as ActionTypes from '../../../constants'; + +const initialState = { + searchTerm: '' +}; + +export default (state = initialState, action) => { + switch (action.type) { + case ActionTypes.SET_SEARCH_TERM: + return { ...state, searchTerm: action.query }; + default: + return state; + } +}; diff --git a/client/modules/IDE/selectors/projects.js b/client/modules/IDE/selectors/projects.js index 9ff655d3..b2230826 100644 --- a/client/modules/IDE/selectors/projects.js +++ b/client/modules/IDE/selectors/projects.js @@ -6,9 +6,27 @@ import { DIRECTION } from '../actions/sorting'; const getSketches = state => state.sketches; const getField = state => state.sorting.field; const getDirection = state => state.sorting.direction; +const getSearchTerm = state => state.search.searchTerm; + +const getFilteredSketches = createSelector( + getSketches, + getSearchTerm, + (sketches, search) => { + if (search) { + const searchStrings = sketches.map((sketch) => { + const smallSketch = { + name: sketch.name + }; + return { ...sketch, searchString: Object.values(smallSketch).join(' ').toLowerCase() }; + }); + return searchStrings.filter(sketch => sketch.searchString.includes(search.toLowerCase())); + } + return sketches; + } +); const getSortedSketches = createSelector( - getSketches, + getFilteredSketches, getField, getDirection, (sketches, field, direction) => { diff --git a/client/modules/User/actions.js b/client/modules/User/actions.js index 93a874fb..3793b2dc 100644 --- a/client/modules/User/actions.js +++ b/client/modules/User/actions.js @@ -64,9 +64,8 @@ export function validateAndLoginUser(previousPath, formProps, dispatch) { browserHistory.push(previousPath); resolve(); }) - .catch((response) => { - reject({ password: response.data.message, _error: 'Login failed!' }); // eslint-disable-line - }); + .catch(error => + reject({ password: error.response.data.message, _error: 'Login failed!' })); // eslint-disable-line }); } @@ -84,7 +83,8 @@ export function getUser() { }); }) .catch((response) => { - dispatch(authError(response.data.error)); + const message = response.message || response.data.error; + dispatch(authError(message)); }); }; } diff --git a/client/modules/User/components/ResetPasswordForm.jsx b/client/modules/User/components/ResetPasswordForm.jsx index ed724b5f..6ed00455 100644 --- a/client/modules/User/components/ResetPasswordForm.jsx +++ b/client/modules/User/components/ResetPasswordForm.jsx @@ -17,6 +17,7 @@ function ResetPasswordForm(props) { id="email" {...domOnlyProps(email)} /> + {email.touched && email.error && {email.error}}

1) { // not enter and more than 1 character to search startSearch(cm, getSearchState(cm), searchField.value); + } else if (searchField.value.length <= 1) { + cm.display.wrapper.querySelector('.CodeMirror-search-results').innerText = ''; } }); @@ -260,6 +262,7 @@ export default function(CodeMirror) {
+