Skip to content

Commit e51f342

Browse files
committed
Lazily load CodeMirror assets
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.
1 parent 3ffedb6 commit e51f342

File tree

6 files changed

+219
-79
lines changed

6 files changed

+219
-79
lines changed

blocks/library/html/test/__snapshots__/index.js.snap

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,19 @@ exports[`core/html block edit matches snapshot 1`] = `
44
<div
55
class="wp-block-html"
66
>
7-
<textarea />
7+
<div
8+
class="components-placeholder"
9+
>
10+
<div
11+
class="components-placeholder__label"
12+
/>
13+
<div
14+
class="components-placeholder__fieldset"
15+
>
16+
<span
17+
class="spinner is-active"
18+
/>
19+
</div>
20+
</div>
821
</div>
922
`;

components/code-editor/editor.js

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/**
2+
* WordPress dependencies
3+
*/
4+
import { Component } from '@wordpress/element';
5+
import { keycodes } from '@wordpress/utils';
6+
7+
/**
8+
* Module constants
9+
*/
10+
const { UP, DOWN } = keycodes;
11+
12+
class CodeEditor extends Component {
13+
constructor() {
14+
super( ...arguments );
15+
16+
this.onFocus = this.onFocus.bind( this );
17+
this.onBlur = this.onBlur.bind( this );
18+
this.onCursorActivity = this.onCursorActivity.bind( this );
19+
this.onKeyHandled = this.onKeyHandled.bind( this );
20+
}
21+
22+
componentDidMount() {
23+
const instance = wp.codeEditor.initialize( this.textarea, window._wpGutenbergCodeEditorSettings );
24+
this.editor = instance.codemirror;
25+
26+
this.editor.on( 'focus', this.onFocus );
27+
this.editor.on( 'blur', this.onBlur );
28+
this.editor.on( 'cursorActivity', this.onCursorActivity );
29+
this.editor.on( 'keyHandled', this.onKeyHandled );
30+
31+
this.updateFocus();
32+
}
33+
34+
componentDidUpdate( prevProps ) {
35+
if ( this.props.value !== prevProps.value && this.editor.getValue() !== this.props.value ) {
36+
this.editor.setValue( this.props.value );
37+
}
38+
39+
if ( this.props.focus !== prevProps.focus ) {
40+
this.updateFocus();
41+
}
42+
}
43+
44+
componentWillUnmount() {
45+
this.editor.on( 'focus', this.onFocus );
46+
this.editor.off( 'blur', this.onBlur );
47+
this.editor.off( 'cursorActivity', this.onCursorActivity );
48+
this.editor.off( 'keyHandled', this.onKeyHandled );
49+
50+
this.editor.toTextArea();
51+
this.editor = null;
52+
}
53+
54+
onFocus() {
55+
if ( this.props.onFocus ) {
56+
this.props.onFocus();
57+
}
58+
}
59+
60+
onBlur( editor ) {
61+
if ( this.props.onChange ) {
62+
this.props.onChange( editor.getValue() );
63+
}
64+
}
65+
66+
onCursorActivity( editor ) {
67+
this.lastCursor = editor.getCursor();
68+
}
69+
70+
onKeyHandled( editor, name, event ) {
71+
/*
72+
* Pressing UP/DOWN should only move focus to another block if the cursor is
73+
* at the start or end of the editor.
74+
*
75+
* We do this by stopping UP/DOWN from propagating if:
76+
* - We know what the cursor was before this event; AND
77+
* - This event caused the cursor to move
78+
*/
79+
if ( event.keyCode === UP || event.keyCode === DOWN ) {
80+
const areCursorsEqual = ( a, b ) => a.line === b.line && a.ch === b.ch;
81+
if ( this.lastCursor && ! areCursorsEqual( editor.getCursor(), this.lastCursor ) ) {
82+
event.stopImmediatePropagation();
83+
}
84+
}
85+
}
86+
87+
updateFocus() {
88+
if ( this.props.focus && ! this.editor.hasFocus() ) {
89+
// Need to wait for the next frame to be painted before we can focus the editor
90+
window.requestAnimationFrame( () => {
91+
this.editor.focus();
92+
} );
93+
}
94+
95+
if ( ! this.props.focus && this.editor.hasFocus() ) {
96+
document.activeElement.blur();
97+
}
98+
}
99+
100+
render() {
101+
return <textarea ref={ ref => ( this.textarea = ref ) } value={ this.props.value } />;
102+
}
103+
}
104+
105+
export default CodeEditor;

