Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CodeMirror to the HTML block #4348

Merged
merged 12 commits into from
Feb 27, 2018
32 changes: 24 additions & 8 deletions blocks/library/html/editor.scss
Original file line number Diff line number Diff line change
@@ -1,9 +1,25 @@
.wp-block-html.blocks-plain-text {
font-family: $editor-html-font;
font-size: $text-editor-font-size;
color: $dark-gray-800;
padding: .8em 1.6em;
overflow-x: auto !important;
border: 1px solid $light-gray-500;
border-radius: 4px;
.gutenberg .wp-block-html {
iframe {
display: block;

// Disable pointer events so that we can click on the block to select it
pointer-events: none;
}

.CodeMirror {
border-radius: 4px;
border: 1px solid $light-gray-500;
font-family: $editor-html-font;
font-size: $text-editor-font-size;
height: auto;
}

.CodeMirror-gutters {
background: $white;
border-right: none;
}

.CodeMirror-lines {
padding: 8px 8px 8px 0;
}
}
64 changes: 33 additions & 31 deletions blocks/library/html/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@
*/
import { RawHTML } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { withState } from '@wordpress/components';
import { withState, SandBox, CodeEditor } from '@wordpress/components';

/**
* Internal dependencies
*/
import './editor.scss';
import BlockControls from '../../block-controls';
import PlainText from '../../plain-text';

export const name = 'core/html';

Expand Down Expand Up @@ -49,35 +48,38 @@ export const settings = {

edit: withState( {
preview: false,
} )( ( { attributes, setAttributes, setState, isSelected, preview } ) => [
isSelected && (
<BlockControls key="controls">
<div className="components-toolbar">
<button
className={ `components-tab-button ${ ! preview ? 'is-active' : '' }` }
onClick={ () => setState( { preview: false } ) }>
<span>HTML</span>
</button>
<button
className={ `components-tab-button ${ preview ? 'is-active' : '' }` }
onClick={ () => setState( { preview: true } ) }>
<span>{ __( 'Preview' ) }</span>
</button>
</div>
</BlockControls>
),
preview ?
<div
key="preview"
dangerouslySetInnerHTML={ { __html: attributes.content } } /> :
<PlainText
className="wp-block-html"
key="editor"
value={ attributes.content }
onChange={ ( content ) => setAttributes( { content } ) }
aria-label={ __( 'HTML' ) }
/>,
] ),
} )( ( { attributes, setAttributes, setState, isSelected, toggleSelection, preview } ) => (
<div className="wp-block-html">
{ isSelected && (
<BlockControls>
<div className="components-toolbar">
<button
className={ `components-tab-button ${ ! preview ? 'is-active' : '' }` }
onClick={ () => setState( { preview: false } ) }
>
<span>HTML</span>
</button>
<button
className={ `components-tab-button ${ preview ? 'is-active' : '' }` }
onClick={ () => setState( { preview: true } ) }
>
<span>{ __( 'Preview' ) }</span>
</button>
</div>
</BlockControls>
) }
{ preview ? (
<SandBox html={ attributes.content } />
) : (
<CodeEditor
value={ attributes.content }
focus={ isSelected }
onFocus={ toggleSelection }
onChange={ content => setAttributes( { content } ) }
/>
) }
</div>
) ),

save( { attributes } ) {
return <RawHTML>{ attributes.content }</RawHTML>;
Expand Down
23 changes: 18 additions & 5 deletions blocks/library/html/test/__snapshots__/index.js.snap
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`core/html block edit matches snapshot 1`] = `
<textarea
aria-label="HTML"
class="blocks-plain-text wp-block-html"
rows="1"
/>
<div
class="wp-block-html"
>
<div
class="components-placeholder"
>
<div
class="components-placeholder__label"
/>
<div
class="components-placeholder__fieldset"
>
<span
class="spinner is-active"
/>
</div>
</div>
</div>
`;
56 changes: 56 additions & 0 deletions components/code-editor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
CodeEditor
=======

