Editor: Add CodeMirror-powered code editor with syntax highlighting, linting, and auto-completion.

* Code editor is integrated into the Theme/Plugin Editor, Additional CSS in Customizer, and Custom HTML widget. Code editor is not yet integrated into the post editor, and it may not be until accessibility concerns are addressed.
* The CodeMirror component in the Custom HTML widget is integrated in a similar way to TinyMCE being integrated into the Text widget, adopting the same approach for integrating dynamic JavaScript-initialized fields.
* Linting is performed for JS, CSS, HTML, and JSON via JSHint, CSSLint, HTMLHint, and JSONLint respectively. Linting is not yet supported for PHP.
* When user lacks `unfiltered_html` the capability, the Custom HTML widget will report any Kses-invalid elements and attributes as errors via a custom Kses rule for HTMLHint.
* When linting errors are detected, the user will be prevented from saving the code until the errors are fixed, reducing instances of broken websites.
* The placeholder value is removed from Custom CSS in favor of a fleshed-out section description which now auto-expands when the CSS field is empty. See #39892.
* The CodeMirror library is included as `wp.CodeMirror` to prevent conflicts with any existing `CodeMirror` global.
* An `wp.codeEditor.initialize()` API in JS is provided to convert a `textarea` into CodeMirror, with a `wp_enqueue_code_editor()` function in PHP to manage enqueueing the assets and settings needed to edit a given type of code.
* A user preference is added to manage whether or not "syntax highlighting" is enabled. The feature is opt-out, being enabled by default.
* Allowed file extensions in the theme and plugin editors have been updated to include formats which CodeMirror has modes for: `conf`, `css`, `diff`, `patch`, `html`, `htm`, `http`, `js`, `json`, `jsx`, `less`, `md`, `php`, `phtml`, `php3`, `php4`, `php5`, `php7`, `phps`, `scss`, `sass`, `sh`, `bash`, `sql`, `svg`, `xml`, `yml`, `yaml`, `txt`.

Props westonruter, georgestephanis, obenland, melchoyce, pixolin, mizejewski, michelleweber, afercia, grahamarmfield, samikeijonen, rianrietveld, iseulde.
See #38707.
Fixes #12423, #39892.

Built from https://develop.svn.wordpress.org/trunk@41376


git-svn-id: http://core.svn.wordpress.org/trunk@41209 1a063a9b-81f0-0310-95a4-ce76da25c4cd
This commit is contained in:
Weston Ruter 2017-09-13 06:08:47 +00:00
parent f36cccde7e
commit 90bedf8f9d
45 changed files with 37937 additions and 283 deletions

View File

@ -0,0 +1,62 @@
.wrap [class*="CodeMirror-lint-marker"],
.wp-core-ui [class*="CodeMirror-lint-message"],
.wrap .CodeMirror-lint-marker-multiple {
background-image: none;
}
.wrap [class*="CodeMirror-lint-marker"]:before {
font: normal 18px/1 dashicons;
position: relative;
top: -2px;
}
.wp-core-ui [class*="CodeMirror-lint-message"]:before {
font: normal 16px/1 dashicons;
right: 16px;
position: absolute;
}
.wp-core-ui .CodeMirror-lint-message-error,
.wp-core-ui .CodeMirror-lint-message-warning {
box-shadow: 0 1px 1px 0 rgba( 0, 0, 0, 0.1 );
margin: 5px 0 2px;
padding: 3px 28px 3px 12px;
}
.wp-core-ui .CodeMirror-lint-message-warning {
background-color: #fff8e5;
border-right: 4px solid #ffb900;
}
.wrap .CodeMirror-lint-marker-warning:before,
.wp-core-ui .CodeMirror-lint-message-warning:before {
content: "\f534";
color: #f6a306;
}
.wp-core-ui .CodeMirror-lint-message-error {
background-color: #fbeaea;
border-right: 4px solid #dc3232;
}
.wrap .CodeMirror-lint-marker-error:before,
.wp-core-ui .CodeMirror-lint-message-error:before {
content: "\f153";
color: #dc3232;
}
.wp-core-ui .CodeMirror-lint-tooltip {
background: none;
border: none;
border-radius: 0;
direction: rtl;
}
.wrap .CodeMirror .CodeMirror-matchingbracket {
background: rgba(255, 150, 0, .3);
color: inherit;
}
.CodeMirror {
text-align: right;
}

2
wp-admin/css/code-editor-rtl.min.css vendored Normal file
View File

@ -0,0 +1,2 @@
/*! This file is auto-generated */
.wp-core-ui [class*=CodeMirror-lint-message],.wrap .CodeMirror-lint-marker-multiple,.wrap [class*=CodeMirror-lint-marker]{background-image:none}.wrap [class*=CodeMirror-lint-marker]:before{font:400 18px/1 dashicons;position:relative;top:-2px}.wp-core-ui [class*=CodeMirror-lint-message]:before{font:400 16px/1 dashicons;right:16px;position:absolute}.wp-core-ui .CodeMirror-lint-message-error,.wp-core-ui .CodeMirror-lint-message-warning{box-shadow:0 1px 1px 0 rgba(0,0,0,.1);margin:5px 0 2px;padding:3px 28px 3px 12px}.wp-core-ui .CodeMirror-lint-message-warning{background-color:#fff8e5;border-right:4px solid #ffb900}.wp-core-ui .CodeMirror-lint-message-warning:before,.wrap .CodeMirror-lint-marker-warning:before{content:"\f534";color:#f6a306}.wp-core-ui .CodeMirror-lint-message-error{background-color:#fbeaea;border-right:4px solid #dc3232}.wp-core-ui .CodeMirror-lint-message-error:before,.wrap .CodeMirror-lint-marker-error:before{content:"\f153";color:#dc3232}.wp-core-ui .CodeMirror-lint-tooltip{background:0 0;border:none;border-radius:0;direction:rtl}.wrap .CodeMirror .CodeMirror-matchingbracket{background:rgba(255,150,0,.3);color:inherit}.CodeMirror{text-align:right}

View File

@ -0,0 +1,62 @@
.wrap [class*="CodeMirror-lint-marker"],
.wp-core-ui [class*="CodeMirror-lint-message"],
.wrap .CodeMirror-lint-marker-multiple {
background-image: none;
}
.wrap [class*="CodeMirror-lint-marker"]:before {
font: normal 18px/1 dashicons;
position: relative;
top: -2px;
}
.wp-core-ui [class*="CodeMirror-lint-message"]:before {
font: normal 16px/1 dashicons;
left: 16px;
position: absolute;
}
.wp-core-ui .CodeMirror-lint-message-error,
.wp-core-ui .CodeMirror-lint-message-warning {
box-shadow: 0 1px 1px 0 rgba( 0, 0, 0, 0.1 );
margin: 5px 0 2px;
padding: 3px 12px 3px 28px;
}
.wp-core-ui .CodeMirror-lint-message-warning {
background-color: #fff8e5;
border-left: 4px solid #ffb900;
}
.wrap .CodeMirror-lint-marker-warning:before,
.wp-core-ui .CodeMirror-lint-message-warning:before {
content: "\f534";
color: #f6a306;
}
.wp-core-ui .CodeMirror-lint-message-error {
background-color: #fbeaea;
border-left: 4px solid #dc3232;
}
.wrap .CodeMirror-lint-marker-error:before,
.wp-core-ui .CodeMirror-lint-message-error:before {
content: "\f153";
color: #dc3232;
}
.wp-core-ui .CodeMirror-lint-tooltip {
background: none;
border: none;
border-radius: 0;
direction: ltr;
}
.wrap .CodeMirror .CodeMirror-matchingbracket {
background: rgba(255, 150, 0, .3);
color: inherit;
}
.CodeMirror {
text-align: left;
}

2
wp-admin/css/code-editor.min.css vendored Normal file
View File

@ -0,0 +1,2 @@
/*! This file is auto-generated */
.wp-core-ui [class*=CodeMirror-lint-message],.wrap .CodeMirror-lint-marker-multiple,.wrap [class*=CodeMirror-lint-marker]{background-image:none}.wrap [class*=CodeMirror-lint-marker]:before{font:400 18px/1 dashicons;position:relative;top:-2px}.wp-core-ui [class*=CodeMirror-lint-message]:before{font:400 16px/1 dashicons;left:16px;position:absolute}.wp-core-ui .CodeMirror-lint-message-error,.wp-core-ui .CodeMirror-lint-message-warning{box-shadow:0 1px 1px 0 rgba(0,0,0,.1);margin:5px 0 2px;padding:3px 12px 3px 28px}.wp-core-ui .CodeMirror-lint-message-warning{background-color:#fff8e5;border-left:4px solid #ffb900}.wp-core-ui .CodeMirror-lint-message-warning:before,.wrap .CodeMirror-lint-marker-warning:before{content:"\f534";color:#f6a306}.wp-core-ui .CodeMirror-lint-message-error{background-color:#fbeaea;border-left:4px solid #dc3232}.wp-core-ui .CodeMirror-lint-message-error:before,.wrap .CodeMirror-lint-marker-error:before{content:"\f153";color:#dc3232}.wp-core-ui .CodeMirror-lint-tooltip{background:0 0;border:none;border-radius:0;direction:ltr}.wrap .CodeMirror .CodeMirror-matchingbracket{background:rgba(255,150,0,.3);color:inherit}.CodeMirror{text-align:left}

View File

@ -2200,7 +2200,7 @@ h1.nav-tab-wrapper, /* Back-compat for pre-4.4 */
visibility: visible;
}
#template div {
#template > div {
margin-left: 190px;
}
@ -3015,13 +3015,18 @@ img {
#template textarea {
font-family: Consolas, Monaco, monospace;
font-size: 13px;
width: 97%;
background: #f9f9f9;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
}
#template textarea,
#template .CodeMirror {
width: 97%;
height: calc( 100vh - 220px );
}
/* rtl:ignore */
#template textarea,
#docs-list {
@ -3032,6 +3037,25 @@ img {
width: 97%;
}
#file-editor-linting-error {
margin-top: 1em;
margin-bottom: 1em;
}
#file-editor-linting-error > .notice {
margin: 0;
display: inline-block;
}
#file-editor-linting-error > .notice > p {
width: auto;
}
#template .submit {
margin-top: 1em;
padding: 0;
}
#template .submit input[type=submit][disabled] {
cursor: not-allowed;
}
#templateside {
float: left;
width: 190px;
@ -3585,12 +3609,13 @@ img {
margin-top: -5px;
}
#template div {
#template > div {
float: none;
margin: 0;
width: auto;
}
#template .CodeMirror,
#template textarea {
width: 100%;
}

File diff suppressed because one or more lines are too long

View File

@ -2200,7 +2200,7 @@ h1.nav-tab-wrapper, /* Back-compat for pre-4.4 */
visibility: visible;
}
#template div {
#template > div {
margin-right: 190px;
}
@ -3015,13 +3015,18 @@ img {
#template textarea {
font-family: Consolas, Monaco, monospace;
font-size: 13px;
width: 97%;
background: #f9f9f9;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
}
#template textarea,
#template .CodeMirror {
width: 97%;
height: calc( 100vh - 220px );
}
/* rtl:ignore */
#template textarea,
#docs-list {
@ -3032,6 +3037,25 @@ img {
width: 97%;
}
#file-editor-linting-error {
margin-top: 1em;
margin-bottom: 1em;
}
#file-editor-linting-error > .notice {
margin: 0;
display: inline-block;
}
#file-editor-linting-error > .notice > p {
width: auto;
}
#template .submit {
margin-top: 1em;
padding: 0;
}
#template .submit input[type=submit][disabled] {
cursor: not-allowed;
}
#templateside {
float: right;
width: 190px;
@ -3585,12 +3609,13 @@ img {
margin-top: -5px;
}
#template div {
#template > div {
float: none;
margin: 0;
width: auto;
}
#template .CodeMirror,
#template textarea {
width: 100%;
}

File diff suppressed because one or more lines are too long

View File

@ -550,6 +550,20 @@ p.customize-section-description {
margin-top: 22px;
margin-bottom: 0;
}
.customize-section-description ul {
margin-right: 1em;
}
.customize-section-description ul > li {
list-style: disc;
}
.section-description-buttons {
text-align: left;
}
.section-description-buttons button.button-link {
color: #0073aa;
text-decoration: underline;
}
.customize-control {
width: 100%;
@ -1154,6 +1168,19 @@ p.customize-section-description {
margin-bottom: -12px;
}
.customize-section-description-container + #customize-control-custom_css:last-child .CodeMirror {
height: calc( 100vh - 185px );
}
.CodeMirror-lint-tooltip,
.CodeMirror-hints {
z-index: 500000 !important;
}
.customize-section-description-container + #customize-control-custom_css:last-child .customize-control-notifications-container {
margin-right: 12px;
margin-left: 12px;
}
@media screen and ( max-width: 640px ) {
.customize-section-description-container + #customize-control-custom_css:last-child {
margin-left: 0;

File diff suppressed because one or more lines are too long

View File

@ -550,6 +550,20 @@ p.customize-section-description {
margin-top: 22px;
margin-bottom: 0;
}
.customize-section-description ul {
margin-left: 1em;
}
.customize-section-description ul > li {
list-style: disc;
}
.section-description-buttons {
text-align: right;
}
.section-description-buttons button.button-link {
color: #0073aa;
text-decoration: underline;
}
.customize-control {
width: 100%;
@ -1154,6 +1168,19 @@ p.customize-section-description {
margin-bottom: -12px;
}
.customize-section-description-container + #customize-control-custom_css:last-child .CodeMirror {
height: calc( 100vh - 185px );
}
.CodeMirror-lint-tooltip,
.CodeMirror-hints {
z-index: 500000 !important;
}
.customize-section-description-container + #customize-control-custom_css:last-child .customize-control-notifications-container {
margin-left: 12px;
margin-right: 12px;
}
@media screen and ( max-width: 640px ) {
.customize-section-description-container + #customize-control-custom_css:last-child {
margin-right: 0;

File diff suppressed because one or more lines are too long

View File

@ -645,6 +645,20 @@ div#widgets-right .widget-top:hover,
line-height: 1.4em;
}
.custom-html-widget-fields > p > .CodeMirror {
border: 1px solid #e5e5e5;
}
.custom-html-widget-fields code {
padding-top: 1px;
padding-bottom: 1px;
}
ul.CodeMirror-hints {
z-index: 101; /* Due to z-index 100 set on .widget.open */
}
.widget-control-actions .custom-html-widget-save-button.button.validation-blocked {
cursor: not-allowed;
}
/* =Media Queries
-------------------------------------------------------------- */