components/code-editor/index.js

Lines changed: 63 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -2,104 +2,94 @@
22
* WordPress dependencies
33
*/
44
import { Component } from '@wordpress/element';
5-
import { keycodes } from '@wordpress/utils';
5+
import { __ } from '@wordpress/i18n';
66

77
/**
8-
* Module constants
8+
* Internal dependencies
99
*/
10-
const { UP, DOWN } = keycodes;
10+
import CodeEditor from './editor';
11+
import Placeholder from '../placeholder';
12+
import Spinner from '../spinner';
1113

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

16-
this.onFocus = this.onFocus.bind( this );
17-
this.onBlur = this.onBlur.bind( this );
18-
this.onCursorActivity = this.onCursorActivity.bind( this );
19-
this.onKeyHandled = this.onKeyHandled.bind( this );
20-
}
18+
// Don't load htmlhint-kses unless we need it
19+
if ( window._wpGutenbergCodeEditorSettings.htmlhint.kses ) {
20+
handles.push( 'htmlhint-kses' );
21+
}
2122

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

26-
this.editor.on( 'focus', this.onFocus );
27-
this.editor.on( 'blur', this.onBlur );
28-
this.editor.on( 'cursorActivity', this.onCursorActivity );
29-
this.editor.on( 'keyHandled', this.onKeyHandled );
28+
document.head.appendChild( script );
29+
} );
30+
}
3031

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

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

39-
if ( this.props.focus !== prevProps.focus ) {
40-
this.updateFocus();
41-
}
42-
}
42+
document.head.appendChild( style );
43+
} );
44+
}
4345

