Customize: Introduce extensible code editor Customizer control for CodeMirror.
* Adds `WP_Customize_Code_Editor_Control` and `wp.customize.CodeEditorControl()`. * Control respects user preference for syntax highlighting, showing a textarea when user opts out. * Code editor control takes the ad hoc code for Additional CSS and makes it reusable and extensible, for Additional CSS in core and plugins to use (such as Jetpack). * Replace `settings` arg in `wp_enqueue_code_editor()` with separate args for `codemirror`, `csslint`, `jshint`, and `htmlhint`. * Prefix `codemirror` script and style handles with `wp-` to prevent collisions, as also the object is exported as `wp.CodeMirror` in JS. * Reduce indent size in Customizer code editor instances and Custom HTML widget to use tab size of 2 instead of 4 to save on space. See #12423, #38707, #35395. Fixes #41897. Built from https://develop.svn.wordpress.org/trunk@41558 git-svn-id: http://core.svn.wordpress.org/trunk@41391 1a063a9b-81f0-0310-95a4-ce76da25c4cd
This commit is contained in:
@@ -1164,12 +1164,26 @@ p.customize-section-description {
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom CSS Section
|
||||
* Code Editor Control and Custom CSS Section
|
||||
*
|
||||
* Modifications to the Section Container to make the textarea full-width and
|
||||
* full-height, if the control is the only control in the section.
|
||||
*/
|
||||
|
||||
.customize-control-code_editor textarea {
|
||||
width: 100%;
|
||||
font-family: Consolas, Monaco, monospace;
|
||||
font-size: 12px;
|
||||
padding: 6px 8px;
|
||||
-moz-tab-size: 2;
|
||||
-o-tab-size: 2;
|
||||
tab-size: 2;
|
||||
}
|
||||
.customize-control-code_editor textarea,
|
||||
.customize-control-code_editor .CodeMirror {
|
||||
height: 14em;
|
||||
}
|
||||
|
||||
#customize-controls .customize-section-description-container.section-meta.customize-info {
|
||||
border-bottom: none;
|
||||
}
|
||||
@@ -1180,13 +1194,7 @@ p.customize-section-description {
|
||||
|
||||
#customize-control-custom_css textarea {
|
||||
display: block;
|
||||
font-family: Consolas, Monaco, monospace;
|
||||
font-size: 12px;
|
||||
padding: 6px 8px;
|
||||
height: 500px;
|
||||
-moz-tab-size: 4;
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
}
|
||||
|
||||
.customize-section-description-container + #customize-control-custom_css:last-child textarea {
|
||||
|
||||
2
wp-admin/css/customize-controls-rtl.min.css
vendored
2
wp-admin/css/customize-controls-rtl.min.css
vendored
File diff suppressed because one or more lines are too long
@@ -1164,12 +1164,26 @@ p.customize-section-description {
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom CSS Section
|
||||
* Code Editor Control and Custom CSS Section
|
||||
*
|
||||
* Modifications to the Section Container to make the textarea full-width and
|
||||
* full-height, if the control is the only control in the section.
|
||||
*/
|
||||
|
||||
.customize-control-code_editor textarea {
|
||||
width: 100%;
|
||||
font-family: Consolas, Monaco, monospace;
|
||||
font-size: 12px;
|
||||
padding: 6px 8px;
|
||||
-moz-tab-size: 2;
|
||||
-o-tab-size: 2;
|
||||
tab-size: 2;
|
||||
}
|
||||
.customize-control-code_editor textarea,
|
||||
.customize-control-code_editor .CodeMirror {
|
||||
height: 14em;
|
||||
}
|
||||
|
||||
#customize-controls .customize-section-description-container.section-meta.customize-info {
|
||||
border-bottom: none;
|
||||
}
|
||||
@@ -1180,13 +1194,7 @@ p.customize-section-description {
|
||||
|
||||
#customize-control-custom_css textarea {
|
||||
display: block;
|
||||
font-family: Consolas, Monaco, monospace;
|
||||
font-size: 12px;
|
||||
padding: 6px 8px;
|
||||
height: 500px;
|
||||
-moz-tab-size: 4;
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
}
|
||||
|
||||
.customize-section-description-container + #customize-control-custom_css:last-child textarea {
|
||||
|
||||
2
wp-admin/css/customize-controls.min.css
vendored
2
wp-admin/css/customize-controls.min.css
vendored
File diff suppressed because one or more lines are too long
@@ -3677,6 +3677,250 @@
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Class wp.customize.CodeEditorControl
|
||||
*
|
||||
* @since 4.9.0
|
||||
*
|
||||
* @constructor
|
||||
* @augments wp.customize.Control
|
||||
* @augments wp.customize.Class
|
||||
*/
|
||||
api.CodeEditorControl = api.Control.extend({
|
||||
|
||||
/**
|
||||
* Initialize the editor when the containing section is ready and expanded.
|
||||
*
|
||||
* @since 4.9.0
|
||||
* @returns {void}
|
||||
*/
|
||||
ready: function() {
|
||||
var control = this;
|
||||
if ( ! control.section() ) {
|
||||
control.initEditor();
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait to initialize editor until section is embedded and expanded.
|
||||
api.section( control.section(), function( section ) {
|
||||
section.deferred.embedded.done( function() {
|
||||
var onceExpanded;
|
||||
if ( section.expanded() ) {
|
||||
control.initEditor();
|
||||
} else {
|
||||
onceExpanded = function( isExpanded ) {
|
||||
if ( isExpanded ) {
|
||||
control.initEditor();
|
||||
section.expanded.unbind( onceExpanded );
|
||||
}
|
||||
};
|
||||
section.expanded.bind( onceExpanded );
|
||||
}
|
||||
} );
|
||||
} );
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize editor.
|
||||
*
|
||||
* @since 4.9.0
|
||||
* @returns {void}
|
||||
*/
|
||||
initEditor: function() {
|
||||
var control = this, element;
|
||||
|
||||
element = new api.Element( control.container.find( 'textarea' ) );
|
||||
control.elements.push( element );
|
||||
element.sync( control.setting );
|
||||
element.set( control.setting() );
|
||||
|
||||
if ( control.params.editor_settings ) {
|
||||
control.initSyntaxHighlightingEditor( control.params.editor_settings );
|
||||
} else {
|
||||
control.initPlainTextareaEditor();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Make sure editor gets focused when control is focused.
|
||||
*
|
||||
* @since 4.9.0
|
||||
* @param {Object} [params] - Focus params.
|
||||
* @param {Function} [params.completeCallback] - Function to call when expansion is complete.
|
||||
* @returns {void}
|
||||
*/
|
||||
focus: function( params ) {
|
||||
var control = this, extendedParams = _.extend( {}, params ), originalCompleteCallback;
|
||||
originalCompleteCallback = extendedParams.completeCallback;
|
||||
extendedParams.completeCallback = function() {
|
||||
if ( originalCompleteCallback ) {
|
||||
originalCompleteCallback();
|
||||
}
|
||||
if ( control.editor ) {
|
||||
control.editor.codemirror.focus();
|
||||
}
|
||||
};
|
||||
api.Control.prototype.focus.call( control, extendedParams );
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize syntax-highlighting editor.
|
||||
*
|
||||
* @since 4.9.0
|
||||
* @param {object} codeEditorSettings - Code editor settings.
|
||||
* @returns {void}
|
||||
*/
|
||||
initSyntaxHighlightingEditor: function( codeEditorSettings ) {
|
||||
var control = this, $textarea = control.container.find( 'textarea' ), settings, suspendEditorUpdate = false;
|
||||
|
||||
settings = _.extend( {}, codeEditorSettings, {
|
||||
onTabNext: _.bind( control.onTabNext, control ),
|
||||
onTabPrevious: _.bind( control.onTabPrevious, control ),
|
||||
onUpdateErrorNotice: _.bind( control.onUpdateErrorNotice, control )
|
||||
});
|
||||
|
||||
control.editor = wp.codeEditor.initialize( $textarea, settings );
|
||||
|
||||
// Refresh when receiving focus.
|
||||
control.editor.codemirror.on( 'focus', function( codemirror ) {
|
||||
codemirror.refresh();
|
||||
});
|
||||
|
||||
/*
|
||||
* When the CodeMirror instance changes, mirror to the textarea,
|
||||
* where we have our "true" change event handler bound.
|
||||
*/
|
||||
control.editor.codemirror.on( 'change', function( codemirror ) {
|
||||
suspendEditorUpdate = true;
|
||||
$textarea.val( codemirror.getValue() ).trigger( 'change' );
|
||||
suspendEditorUpdate = false;
|
||||
});
|
||||
|
||||
// Update CodeMirror when the setting is changed by another plugin.
|
||||
control.setting.bind( function( value ) {
|
||||
if ( ! suspendEditorUpdate ) {
|
||||
control.editor.codemirror.setValue( value );
|
||||
}
|
||||
});
|
||||
|
||||
// Prevent collapsing section when hitting Esc to tab out of editor.
|
||||
control.editor.codemirror.on( 'keydown', function onKeydown( codemirror, event ) {
|
||||
var escKeyCode = 27;
|
||||
if ( escKeyCode === event.keyCode ) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle tabbing to the field after the editor.
|
||||
*
|
||||
* @since 4.9.0
|
||||
* @returns {void}
|
||||
*/
|
||||
onTabNext: function onTabNext() {
|
||||
var control = this, controls, controlIndex, section;
|
||||
section = api.section( control.section() );
|
||||
controls = section.controls();
|
||||
controlIndex = controls.indexOf( control );
|
||||
if ( controls.length === controlIndex + 1 ) {
|
||||
$( '#customize-footer-actions .collapse-sidebar' ).focus();
|
||||
} else {
|
||||
controls[ controlIndex + 1 ].container.find( ':focusable:first' ).focus();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle tabbing to the field before the editor.
|
||||
*
|
||||
* @since 4.9.0
|
||||
* @returns {void}
|
||||
*/
|
||||
onTabPrevious: function onTabPrevious() {
|
||||
var control = this, controls, controlIndex, section;
|
||||
section = api.section( control.section() );
|
||||
controls = section.controls();
|
||||
controlIndex = controls.indexOf( control );
|
||||
if ( 0 === controlIndex ) {
|
||||
section.contentContainer.find( '.customize-section-title .customize-help-toggle, .customize-section-title .customize-section-description.open .section-description-close' ).last().focus();
|
||||
} else {
|
||||
controls[ controlIndex - 1 ].contentContainer.find( ':focusable:first' ).focus();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update error notice.
|
||||
*
|
||||
* @since 4.9.0
|
||||
* @param {Array} errorAnnotations - Error annotations.
|
||||
* @returns {void}
|
||||
*/
|
||||
onUpdateErrorNotice: function onUpdateErrorNotice( errorAnnotations ) {
|
||||
var control = this, message;
|
||||
control.setting.notifications.remove( 'csslint_error' );
|
||||
|
||||
if ( 0 !== errorAnnotations.length ) {
|
||||
if ( 1 === errorAnnotations.length ) {
|
||||
message = api.l10n.customCssError.singular.replace( '%d', '1' );
|
||||
} else {
|
||||
message = api.l10n.customCssError.plural.replace( '%d', String( errorAnnotations.length ) );
|
||||
}
|
||||
control.setting.notifications.add( 'csslint_error', new api.Notification( 'csslint_error', {
|
||||
message: message,
|
||||
type: 'error'
|
||||
} ) );
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize plain-textarea editor when syntax highlighting is disabled.
|
||||
*
|
||||
* @since 4.9.0
|
||||
* @returns {void}
|
||||
*/
|
||||
initPlainTextareaEditor: function() {
|
||||
var control = this, $textarea = control.container.find( 'textarea' ), textarea = $textarea[0];
|
||||
|
||||
$textarea.on( 'blur', function onBlur() {
|
||||
$textarea.data( 'next-tab-blurs', false );
|
||||
} );
|
||||
|
||||
$textarea.on( 'keydown', function onKeydown( event ) {
|
||||
var selectionStart, selectionEnd, value, tabKeyCode = 9, escKeyCode = 27;
|
||||
|
||||
if ( escKeyCode === event.keyCode ) {
|
||||
if ( ! $textarea.data( 'next-tab-blurs' ) ) {
|
||||
$textarea.data( 'next-tab-blurs', true );
|
||||
event.stopPropagation(); // Prevent collapsing the section.
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Short-circuit if tab key is not being pressed or if a modifier key *is* being pressed.
|
||||
if ( tabKeyCode !== event.keyCode || event.ctrlKey || event.altKey || event.shiftKey ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent capturing Tab characters if Esc was pressed.
|
||||
if ( $textarea.data( 'next-tab-blurs' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
selectionStart = textarea.selectionStart;
|
||||
selectionEnd = textarea.selectionEnd;
|
||||
value = textarea.value;
|
||||
|
||||
if ( selectionStart >= 0 ) {
|
||||
textarea.value = value.substring( 0, selectionStart ).concat( '\t', value.substring( selectionEnd ) );
|
||||
$textarea.selectionStart = textarea.selectionEnd = selectionStart + 1;
|
||||
}
|
||||
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Change objects contained within the main customize object to Settings.
|
||||
api.defaultConstructor = api.Setting;
|
||||
|
||||
@@ -4372,7 +4616,8 @@
|
||||
header: api.HeaderControl,
|
||||
background: api.BackgroundControl,
|
||||
background_position: api.BackgroundPositionControl,
|
||||
theme: api.ThemeControl
|
||||
theme: api.ThemeControl,
|
||||
code_editor: api.CodeEditorControl
|
||||
};
|
||||
api.panelConstructor = {};
|
||||
api.sectionConstructor = {
|
||||
@@ -5712,7 +5957,7 @@
|
||||
|
||||
// Add code editor for Custom CSS.
|
||||
(function() {
|
||||
var ready, sectionReady = $.Deferred(), controlReady = $.Deferred();
|
||||
var sectionReady = $.Deferred();
|
||||
|
||||
api.section( 'custom_css', function( section ) {
|
||||
section.deferred.embedded.done( function() {
|
||||
@@ -5727,16 +5972,10 @@
|
||||
}
|
||||
});
|
||||
});
|
||||
api.control( 'custom_css', function( control ) {
|
||||
control.deferred.embedded.done( function() {
|
||||
controlReady.resolve( control );
|
||||
});
|
||||
});
|
||||
|
||||
ready = $.when( sectionReady, controlReady );
|
||||
|
||||
// Set up the section desription behaviors.
|
||||
ready.done( function setupSectionDescription( section, control ) {
|
||||
sectionReady.done( function setupSectionDescription( section ) {
|
||||
var control = api.control( 'custom_css' );
|
||||
|
||||
// Close the section description when clicking the close button.
|
||||
section.container.find( '.section-description-buttons .section-description-close' ).on( 'click', function() {
|
||||
@@ -5747,174 +5986,13 @@
|
||||
});
|
||||
|
||||
// Reveal help text if setting is empty.
|
||||
if ( ! control.setting.get() ) {
|
||||
if ( control && ! control.setting.get() ) {
|
||||
section.container.find( '.section-meta .customize-section-description:first' )
|
||||
.addClass( 'open' )
|
||||
.show()
|
||||
.attr( 'aria-expanded', 'true' );
|
||||
}
|
||||
});
|
||||
|
||||
// Set up the code editor itself.
|
||||
if ( api.settings.customCss && api.settings.customCss.codeEditor ) {
|
||||
|
||||
// Set up the syntax highlighting editor.
|
||||
ready.done( function setupSyntaxHighlightingEditor( section, control ) {
|
||||
var $textarea = control.container.find( 'textarea' ), settings, suspendEditorUpdate = false;
|
||||
|
||||
// Make sure editor gets focused when control is focused.
|
||||
control.focus = (function( originalFocus ) { // eslint-disable-line max-nested-callbacks
|
||||
return function( params ) { // eslint-disable-line max-nested-callbacks
|
||||
var extendedParams = _.extend( {}, params ), originalCompleteCallback;
|
||||
originalCompleteCallback = extendedParams.completeCallback;
|
||||
extendedParams.completeCallback = function() {
|
||||
if ( originalCompleteCallback ) {
|
||||
originalCompleteCallback();
|
||||
}
|
||||
if ( control.editor ) {
|
||||
control.editor.codemirror.focus();
|
||||
}
|
||||
};
|
||||
originalFocus.call( this, extendedParams );
|
||||
};
|
||||
})( control.focus );
|
||||
|
||||
settings = _.extend( {}, api.settings.customCss.codeEditor, {
|
||||
|
||||
/**
|
||||
* Handle tabbing to the field after the editor.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
onTabNext: function onTabNext() {
|
||||
var controls, controlIndex;
|
||||
controls = section.controls();
|
||||
controlIndex = controls.indexOf( control );
|
||||
if ( controls.length === controlIndex + 1 ) {
|
||||
$( '#customize-footer-actions .collapse-sidebar' ).focus();
|
||||
} else {
|
||||
controls[ controlIndex + 1 ].container.find( ':focusable:first' ).focus();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle tabbing to the field before the editor.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
onTabPrevious: function onTabPrevious() {
|
||||
var controls, controlIndex;
|
||||
controls = section.controls();
|
||||
controlIndex = controls.indexOf( control );
|
||||
if ( 0 === controlIndex ) {
|
||||
section.contentContainer.find( '.customize-section-title .customize-help-toggle, .customize-section-title .customize-section-description.open .section-description-close' ).last().focus();
|
||||
} else {
|
||||
controls[ controlIndex - 1 ].contentContainer.find( ':focusable:first' ).focus();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update error notice.
|
||||
*
|
||||
* @param {Array} errorAnnotations - Error annotations.
|
||||
* @returns {void}
|
||||
*/
|
||||
onUpdateErrorNotice: function onUpdateErrorNotice( errorAnnotations ) {
|
||||
var message;
|
||||
control.setting.notifications.remove( 'csslint_error' );
|
||||
|
||||
if ( 0 !== errorAnnotations.length ) {
|
||||
if ( 1 === errorAnnotations.length ) {
|
||||
message = api.l10n.customCssError.singular.replace( '%d', '1' );
|
||||
} else {
|
||||
message = api.l10n.customCssError.plural.replace( '%d', String( errorAnnotations.length ) );
|
||||
}
|
||||
control.setting.notifications.add( 'csslint_error', new api.Notification( 'csslint_error', {
|
||||
message: message,
|
||||
type: 'error'
|
||||
} ) );
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
control.editor = wp.codeEditor.initialize( $textarea, settings );
|
||||
|
||||
// Refresh when receiving focus.
|
||||
control.editor.codemirror.on( 'focus', function( codemirror ) {
|
||||
codemirror.refresh();
|
||||
});
|
||||
|
||||
/*
|
||||
* When the CodeMirror instance changes, mirror to the textarea,
|
||||
* where we have our "true" change event handler bound.
|
||||
*/
|
||||
control.editor.codemirror.on( 'change', function( codemirror ) {
|
||||
suspendEditorUpdate = true;
|
||||
$textarea.val( codemirror.getValue() ).trigger( 'change' );
|
||||
suspendEditorUpdate = false;
|
||||
});
|
||||
|
||||
// Update CodeMirror when the setting is changed by another plugin.
|
||||
control.setting.bind( function( value ) {
|
||||
if ( ! suspendEditorUpdate ) {
|
||||
control.editor.codemirror.setValue( value );
|
||||
}
|
||||
});
|
||||
|
||||
// Prevent collapsing section when hitting Esc to tab out of editor.
|
||||
control.editor.codemirror.on( 'keydown', function onKeydown( codemirror, event ) {
|
||||
var escKeyCode = 27;
|
||||
if ( escKeyCode === event.keyCode ) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
|
||||
// Allow tabs to be entered in Custom CSS textarea.
|
||||
ready.done( function allowTabs( section, control ) {
|
||||
|
||||
var $textarea = control.container.find( 'textarea' ), textarea = $textarea[0];
|
||||
|
||||
$textarea.on( 'blur', function onBlur() {
|
||||
$textarea.data( 'next-tab-blurs', false );
|
||||
} );
|
||||
|
||||
$textarea.on( 'keydown', function onKeydown( event ) {
|
||||
var selectionStart, selectionEnd, value, tabKeyCode = 9, escKeyCode = 27;
|
||||
|
||||
if ( escKeyCode === event.keyCode ) {
|
||||
if ( ! $textarea.data( 'next-tab-blurs' ) ) {
|
||||
$textarea.data( 'next-tab-blurs', true );
|
||||
event.stopPropagation(); // Prevent collapsing the section.
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Short-circuit if tab key is not being pressed or if a modifier key *is* being pressed.
|
||||
if ( tabKeyCode !== event.keyCode || event.ctrlKey || event.altKey || event.shiftKey ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent capturing Tab characters if Esc was pressed.
|
||||
if ( $textarea.data( 'next-tab-blurs' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
selectionStart = textarea.selectionStart;
|
||||
selectionEnd = textarea.selectionEnd;
|
||||
value = textarea.value;
|
||||
|
||||
if ( selectionStart >= 0 ) {
|
||||
textarea.value = value.substring( 0, selectionStart ).concat( '\t', value.substring( selectionEnd ) );
|
||||
$textarea.selectionStart = textarea.selectionEnd = selectionStart + 1;
|
||||
}
|
||||
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
});
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
// Toggle visibility of Header Video notice when active state change.
|
||||
|
||||
4
wp-admin/js/customize-controls.min.js
vendored
4
wp-admin/js/customize-controls.min.js
vendored
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user