File diff suppressed because one or more lines are too long

View File

@ -645,6 +645,20 @@ div#widgets-right .widget-top:hover,
line-height: 1.4em;
}
.custom-html-widget-fields > p > .CodeMirror {
border: 1px solid #e5e5e5;
}
.custom-html-widget-fields code {
padding-top: 1px;
padding-bottom: 1px;
}
ul.CodeMirror-hints {
z-index: 101; /* Due to z-index 100 set on .widget.open */
}
.widget-control-actions .custom-html-widget-save-button.button.validation-blocked {
cursor: not-allowed;
}
/* =Media Queries
-------------------------------------------------------------- */

File diff suppressed because one or more lines are too long

View File

@ -91,7 +91,8 @@ function edit_user( $user_id = 0 ) {
}
if ( $update ) {
$user->rich_editing = isset( $_POST['rich_editing'] ) && 'false' == $_POST['rich_editing'] ? 'false' : 'true';
$user->rich_editing = isset( $_POST['rich_editing'] ) && 'false' === $_POST['rich_editing'] ? 'false' : 'true';
$user->syntax_highlighting = isset( $_POST['syntax_highlighting'] ) && 'false' === $_POST['syntax_highlighting'] ? 'false' : 'true';
$user->admin_color = isset( $_POST['admin_color'] ) ? sanitize_text_field( $_POST['admin_color'] ) : 'fresh';
$user->show_admin_bar_front = isset( $_POST['admin_bar_front'] ) ? 'true' : 'false';
$user->locale = '';

309
wp-admin/js/code-editor.js Normal file
View File

@ -0,0 +1,309 @@
if ( 'undefined' === typeof window.wp ) {
window.wp = {};
}
if ( 'undefined' === typeof window.wp.codeEditor ) {
window.wp.codeEditor = {};
}
( function( $, wp ) {
'use strict';
/**
* Default settings for code editor.
*
* @since 4.9.0
* @type {object}
*/
wp.codeEditor.defaultSettings = {
codemirror: {},
csslint: {},
htmlhint: {},
jshint: {},
onTabNext: function() {},
onTabPrevious: function() {},
onChangeLintingErrors: function() {},
onUpdateErrorNotice: function() {}
};
/**
* All instances of code editors.
*
* @since 4.9.0
* @type {Array.<CodeEditorInstance>}
*/
wp.codeEditor.instances = [];
/**
* Configure linting.
*
* @param {CodeMirror} editor - Editor.
* @param {object} settings - Code editor settings.
* @param {object} settings.codeMirror - Settings for CodeMirror.
* @param {Function} settings.onChangeLintingErrors - Callback for when there are changes to linting errors.
* @param {Function} settings.onUpdateErrorNotice - Callback to update error notice.
* @returns {void}
*/
function configureLinting( editor, settings ) { // eslint-disable-line complexity
var lintOptions = editor.getOption( 'lint' ), currentErrorAnnotations = [], updateErrorNotice, previouslyShownErrorAnnotations = [];
if ( ! lintOptions ) {
return;
}
if ( true === lintOptions ) {
lintOptions = {};
} else {
lintOptions = $.extend( {}, lintOptions );
}
// Note that rules must be sent in the "deprecated" lint.options property to prevent linter from complaining about unrecognized options. See <https://github.com/codemirror/CodeMirror/pull/4944>.
if ( ! lintOptions.options ) {
lintOptions.options = {};
}
// Configure JSHint.
if ( 'javascript' === settings.codemirror.mode && settings.jshint ) {
$.extend( lintOptions.options, settings.jshint );
}
// Configure CSSLint.
if ( 'css' === settings.codemirror.mode && settings.csslint ) {
$.extend( lintOptions.options, settings.csslint );
}
// Configure HTMLHint.
if ( 'htmlmixed' === settings.codemirror.mode && settings.htmlhint ) {
lintOptions.options.rules = $.extend( {}, settings.htmlhint );
if ( settings.jshint ) {
lintOptions.options.rules.jshint = settings.jshint;
}
if ( settings.csslint ) {
lintOptions.options.rules.csslint = settings.csslint;
}
}
/**
* Call the onUpdateErrorNotice if there are new errors to show.
*
* @returns {void}
*/
updateErrorNotice = function() {
if ( settings.onUpdateErrorNotice && ! _.isEqual( currentErrorAnnotations, previouslyShownErrorAnnotations ) ) {
settings.onUpdateErrorNotice( currentErrorAnnotations, editor );
previouslyShownErrorAnnotations = currentErrorAnnotations;
}
};
// Wrap the onUpdateLinting CodeMirror event to route to onChangeLintingErrors and onUpdateErrorNotice.
lintOptions.onUpdateLinting = (function( onUpdateLintingOverridden ) {
return function( annotations, annotationsSorted, cm ) {
var errorAnnotations = _.filter( annotations, function( annotation ) {
return 'error' === annotation.severity;
} );
if ( onUpdateLintingOverridden ) {
onUpdateLintingOverridden.apply( annotations, annotationsSorted, cm );
}
// Skip if there are no changes to the errors.
if ( _.isEqual( errorAnnotations, currentErrorAnnotations ) ) {
return;
}
currentErrorAnnotations = errorAnnotations;
if ( settings.onChangeLintingErrors ) {
settings.onChangeLintingErrors( errorAnnotations, annotations, annotationsSorted, cm );
}
/*
* Update notifications when the editor is not focused to prevent error message
* from overwhelming the user during input, unless there are now no errors or there
* were previously errors shown. In these cases, update immediately so they can know
* that they fixed the errors.
*/
if ( ! cm.state.focused || 0 === currentErrorAnnotations.length || previouslyShownErrorAnnotations.length > 0 ) {
updateErrorNotice();
}
};
})( lintOptions.onUpdateLinting );
editor.setOption( 'lint', lintOptions );
// Update error notice when leaving the editor.
editor.on( 'blur', updateErrorNotice );
// Work around hint selection with mouse causing focus to leave editor.
editor.on( 'startCompletion', function() {
editor.off( 'blur', updateErrorNotice );
} );
editor.on( 'endCompletion', function() {
var editorRefocusWait = 500;
editor.on( 'blur', updateErrorNotice );
// Wait for editor to possibly get re-focused after selection.
_.delay( function() {
if ( ! editor.state.focused ) {
updateErrorNotice();
}
}, editorRefocusWait );
});
/*
* Make sure setting validities are set if the user tries to click Publish
* while an autocomplete dropdown is still open. The Customizer will block
* saving when a setting has an error notifications on it. This is only
* necessary for mouse interactions because keyboards will have already
* blurred the field and cause onUpdateErrorNotice to have already been
* called.
*/
$( document.body ).on( 'mousedown', function( event ) {
if ( editor.state.focused && ! $.contains( editor.display.wrapper, event.target ) && ! $( event.target ).hasClass( 'CodeMirror-hint' ) ) {
updateErrorNotice();
}
});
}
/**
* Configure tabbing.
*
* @param {CodeMirror} codemirror - Editor.
* @param {object} settings - Code editor settings.
* @param {object} settings.codeMirror - Settings for CodeMirror.
* @param {Function} settings.onTabNext - Callback to handle tabbing to the next tabbable element.
* @param {Function} settings.onTabPrevious - Callback to handle tabbing to the previous tabbable element.
* @returns {void}
*/
function configureTabbing( codemirror, settings ) {
var $textarea = $( codemirror.getTextArea() );
codemirror.on( 'blur', function() {
$textarea.data( 'next-tab-blurs', false );
});
codemirror.on( 'focus', function() {
if ( codemirror.display.wrapper.scrollIntoViewIfNeeded ) {
codemirror.display.wrapper.scrollIntoViewIfNeeded();
} else {
codemirror.display.wrapper.scrollIntoView();
}
});
codemirror.on( 'keydown', function onKeydown( editor, event ) {
var tabKeyCode = 9, escKeyCode = 27;
// Take note of the ESC keypress so that the next TAB can focus outside the editor.
if ( escKeyCode === event.keyCode ) {
$textarea.data( 'next-tab-blurs', true );
return;
}
// Short-circuit if tab key is not being pressed or the tab key press should move focus.
if ( tabKeyCode !== event.keyCode || ! $textarea.data( 'next-tab-blurs' ) ) {
return;
}
// Focus on previous or next focusable item.
if ( event.shiftKey ) {
settings.onTabPrevious( codemirror, event );
} else {
settings.onTabNext( codemirror, event );
}
// Reset tab state.
$textarea.data( 'next-tab-blurs', false );
// Prevent tab character from being added.
event.preventDefault();
});
}
/**
* @typedef {object} CodeEditorInstance
* @property {object} settings - The code editor settings.
* @property {CodeMirror} codemirror - The CodeMirror instance.
*/
/**
* Initialize Code Editor (CodeMirror) for an existing textarea.
*
* @since 4.9.0
*
* @param {string|jQuery|Element} textarea - The HTML id, jQuery object, or DOM Element for the textarea that is used for the editor.
* @param {object} [settings] - Settings to override defaults.
* @param {Function} [settings.onChangeLintingErrors] - Callback for when the linting errors have changed.
* @param {Function} [settings.onUpdateErrorNotice] - Callback for when error notice should be displayed.
* @param {Function} [settings.onTabPrevious] - Callback to handle tabbing to the previous tabbable element.
* @param {Function} [settings.onTabNext] - Callback to handle tabbing to the next tabbable element.
* @param {object} [settings.codemirror] - Options for CodeMirror.
* @param {object} [settings.csslint] - Rules for CSSLint.
* @param {object} [settings.htmlhint] - Rules for HTMLHint.
* @param {object} [settings.jshint] - Rules for JSHint.
* @returns {CodeEditorInstance} Instance.
*/
wp.codeEditor.initialize = function initialize( textarea, settings ) {
var $textarea, codemirror, instanceSettings, instance;
if ( 'string' === typeof textarea ) {
$textarea = $( '#' + textarea );
} else {
$textarea = $( textarea );
}
instanceSettings = $.extend( {}, wp.codeEditor.defaultSettings, settings );
instanceSettings.codemirror = $.extend( {}, instanceSettings.codemirror );
codemirror = wp.CodeMirror.fromTextArea( $textarea[0], instanceSettings.codemirror );
configureLinting( codemirror, instanceSettings );
instance = {
settings: instanceSettings,
codemirror: codemirror
};
// Keep track of the instances that have been created.
wp.codeEditor.instances.push( instance );
if ( codemirror.showHint ) {
codemirror.on( 'keyup', function( editor, event ) { // eslint-disable-line complexity
var shouldAutocomplete, isAlphaKey = /^[a-zA-Z]$/.test( event.key ), lineBeforeCursor, innerMode, token;
if ( codemirror.state.completionActive && isAlphaKey ) {
return;
}
// Prevent autocompletion in string literals or comments.
token = codemirror.getTokenAt( codemirror.getCursor() );
if ( 'string' === token.type || 'comment' === token.type ) {
return;
}
innerMode = wp.CodeMirror.innerMode( codemirror.getMode(), token.state ).mode.name;
lineBeforeCursor = codemirror.doc.getLine( codemirror.doc.getCursor().line ).substr( 0, codemirror.doc.getCursor().ch );
if ( 'html' === innerMode || 'xml' === innerMode ) {
shouldAutocomplete =
'<' === event.key ||
'/' === event.key && 'tag' === token.type ||
isAlphaKey && 'tag' === token.type ||
isAlphaKey && 'attribute' === token.type ||
'=' === token.string && token.state.htmlState && token.state.htmlState.tagName;
} else if ( 'css' === innerMode ) {
shouldAutocomplete =
isAlphaKey ||
':' === event.key ||
' ' === event.key && /:\s+$/.test( lineBeforeCursor );
} else if ( 'javascript' === innerMode ) {
shouldAutocomplete = isAlphaKey || '.' === event.key;
} else if ( 'clike' === innerMode && 'application/x-httpd-php' === codemirror.options.mode ) {
shouldAutocomplete = 'keyword' === token.type || 'variable' === token.type;
}
if ( shouldAutocomplete ) {
codemirror.showHint( { completeSingle: false } );
}
});
}
// Facilitate tabbing out of the editor.
configureTabbing( codemirror, settings );
return instance;
};
})( window.jQuery, window.wp );

1
wp-admin/js/code-editor.min.js vendored Normal file
View File

@ -0,0 +1 @@
"undefined"==typeof window.wp&&(window.wp={}),"undefined"==typeof window.wp.codeEditor&&(window.wp.codeEditor={}),function(a,b){"use strict";function c(b,c){var d,e=b.getOption("lint"),f=[],g=[];e&&(e=!0===e?{}:a.extend({},e),e.options||(e.options={}),"javascript"===c.codemirror.mode&&c.jshint&&a.extend(e.options,c.jshint),"css"===c.codemirror.mode&&c.csslint&&a.extend(e.options,c.csslint),"htmlmixed"===c.codemirror.mode&&c.htmlhint&&(e.options.rules=a.extend({},c.htmlhint),c.jshint&&(e.options.rules.jshint=c.jshint),c.csslint&&(e.options.rules.csslint=c.csslint)),d=function(){c.onUpdateErrorNotice&&!_.isEqual(f,g)&&(c.onUpdateErrorNotice(f,b),g=f)},e.onUpdateLinting=function(a){return function(b,e,h){var i=_.filter(b,function(a){return"error"===a.severity});a&&a.apply(b,e,h),_.isEqual(i,f)||(f=i,c.onChangeLintingErrors&&c.onChangeLintingErrors(i,b,e,h),(!h.state.focused||0===f.length||g.length>0)&&d())}}(e.onUpdateLinting),b.setOption("lint",e),b.on("blur",d),b.on("startCompletion",function(){b.off("blur",d)}),b.on("endCompletion",function(){var a=500;b.on("blur",d),_.delay(function(){b.state.focused||d()},a)}),a(document.body).on("mousedown",function(c){!b.state.focused||a.contains(b.display.wrapper,c.target)||a(c.target).hasClass("CodeMirror-hint")||d()}))}function d(b,c){var d=a(b.getTextArea());b.on("blur",function(){d.data("next-tab-blurs",!1)}),b.on("focus",function(){b.display.wrapper.scrollIntoViewIfNeeded?b.display.wrapper.scrollIntoViewIfNeeded():b.display.wrapper.scrollIntoView()}),b.on("keydown",function(a,e){var f=9,g=27;return g===e.keyCode?void d.data("next-tab-blurs",!0):void(f===e.keyCode&&d.data("next-tab-blurs")&&(e.shiftKey?c.onTabPrevious(b,e):c.onTabNext(b,e),d.data("next-tab-blurs",!1),e.preventDefault()))})}b.codeEditor.defaultSettings={codemirror:{},csslint:{},htmlhint:{},jshint:{},onTabNext:function(){},onTabPrevious:function(){},onChangeLintingErrors:function(){},onUpdateErrorNotice:function(){}},b.codeEditor.instances=[],b.codeEditor.initialize=function(e,f){var g,h,i,j;return g=a("string"==typeof e?"#"+e:e),i=a.extend({},b.codeEditor.defaultSettings,f),i.codemirror=a.extend({},i.codemirror),h=b.CodeMirror.fromTextArea(g[0],i.codemirror),c(h,i),j={settings:i,codemirror:h},b.codeEditor.instances.push(j),h.showHint&&h.on("keyup",function(a,c){var d,e,f,g,i=/^[a-zA-Z]$/.test(c.key);h.state.completionActive&&i||(g=h.getTokenAt(h.getCursor()),"string"!==g.type&&"comment"!==g.type&&(f=b.CodeMirror.innerMode(h.getMode(),g.state).mode.name,e=h.doc.getLine(h.doc.getCursor().line).substr(0,h.doc.getCursor().ch),"html"===f||"xml"===f?d="<"===c.key||"/"===c.key&&"tag"===g.type||i&&"tag"===g.type||i&&"attribute"===g.type||"="===g.string&&g.state.htmlState&&g.state.htmlState.tagName:"css"===f?d=i||":"===c.key||" "===c.key&&/:\s+$/.test(e):"javascript"===f?d=i||"."===c.key:"clike"===f&&"application/x-httpd-php"===h.options.mode&&(d="keyword"===g.type||"variable"===g.type),d&&h.showHint({completeSingle:!1})))}),d(h,f),j}}(window.jQuery,window.wp);

View File

@ -5631,9 +5631,170 @@
});
});
// Add code editor for Custom CSS.
(function() {
var ready, sectionReady = $.Deferred(), controlReady = $.Deferred();
api.section( 'custom_css', function( section ) {
section.deferred.embedded.done( function() {
if ( section.expanded() ) {
sectionReady.resolve( section );
} else {
section.expanded.bind( function( isExpanded ) {
if ( isExpanded ) {
sectionReady.resolve( section );
}
} );
}
});
});
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 ) {
// Close the section description when clicking the close button.
section.container.find( '.section-description-buttons .section-description-close' ).on( 'click', function() {
section.container.find( '.section-meta .customize-section-description:first' )
.removeClass( 'open' )
.slideUp()
.attr( 'aria-expanded', 'false' );
});
// Reveal help text if setting is empty.
if ( ! 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.
api.control( 'custom_css', function setupCustomCssControl( control ) {
control.deferred.embedded.done( function allowTabs() {
ready.done( function allowTabs( section, control ) {
var $textarea = control.container.find( 'textarea' ), textarea = $textarea[0];
$textarea.on( 'blur', function onBlur() {
@ -5672,9 +5833,10 @@
event.stopPropagation();
event.preventDefault();
} );
} );
} );
});
});
}
})();
// Toggle visibility of Header Video notice when active state change.
api.control( 'header_video', function( headerVideoControl ) {

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,90 @@
/* eslint no-magic-numbers: ["error", { "ignore": [-1, 0, 1] }] */
if ( ! window.wp ) {
window.wp = {};
}
wp.themePluginEditor = (function( $ ) {
'use strict';
var component = {
l10n: {
lintError: {
singular: '',
plural: ''
}
},
instance: null
};
/**
* Initialize component.
*
* @param {object} settings Settings.
* @returns {void}
*/
component.init = function( settings ) {
var codeEditorSettings, noticeContainer, errorNotice = [], editor;
codeEditorSettings = $.extend( {}, settings );
/**
* Handle tabbing to the field before the editor.
*
* @returns {void}
*/
codeEditorSettings.onTabPrevious = function() {
$( '#templateside' ).find( ':tabbable' ).last().focus();
};
/**
* Handle tabbing to the field after the editor.
*
* @returns {void}
*/
codeEditorSettings.onTabNext = function() {
$( '#template' ).find( ':tabbable:not(.CodeMirror-code)' ).first().focus();
};
// Create the error notice container.
noticeContainer = $( '<div id="file-editor-linting-error"></div>' );
errorNotice = $( '<div class="inline notice notice-error"></div>' );
noticeContainer.append( errorNotice );
noticeContainer.hide();
$( 'p.submit' ).before( noticeContainer );
/**
* Update error notice.
*
* @param {Array} errorAnnotations - Error annotations.
* @returns {void}
*/
codeEditorSettings.onUpdateErrorNotice = function onUpdateErrorNotice( errorAnnotations ) {
var message;
$( '#submit' ).prop( 'disabled', 0 !== errorAnnotations.length );
if ( 0 !== errorAnnotations.length ) {
errorNotice.empty();
if ( 1 === errorAnnotations.length ) {
message = component.l10n.singular.replace( '%d', '1' );
} else {
message = component.l10n.plural.replace( '%d', String( errorAnnotations.length ) );
}
errorNotice.append( $( '<p></p>', {
text: message
} ) );
noticeContainer.slideDown( 'fast' );
wp.a11y.speak( message );
} else {
noticeContainer.slideUp( 'fast' );
}
};
editor = wp.codeEditor.initialize( $( '#newcontent' ), codeEditorSettings );
component.instance = editor;
};
return component;
})( jQuery );

View File

@ -0,0 +1 @@
window.wp||(window.wp={}),wp.themePluginEditor=function(a){"use strict";var b={l10n:{lintError:{singular:"",plural:""}},instance:null};return b.init=function(c){var d,e,f,g=[];d=a.extend({},c),d.onTabPrevious=function(){a("#templateside").find(":tabbable").last().focus()},d.onTabNext=function(){a("#template").find(":tabbable:not(.CodeMirror-code)").first().focus()},e=a('<div id="file-editor-linting-error"></div>'),g=a('<div class="inline notice notice-error"></div>'),e.append(g),e.hide(),a("p.submit").before(e),d.onUpdateErrorNotice=function(c){var d;a("#submit").prop("disabled",0!==c.length),0!==c.length?(g.empty(),d=1===c.length?b.l10n.singular.replace("%d","1"):b.l10n.plural.replace("%d",String(c.length)),g.append(a("<p></p>",{text:d})),e.slideDown("fast"),wp.a11y.speak(d)):e.slideUp("fast")},f=wp.codeEditor.initialize(a("#newcontent"),d),b.instance=f},b}(jQuery);

View File

@ -0,0 +1,414 @@
/* global wp */
/* eslint consistent-this: [ "error", "control" ] */
/* eslint no-magic-numbers: ["error", { "ignore": [0,1,-1] }] */
wp.customHtmlWidgets = ( function( $ ) {
'use strict';
var component = {
idBases: [ 'custom_html' ],
codeEditorSettings: {},
l10n: {
errorNotice: {
singular: '',
plural: ''
}
}
};
/**
* Text widget control.
*
* @class CustomHtmlWidgetControl
* @constructor
* @abstract
*/
component.CustomHtmlWidgetControl = Backbone.View.extend({
/**
* View events.
*
* @type {Object}
*/
events: {},
/**
* Initialize.
*
* @param {Object} options - Options.
* @param {jQuery} options.el - Control field container element.
* @param {jQuery} options.syncContainer - Container element where fields are synced for the server.
* @returns {void}
*/
initialize: function initialize( options ) {
var control = this;
if ( ! options.el ) {
throw new Error( 'Missing options.el' );
}
if ( ! options.syncContainer ) {
throw new Error( 'Missing options.syncContainer' );
}
Backbone.View.prototype.initialize.call( control, options );
control.syncContainer = options.syncContainer;
control.widgetIdBase = control.syncContainer.parent().find( '.id_base' ).val();
control.widgetNumber = control.syncContainer.parent().find( '.widget_number' ).val();
control.customizeSettingId = 'widget_' + control.widgetIdBase + '[' + String( control.widgetNumber ) + ']';
control.$el.addClass( 'custom-html-widget-fields' );
control.$el.html( wp.template( 'widget-custom-html-control-fields' )( { codeEditorDisabled: component.codeEditorSettings.disabled } ) );
control.errorNoticeContainer = control.$el.find( '.code-editor-error-container' );
control.currentErrorAnnotations = [];
control.saveButton = control.syncContainer.add( control.syncContainer.parent().find( '.widget-control-actions' ) ).find( '.widget-control-save, #savewidget' );
control.saveButton.addClass( 'custom-html-widget-save-button' ); // To facilitate style targeting.
control.fields = {
title: control.$el.find( '.title' ),
content: control.$el.find( '.content' )
};
// Sync input fields to hidden sync fields which actually get sent to the server.
_.each( control.fields, function( fieldInput, fieldName ) {
fieldInput.on( 'input change', function updateSyncField() {
var syncInput = control.syncContainer.find( '.sync-input.' + fieldName );
if ( syncInput.val() !== fieldInput.val() ) {
syncInput.val( fieldInput.val() );
syncInput.trigger( 'change' );
}
});
// Note that syncInput cannot be re-used because it will be destroyed with each widget-updated event.
fieldInput.val( control.syncContainer.find( '.sync-input.' + fieldName ).val() );
});
},
/**
* Update input fields from the sync fields.
*
* This function is called at the widget-updated and widget-synced events.
* A field will only be updated if it is not currently focused, to avoid
* overwriting content that the user is entering.
*
* @returns {void}
*/
updateFields: function updateFields() {
var control = this, syncInput;
if ( ! control.fields.title.is( document.activeElement ) ) {
syncInput = control.syncContainer.find( '.sync-input.title' );
control.fields.title.val( syncInput.val() );
}
/*
* Prevent updating content when the editor is focused or if there are current error annotations,
* to prevent the editor's contents from getting sanitized as soon as a user removes focus from
* the editor. This is particularly important for users who cannot unfiltered_html.
*/
control.contentUpdateBypassed = control.fields.content.is( document.activeElement ) || control.editor && control.editor.codemirror.state.focused || 0 !== control.currentErrorAnnotations;
if ( ! control.contentUpdateBypassed ) {
syncInput = control.syncContainer.find( '.sync-input.content' );
control.fields.content.val( syncInput.val() ).trigger( 'change' );
}
},
/**
* Show linting error notice.
*
* @param {Array} errorAnnotations - Error annotations.
* @returns {void}
*/
updateErrorNotice: function( errorAnnotations ) {
var control = this, errorNotice, message = '', customizeSetting;
if ( 1 === errorAnnotations.length ) {
message = component.l10n.errorNotice.singular.replace( '%d', '1' );
} else if ( errorAnnotations.length > 1 ) {
message = component.l10n.errorNotice.plural.replace( '%d', String( errorAnnotations.length ) );
}
if ( control.fields.content[0].setCustomValidity ) {
control.fields.content[0].setCustomValidity( message );
}
if ( wp.customize && wp.customize.has( control.customizeSettingId ) ) {
customizeSetting = wp.customize( control.customizeSettingId );
customizeSetting.notifications.remove( 'htmlhint_error' );
if ( 0 !== errorAnnotations.length ) {
customizeSetting.notifications.add( 'htmlhint_error', new wp.customize.Notification( 'htmlhint_error', {
message: message,
type: 'error'
} ) );
}
} else if ( 0 !== errorAnnotations.length ) {
errorNotice = $( '<div class="inline notice notice-error notice-alt"></div>' );
errorNotice.append( $( '<p></p>', {
text: message
} ) );
control.errorNoticeContainer.empty();
control.errorNoticeContainer.append( errorNotice );
control.errorNoticeContainer.slideDown( 'fast' );
wp.a11y.speak( message );
} else {
control.errorNoticeContainer.slideUp( 'fast' );
}
},
/**
* Initialize editor.
*
* @returns {void}
*/
initializeEditor: function initializeEditor() {
var control = this, settings;
if ( component.codeEditorSettings.disabled ) {
return;
}
settings = _.extend( {}, component.codeEditorSettings, {
/**
* Handle tabbing to the field before the editor.
*
* @returns {void}
*/
onTabPrevious: function onTabPrevious() {
control.fields.title.focus();
},
/**
* Handle tabbing to the field after the editor.
*
* @returns {void}
*/
onTabNext: function onTabNext() {
var tabbables = control.syncContainer.add( control.syncContainer.parent().find( '.widget-position, .widget-control-actions' ) ).find( ':tabbable' );
tabbables.first().focus();
},
/**
* Disable save button and store linting errors for use in updateFields.
*
* @param {Array} errorAnnotations - Error notifications.
* @returns {void}
*/
onChangeLintingErrors: function onChangeLintingErrors( errorAnnotations ) {
control.currentErrorAnnotations = errorAnnotations;
},
/**
* Update error notice.
*
* @param {Array} errorAnnotations - Error annotations.
* @returns {void}
*/
onUpdateErrorNotice: function onUpdateErrorNotice( errorAnnotations ) {
control.saveButton.toggleClass( 'validation-blocked', errorAnnotations.length );
control.updateErrorNotice( errorAnnotations );
}
});
control.editor = wp.codeEditor.initialize( control.fields.content, settings );
control.fields.content.on( 'change', function() {
if ( this.value !== control.editor.codemirror.getValue() ) {
control.editor.codemirror.setValue( this.value );
}
});
control.editor.codemirror.on( 'change', function() {
var value = control.editor.codemirror.getValue();
if ( value !== control.fields.content.val() ) {
control.fields.content.val( value ).trigger( 'change' );
}
});
// Make sure the editor gets updated if the content was updated on the server (sanitization) but not updated in the editor since it was focused.
control.editor.codemirror.on( 'blur', function() {
if ( control.contentUpdateBypassed ) {
control.syncContainer.find( '.sync-input.content' ).trigger( 'change' );
}
});
// Prevent hitting Esc from collapsing the widget control.
if ( wp.customize ) {
control.editor.codemirror.on( 'keydown', function onKeydown( codemirror, event ) {
var escKeyCode = 27;
if ( escKeyCode === event.keyCode ) {
event.stopPropagation();
}
});
}
}
});
/**
* Mapping of widget ID to instances of CustomHtmlWidgetControl subclasses.
*
* @type {Object.<string, wp.textWidgets.CustomHtmlWidgetControl>}
*/
component.widgetControls = {};
/**
* Handle widget being added or initialized for the first time at the widget-added event.
*
* @param {jQuery.Event} event - Event.
* @param {jQuery} widgetContainer - Widget container element.
* @returns {void}
*/
component.handleWidgetAdded = function handleWidgetAdded( event, widgetContainer ) {
var widgetForm, idBase, widgetControl, widgetId, animatedCheckDelay = 50, renderWhenAnimationDone, fieldContainer, syncContainer;
widgetForm = widgetContainer.find( '> .widget-inside > .form, > .widget-inside > form' ); // Note: '.form' appears in the customizer, whereas 'form' on the widgets admin screen.
idBase = widgetForm.find( '> .id_base' ).val();
if ( -1 === component.idBases.indexOf( idBase ) ) {
return;
}
// Prevent initializing already-added widgets.
widgetId = widgetForm.find( '.widget-id' ).val();
if ( component.widgetControls[ widgetId ] ) {
return;
}
/*
* Create a container element for the widget control fields.
* This is inserted into the DOM immediately before the the .widget-content
* element because the contents of this element are essentially "managed"
* by PHP, where each widget update cause the entire element to be emptied
* and replaced with the rendered output of WP_Widget::form() which is
* sent back in Ajax request made to save/update the widget instance.
* To prevent a "flash of replaced DOM elements and re-initialized JS
* components", the JS template is rendered outside of the normal form
* container.
*/
fieldContainer = $( '<div></div>' );
syncContainer = widgetContainer.find( '.widget-content:first' );
syncContainer.before( fieldContainer );
widgetControl = new component.CustomHtmlWidgetControl({
el: fieldContainer,
syncContainer: syncContainer
});
component.widgetControls[ widgetId ] = widgetControl;
/*
* Render the widget once the widget parent's container finishes animating,
* as the widget-added event fires with a slideDown of the container.
* This ensures that the textarea is visible and the editor can be initialized.
*/
renderWhenAnimationDone = function() {
if ( ! ( wp.customize ? widgetContainer.parent().hasClass( 'expanded' ) : widgetContainer.hasClass( 'open' ) ) ) { // Core merge: The wp.customize condition can be eliminated with this change being in core: https://github.com/xwp/wordpress-develop/pull/247/commits/5322387d
setTimeout( renderWhenAnimationDone, animatedCheckDelay );
} else {
widgetControl.initializeEditor();
}
};
renderWhenAnimationDone();
};
/**
* Setup widget in accessibility mode.
*
* @returns {void}
*/
component.setupAccessibleMode = function setupAccessibleMode() {
var widgetForm, idBase, widgetControl, fieldContainer, syncContainer;
widgetForm = $( '.editwidget > form' );
if ( 0 === widgetForm.length ) {
return;
}
idBase = widgetForm.find( '> .widget-control-actions > .id_base' ).val();
if ( -1 === component.idBases.indexOf( idBase ) ) {
return;
}
fieldContainer = $( '<div></div>' );
syncContainer = widgetForm.find( '> .widget-inside' );
syncContainer.before( fieldContainer );
widgetControl = new component.CustomHtmlWidgetControl({
el: fieldContainer,
syncContainer: syncContainer
});
widgetControl.initializeEditor();
};
/**
* Sync widget instance data sanitized from server back onto widget model.
*
* This gets called via the 'widget-updated' event when saving a widget from
* the widgets admin screen and also via the 'widget-synced' event when making
* a change to a widget in the customizer.
*
* @param {jQuery.Event} event - Event.
* @param {jQuery} widgetContainer - Widget container element.
* @returns {void}
*/
component.handleWidgetUpdated = function handleWidgetUpdated( event, widgetContainer ) {
var widgetForm, widgetId, widgetControl, idBase;
widgetForm = widgetContainer.find( '> .widget-inside > .form, > .widget-inside > form' );
idBase = widgetForm.find( '> .id_base' ).val();
if ( -1 === component.idBases.indexOf( idBase ) ) {
return;
}
widgetId = widgetForm.find( '> .widget-id' ).val();
widgetControl = component.widgetControls[ widgetId ];
if ( ! widgetControl ) {
return;
}
widgetControl.updateFields();
};
/**
* Initialize functionality.
*
* This function exists to prevent the JS file from having to boot itself.
* When WordPress enqueues this script, it should have an inline script
* attached which calls wp.textWidgets.init().
*
* @param {object} settings - Options for code editor, exported from PHP.
* @returns {void}
*/
component.init = function init( settings ) {
var $document = $( document );
_.extend( component.codeEditorSettings, settings );
$document.on( 'widget-added', component.handleWidgetAdded );
$document.on( 'widget-synced widget-updated', component.handleWidgetUpdated );
/*
* Manually trigger widget-added events for media widgets on the admin
* screen once they are expanded. The widget-added event is not triggered
* for each pre-existing widget on the widgets admin screen like it is
* on the customizer. Likewise, the customizer only triggers widget-added
* when the widget is expanded to just-in-time construct the widget form
* when it is actually going to be displayed. So the following implements
* the same for the widgets admin screen, to invoke the widget-added
* handler when a pre-existing media widget is expanded.
*/
$( function initializeExistingWidgetContainers() {
var widgetContainers;
if ( 'widgets' !== window.pagenow ) {
return;
}
widgetContainers = $( '.widgets-holder-wrap:not(#available-widgets)' ).find( 'div.widget' );
widgetContainers.one( 'click.toggle-widget-expanded', function toggleWidgetExpanded() {
var widgetContainer = $( this );
component.handleWidgetAdded( new jQuery.Event( 'widget-added' ), widgetContainer );
});
// Accessibility mode.
$( window ).on( 'load', function() {
component.setupAccessibleMode();
});
});
};
return component;
})( jQuery );

File diff suppressed because one or more lines are too long

View File

@ -115,7 +115,40 @@ if ( isset( $_REQUEST['action'] ) && 'update' === $_REQUEST['action'] ) {
}
// List of allowable extensions
$editable_extensions = array('php', 'txt', 'text', 'js', 'css', 'html', 'htm', 'xml', 'inc', 'include');
$editable_extensions = array(
'bash',
'conf',
'css',
'diff',
'htm',
'html',
'http',
'inc',
'include',
'js',
'json',
'jsx',
'less',
'md',
'patch',
'php',
'php3',
'php4',
'php5',
'php7',
'phps',
'phtml',
'sass',
'scss',
'sh',
'sql',
'svg',
'text',
'txt',
'xml',
'yaml',
'yml',
);
/**
* Filters file type extensions editable in the plugin editor.
@ -157,6 +190,12 @@ if ( isset( $_REQUEST['action'] ) && 'update' === $_REQUEST['action'] ) {
'<p>' . __('<a href="https://wordpress.org/support/">Support Forums</a>') . '</p>'
);
$settings = wp_enqueue_code_editor( array( 'file' => $real_file ) );
if ( ! empty( $settings ) ) {
wp_enqueue_script( 'wp-theme-plugin-editor' );
wp_add_inline_script( 'wp-theme-plugin-editor', sprintf( 'jQuery( function() { wp.themePluginEditor.init( %s ); } )', wp_json_encode( $settings ) ) );
}
require_once(ABSPATH . 'wp-admin/admin-header.php');
update_recently_edited(WP_PLUGIN_DIR . '/' . $file);

View File

@ -63,7 +63,40 @@ if ( $theme->errors() && 'theme_no_stylesheet' == $theme->errors()->get_error_co
$allowed_files = $style_files = array();
$has_templates = false;
$default_types = array( 'php', 'css' );
$default_types = array(
'bash',
'conf',
'css',
'diff',
'htm',
'html',
'http',
'inc',
'include',
'js',
'json',
'jsx',
'less',
'md',
'patch',
'php',
'php3',
'php4',
'php5',
'php7',
'phps',
'phtml',
'sass',
'scss',
'sh',
'sql',
'svg',
'text',
'txt',
'xml',
'yaml',
'yml',
);
/**
* Filters the list of file types allowed for editing in the Theme editor.
@ -126,6 +159,12 @@ case 'update':
default:
$settings = wp_enqueue_code_editor( compact( 'file' ) );
if ( ! empty( $settings ) ) {
wp_enqueue_script( 'wp-theme-plugin-editor' );
wp_add_inline_script( 'wp-theme-plugin-editor', sprintf( 'jQuery( function() { wp.themePluginEditor.init( %s ); } )', wp_json_encode( $settings ) ) );
}
require_once( ABSPATH . 'wp-admin/admin-header.php' );
update_recently_edited( $file );

View File

@ -247,6 +247,26 @@ if ( ! IS_PROFILE_PAGE ) {
<td><label for="rich_editing"><input name="rich_editing" type="checkbox" id="rich_editing" value="false" <?php if ( ! empty( $profileuser->rich_editing ) ) checked( 'false', $profileuser->rich_editing ); ?> /> <?php _e( 'Disable the visual editor when writing' ); ?></label></td>
</tr>
<?php endif; ?>
<?php
$show_syntax_highlighting_preference = (
// For Custom HTML widget and Additional CSS in Customizer.
user_can( $profileuser, 'edit_theme_options' )
||
// Edit plugins.
user_can( $profileuser, 'edit_plugins' )
||
// Edit themes.
user_can( $profileuser, 'edit_themes' )
);
?>
<?php if ( $show_syntax_highlighting_preference ) : ?>
<tr class="user-syntax-highlighting-wrap">
<th scope="row"><?php _e( 'Syntax Highlighting' ); ?></th>
<td>
<label for="syntax_highlighting"><input name="syntax_highlighting" type="checkbox" id="syntax_highlighting" value="false" <?php if ( ! empty( $profileuser->syntax_highlighting ) ) checked( 'false', $profileuser->syntax_highlighting ); ?> /> <?php _e( 'Disable syntax highlighting when editing code' ); ?></label>
</td>
</tr>
<?php endif; ?>
<?php if ( count($_wp_admin_css_colors) > 1 && has_action('admin_color_scheme_picker') ) : ?>
<tr class="user-admin-color-wrap">
<th scope="row"><?php _e('Admin Color Scheme')?></th>

View File

@ -128,6 +128,7 @@ function the_modified_author() {
* - plugins_last_view
* - plugins_per_page
* - rich_editing
* - syntax_highlighting
* - user_activation_key
* - user_description
* - user_email

View File

@ -212,6 +212,21 @@ final class WP_Customize_Manager {
*/
private $_changeset_data;
/**
* Code Editor Settings for Custom CSS.
*
* This variable contains the settings returned by `wp_enqueue_code_editor()` which are then later output
* to the client in `WP_Customize_Manager::customize_pane_settings()`. A value of false means that the
* Custom CSS section or control was removed, or that the Syntax Highlighting user pref was turned off.
*
* @see wp_enqueue_code_editor()
* @see WP_Customize_Manager::enqueue_control_scripts()
* @see WP_Customize_Manager::customize_pane_settings()
* @since 4.9.0
* @var array|false
*/
private $_custom_css_code_editor_settings = false;
/**
* Constructor.
*
@ -3322,6 +3337,12 @@ final class WP_Customize_Manager {
foreach ( $this->controls as $control ) {
$control->enqueue();
}
if ( $this->get_section( 'custom_css' ) && $this->get_control( 'custom_css' ) ) {
$this->_custom_css_code_editor_settings = wp_enqueue_code_editor( array(
'type' => 'text/css',
) );
}
}
/**
@ -3579,6 +3600,9 @@ final class WP_Customize_Manager {
'stylesheet' => $this->get_stylesheet(),
'active' => $this->is_theme_active(),
),
'customCss' => array(
'codeEditor' => $this->_custom_css_code_editor_settings,
),
'url' => array(
'preview' => esc_url_raw( $this->get_preview_url() ),
'parent' => esc_url_raw( admin_url() ),
@ -4177,22 +4201,53 @@ final class WP_Customize_Manager {
) );
/* Custom CSS */
$section_description = '<p>';
$section_description .= __( 'Add your own CSS code here to customize the appearance and layout of your site.', 'better-code-editing' );
$section_description .= sprintf(
' <a href="%1$s" class="external-link" target="_blank">%2$s<span class="screen-reader-text">%3$s</span></a>',
esc_url( __( 'https://codex.wordpress.org/CSS', 'default' ) ),
__( 'Learn more about CSS', 'default' ),
/* translators: accessibility text */
__( '(opens in a new window)', 'default' )
);
$section_description .= '</p>';
$section_description .= '<p>' . __( 'When using a keyboard to navigate:', 'better-code-editing' ) . '</p>';
$section_description .= '<ul>';
$section_description .= '<li>' . __( 'In the CSS edit field, Tab enters a tab character.', 'better-code-editing' ) . '</li>';
$section_description .= '<li>' . __( 'To move keyboard focus, press Esc then Tab for the next element, or Esc then Shift+Tab for the previous element.', 'better-code-editing' ) . '</li>';
$section_description .= '</ul>';
if ( 'false' !== wp_get_current_user()->syntax_highlighting ) {
$section_description .= '<p>';
$section_description .= sprintf(
/* translators: placeholder is link to user profile */
__( 'The edit field automatically highlights code syntax. You can disable this in your %s to work in plain text mode.', 'better-code-editing' ),
sprintf(
' <a href="%1$s" class="external-link" target="_blank">%2$s<span class="screen-reader-text">%3$s</span></a>',
esc_url( get_edit_profile_url() . '#syntax_highlighting' ),
__( 'user profile', 'better-code-editing' ),
/* translators: accessibility text */
__( '(opens in a new window)', 'default' )
)
);
$section_description .= '</p>';
}
$section_description .= '<p class="section-description-buttons">';
$section_description .= '<button type="button" class="button-link section-description-close">' . __( 'Close', 'default' ) . '</button>';
$section_description .= '</p>';
$this->add_section( 'custom_css', array(
'title' => __( 'Additional CSS' ),
'priority' => 200,
'description_hidden' => true,
'description' => sprintf( '%s<br /><a href="%s" class="external-link" target="_blank">%s<span class="screen-reader-text">%s</span></a>',
__( 'CSS allows you to customize the appearance and layout of your site with code. Separate CSS is saved for each of your themes. In the editing area the Tab key enters a tab character. To move below this area by pressing Tab, press the Esc key followed by the Tab key.' ),
esc_url( __( 'https://codex.wordpress.org/CSS' ) ),
__( 'Learn more about CSS' ),
/* translators: accessibility text */
__( '(opens in a new window)' )
),
'description' => $section_description,
) );
$custom_css_setting = new WP_Customize_Custom_CSS_Setting( $this, sprintf( 'custom_css[%s]', get_stylesheet() ), array(
'capability' => 'edit_css',
'default' => sprintf( "/*\n%s\n*/", __( "You can add your own CSS here.\n\nClick the help icon above to learn more." ) ),
'default' => '',
) );
$this->add_setting( $custom_css_setting );

View File

@ -32,6 +32,8 @@
* @property string $spam
* @property string $deleted
* @property string $locale
* @property string $rich_editing
* @property string $syntax_highlighting
*/
class WP_User {
/**

View File

@ -148,9 +148,8 @@ final class WP_Customize_Custom_CSS_Setting extends WP_Customize_Setting {
* Checks for imbalanced braces, brackets, and comments.
* Notifications are rendered when the customizer state is saved.
*
* @todo There are cases where valid CSS can be incorrectly marked as invalid when strings or comments include balancing characters. To fix, CSS tokenization needs to be used.
*
* @since 4.7.0
* @since 4.9.0 Checking for balanced characters has been moved client-side via linting in code editor.
*
* @param string $css The input string.
* @return true|WP_Error True if the input was validated, otherwise WP_Error.
@ -162,94 +161,6 @@ final class WP_Customize_Custom_CSS_Setting extends WP_Customize_Setting {
$validity->add( 'illegal_markup', __( 'Markup is not allowed in CSS.' ) );
}
$imbalanced = false;
// Make sure that there is a closing brace for each opening brace.
if ( ! $this->validate_balanced_characters( '{', '}', $css ) ) {
$validity->add( 'imbalanced_curly_brackets', sprintf(
/* translators: 1: {}, 2: }, 3: { */
__( 'Your curly brackets %1$s are imbalanced. Make sure there is a closing %2$s for every opening %3$s.' ),
'<code>{}</code>',
'<code>}</code>',
'<code>{</code>'
) );
$imbalanced = true;
}
// Ensure brackets are balanced.
if ( ! $this->validate_balanced_characters( '[', ']', $css ) ) {
$validity->add( 'imbalanced_braces', sprintf(
/* translators: 1: [], 2: ], 3: [ */
__( 'Your brackets %1$s are imbalanced. Make sure there is a closing %2$s for every opening %3$s.' ),
'<code>[]</code>',
'<code>]</code>',
'<code>[</code>'
) );
$imbalanced = true;
}
// Ensure parentheses are balanced.
if ( ! $this->validate_balanced_characters( '(', ')', $css ) ) {
$validity->add( 'imbalanced_parentheses', sprintf(
/* translators: 1: (), 2: ), 3: ( */
__( 'Your parentheses %1$s are imbalanced. Make sure there is a closing %2$s for every opening %3$s.' ),
'<code>()</code>',
'<code>)</code>',
'<code>(</code>'
) );
$imbalanced = true;
}
// Ensure double quotes are equal.
if ( ! $this->validate_equal_characters( '"', $css ) ) {
$validity->add( 'unequal_double_quotes', sprintf(
/* translators: 1: " (double quote) */
__( 'Your double quotes %1$s are uneven. Make sure there is a closing %1$s for every opening %1$s.' ),
'<code>"</code>'
) );
$imbalanced = true;
}
/*
* Make sure any code comments are closed properly.
*
* The first check could miss stray an unpaired comment closing figure, so if
* The number appears to be balanced, then check for equal numbers
* of opening/closing comment figures.
*
* Although it may initially appear redundant, we use the first method
* to give more specific feedback to the user.
*/
$unclosed_comment_count = $this->validate_count_unclosed_comments( $css );
if ( 0 < $unclosed_comment_count ) {
$validity->add( 'unclosed_comment', sprintf(
/* translators: 1: number of unclosed comments, 2: */ */
_n(
'There is %1$s unclosed code comment. Close each comment with %2$s.',
'There are %1$s unclosed code comments. Close each comment with %2$s.',
$unclosed_comment_count
),
$unclosed_comment_count,
'<code>*/</code>'
) );
$imbalanced = true;
} elseif ( ! $this->validate_balanced_characters( '/*', '*/', $css ) ) {
$validity->add( 'imbalanced_comments', sprintf(
/* translators: 1: */, 2: /* */
__( 'There is an extra %1$s, indicating an end to a comment. Be sure that there is an opening %2$s for every closing %1$s.' ),
'<code>*/</code>',
'<code>/*</code>'
) );
$imbalanced = true;
}
if ( $imbalanced && $this->is_possible_content_error( $css ) ) {
$validity->add( 'possible_false_positive', sprintf(
/* translators: %s: content: ""; */
__( 'Imbalanced/unclosed character errors can be caused by %s declarations. You may need to remove this or add it to a custom CSS file.' ),
'<code>content: "";</code>'
) );
}
if ( empty( $validity->errors ) ) {
$validity = parent::validate( $css );
}
@ -285,109 +196,4 @@ final class WP_Customize_Custom_CSS_Setting extends WP_Customize_Setting {
return $post_id;
}
/**
* Ensure there are a balanced number of paired characters.
*
* This is used to check that the number of opening and closing
* characters is equal.
*
* For instance, there should be an equal number of braces ("{", "}")
* in the CSS.
*
* @since 4.7.0
*
* @param string $opening_char The opening character.
* @param string $closing_char The closing character.
* @param string $css The CSS input string.
*
* @return bool
*/
private function validate_balanced_characters( $opening_char, $closing_char, $css ) {
return substr_count( $css, $opening_char ) === substr_count( $css, $closing_char );
}
/**
* Ensure there are an even number of paired characters.
*
* This is used to check that the number of a specific
* character is even.
*
* For instance, there should be an even number of double quotes
* in the CSS.
*
* @since 4.7.0
*
* @param string $char A character.
* @param string $css The CSS input string.
* @return bool Equality.
*/
private function validate_equal_characters( $char, $css ) {
$char_count = substr_count( $css, $char );
return ( 0 === $char_count % 2 );
}
/**
* Count unclosed CSS Comments.
*
* Used during validation.
*
* @see self::validate()
*
* @since 4.7.0
*
* @param string $css The CSS input string.
* @return int Count.
*/
private function validate_count_unclosed_comments( $css ) {
$count = 0;
$comments = explode( '/*', $css );
if ( ! is_array( $comments ) || ( 1 >= count( $comments ) ) ) {
return $count;
}
unset( $comments[0] ); // The first item is before the first comment.
foreach ( $comments as $comment ) {
if ( false === strpos( $comment, '*/' ) ) {
$count++;
}
}
return $count;
}
/**
* Find "content:" within a string.
*
* Imbalanced/Unclosed validation errors may be caused
* when a character is used in a "content:" declaration.
*
* This function is used to detect if this is a possible
* cause of the validation error, so that if it is,
* a notification may be added to the Validation Errors.
*
* Example:
* .element::before {
* content: "(\"";
* }
* .element::after {
* content: "\")";
* }
*
* Using ! empty() because strpos() may return non-boolean values
* that evaluate to false. This would be problematic when
* using a strict "false === strpos()" comparison.
*
* @since 4.7.0
*
* @param string $css The CSS input string.
* @return bool
*/
private function is_possible_content_error( $css ) {
$found = preg_match( '/\bcontent\s*:/', $css );
if ( ! empty( $found ) ) {
return true;
}
return false;
}
}