44-
componentWillUnmount() {
45-
this.editor.on( 'focus', this.onFocus );
46-
this.editor.off( 'blur', this.onBlur );
47-
this.editor.off( 'cursorActivity', this.onCursorActivity );
48-
this.editor.off( 'keyHandled', this.onKeyHandled );
46+
let hasAlreadyLoadedAssets = false;
4947

50-
this.editor.toTextArea();
51-
this.editor = null;
48+
function loadAssets() {
49+
if ( hasAlreadyLoadedAssets ) {
50+
return Promise.resolve();
5251
}
5352

54-
onFocus() {
55-
if ( this.props.onFocus ) {
56-
this.props.onFocus();
57-
}
58-
}
53+
return Promise.all( [ loadScript(), loadStyle() ] ).then( () => {
54+
hasAlreadyLoadedAssets = true;
55+
} );
56+
}
5957

60-
onBlur( editor ) {
61-
if ( this.props.onChange ) {
62-
this.props.onChange( editor.getValue() );
63-
}
64-
}
58+
class LazyCodeEditor extends Component {
59+
constructor() {
60+
super( ...arguments );
6561

66-
onCursorActivity( editor ) {
67-
this.lastCursor = editor.getCursor();
62+
this.state = {
63+
status: 'pending',
64+
};
6865
}
6966

70-
onKeyHandled( editor, name, event ) {
71-
/*
72-
* Pressing UP/DOWN should only move focus to another block if the cursor is
73-
* at the start or end of the editor.
74-
*
75-
* We do this by stopping UP/DOWN from propagating if:
76-
* - We know what the cursor was before this event; AND
77-
* - This event caused the cursor to move
78-
*/
79-
if ( event.keyCode === UP || event.keyCode === DOWN ) {
80-
const areCursorsEqual = ( a, b ) => a.line === b.line && a.ch === b.ch;
81-
if ( this.lastCursor && ! areCursorsEqual( editor.getCursor(), this.lastCursor ) ) {
82-
event.stopImmediatePropagation();
67+
componentDidMount() {
68+
loadAssets().then(
69+
() => {
70+
this.setState( { status: 'success' } );
71+
},
72+
() => {
73+
this.setState( { status: 'error' } );
8374
}
84-
}
75+
);
8576
}
8677

87-
updateFocus() {
88-
if ( this.props.focus && ! this.editor.hasFocus() ) {
89-
// Need to wait for the next frame to be painted before we can focus the editor
90-
window.requestAnimationFrame( () => {
91-
this.editor.focus();
92-
} );
78+
render() {
79+
if ( this.state.status === 'pending' ) {
80+
return (
81+
<Placeholder>
82+
<Spinner />
83+
</Placeholder>
84+
);
9385
}
9486

95-
if ( ! this.props.focus && this.editor.hasFocus() ) {
96-
document.activeElement.blur();
87+
if ( this.state.status === 'error' ) {
88+
return <Placeholder>{ __( 'An unknown error occurred.' ) }</Placeholder>;
9789
}
98-
}
9990

100-
render() {
101-
return <textarea ref={ ref => ( this.textarea = ref ) } value={ this.props.value } />;
91+
return <CodeEditor { ...this.props } />;
10292
}
10393
}
10494

105-
export default CodeEditor;
95+
export default LazyCodeEditor;

components/code-editor/test/__snapshots__/index.js.snap renamed to components/code-editor/test/__snapshots__/editor.js.snap

File renamed without changes.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { set, noop } from 'lodash';
77
/**
88
* Internal dependencies
99
*/
10-
import CodeEditor from '../';
10+
import CodeEditor from '../editor';
1111

1212
describe( 'CodeEditor', () => {
1313
it( 'should render without an error', () => {

lib/client-assets.php

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -703,6 +703,32 @@ function gutenberg_color_palette() {
703703
);
704704
}
705705

706+
/**
707+
* The code editor settings that were last captured by
708+
* gutenberg_capture_code_editor_settings().
709+
*
710+
* @var array|false
711+
*/
712+
$gutenberg_captured_code_editor_settings = false;
713+
714+
/**
715+
* When passed to the wp_code_editor_settings filter, this function captures
716+
* the code editor settings given to it and then prevents
717+
* wp_enqueue_code_editor() from enqueuing any assets.
718+
*
719+
* This is a workaround until e.g. code_editor_settings() is added to Core.
720+
*
721+
* @since 2.1.0
722+
*
723+
* @param array $settings Code editor settings.
724+
* @return false
725+
*/
726+
function gutenberg_capture_code_editor_settings( $settings ) {
727+
global $gutenberg_captured_code_editor_settings;
728+
$gutenberg_captured_code_editor_settings = $settings;
729+
return false;
730+
}
731+
706732
/**
707733
* Scripts & Styles.
708734
*
@@ -870,6 +896,16 @@ function gutenberg_editor_scripts_and_styles( $hook ) {
870896
), $meta_box_url );
871897
wp_localize_script( 'wp-editor', '_wpMetaBoxUrl', $meta_box_url );
872898

899+
// Populate default code editor settings by short-circuiting wp_enqueue_code_editor.
900+
global $gutenberg_captured_code_editor_settings;
901+
add_filter( 'wp_code_editor_settings', 'gutenberg_capture_code_editor_settings' );
902+
wp_enqueue_code_editor( array( 'type' => 'text/html' ) );
903+
remove_filter( 'wp_code_editor_settings', 'gutenberg_capture_code_editor_settings' );
904+
wp_add_inline_script( 'wp-editor', sprintf(
905+
'window._wpGutenbergCodeEditorSettings = %s;',
906+
wp_json_encode( $gutenberg_captured_code_editor_settings )
907+
) );
908+
873909
// Initialize the editor.
874910
$gutenberg_theme_support = get_theme_support( 'gutenberg' );
875911
$align_wide = get_theme_support( 'align-wide' );
@@ -934,14 +970,10 @@ function gutenberg_editor_scripts_and_styles( $hook ) {
934970
'post' => $post_to_edit['id'],
935971
) );
936972
wp_enqueue_editor();
937-
wp_enqueue_code_editor( array(
938-
'type' => 'text/html',
939-
) );
940973

941974
/**
942975
* Styles
943976
*/
944-
945977
wp_enqueue_style( 'wp-edit-post' );
946978

947979
/**

0 commit comments

Comments
 (0)