Skip to content

Commit

Permalink
Lazily load CodeMirror assets
Browse files Browse the repository at this point in the history
Wraps CodeEditor with a component that lazily loads the scripts and
styles that CodeMirror needs. This lets us avoid using
wp_enqueue_code_editor() which adds considerable bulk (~ 1.9 MB) to the
page load.
  • Loading branch information
noisysocks committed Jan 29, 2018
1 parent 3ffedb6 commit e51f342
Show file tree
Hide file tree
Showing 6 changed files with 219 additions and 79 deletions.
15 changes: 14 additions & 1 deletion blocks/library/html/test/__snapshots__/index.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@ exports[`core/html block edit matches snapshot 1`] = `
<div
class="wp-block-html"
>
<textarea />
<div
class="components-placeholder"
>
<div
class="components-placeholder__label"
/>
<div
class="components-placeholder__fieldset"
>
<span
class="spinner is-active"
/>
</div>
</div>
</div>
`;
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 ) {
/*
* 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;
136 changes: 63 additions & 73 deletions components/code-editor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,104 +2,94 @@
* WordPress dependencies
*/
import { Component } from '@wordpress/element';
import { keycodes } from '@wordpress/utils';
import { __ } from '@wordpress/i18n';

/**
* Module constants
* Internal dependencies
*/
const { UP, DOWN } = keycodes;
import CodeEditor from './editor';
import Placeholder from '../placeholder';
import Spinner from '../spinner';

class CodeEditor extends Component {
constructor() {
super( ...arguments );
function loadScript() {
return new Promise( ( resolve, reject ) => {
const handles = [ 'wp-codemirror', 'code-editor', 'htmlhint', 'csslint', 'jshint' ];

this.onFocus = this.onFocus.bind( this );
this.onBlur = this.onBlur.bind( this );
this.onCursorActivity = this.onCursorActivity.bind( this );
this.onKeyHandled = this.onKeyHandled.bind( this );
}
// Don't load htmlhint-kses unless we need it
if ( window._wpGutenbergCodeEditorSettings.htmlhint.kses ) {
handles.push( 'htmlhint-kses' );
}

componentDidMount() {
const instance = wp.codeEditor.initialize( this.textarea );
this.editor = instance.codemirror;
const script = document.createElement( 'script' );
script.src = `/wp-admin/load-scripts.php?load=${ handles.join( ',' ) }`;
script.onload = resolve;
script.onerror = reject;

this.editor.on( 'focus', this.onFocus );
this.editor.on( 'blur', this.onBlur );
this.editor.on( 'cursorActivity', this.onCursorActivity );
this.editor.on( 'keyHandled', this.onKeyHandled );
document.head.appendChild( script );
} );
}

this.updateFocus();
}
function loadStyle() {
return new Promise( ( resolve, reject ) => {
const handles = [ 'wp-codemirror', 'code-editor' ];

componentDidUpdate( prevProps ) {
if ( this.props.value !== prevProps.value && this.editor.getValue() !== this.props.value ) {
this.editor.setValue( this.props.value );
}
const style = document.createElement( 'link' );
style.rel = 'stylesheet';
style.href = `/wp-admin/load-styles.php?load=${ handles.join( ',' ) }`;
style.onload = resolve;
style.onerror = reject;

if ( this.props.focus !== prevProps.focus ) {
this.updateFocus();
}
}
document.head.appendChild( style );
} );
}

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 );
let hasAlreadyLoadedAssets = false;

this.editor.toTextArea();
this.editor = null;
function loadAssets() {
if ( hasAlreadyLoadedAssets ) {
return Promise.resolve();
}

onFocus() {
if ( this.props.onFocus ) {
this.props.onFocus();
}
}
return Promise.all( [ loadScript(), loadStyle() ] ).then( () => {
hasAlreadyLoadedAssets = true;
} );
}

onBlur( editor ) {
if ( this.props.onChange ) {
this.props.onChange( editor.getValue() );
}
}
class LazyCodeEditor extends Component {
constructor() {
super( ...arguments );

onCursorActivity( editor ) {
this.lastCursor = editor.getCursor();
this.state = {
status: 'pending',
};
}

onKeyHandled( editor, name, event ) {
/*
* 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();
componentDidMount() {
loadAssets().then(
() => {
this.setState( { status: 'success' } );
},
() => {
this.setState( { status: 'error' } );
}
}
);
}

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();
} );
render() {
if ( this.state.status === 'pending' ) {
return (
<Placeholder>
<Spinner />
</Placeholder>
);
}

if ( ! this.props.focus && this.editor.hasFocus() ) {
document.activeElement.blur();
if ( this.state.status === 'error' ) {
return <Placeholder>{ __( 'An unknown error occurred.' ) }</Placeholder>;
}
}

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

export default CodeEditor;
export default LazyCodeEditor;
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { set, noop } from 'lodash';
/**
* Internal dependencies
*/
import CodeEditor from '../';
import CodeEditor from '../editor';

describe( 'CodeEditor', () => {
it( 'should render without an error', () => {
Expand Down
40 changes: 36 additions & 4 deletions lib/client-assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -703,6 +703,32 @@ function gutenberg_color_palette() {
);
}

/**
* The code editor settings that were last captured by
* gutenberg_capture_code_editor_settings().
*
* @var array|false
*/
$gutenberg_captured_code_editor_settings = false;

/**
* When passed to the wp_code_editor_settings filter, this function captures
* the code editor settings given to it and then prevents
* wp_enqueue_code_editor() from enqueuing any assets.
*
* This is a workaround until e.g. code_editor_settings() is added to Core.
*
* @since 2.1.0
*
* @param array $settings Code editor settings.
* @return false
*/
function gutenberg_capture_code_editor_settings( $settings ) {
global $gutenberg_captured_code_editor_settings;
$gutenberg_captured_code_editor_settings = $settings;
return false;
}

/**
* Scripts & Styles.
*
Expand Down Expand Up @@ -870,6 +896,16 @@ function gutenberg_editor_scripts_and_styles( $hook ) {
), $meta_box_url );
wp_localize_script( 'wp-editor', '_wpMetaBoxUrl', $meta_box_url );

// Populate default code editor settings by short-circuiting wp_enqueue_code_editor.
global $gutenberg_captured_code_editor_settings;
add_filter( 'wp_code_editor_settings', 'gutenberg_capture_code_editor_settings' );
wp_enqueue_code_editor( array( 'type' => 'text/html' ) );
remove_filter( 'wp_code_editor_settings', 'gutenberg_capture_code_editor_settings' );
wp_add_inline_script( 'wp-editor', sprintf(
'window._wpGutenbergCodeEditorSettings = %s;',
wp_json_encode( $gutenberg_captured_code_editor_settings )
) );

// Initialize the editor.
$gutenberg_theme_support = get_theme_support( 'gutenberg' );
$align_wide = get_theme_support( 'align-wide' );
Expand Down Expand Up @@ -934,14 +970,10 @@ function gutenberg_editor_scripts_and_styles( $hook ) {
'post' => $post_to_edit['id'],
) );
wp_enqueue_editor();
wp_enqueue_code_editor( array(
'type' => 'text/html',
) );

/**
* Styles
*/

wp_enqueue_style( 'wp-edit-post' );

/**
Expand Down

0 comments on commit e51f342

Please sign in to comment.