View File

@ -3115,6 +3115,394 @@ function wp_enqueue_editor() {
_WP_Editors::enqueue_default_editor();
}
/**
* Enqueue assets needed by the code editor for the given settings.
*
* @since 4.9.0
*
* @see wp_enqueue_editor()
* @see _WP_Editors::parse_settings()
* @param array $args {
* Args.
*
* @type string $type The MIME type of the file to be edited.
* @type string $file Filename to be edited. Extension is used to sniff the type. Can be supplied as alternative to `$type` param.
* @type array $settings Settings to merge on top of defaults which derive from `$type` or `$file` args.
* @type WP_Theme $theme Theme being edited when on theme editor.
* @type string $plugin Plugin being edited when on plugin editor.
* }
* @returns array|false Settings for the enqueued code editor, or false if the editor was not enqueued .
*/
function wp_enqueue_code_editor( $args ) {
if ( is_user_logged_in() && 'false' === wp_get_current_user()->syntax_highlighting ) {
return false;
}
$settings = array(
'codemirror' => array(
'indentUnit' => 4,
'indentWithTabs' => true,
'inputStyle' => 'contenteditable',
'lineNumbers' => true,
'lineWrapping' => true,
'styleActiveLine' => true,
'continueComments' => true,
'extraKeys' => array(
'Ctrl-Space' => 'autocomplete',
'Ctrl-/' => 'toggleComment',
'Cmd-/' => 'toggleComment',
'Alt-F' => 'findPersistent',
),
'direction' => 'ltr', // Code is shown in LTR even in RTL languages.
),
'csslint' => array(
'errors' => true, // Parsing errors.
'box-model' => true,
'display-property-grouping' => true,
'duplicate-properties' => true,
'known-properties' => true,
'outline-none' => true,
),
'jshint' => array(
// The following are copied from <https://github.com/WordPress/wordpress-develop/blob/4.8.1/.jshintrc>.
'boss' => true,
'curly' => true,
'eqeqeq' => true,
'eqnull' => true,
'es3' => true,
'expr' => true,
'immed' => true,
'noarg' => true,
'nonbsp' => true,
'onevar' => true,
'quotmark' => 'single',
'trailing' => true,
'undef' => true,
'unused' => true,
'browser' => true,
'globals' => array(
'_' => false,
'Backbone' => false,
'jQuery' => false,
'JSON' => false,
'wp' => false,
),
),
'htmlhint' => array(
'tagname-lowercase' => true,
'attr-lowercase' => true,
'attr-value-double-quotes' => true,
'doctype-first' => false,
'tag-pair' => true,
'spec-char-escape' => true,
'id-unique' => true,
'src-not-empty' => true,
'attr-no-duplication' => true,
'alt-require' => true,
'space-tab-mixed-disabled' => 'tab',
'attr-unsafe-chars' => true,
),
);
$type = '';
if ( isset( $args['type'] ) ) {
$type = $args['type'];
// Remap MIME types to ones that CodeMirror modes will recognize.
if ( 'application/x-patch' === $type || 'text/x-patch' === $type ) {
$type = 'text/x-diff';
}
} elseif ( isset( $args['file'] ) && false !== strpos( basename( $args['file'] ), '.' ) ) {
$extension = strtolower( pathinfo( $args['file'], PATHINFO_EXTENSION ) );
foreach ( wp_get_mime_types() as $exts => $mime ) {
if ( preg_match( '!^(' . $exts . ')$!i', $extension ) ) {
$type = $mime;
break;
}
}
// Supply any types that are not matched by wp_get_mime_types().
if ( empty( $type ) ) {
switch ( $extension ) {
case 'conf':
$type = 'text/nginx';
break;
case 'css':
$type = 'text/css';
break;
case 'diff':
case 'patch':
$type = 'text/x-diff';
break;
case 'html':
case 'htm':
$type = 'text/html';
break;
case 'http':
$type = 'message/http';
break;
case 'js':
$type = 'text/javascript';
break;
case 'json':
$type = 'application/json';
break;
case 'jsx':
$type = 'text/jsx';
break;
case 'less':
$type = 'text/x-less';
break;
case 'md':
$type = 'text/x-gfm';
break;
case 'php':
case 'phtml':
case 'php3':
case 'php4':
case 'php5':
case 'php7':
case 'phps':
$type = 'application/x-httpd-php';
break;
case 'scss':
$type = 'text/x-scss';
break;
case 'sass':
$type = 'text/x-sass';
break;
case 'sh':
case 'bash':
$type = 'text/x-sh';
break;
case 'sql':
$type = 'text/x-sql';
break;
case 'svg':
$type = 'application/svg+xml';
break;
case 'xml':
$type = 'text/xml';
break;
case 'yml':
case 'yaml':
$type = 'text/x-yaml';
break;
case 'txt':
default:
$type = 'text/plain';
break;
}
}
}
if ( 'text/css' === $type ) {
$settings['codemirror'] = array_merge( $settings['codemirror'], array(
'mode' => 'css',
'lint' => true,
'autoCloseBrackets' => true,
'matchBrackets' => true,
) );
} elseif ( 'text/x-scss' === $type || 'text/x-less' === $type || 'text/x-sass' === $type ) {
$settings['codemirror'] = array_merge( $settings['codemirror'], array(
'mode' => $type,
'autoCloseBrackets' => true,
'matchBrackets' => true,
) );
} elseif ( 'text/x-diff' === $type ) {
$settings['codemirror'] = array_merge( $settings['codemirror'], array(
'mode' => 'diff',
) );
} elseif ( 'text/html' === $type ) {
$settings['codemirror'] = array_merge( $settings['codemirror'], array(
'mode' => 'htmlmixed',
'lint' => true,
'autoCloseBrackets' => true,
'autoCloseTags' => true,
'matchTags' => array(
'bothTags' => true,
),
) );
if ( ! current_user_can( 'unfiltered_html' ) ) {
$settings['htmlhint']['kses'] = wp_kses_allowed_html( 'post' );
}
} elseif ( 'text/x-gfm' === $type ) {
$settings['codemirror'] = array_merge( $settings['codemirror'], array(
'mode' => 'gfm',
'highlightFormatting' => true,
) );
} elseif ( 'application/javascript' === $type || 'text/javascript' === $type ) {
$settings['codemirror'] = array_merge( $settings['codemirror'], array(
'mode' => 'javascript',
'lint' => true,
'autoCloseBrackets' => true,
'matchBrackets' => true,
) );
} elseif ( false !== strpos( $type, 'json' ) ) {
$settings['codemirror'] = array_merge( $settings['codemirror'], array(
'mode' => array(
'name' => 'javascript',
),
'lint' => true,
'autoCloseBrackets' => true,
'matchBrackets' => true,
) );
if ( 'application/ld+json' === $type ) {
$settings['codemirror']['mode']['jsonld'] = true;
} else {
$settings['codemirror']['mode']['json'] = true;
}
} elseif ( false !== strpos( $type, 'jsx' ) ) {
$settings['codemirror'] = array_merge( $settings['codemirror'], array(
'mode' => 'jsx',
'autoCloseBrackets' => true,
'matchBrackets' => true,
) );
} elseif ( 'text/x-markdown' === $type ) {
$settings['codemirror'] = array_merge( $settings['codemirror'], array(
'mode' => 'markdown',
'highlightFormatting' => true,
) );
} elseif ( 'text/nginx' === $type ) {
$settings['codemirror'] = array_merge( $settings['codemirror'], array(
'mode' => 'nginx',
) );
} elseif ( 'application/x-httpd-php' === $type ) {
$settings['codemirror'] = array_merge( $settings['codemirror'], array(
'mode' => 'php',
'autoCloseBrackets' => true,
'autoCloseTags' => true,
'matchBrackets' => true,
'matchTags' => array(
'bothTags' => true,
),
) );
} elseif ( 'text/x-sql' === $type || 'text/x-mysql' === $type ) {
$settings['codemirror'] = array_merge( $settings['codemirror'], array(
'mode' => 'sql',
'autoCloseBrackets' => true,
'matchBrackets' => true,
) );
} elseif ( false !== strpos( $type, 'xml' ) ) {
$settings['codemirror'] = array_merge( $settings['codemirror'], array(
'mode' => 'xml',
'autoCloseBrackets' => true,
'autoCloseTags' => true,
'matchTags' => array(
'bothTags' => true,
),
) );
} elseif ( 'text/x-yaml' === $type ) {
$settings['codemirror'] = array_merge( $settings['codemirror'], array(
'mode' => 'yaml',
) );
} else {
$settings['codemirror']['mode'] = $type;
}
if ( ! empty( $settings['codemirror']['lint'] ) ) {
$settings['codemirror']['gutters'][] = 'CodeMirror-lint-markers';
}
// Let settings supplied via args override any defaults.
if ( isset( $args['settings'] ) ) {
foreach ( $args['settings'] as $key => $value ) {
$settings[ $key ] = array_merge(
$settings[ $key ],
$value
);
}
}
/**
* Filters settings that are passed into the code editor.
*
* Returning a falsey value will disable the syntax-highlighting code editor.
*
* @since 4.9.0
*
* @param array $settings The array of settings passed to the code editor. A falsey value disables the editor.
* @param array $args {
* Args passed when calling `wp_enqueue_code_editor()`.
*
* @type string $type The MIME type of the file to be edited.
* @type string $file Filename being edited.
* @type array $settings Settings to merge on top of defaults which derive from `$type` or `$file` args.
* @type WP_Theme $theme Theme being edited when on theme editor.
* @type string $plugin Plugin being edited when on plugin editor.
* }
*/
$settings = apply_filters( 'wp_code_editor_settings', $settings, $args );
if ( empty( $settings ) || empty( $settings['codemirror'] ) ) {
return false;
}
wp_enqueue_script( 'code-editor' );
wp_enqueue_style( 'code-editor' );
wp_enqueue_script( 'codemirror' );
wp_enqueue_style( 'codemirror' );
if ( isset( $settings['codemirror']['mode'] ) ) {
$mode = $settings['codemirror']['mode'];
if ( is_string( $mode ) ) {
$mode = array(
'name' => $mode,
);
}
if ( ! empty( $settings['codemirror']['lint'] ) ) {
switch ( $mode['name'] ) {
case 'css':
case 'text/css':
case 'text/x-scss':
case 'text/x-less':
wp_enqueue_script( 'csslint' );
break;
case 'htmlmixed':
case 'text/html':
case 'php':
case 'application/x-httpd-php':
case 'text/x-php':
wp_enqueue_script( 'htmlhint' );
wp_enqueue_script( 'csslint' );
wp_enqueue_script( 'jshint' );
if ( ! current_user_can( 'unfiltered_html' ) ) {
wp_enqueue_script( 'htmlhint-kses' );
}
break;
case 'javascript':
case 'application/ecmascript':
case 'application/json':
case 'application/javascript':
case 'application/ld+json':
case 'text/typescript':
case 'application/typescript':
wp_enqueue_script( 'jshint' );
wp_enqueue_script( 'jsonlint' );
break;
}
}
}
wp_add_inline_script( 'code-editor', sprintf( 'jQuery.extend( wp.codeEditor.defaultSettings, %s );', wp_json_encode( $settings ) ) );
/**
* Fires when scripts and styles are enqueued for the code editor.
*
* @since 4.9.0
*
* @param array $settings Settings for the enqueued code editor.
*/
do_action( 'wp_enqueue_code_editor', $settings );
return $settings;
}
/**
* Retrieves the contents of the search WordPress query variable.
*

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,30 @@
/* global HTMLHint */
/* eslint no-magic-numbers: ["error", { "ignore": [0, 1] }] */
HTMLHint.addRule({
id: 'kses',
description: 'Element or attribute cannot be used.',
init: function( parser, reporter, options ) {
'use strict';
var self = this;
parser.addListener( 'tagstart', function( event ) {
var attr, col, attrName, allowedAttributes, i, len, tagName;
tagName = event.tagName.toLowerCase();
if ( ! options[ tagName ] ) {
reporter.error( 'Tag <' + event.tagName + '> is not allowed.', event.line, event.col, self, event.raw );
return;
}
allowedAttributes = options[ tagName ];
col = event.col + event.tagName.length + 1;
for ( i = 0, len = event.attrs.length; i < len; i++ ) {
attr = event.attrs[ i ];
attrName = attr.name.toLowerCase();
if ( ! allowedAttributes[ attrName ] ) {
reporter.error( 'Tag attribute [' + attr.raw + ' ] is not allowed.', event.line, col + attr.index, self, attr.raw );
}
}
});
}
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,432 @@
/* Jison generated parser */
var jsonlint = (function(){
var parser = {trace: function trace() { },
yy: {},
symbols_: {"error":2,"JSONString":3,"STRING":4,"JSONNumber":5,"NUMBER":6,"JSONNullLiteral":7,"NULL":8,"JSONBooleanLiteral":9,"TRUE":10,"FALSE":11,"JSONText":12,"JSONValue":13,"EOF":14,"JSONObject":15,"JSONArray":16,"{":17,"}":18,"JSONMemberList":19,"JSONMember":20,":":21,",":22,"[":23,"]":24,"JSONElementList":25,"$accept":0,"$end":1},
terminals_: {2:"error",4:"STRING",6:"NUMBER",8:"NULL",10:"TRUE",11:"FALSE",14:"EOF",17:"{",18:"}",21:":",22:",",23:"[",24:"]"},
productions_: [0,[3,1],[5,1],[7,1],[9,1],[9,1],[12,2],[13,1],[13,1],[13,1],[13,1],[13,1],[13,1],[15,2],[15,3],[20,3],[19,1],[19,3],[16,2],[16,3],[25,1],[25,3]],
performAction: function anonymous(yytext,yyleng,yylineno,yy,yystate,$$,_$) {
var $0 = $$.length - 1;
switch (yystate) {
case 1: // replace escaped characters with actual character
this.$ = yytext.replace(/\\(\\|")/g, "$"+"1")
.replace(/\\n/g,'\n')
.replace(/\\r/g,'\r')
.replace(/\\t/g,'\t')
.replace(/\\v/g,'\v')
.replace(/\\f/g,'\f')
.replace(/\\b/g,'\b');
break;
case 2:this.$ = Number(yytext);
break;
case 3:this.$ = null;
break;
case 4:this.$ = true;
break;
case 5:this.$ = false;
break;
case 6:return this.$ = $$[$0-1];
break;
case 13:this.$ = {};
break;
case 14:this.$ = $$[$0-1];
break;
case 15:this.$ = [$$[$0-2], $$[$0]];
break;
case 16:this.$ = {}; this.$[$$[$0][0]] = $$[$0][1];
break;
case 17:this.$ = $$[$0-2]; $$[$0-2][$$[$0][0]] = $$[$0][1];
break;
case 18:this.$ = [];
break;
case 19:this.$ = $$[$0-1];
break;
case 20:this.$ = [$$[$0]];
break;
case 21:this.$ = $$[$0-2]; $$[$0-2].push($$[$0]);
break;
}
},
table: [{3:5,4:[1,12],5:6,6:[1,13],7:3,8:[1,9],9:4,10:[1,10],11:[1,11],12:1,13:2,15:7,16:8,17:[1,14],23:[1,15]},{1:[3]},{14:[1,16]},{14:[2,7],18:[2,7],22:[2,7],24:[2,7]},{14:[2,8],18:[2,8],22:[2,8],24:[2,8]},{14:[2,9],18:[2,9],22:[2,9],24:[2,9]},{14:[2,10],18:[2,10],22:[2,10],24:[2,10]},{14:[2,11],18:[2,11],22:[2,11],24:[2,11]},{14:[2,12],18:[2,12],22:[2,12],24:[2,12]},{14:[2,3],18:[2,3],22:[2,3],24:[2,3]},{14:[2,4],18:[2,4],22:[2,4],24:[2,4]},{14:[2,5],18:[2,5],22:[2,5],24:[2,5]},{14:[2,1],18:[2,1],21:[2,1],22:[2,1],24:[2,1]},{14:[2,2],18:[2,2],22:[2,2],24:[2,2]},{3:20,4:[1,12],18:[1,17],19:18,20:19},{3:5,4:[1,12],5:6,6:[1,13],7:3,8:[1,9],9:4,10:[1,10],11:[1,11],13:23,15:7,16:8,17:[1,14],23:[1,15],24:[1,21],25:22},{1:[2,6]},{14:[2,13],18:[2,13],22:[2,13],24:[2,13]},{18:[1,24],22:[1,25]},{18:[2,16],22:[2,16]},{21:[1,26]},{14:[2,18],18:[2,18],22:[2,18],24:[2,18]},{22:[1,28],24:[1,27]},{22:[2,20],24:[2,20]},{14:[2,14],18:[2,14],22:[2,14],24:[2,14]},{3:20,4:[1,12],20:29},{3:5,4:[1,12],5:6,6:[1,13],7:3,8:[1,9],9:4,10:[1,10],11:[1,11],13:30,15:7,16:8,17:[1,14],23:[1,15]},{14:[2,19],18:[2,19],22:[2,19],24:[2,19]},{3:5,4:[1,12],5:6,6:[1,13],7:3,8:[1,9],9:4,10:[1,10],11:[1,11],13:31,15:7,16:8,17:[1,14],23:[1,15]},{18:[2,17],22:[2,17]},{18:[2,15],22:[2,15]},{22:[2,21],24:[2,21]}],
defaultActions: {16:[2,6]},
parseError: function parseError(str, hash) {
throw new Error(str);
},
parse: function parse(input) {
var self = this,
stack = [0],
vstack = [null], // semantic value stack
lstack = [], // location stack
table = this.table,
yytext = '',
yylineno = 0,
yyleng = 0,
recovering = 0,
TERROR = 2,
EOF = 1;
//this.reductionCount = this.shiftCount = 0;
this.lexer.setInput(input);
this.lexer.yy = this.yy;
this.yy.lexer = this.lexer;
if (typeof this.lexer.yylloc == 'undefined')
this.lexer.yylloc = {};
var yyloc = this.lexer.yylloc;
lstack.push(yyloc);
if (typeof this.yy.parseError === 'function')
this.parseError = this.yy.parseError;
function popStack (n) {
stack.length = stack.length - 2*n;
vstack.length = vstack.length - n;
lstack.length = lstack.length - n;
}
function lex() {
var token;
token = self.lexer.lex() || 1; // $end = 1
// if token isn't its numeric value, convert
if (typeof token !== 'number') {
token = self.symbols_[token] || token;
}
return token;
}
var symbol, preErrorSymbol, state, action, a, r, yyval={},p,len,newState, expected;
while (true) {
// retreive state number from top of stack
state = stack[stack.length-1];
// use default actions if available
if (this.defaultActions[state]) {
action = this.defaultActions[state];
} else {
if (symbol == null)
symbol = lex();
// read action for current state and first input
action = table[state] && table[state][symbol];
}
// handle parse error
_handle_error:
if (typeof action === 'undefined' || !action.length || !action[0]) {
if (!recovering) {
// Report error
expected = [];
for (p in table[state]) if (this.terminals_[p] && p > 2) {
expected.push("'"+this.terminals_[p]+"'");
}
var errStr = '';
if (this.lexer.showPosition) {
errStr = 'Parse error on line '+(yylineno+1)+":\n"+this.lexer.showPosition()+"\nExpecting "+expected.join(', ') + ", got '" + this.terminals_[symbol]+ "'";
} else {
errStr = 'Parse error on line '+(yylineno+1)+": Unexpected " +
(symbol == 1 /*EOF*/ ? "end of input" :
("'"+(this.terminals_[symbol] || symbol)+"'"));
}
this.parseError(errStr,
{text: this.lexer.match, token: this.terminals_[symbol] || symbol, line: this.lexer.yylineno, loc: yyloc, expected: expected});
}
// just recovered from another error
if (recovering == 3) {
if (symbol == EOF) {
throw new Error(errStr || 'Parsing halted.');
}
// discard current lookahead and grab another
yyleng = this.lexer.yyleng;
yytext = this.lexer.yytext;
yylineno = this.lexer.yylineno;
yyloc = this.lexer.yylloc;
symbol = lex();
}
// try to recover from error
while (1) {
// check for error recovery rule in this state
if ((TERROR.toString()) in table[state]) {
break;
}
if (state == 0) {
throw new Error(errStr || 'Parsing halted.');
}
popStack(1);
state = stack[stack.length-1];
}
preErrorSymbol = symbol; // save the lookahead token
symbol = TERROR; // insert generic error symbol as new lookahead
state = stack[stack.length-1];
action = table[state] && table[state][TERROR];
recovering = 3; // allow 3 real symbols to be shifted before reporting a new error
}
// this shouldn't happen, unless resolve defaults are off
if (action[0] instanceof Array && action.length > 1) {
throw new Error('Parse Error: multiple actions possible at state: '+state+', token: '+symbol);
}
switch (action[0]) {
case 1: // shift
//this.shiftCount++;
stack.push(symbol);
vstack.push(this.lexer.yytext);
lstack.push(this.lexer.yylloc);
stack.push(action[1]); // push state
symbol = null;
if (!preErrorSymbol) { // normal execution/no error
yyleng = this.lexer.yyleng;
yytext = this.lexer.yytext;
yylineno = this.lexer.yylineno;
yyloc = this.lexer.yylloc;
if (recovering > 0)
recovering--;
} else { // error just occurred, resume old lookahead f/ before error
symbol = preErrorSymbol;
preErrorSymbol = null;
}
break;
case 2: // reduce
//this.reductionCount++;
len = this.productions_[action[1]][1];
// perform semantic action
yyval.$ = vstack[vstack.length-len]; // default to $$ = $1
// default location, uses first token for firsts, last for lasts
yyval._$ = {
first_line: lstack[lstack.length-(len||1)].first_line,
last_line: lstack[lstack.length-1].last_line,
first_column: lstack[lstack.length-(len||1)].first_column,
last_column: lstack[lstack.length-1].last_column
};
r = this.performAction.call(yyval, yytext, yyleng, yylineno, this.yy, action[1], vstack, lstack);
if (typeof r !== 'undefined') {
return r;
}
// pop off stack
if (len) {
stack = stack.slice(0,-1*len*2);
vstack = vstack.slice(0, -1*len);
lstack = lstack.slice(0, -1*len);
}
stack.push(this.productions_[action[1]][0]); // push nonterminal (reduce)
vstack.push(yyval.$);
lstack.push(yyval._$);
// goto new state = table[STATE][NONTERMINAL]
newState = table[stack[stack.length-2]][stack[stack.length-1]];
stack.push(newState);
break;
case 3: // accept
return true;
}
}
return true;
}};
/* Jison generated lexer */
var lexer = (function(){
var lexer = ({EOF:1,
parseError:function parseError(str, hash) {
if (this.yy.parseError) {
this.yy.parseError(str, hash);
} else {
throw new Error(str);
}
},
setInput:function (input) {
this._input = input;
this._more = this._less = this.done = false;
this.yylineno = this.yyleng = 0;
this.yytext = this.matched = this.match = '';
this.conditionStack = ['INITIAL'];
this.yylloc = {first_line:1,first_column:0,last_line:1,last_column:0};
return this;
},
input:function () {
var ch = this._input[0];
this.yytext+=ch;
this.yyleng++;
this.match+=ch;
this.matched+=ch;
var lines = ch.match(/\n/);
if (lines) this.yylineno++;
this._input = this._input.slice(1);
return ch;
},
unput:function (ch) {
this._input = ch + this._input;
return this;
},
more:function () {
this._more = true;
return this;
},
less:function (n) {
this._input = this.match.slice(n) + this._input;
},
pastInput:function () {
var past = this.matched.substr(0, this.matched.length - this.match.length);
return (past.length > 20 ? '...':'') + past.substr(-20).replace(/\n/g, "");
},
upcomingInput:function () {
var next = this.match;
if (next.length < 20) {
next += this._input.substr(0, 20-next.length);
}
return (next.substr(0,20)+(next.length > 20 ? '...':'')).replace(/\n/g, "");
},
showPosition:function () {
var pre = this.pastInput();
var c = new Array(pre.length + 1).join("-");
return pre + this.upcomingInput() + "\n" + c+"^";
},
next:function () {
if (this.done) {
return this.EOF;
}
if (!this._input) this.done = true;
var token,
match,
tempMatch,
index,
col,
lines;
if (!this._more) {
this.yytext = '';
this.match = '';
}
var rules = this._currentRules();
for (var i=0;i < rules.length; i++) {
tempMatch = this._input.match(this.rules[rules[i]]);
if (tempMatch && (!match || tempMatch[0].length > match[0].length)) {
match = tempMatch;
index = i;
if (!this.options.flex) break;
}
}
if (match) {
lines = match[0].match(/\n.*/g);
if (lines) this.yylineno += lines.length;
this.yylloc = {first_line: this.yylloc.last_line,
last_line: this.yylineno+1,
first_column: this.yylloc.last_column,
last_column: lines ? lines[lines.length-1].length-1 : this.yylloc.last_column + match[0].length}
this.yytext += match[0];
this.match += match[0];
this.yyleng = this.yytext.length;
this._more = false;
this._input = this._input.slice(match[0].length);
this.matched += match[0];
token = this.performAction.call(this, this.yy, this, rules[index],this.conditionStack[this.conditionStack.length-1]);
if (this.done && this._input) this.done = false;
if (token) return token;
else return;
}
if (this._input === "") {
return this.EOF;
} else {
this.parseError('Lexical error on line '+(this.yylineno+1)+'. Unrecognized text.\n'+this.showPosition(),
{text: "", token: null, line: this.yylineno});
}
},
lex:function lex() {
var r = this.next();
if (typeof r !== 'undefined') {
return r;
} else {
return this.lex();
}
},
begin:function begin(condition) {
this.conditionStack.push(condition);
},
popState:function popState() {
return this.conditionStack.pop();
},
_currentRules:function _currentRules() {
return this.conditions[this.conditionStack[this.conditionStack.length-1]].rules;
},
topState:function () {
return this.conditionStack[this.conditionStack.length-2];
},
pushState:function begin(condition) {
this.begin(condition);
}});
lexer.options = {};
lexer.performAction = function anonymous(yy,yy_,$avoiding_name_collisions,YY_START) {
var YYSTATE=YY_START
switch($avoiding_name_collisions) {
case 0:/* skip whitespace */
break;
case 1:return 6
break;
case 2:yy_.yytext = yy_.yytext.substr(1,yy_.yyleng-2); return 4
break;
case 3:return 17
break;
case 4:return 18
break;
case 5:return 23
break;
case 6:return 24
break;
case 7:return 22
break;
case 8:return 21
break;
case 9:return 10
break;
case 10:return 11
break;
case 11:return 8
break;
case 12:return 14
break;
case 13:return 'INVALID'
break;
}
};
lexer.rules = [/^(?:\s+)/,/^(?:(-?([0-9]|[1-9][0-9]+))(\.[0-9]+)?([eE][-+]?[0-9]+)?\b)/,/^(?:"(?:\\[\\"bfnrt/]|\\u[a-fA-F0-9]{4}|[^\\\0-\x09\x0a-\x1f"])*")/,/^(?:\{)/,/^(?:\})/,/^(?:\[)/,/^(?:\])/,/^(?:,)/,/^(?::)/,/^(?:true\b)/,/^(?:false\b)/,/^(?:null\b)/,/^(?:$)/,/^(?:.)/];
lexer.conditions = {"INITIAL":{"rules":[0,1,2,3,4,5,6,7,8,9,10,11,12,13],"inclusive":true}};
;
return lexer;})()
parser.lexer = lexer;
return parser;
})();
if (typeof require !== 'undefined' && typeof exports !== 'undefined') {
exports.parser = jsonlint;
exports.parse = function () { return jsonlint.parse.apply(jsonlint, arguments); }
exports.main = function commonjsMain(args) {
if (!args[1])
throw new Error('Usage: '+args[0]+' FILE');
if (typeof process !== 'undefined') {
var source = require('fs').readFileSync(require('path').join(process.cwd(), args[1]), "utf8");
} else {
var cwd = require("file").path(require("file").cwd());
var source = cwd.join(args[1]).read({charset: "utf-8"});
}
return exports.parser.parse(source);
}
if (typeof module !== 'undefined' && require.main === module) {
exports.main(typeof process !== 'undefined' ? process.argv.slice(1) : require("system").args);
}
}

View File

@ -466,6 +466,20 @@ function wp_default_scripts( &$scripts ) {
apply_filters( 'mejs_settings', $mejs_settings )
);
$scripts->add( 'codemirror', '/wp-includes/js/codemirror/codemirror.min.js', array(), '5.29.1-alpha-ee20357' );
$scripts->add( 'csslint', '/wp-includes/js/codemirror/csslint.js', array(), '1.0.5' );
$scripts->add( 'jshint', '/wp-includes/js/codemirror/jshint.js', array(), '2.9.5' );
$scripts->add( 'jsonlint', '/wp-includes/js/codemirror/jsonlint.js', array(), '1.6.2' );
$scripts->add( 'htmlhint', '/wp-includes/js/codemirror/htmlhint.js', array(), '0.9.14-xwp' );
$scripts->add( 'htmlhint-kses', '/wp-includes/js/codemirror/htmlhint-kses.js', array( 'htmlhint' ) );
$scripts->add( 'code-editor', "/wp-admin/js/code-editor$suffix.js", array( 'jquery', 'codemirror' ) );
$scripts->add( 'wp-theme-plugin-editor', "/wp-admin/js/theme-plugin-editor$suffix.js", array( 'code-editor', 'jquery', 'jquery-ui-core', 'wp-a11y', 'underscore' ) );
did_action( 'init' ) && $scripts->add_inline_script( 'wp-theme-plugin-editor', sprintf( 'wp.themePluginEditor.l10n = %s;', wp_json_encode( wp_array_slice_assoc(
/* translators: placeholder is error count */
_n_noop( 'There is %d error which must be fixed before you can save.', 'There are %d errors which must be fixed before you can save.' ),
array( 'singular', 'plural' )
) ) ) );
$scripts->add( 'wp-playlist', "/wp-includes/js/mediaelement/wp-playlist$suffix.js", array( 'wp-util', 'backbone', 'mediaelement' ), false, 1 );
$scripts->add( 'zxcvbn-async', "/wp-includes/js/zxcvbn-async$suffix.js", array(), '1.0' );
@ -551,6 +565,11 @@ function wp_default_scripts( &$scripts ) {
'serverSaveError' => __( 'Failed connecting to the server. Please try saving again.' ),
// Used for overriding the file types allowed in plupload.
'allowedFiles' => __( 'Allowed Files' ),
'customCssError' => wp_array_slice_assoc(
/* translators: placeholder is error count */
_n_noop( 'There is %d error which must be fixed before you can save.', 'There are %d errors which must be fixed before you can save.' ),
array( 'singular', 'plural' )
),
) );
$scripts->add( 'customize-selective-refresh', "/wp-includes/js/customize-selective-refresh$suffix.js", array( 'jquery', 'wp-util', 'customize-preview' ), false, 1 );
@ -688,7 +707,7 @@ function wp_default_scripts( &$scripts ) {
$scripts->add( 'media-image-widget', "/wp-admin/js/widgets/media-image-widget$suffix.js", array( 'media-widgets' ) );
$scripts->add( 'media-video-widget', "/wp-admin/js/widgets/media-video-widget$suffix.js", array( 'media-widgets', 'media-audiovideo', 'wp-api-request' ) );
$scripts->add( 'text-widgets', "/wp-admin/js/widgets/text-widgets$suffix.js", array( 'jquery', 'backbone', 'editor', 'wp-util', 'wp-a11y' ) );
$scripts->add_inline_script( 'text-widgets', 'wp.textWidgets.init();', 'after' );
$scripts->add( 'custom-html-widgets', "/wp-admin/js/widgets/custom-html-widgets$suffix.js", array( 'code-editor', 'jquery', 'backbone', 'wp-util', 'jquery-ui-core', 'wp-a11y' ) );
$scripts->add( 'theme', "/wp-admin/js/theme$suffix.js", array( 'wp-backbone', 'wp-a11y' ), false, 1 );
@ -929,6 +948,7 @@ function wp_default_styles( &$styles ) {
$styles->add( 'widgets', "/wp-admin/css/widgets$suffix.css", array( 'wp-pointer' ) );
$styles->add( 'site-icon', "/wp-admin/css/site-icon$suffix.css" );
$styles->add( 'l10n', "/wp-admin/css/l10n$suffix.css" );
$styles->add( 'code-editor', "/wp-admin/css/code-editor$suffix.css", array( 'codemirror' ) );
$styles->add( 'wp-admin', false, array( 'dashicons', 'common', 'forms', 'admin-menu', 'dashboard', 'list-tables', 'edit', 'revisions', 'media', 'themes', 'about', 'nav-menus', 'widgets', 'site-icon', 'l10n' ) );
@ -963,6 +983,7 @@ function wp_default_styles( &$styles ) {
$styles->add( 'mediaelement', "/wp-includes/js/mediaelement/mediaelementplayer-legacy.min.css", array(), '4.2.5-74e01a40' );
$styles->add( 'wp-mediaelement', "/wp-includes/js/mediaelement/wp-mediaelement$suffix.css", array( 'mediaelement' ) );
$styles->add( 'thickbox', '/wp-includes/js/thickbox/thickbox.css', array( 'dashicons' ) );
$styles->add( 'codemirror', '/wp-includes/js/codemirror/codemirror.min.css', array(), '5.29.1-alpha-ee20357' );
// Deprecated CSS
$styles->add( 'deprecated-media', "/wp-admin/css/deprecated-media$suffix.css" );

View File

@ -1376,7 +1376,7 @@ function validate_username( $username ) {
* Insert a user into the database.
*
* Most of the `$userdata` array fields have filters associated with the values. Exceptions are
* 'ID', 'rich_editing', 'comment_shortcuts', 'admin_color', 'use_ssl',
* 'ID', 'rich_editing', 'syntax_highlighting', 'comment_shortcuts', 'admin_color', 'use_ssl',
* 'user_registered', and 'role'. The filters have the prefix 'pre_user_' followed by the field
* name. An example using 'description' would have the filter called, 'pre_user_description' that
* can be hooked into.
@ -1410,6 +1410,8 @@ function validate_username( $username ) {
* @type string $description The user's biographical description.
* @type string|bool $rich_editing Whether to enable the rich-editor for the user.
* False if not empty.
* @type string|bool $syntax_highlighting Whether to enable the rich code editor for the user.
* False if not empty.
* @type string|bool $comment_shortcuts Whether to enable comment moderation keyboard
* shortcuts for the user. Default false.
* @type string $admin_color Admin color scheme for the user. Default 'fresh'.
@ -1623,6 +1625,8 @@ function wp_insert_user( $userdata ) {
$meta['rich_editing'] = empty( $userdata['rich_editing'] ) ? 'true' : $userdata['rich_editing'];
$meta['syntax_highlighting'] = empty( $userdata['syntax_highlighting'] ) ? 'true' : $userdata['syntax_highlighting'];
$meta['comment_shortcuts'] = empty( $userdata['comment_shortcuts'] ) || 'false' === $userdata['comment_shortcuts'] ? 'false' : 'true';
$admin_color = empty( $userdata['admin_color'] ) ? 'fresh' : $userdata['admin_color'];
@ -1709,6 +1713,7 @@ function wp_insert_user( $userdata ) {
* @type string $last_name The user's last name.
* @type string $description The user's description.
* @type bool $rich_editing Whether to enable the rich-editor for the user. False if not empty.
* @type bool $syntax_highlighting Whether to enable the rich code editor for the user. False if not empty.
* @type bool $comment_shortcuts Whether to enable keyboard shortcuts for the user. Default false.
* @type string $admin_color The color scheme for a user's admin screen. Default 'fresh'.
* @type int|bool $use_ssl Whether to force SSL on the user's admin area. 0|false if SSL is
@ -2035,7 +2040,7 @@ function wp_create_user($username, $password, $email = '') {
* @return array List of user keys to be populated in wp_update_user().
*/
function _get_additional_user_keys( $user ) {
$keys = array( 'first_name', 'last_name', 'nickname', 'description', 'rich_editing', 'comment_shortcuts', 'admin_color', 'use_ssl', 'show_admin_bar_front', 'locale' );
$keys = array( 'first_name', 'last_name', 'nickname', 'description', 'rich_editing', 'syntax_highlighting', 'comment_shortcuts', 'admin_color', 'use_ssl', 'show_admin_bar_front', 'locale' );
return array_merge( $keys, array_keys( wp_get_user_contact_methods( $user ) ) );
}

View File

@ -4,7 +4,7 @@
*
* @global string $wp_version
*/
$wp_version = '4.9-alpha-41375';
$wp_version = '4.9-alpha-41376';
/**
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.

View File

@ -16,6 +16,14 @@
*/
class WP_Widget_Custom_HTML extends WP_Widget {
/**
* Whether or not the widget has been registered yet.
*
* @since 4.9.0
* @var bool
*/
protected $registered = false;
/**
* Default instance.
*
@ -45,6 +53,33 @@ class WP_Widget_Custom_HTML extends WP_Widget {
parent::__construct( 'custom_html', __( 'Custom HTML' ), $widget_ops, $control_ops );
}
/**
* Add hooks for enqueueing assets when registering all widget instances of this widget class.
*
* @since 4.9.0
*
* @param integer $number Optional. The unique order number of this widget instance
* compared to other instances of the same class. Default -1.
*/
public function _register_one( $number = -1 ) {
parent::_register_one( $number );
if ( $this->registered ) {
return;
}
$this->registered = true;
wp_add_inline_script( 'custom-html-widgets', sprintf( 'wp.customHtmlWidgets.idBases.push( %s );', wp_json_encode( $this->id_base ) ) );
// Note that the widgets component in the customizer will also do the 'admin_print_scripts-widgets.php' action in WP_Customize_Widgets::print_scripts().
add_action( 'admin_print_scripts-widgets.php', array( $this, 'enqueue_admin_scripts' ) );
// Note that the widgets component in the customizer will also do the 'admin_footer-widgets.php' action in WP_Customize_Widgets::print_footer_scripts().
add_action( 'admin_footer-widgets.php', array( 'WP_Widget_Custom_HTML', 'render_control_template_scripts' ) );
// Note this action is used to ensure the help text is added to the end.
add_action( 'admin_head-widgets.php', array( 'WP_Widget_Custom_HTML', 'add_help_text' ) );
}
/**
* Outputs the content for the current Custom HTML widget instance.
*
@ -117,25 +152,69 @@ class WP_Widget_Custom_HTML extends WP_Widget {
return $instance;
}
/**
* Loads the required scripts and styles for the widget control.
*
* @since 4.9.0
*/
public function enqueue_admin_scripts() {
$settings = wp_enqueue_code_editor( array(
'type' => 'text/html',
) );
wp_enqueue_script( 'custom-html-widgets' );
if ( empty( $settings ) ) {
$settings = array(
'disabled' => true,
);
}
wp_add_inline_script( 'custom-html-widgets', sprintf( 'wp.customHtmlWidgets.init( %s );', wp_json_encode( $settings ) ), 'after' );
$l10n = array(
'errorNotice' => wp_array_slice_assoc(
/* translators: placeholder is error count */
_n_noop( 'There is %d error which must be fixed before you can save.', 'There are %d errors which must be fixed before you can save.' ),
array( 'singular', 'plural' )
),
);
wp_add_inline_script( 'custom-html-widgets', sprintf( 'jQuery.extend( wp.customHtmlWidgets.l10n, %s );', wp_json_encode( $l10n ) ), 'after' );
}
/**
* Outputs the Custom HTML widget settings form.
*
* @since 4.8.1
* @since 4.9.0 The form contains only hidden sync inputs. For the control UI, see `WP_Widget_Custom_HTML::render_control_template_scripts()`.
*
* @see WP_Widget_Custom_HTML::render_control_template_scripts()
* @param array $instance Current instance.
* @returns void
*/
public function form( $instance ) {
$instance = wp_parse_args( (array) $instance, $this->default_instance );
?>
<input id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" class="title sync-input" type="hidden" value="<?php echo esc_attr( $instance['title'] ); ?>"/>
<textarea id="<?php echo $this->get_field_id( 'content' ); ?>" name="<?php echo $this->get_field_name( 'content' ); ?>" class="content sync-input" hidden><?php echo esc_textarea( $instance['content'] ); ?></textarea>
<?php
}
/**
* Render form template scripts.
*
* @since 4.9.0
*/
public static function render_control_template_scripts() {
?>
<script type="text/html" id="tmpl-widget-custom-html-control-fields">
<# var elementIdPrefix = 'el' + String( Math.random() ).replace( /\D/g, '' ) + '_' #>
<p>
<label for="<?php echo $this->get_field_id( 'title' ); ?>"><?php _e( 'Title:' ); ?></label>
<input class="widefat" id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" type="text" value="<?php echo esc_attr( $instance['title'] ); ?>"/>
<label for="{{ elementIdPrefix }}title"><?php esc_html_e( 'Title:', 'default' ); ?></label>
<input id="{{ elementIdPrefix }}title" type="text" class="widefat title">
</p>
<p>
<label for="<?php echo $this->get_field_id( 'content' ); ?>"><?php _e( 'Content:' ); ?></label>
<textarea class="widefat code" rows="16" cols="20" id="<?php echo $this->get_field_id( 'content' ); ?>" name="<?php echo $this->get_field_name( 'content' ); ?>"><?php echo esc_textarea( $instance['content'] ); ?></textarea>
<label for="{{ elementIdPrefix }}content" class="screen-reader-text"><?php esc_html_e( 'Content:', 'default' ); ?></label>
<textarea id="{{ elementIdPrefix }}content" class="widefat code content" rows="16" cols="20"></textarea>
</p>
<?php if ( ! current_user_can( 'unfiltered_html' ) ) : ?>
@ -145,12 +224,56 @@ class WP_Widget_Custom_HTML extends WP_Widget {
$disallowed_html = array_diff( $probably_unsafe_html, array_keys( $allowed_html ) );
?>
<?php if ( ! empty( $disallowed_html ) ) : ?>
<# if ( data.codeEditorDisabled ) { #>
<p>
<?php _e( 'Some HTML tags are not permitted, including:' ); ?>
<?php _e( 'Some HTML tags are not permitted, including:', 'default' ); ?>
<code><?php echo join( '</code>, <code>', $disallowed_html ); ?></code>
</p>
<# } #>
<?php endif; ?>
<?php endif; ?>
<div class="code-editor-error-container"></div>
</script>
<?php
}
/**
* Add help text to widgets admin screen.
*
* @since 4.9.0
*/
public static function add_help_text() {
$screen = get_current_screen();
$content = '<p>';
$content .= __( 'Use the Custom HTML widget to add arbitrary HTML code to your widget areas.' );
$content .= '</p>';
$content .= '<p>' . __( 'When using a keyboard to navigate:' ) . '</p>';
$content .= '<ul>';
$content .= '<li>' . __( 'In the HTML edit field, Tab enters a tab character.' ) . '</li>';
$content .= '<li>' . __( 'To move keyboard focus, press Esc then Tab for the next element, or Esc then Shift+Tab for the previous element.' ) . '</li>';
$content .= '</ul>';
$content .= '<p>';
$content .= sprintf(
/* translators: placeholder is link to user profile */
__( 'The edit field automatically highlights code syntax. You can disable this in your %s to work in plan text mode.' ),
sprintf(
' <a href="%1$s" class="external-link" target="_blank">%2$s<span class="screen-reader-text">%3$s</span></a>',
esc_url( get_edit_profile_url() . '#syntax_highlighting' ),
__( 'user profile' ),
/* translators: accessibility text */
__( '(opens in a new window)', 'default' )
)
);
$content .= '</p>';
$screen->add_help_tab( array(
'id' => 'custom_html_widget',
'title' => __( 'Custom HTML Widget' ),
'content' => $content,
) );
}
}

View File

@ -353,6 +353,7 @@ class WP_Widget_Text extends WP_Widget {
public function enqueue_admin_scripts() {
wp_enqueue_editor();
wp_enqueue_script( 'text-widgets' );
wp_add_inline_script( 'text-widgets', 'wp.textWidgets.init();', 'after' );
}
/**