CodeEditor is a React component that provides the user with a code editor
that has syntax highlighting and linting.

The components acts as a drop-in replacement for a <textarea>, and uses the
CodeMirror library that is provided as part of WordPress Core.

## Usage

```jsx
import { CodeEditor } from '@wordpress/components';

function editCode() {
return (
<CodeEditor
value={ '<p>This is some <b>HTML</b> code that will have syntax highlighting!</p>' }
onChange={ value => console.log( value ) }
/>
);
}
```

## Props

The component accepts the following props:

### value

The source code to load into the code editor.

- Type: `string`
- Required: Yes

### focus

Whether or not the code editor should be focused.

- Type: `boolean`
- Required: No

### onFocus

The function called when the editor is focused.

- Type: `Function`
- Required: No

### onChange

The function called when the user has modified the source code via the
editor. It is passed the new value as an argument.

- Type: `Function`
- Required: No
105 changes: 105 additions & 0 deletions components/code-editor/editor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/**
* WordPress dependencies
*/
import { Component } from '@wordpress/element';
import { keycodes } from '@wordpress/utils';

/**
* Module constants
*/
const { UP, DOWN } = keycodes;

class CodeEditor extends Component {
constructor() {
super( ...arguments );

this.onFocus = this.onFocus.bind( this );
this.onBlur = this.onBlur.bind( this );
this.onCursorActivity = this.onCursorActivity.bind( this );
this.onKeyHandled = this.onKeyHandled.bind( this );
}

componentDidMount() {
const instance = wp.codeEditor.initialize( this.textarea, window._wpGutenbergCodeEditorSettings );
this.editor = instance.codemirror;

this.editor.on( 'focus', this.onFocus );
this.editor.on( 'blur', this.onBlur );
this.editor.on( 'cursorActivity', this.onCursorActivity );
this.editor.on( 'keyHandled', this.onKeyHandled );

this.updateFocus();
}

componentDidUpdate( prevProps ) {
if ( this.props.value !== prevProps.value && this.editor.getValue() !== this.props.value ) {
this.editor.setValue( this.props.value );
}

if ( this.props.focus !== prevProps.focus ) {
this.updateFocus();
}
}

componentWillUnmount() {
this.editor.on( 'focus', this.onFocus );
this.editor.off( 'blur', this.onBlur );
this.editor.off( 'cursorActivity', this.onCursorActivity );
this.editor.off( 'keyHandled', this.onKeyHandled );

this.editor.toTextArea();
this.editor = null;
}

onFocus() {
if ( this.props.onFocus ) {
this.props.onFocus();
}
}

onBlur( editor ) {
if ( this.props.onChange ) {
this.props.onChange( editor.getValue() );
}
}

onCursorActivity( editor ) {
this.lastCursor = editor.getCursor();
}

onKeyHandled( editor, name, event ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love this.

/*
* Pressing UP/DOWN should only move focus to another block if the cursor is
* at the start or end of the editor.
*
* We do this by stopping UP/DOWN from propagating if:
* - We know what the cursor was before this event; AND
* - This event caused the cursor to move
*/
if ( event.keyCode === UP || event.keyCode === DOWN ) {
const areCursorsEqual = ( a, b ) => a.line === b.line && a.ch === b.ch;
if ( this.lastCursor && ! areCursorsEqual( editor.getCursor(), this.lastCursor ) ) {
event.stopImmediatePropagation();
}
}
}

updateFocus() {
if ( this.props.focus && ! this.editor.hasFocus() ) {
// Need to wait for the next frame to be painted before we can focus the editor
window.requestAnimationFrame( () => {
this.editor.focus();
} );
}

if ( ! this.props.focus && this.editor.hasFocus() ) {
document.activeElement.blur();
}
}

render() {
return <textarea ref={ ref => ( this.textarea = ref ) } value={ this.props.value } />;
}
}

export default CodeEditor;
Loading