Widgets: Extend the Text widget with TinyMCE.
Introduces rich text formatting: bold, italic, lists, links. Props westonruter, azaozz, timmydcrawford, obenland, melchoyce. See #35760. Fixes #35243. git-svn-id: https://develop.svn.wordpress.org/trunk@40631 602fd350-edb4-49c9-b593-d223f7449a82
This commit is contained in:
parent
946bcc9c95
commit
5eb62b94ad
@ -456,7 +456,7 @@ module.exports = function(grunt) {
|
|||||||
dest: BUILD_DIR,
|
dest: BUILD_DIR,
|
||||||
ext: '.min.js',
|
ext: '.min.js',
|
||||||
src: [
|
src: [
|
||||||
'wp-admin/js/*.js',
|
'wp-admin/js/**/*.js',
|
||||||
'wp-includes/js/*.js',
|
'wp-includes/js/*.js',
|
||||||
'wp-includes/js/mediaelement/wp-mediaelement.js',
|
'wp-includes/js/mediaelement/wp-mediaelement.js',
|
||||||
'wp-includes/js/mediaelement/wp-playlist.js',
|
'wp-includes/js/mediaelement/wp-playlist.js',
|
||||||
|
@ -213,6 +213,21 @@
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Text Widget */
|
||||||
|
.wp-customizer div.mce-inline-toolbar-grp,
|
||||||
|
.wp-customizer div.mce-tooltip {
|
||||||
|
z-index: 500100 !important;
|
||||||
|
}
|
||||||
|
.wp-customizer .ui-autocomplete.wplink-autocomplete {
|
||||||
|
z-index: 500110; /* originally 100110, but z-index of .wp-full-overlay is 500000 */
|
||||||
|
}
|
||||||
|
.wp-customizer #wp-link-backdrop {
|
||||||
|
z-index: 500100; /* originally 100100, but z-index of .wp-full-overlay is 500000 */
|
||||||
|
}
|
||||||
|
.wp-customizer #wp-link-wrap {
|
||||||
|
z-index: 500105; /* originally 100105, but z-index of .wp-full-overlay is 500000 */
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Styles for new widget addition panel
|
* Styles for new widget addition panel
|
||||||
*/
|
*/
|
||||||
|
326
src/wp-admin/js/widgets/text-widgets.js
Normal file
326
src/wp-admin/js/widgets/text-widgets.js
Normal file
@ -0,0 +1,326 @@
|
|||||||
|
/* global tinymce, switchEditors */
|
||||||
|
/* eslint consistent-this: [ "error", "control" ] */
|
||||||
|
wp.textWidgets = ( function( $ ) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var component = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Text widget control.
|
||||||
|
*
|
||||||
|
* @class TextWidgetControl
|
||||||
|
* @constructor
|
||||||
|
* @abstract
|
||||||
|
*/
|
||||||
|
component.TextWidgetControl = Backbone.View.extend({
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View events.
|
||||||
|
*
|
||||||
|
* @type {Object}
|
||||||
|
*/
|
||||||
|
events: {},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize.
|
||||||
|
*
|
||||||
|
* @param {Object} options - Options.
|
||||||
|
* @param {Backbone.Model} options.model - Model.
|
||||||
|
* @param {jQuery} options.el - Control container element.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
initialize: function initialize( options ) {
|
||||||
|
var control = this;
|
||||||
|
|
||||||
|
if ( ! options.el ) {
|
||||||
|
throw new Error( 'Missing options.el' );
|
||||||
|
}
|
||||||
|
|
||||||
|
Backbone.View.prototype.initialize.call( control, options );
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
control.fieldContainer = $( '<div class="text-widget-fields"></div>' );
|
||||||
|
control.fieldContainer.html( wp.template( 'widget-text-control-fields' ) );
|
||||||
|
control.widgetContentContainer = control.$el.find( '.widget-content:first' );
|
||||||
|
control.widgetContentContainer.before( control.fieldContainer );
|
||||||
|
|
||||||
|
control.fields = {
|
||||||
|
title: control.fieldContainer.find( '.title' ),
|
||||||
|
text: control.fieldContainer.find( '.text' )
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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.widgetContentContainer.find( 'input[type=hidden].' + fieldName );
|
||||||
|
if ( syncInput.val() !== $( this ).val() ) {
|
||||||
|
syncInput.val( $( this ).val() );
|
||||||
|
syncInput.trigger( 'change' );
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Note that syncInput cannot be re-used because it will be destroyed with each widget-updated event.
|
||||||
|
fieldInput.val( control.widgetContentContainer.find( 'input[type=hidden].' + 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.widgetContentContainer.find( 'input[type=hidden].title' );
|
||||||
|
control.fields.title.val( syncInput.val() );
|
||||||
|
}
|
||||||
|
|
||||||
|
syncInput = control.widgetContentContainer.find( 'input[type=hidden].text' );
|
||||||
|
if ( control.fields.text.is( ':visible' ) ) {
|
||||||
|
if ( ! control.fields.text.is( document.activeElement ) ) {
|
||||||
|
control.fields.text.val( syncInput.val() );
|
||||||
|
}
|
||||||
|
} else if ( control.editor && ! control.editorFocused && syncInput.val() !== control.fields.text.val() ) {
|
||||||
|
control.editor.setContent( wp.editor.autop( syncInput.val() ) );
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize editor.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
initializeEditor: function initializeEditor() {
|
||||||
|
var control = this, changeDebounceDelay = 1000, id, textarea, restoreTextMode = false;
|
||||||
|
textarea = control.fields.text;
|
||||||
|
id = textarea.attr( 'id' );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build (or re-build) the visual editor.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function buildEditor() {
|
||||||
|
var editor, triggerChangeIfDirty, onInit;
|
||||||
|
|
||||||
|
// Abort building if the textarea is gone, likely due to the widget having been deleted entirely.
|
||||||
|
if ( ! document.getElementById( id ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy any existing editor so that it can be re-initialized after a widget-updated event.
|
||||||
|
if ( tinymce.get( id ) ) {
|
||||||
|
restoreTextMode = tinymce.get( id ).isHidden();
|
||||||
|
wp.editor.remove( id );
|
||||||
|
}
|
||||||
|
|
||||||
|
wp.editor.initialize( id, {
|
||||||
|
tinymce: {
|
||||||
|
wpautop: true
|
||||||
|
},
|
||||||
|
quicktags: true
|
||||||
|
} );
|
||||||
|
|
||||||
|
editor = window.tinymce.get( id );
|
||||||
|
if ( ! editor ) {
|
||||||
|
throw new Error( 'Failed to initialize editor' );
|
||||||
|
}
|
||||||
|
onInit = function() {
|
||||||
|
|
||||||
|
// When a widget is moved in the DOM the dynamically-created TinyMCE iframe will be destroyed and has to be re-built.
|
||||||
|
$( editor.getWin() ).on( 'unload', function() {
|
||||||
|
_.defer( buildEditor );
|
||||||
|
});
|
||||||
|
|
||||||
|
// If a prior mce instance was replaced, and it was in text mode, toggle to text mode.
|
||||||
|
if ( restoreTextMode ) {
|
||||||
|
switchEditors.go( id, 'toggle' );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if ( editor.initialized ) {
|
||||||
|
onInit();
|
||||||
|
} else {
|
||||||
|
editor.on( 'init', onInit );
|
||||||
|
}
|
||||||
|
|
||||||
|
control.editorFocused = false;
|
||||||
|
triggerChangeIfDirty = function() {
|
||||||
|
var updateWidgetBuffer = 300; // See wp.customize.Widgets.WidgetControl._setupUpdateUI() which uses 250ms for updateWidgetDebounced.
|
||||||
|
if ( editor.isDirty() ) {
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Account for race condition in customizer where user clicks Save & Publish while
|
||||||
|
* focus was just previously given to to the editor. Since updates to the editor
|
||||||
|
* are debounced at 1 second and since widget input changes are only synced to
|
||||||
|
* settings after 250ms, the customizer needs to be put into the processing
|
||||||
|
* state during the time between the change event is triggered and updateWidget
|
||||||
|
* logic starts. Note that the debounced update-widget request should be able
|
||||||
|
* to be removed with the removal of the update-widget request entirely once
|
||||||
|
* widgets are able to mutate their own instance props directly in JS without
|
||||||
|
* having to make server round-trips to call the respective WP_Widget::update()
|
||||||
|
* callbacks. See <https://core.trac.wordpress.org/ticket/33507>.
|
||||||
|
*/
|
||||||
|
if ( wp.customize ) {
|
||||||
|
wp.customize.state( 'processing' ).set( wp.customize.state( 'processing' ).get() + 1 );
|
||||||
|
_.delay( function() {
|
||||||
|
wp.customize.state( 'processing' ).set( wp.customize.state( 'processing' ).get() - 1 );
|
||||||
|
}, updateWidgetBuffer );
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.save();
|
||||||
|
textarea.trigger( 'change' );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
editor.on( 'focus', function() {
|
||||||
|
control.editorFocused = true;
|
||||||
|
} );
|
||||||
|
editor.on( 'NodeChange', _.debounce( triggerChangeIfDirty, changeDebounceDelay ) );
|
||||||
|
editor.on( 'blur', function() {
|
||||||
|
control.editorFocused = false;
|
||||||
|
triggerChangeIfDirty();
|
||||||
|
} );
|
||||||
|
|
||||||
|
control.editor = editor;
|
||||||
|
}
|
||||||
|
|
||||||
|
buildEditor();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapping of widget ID to instances of TextWidgetControl subclasses.
|
||||||
|
*
|
||||||
|
* @type {Object.<string, wp.textWidgets.TextWidgetControl>}
|
||||||
|
*/
|
||||||
|
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, widgetInside, renderWhenAnimationDone;
|
||||||
|
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 ( 'text' !== idBase ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent initializing already-added widgets.
|
||||||
|
widgetId = widgetForm.find( '> .widget-id' ).val();
|
||||||
|
if ( component.widgetControls[ widgetId ] ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
widgetControl = new component.TextWidgetControl({
|
||||||
|
el: widgetContainer
|
||||||
|
});
|
||||||
|
|
||||||
|
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 an iframe can be embedded
|
||||||
|
* with TinyMCE being able to set contenteditable on it.
|
||||||
|
*/
|
||||||
|
widgetInside = widgetContainer.parent();
|
||||||
|
renderWhenAnimationDone = function() {
|
||||||
|
if ( widgetInside.is( ':animated' ) ) {
|
||||||
|
setTimeout( renderWhenAnimationDone, animatedCheckDelay );
|
||||||
|
} else {
|
||||||
|
widgetControl.initializeEditor();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
renderWhenAnimationDone();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 ( 'text' !== 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().
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
component.init = function init() {
|
||||||
|
var $document = $( document );
|
||||||
|
$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 );
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return component;
|
||||||
|
})( jQuery );
|
@ -164,7 +164,11 @@ add_filter( 'list_cats', 'wptexturize' );
|
|||||||
|
|
||||||
add_filter( 'wp_sprintf', 'wp_sprintf_l', 10, 2 );
|
add_filter( 'wp_sprintf', 'wp_sprintf_l', 10, 2 );
|
||||||
|
|
||||||
add_filter( 'widget_text', 'balanceTags' );
|
add_filter( 'widget_text', 'balanceTags' );
|
||||||
|
add_filter( 'widget_text_content', 'capital_P_dangit', 11 );
|
||||||
|
add_filter( 'widget_text_content', 'wptexturize' );
|
||||||
|
add_filter( 'widget_text_content', 'convert_smilies', 20 );
|
||||||
|
add_filter( 'widget_text_content', 'wpautop' );
|
||||||
|
|
||||||
add_filter( 'date_i18n', 'wp_maybe_decline_date' );
|
add_filter( 'date_i18n', 'wp_maybe_decline_date' );
|
||||||
|
|
||||||
|
@ -602,6 +602,8 @@ function wp_default_scripts( &$scripts ) {
|
|||||||
$scripts->add( 'admin-gallery', "/wp-admin/js/gallery$suffix.js", array( 'jquery-ui-sortable' ) );
|
$scripts->add( 'admin-gallery', "/wp-admin/js/gallery$suffix.js", array( 'jquery-ui-sortable' ) );
|
||||||
|
|
||||||
$scripts->add( 'admin-widgets', "/wp-admin/js/widgets$suffix.js", array( 'jquery-ui-sortable', 'jquery-ui-draggable', 'jquery-ui-droppable' ), false, 1 );
|
$scripts->add( 'admin-widgets', "/wp-admin/js/widgets$suffix.js", array( 'jquery-ui-sortable', 'jquery-ui-draggable', 'jquery-ui-droppable' ), false, 1 );
|
||||||
|
$scripts->add( 'text-widgets', "/wp-admin/js/widgets/text-widgets$suffix.js", array( 'jquery', 'backbone', 'editor', 'wp-util' ) );
|
||||||
|
$scripts->add_inline_script( 'text-widgets', 'wp.textWidgets.init();', 'after' );
|
||||||
|
|
||||||
$scripts->add( 'theme', "/wp-admin/js/theme$suffix.js", array( 'wp-backbone', 'wp-a11y' ), false, 1 );
|
$scripts->add( 'theme', "/wp-admin/js/theme$suffix.js", array( 'wp-backbone', 'wp-a11y' ), false, 1 );
|
||||||
|
|
||||||
|
@ -28,10 +28,30 @@ class WP_Widget_Text extends WP_Widget {
|
|||||||
'description' => __( 'Arbitrary text or HTML.' ),
|
'description' => __( 'Arbitrary text or HTML.' ),
|
||||||
'customize_selective_refresh' => true,
|
'customize_selective_refresh' => true,
|
||||||
);
|
);
|
||||||
$control_ops = array( 'width' => 400, 'height' => 350 );
|
$control_ops = array(
|
||||||
|
'width' => 400,
|
||||||
|
'height' => 350,
|
||||||
|
);
|
||||||
parent::__construct( 'text', __( 'Text' ), $widget_ops, $control_ops );
|
parent::__construct( 'text', __( 'Text' ), $widget_ops, $control_ops );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add hooks for enqueueing assets when registering all widget instances of this widget class.
|
||||||
|
*
|
||||||
|
* @since 4.8.0
|
||||||
|
* @access public
|
||||||
|
*/
|
||||||
|
public function _register() {
|
||||||
|
|
||||||
|
// 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( $this, 'render_control_template_scripts' ) );
|
||||||
|
|
||||||
|
parent::_register();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Outputs the content for the current Text widget instance.
|
* Outputs the content for the current Text widget instance.
|
||||||
*
|
*
|
||||||
@ -61,11 +81,34 @@ class WP_Widget_Text extends WP_Widget {
|
|||||||
*/
|
*/
|
||||||
$text = apply_filters( 'widget_text', $widget_text, $instance, $this );
|
$text = apply_filters( 'widget_text', $widget_text, $instance, $this );
|
||||||
|
|
||||||
|
if ( isset( $instance['filter'] ) ) {
|
||||||
|
if ( 'content' === $instance['filter'] ) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters the content of the Text widget to apply changes expected from the visual (TinyMCE) editor.
|
||||||
|
*
|
||||||
|
* By default a subset of the_content filters are applied, including wpautop and wptexturize.
|
||||||
|
*
|
||||||
|
* @since 4.8.0
|
||||||
|
*
|
||||||
|
* @param string $widget_text The widget content.
|
||||||
|
* @param array $instance Array of settings for the current widget.
|
||||||
|
* @param WP_Widget_Text $this Current Text widget instance.
|
||||||
|
*/
|
||||||
|
$text = apply_filters( 'widget_text_content', $widget_text, $instance, $this );
|
||||||
|
|
||||||
|
} elseif ( $instance['filter'] ) {
|
||||||
|
$text = wpautop( $text ); // Back-compat for instances prior to 4.8.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
echo $args['before_widget'];
|
echo $args['before_widget'];
|
||||||
if ( ! empty( $title ) ) {
|
if ( ! empty( $title ) ) {
|
||||||
echo $args['before_title'] . $title . $args['after_title'];
|
echo $args['before_title'] . $title . $args['after_title'];
|
||||||
} ?>
|
}
|
||||||
<div class="textwidget"><?php echo !empty( $instance['filter'] ) ? wpautop( $text ) : $text; ?></div>
|
|
||||||
|
?>
|
||||||
|
<div class="textwidget"><?php echo $text; ?></div>
|
||||||
<?php
|
<?php
|
||||||
echo $args['after_widget'];
|
echo $args['after_widget'];
|
||||||
}
|
}
|
||||||
@ -89,30 +132,73 @@ class WP_Widget_Text extends WP_Widget {
|
|||||||
} else {
|
} else {
|
||||||
$instance['text'] = wp_kses_post( $new_instance['text'] );
|
$instance['text'] = wp_kses_post( $new_instance['text'] );
|
||||||
}
|
}
|
||||||
$instance['filter'] = ! empty( $new_instance['filter'] );
|
|
||||||
|
/*
|
||||||
|
* Re-use legacy 'filter' (wpautop) property to now indicate content filters will always apply.
|
||||||
|
* Prior to 4.8, this is a boolean value used to indicate whether or not wpautop should be
|
||||||
|
* applied. By re-using this property, downgrading WordPress from 4.8 to 4.7 will ensure
|
||||||
|
* that the content for Text widgets created with TinyMCE will continue to get wpautop.
|
||||||
|
*/
|
||||||
|
$instance['filter'] = 'content';
|
||||||
|
|
||||||
return $instance;
|
return $instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the required scripts and styles for the widget control.
|
||||||
|
*
|
||||||
|
* @since 4.8.0
|
||||||
|
* @access public
|
||||||
|
*/
|
||||||
|
public function enqueue_admin_scripts() {
|
||||||
|
wp_enqueue_editor();
|
||||||
|
wp_enqueue_script( 'text-widgets' );
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Outputs the Text widget settings form.
|
* Outputs the Text widget settings form.
|
||||||
*
|
*
|
||||||
* @since 2.8.0
|
* @since 2.8.0
|
||||||
|
* @since 4.8.0 Form only contains hidden inputs which are synced with JS template.
|
||||||
* @access public
|
* @access public
|
||||||
|
* @see WP_Widget_Visual_Text::render_control_template_scripts()
|
||||||
*
|
*
|
||||||
* @param array $instance Current settings.
|
* @param array $instance Current settings.
|
||||||
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function form( $instance ) {
|
public function form( $instance ) {
|
||||||
$instance = wp_parse_args( (array) $instance, array( 'title' => '', 'text' => '' ) );
|
$instance = wp_parse_args(
|
||||||
$filter = isset( $instance['filter'] ) ? $instance['filter'] : 0;
|
(array) $instance,
|
||||||
$title = sanitize_text_field( $instance['title'] );
|
array(
|
||||||
|
'title' => '',
|
||||||
|
'text' => '',
|
||||||
|
)
|
||||||
|
);
|
||||||
?>
|
?>
|
||||||
<p><label for="<?php echo $this->get_field_id('title'); ?>"><?php _e('Title:'); ?></label>
|
<input id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" class="title" type="hidden" value="<?php echo esc_attr( $instance['title'] ); ?>">
|
||||||
<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($title); ?>" /></p>
|
<input id="<?php echo $this->get_field_id( 'text' ); ?>" name="<?php echo $this->get_field_name( 'text' ); ?>" class="text" type="hidden" value="<?php echo esc_attr( $instance['text'] ); ?>">
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
<p><label for="<?php echo $this->get_field_id( 'text' ); ?>"><?php _e( 'Content:' ); ?></label>
|
/**
|
||||||
<textarea class="widefat" rows="16" cols="20" id="<?php echo $this->get_field_id('text'); ?>" name="<?php echo $this->get_field_name('text'); ?>"><?php echo esc_textarea( $instance['text'] ); ?></textarea></p>
|
* Render form template scripts.
|
||||||
|
*
|
||||||
<p><input id="<?php echo $this->get_field_id('filter'); ?>" name="<?php echo $this->get_field_name('filter'); ?>" type="checkbox"<?php checked( $filter ); ?> /> <label for="<?php echo $this->get_field_id('filter'); ?>"><?php _e('Automatically add paragraphs'); ?></label></p>
|
* @since 4.8.0
|
||||||
|
* @access public
|
||||||
|
*/
|
||||||
|
public function render_control_template_scripts() {
|
||||||
|
?>
|
||||||
|
<script type="text/html" id="tmpl-widget-text-control-fields">
|
||||||
|
<# var elementIdPrefix = 'el' + String( Math.random() ).replace( /\D/g, '' ) + '_' #>
|
||||||
|
<p>
|
||||||
|
<label for="{{ elementIdPrefix }}title"><?php esc_html_e( 'Title:' ); ?></label>
|
||||||
|
<input id="{{ elementIdPrefix }}title" type="text" class="widefat title">
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label for="{{ elementIdPrefix }}text" class="screen-reader-text"><?php esc_html_e( 'Content:' ); ?></label>
|
||||||
|
<textarea id="{{ elementIdPrefix }}text" class="widefat text" style="height: 200px" rows="16" cols="20"></textarea>
|
||||||
|
</p>
|
||||||
|
</script>
|
||||||
<?php
|
<?php
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
249
tests/phpunit/tests/widgets/text-widget.php
Normal file
249
tests/phpunit/tests/widgets/text-widget.php
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Unit tests covering WP_Widget_Text functionality.
|
||||||
|
*
|
||||||
|
* @package WordPress
|
||||||
|
* @subpackage widgets
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test wp-includes/widgets/class-wp-widget-text.php
|
||||||
|
*
|
||||||
|
* @group widgets
|
||||||
|
*/
|
||||||
|
class Test_WP_Widget_Text extends WP_UnitTestCase {
|
||||||
|
/**
|
||||||
|
* Args passed to the widget_text filter.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $widget_text_args;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Args passed to the widget_text_content filter.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $widget_text_content_args;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up global scope.
|
||||||
|
*
|
||||||
|
* @global WP_Scripts $wp_scripts
|
||||||
|
* @global WP_Styles $wp_style
|
||||||
|
*/
|
||||||
|
function clean_up_global_scope() {
|
||||||
|
global $wp_scripts, $wp_styles;
|
||||||
|
parent::clean_up_global_scope();
|
||||||
|
$wp_scripts = null;
|
||||||
|
$wp_styles = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test enqueue_admin_scripts method.
|
||||||
|
*
|
||||||
|
* @covers WP_Widget_Text::_register
|
||||||
|
*/
|
||||||
|
function test__register() {
|
||||||
|
set_current_screen( 'widgets.php' );
|
||||||
|
$widget = new WP_Widget_Text();
|
||||||
|
$widget->_register();
|
||||||
|
|
||||||
|
$this->assertEquals( 10, has_action( 'admin_print_scripts-widgets.php', array( $widget, 'enqueue_admin_scripts' ) ) );
|
||||||
|
$this->assertEquals( 10, has_action( 'admin_footer-widgets.php', array( $widget, 'render_control_template_scripts' ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test widget method.
|
||||||
|
*
|
||||||
|
* @covers WP_Widget_Text::widget
|
||||||
|
*/
|
||||||
|
function test_widget() {
|
||||||
|
$widget = new WP_Widget_Text();
|
||||||
|
$text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n Praesent ut turpis consequat lorem volutpat bibendum vitae vitae ante.";
|
||||||
|
|
||||||
|
$args = array(
|
||||||
|
'before_title' => '<h2>',
|
||||||
|
'after_title' => "</h2>\n",
|
||||||
|
'before_widget' => '<section>',
|
||||||
|
'after_widget' => "</section>\n",
|
||||||
|
);
|
||||||
|
$instance = array(
|
||||||
|
'title' => 'Foo',
|
||||||
|
'text' => $text,
|
||||||
|
'filter' => false,
|
||||||
|
);
|
||||||
|
|
||||||
|
add_filter( 'widget_text_content', array( $this, 'filter_widget_text_content' ), 10, 3 );
|
||||||
|
add_filter( 'widget_text', array( $this, 'filter_widget_text' ), 10, 3 );
|
||||||
|
|
||||||
|
// Test with filter=false.
|
||||||
|
ob_start();
|
||||||
|
$widget->widget( $args, $instance );
|
||||||
|
$output = ob_get_clean();
|
||||||
|
$this->assertNotContains( '<p>', $output );
|
||||||
|
$this->assertNotContains( '<br />', $output );
|
||||||
|
$this->assertEmpty( $this->widget_text_content_args );
|
||||||
|
$this->assertNotEmpty( $this->widget_text_args );
|
||||||
|
|
||||||
|
// Test with filter=true.
|
||||||
|
$instance['filter'] = true;
|
||||||
|
ob_start();
|
||||||
|
$widget->widget( $args, $instance );
|
||||||
|
$output = ob_get_clean();
|
||||||
|
$this->assertContains( '<p>', $output );
|
||||||
|
$this->assertContains( '<br />', $output );
|
||||||
|
$this->assertNotEmpty( $this->widget_text_args );
|
||||||
|
$this->assertEquals( $instance['text'], $this->widget_text_args[0] );
|
||||||
|
$this->assertEquals( $instance, $this->widget_text_args[1] );
|
||||||
|
$this->assertEquals( $widget, $this->widget_text_args[2] );
|
||||||
|
$this->assertEmpty( $this->widget_text_content_args );
|
||||||
|
|
||||||
|
// Test with filter=content, the upgraded widget.
|
||||||
|
$instance['filter'] = 'content';
|
||||||
|
ob_start();
|
||||||
|
$widget->widget( $args, $instance );
|
||||||
|
$output = ob_get_clean();
|
||||||
|
$this->assertContains( '<p>', $output );
|
||||||
|
$this->assertContains( '<br />', $output );
|
||||||
|
$this->assertCount( 3, $this->widget_text_args );
|
||||||
|
$this->assertEquals( $instance['text'], $this->widget_text_args[0] );
|
||||||
|
$this->assertEquals( $instance, $this->widget_text_args[1] );
|
||||||
|
$this->assertEquals( $widget, $this->widget_text_args[2] );
|
||||||
|
$this->assertCount( 3, $this->widget_text_content_args );
|
||||||
|
$this->assertEquals( wpautop( $instance['text'] ), $this->widget_text_content_args[0] );
|
||||||
|
$this->assertEquals( $instance, $this->widget_text_content_args[1] );
|
||||||
|
$this->assertEquals( $widget, $this->widget_text_content_args[2] );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters the content of the Text widget.
|
||||||
|
*
|
||||||
|
* @param string $widget_text The widget content.
|
||||||
|
* @param array $instance Array of settings for the current widget.
|
||||||
|
* @param WP_Widget_Text $widget Current Text widget instance.
|
||||||
|
* @return string Widget text.
|
||||||
|
*/
|
||||||
|
function filter_widget_text( $widget_text, $instance, $widget ) {
|
||||||
|
$this->widget_text_args = func_get_args();
|
||||||
|
|
||||||
|
return $widget_text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters the content of the Text widget to apply changes expected from the visual (TinyMCE) editor.
|
||||||
|
*
|
||||||
|
* @param string $widget_text The widget content.
|
||||||
|
* @param array $instance Array of settings for the current widget.
|
||||||
|
* @param WP_Widget_Text $widget Current Text widget instance.
|
||||||
|
* @return string Widget content.
|
||||||
|
*/
|
||||||
|
function filter_widget_text_content( $widget_text, $instance, $widget ) {
|
||||||
|
$this->widget_text_content_args = func_get_args();
|
||||||
|
|
||||||
|
return $widget_text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test update method.
|
||||||
|
*
|
||||||
|
* @covers WP_Widget_Text::update
|
||||||
|
*/
|
||||||
|
function test_update() {
|
||||||
|
$widget = new WP_Widget_Text();
|
||||||
|
$instance = array(
|
||||||
|
'title' => "The\nTitle",
|
||||||
|
'text' => "The\n\nText",
|
||||||
|
'filter' => false,
|
||||||
|
);
|
||||||
|
|
||||||
|
wp_set_current_user( $this->factory()->user->create( array(
|
||||||
|
'role' => 'administrator',
|
||||||
|
) ) );
|
||||||
|
|
||||||
|
// Should return valid instance.
|
||||||
|
$expected = array(
|
||||||
|
'title' => sanitize_text_field( $instance['title'] ),
|
||||||
|
'text' => $instance['text'],
|
||||||
|
'filter' => 'content',
|
||||||
|
);
|
||||||
|
$result = $widget->update( $instance, array() );
|
||||||
|
$this->assertEquals( $result, $expected );
|
||||||
|
$this->assertTrue( ! empty( $expected['filter'] ), 'Expected filter prop to be truthy, to handle case where 4.8 is downgraded to 4.7.' );
|
||||||
|
|
||||||
|
// Make sure KSES is applying as expected.
|
||||||
|
add_filter( 'map_meta_cap', array( $this, 'grant_unfiltered_html_cap' ), 10, 2 );
|
||||||
|
$this->assertTrue( current_user_can( 'unfiltered_html' ) );
|
||||||
|
$instance['text'] = '<script>alert( "Howdy!" );</script>';
|
||||||
|
$expected['text'] = $instance['text'];
|
||||||
|
$result = $widget->update( $instance, array() );
|
||||||
|
$this->assertEquals( $result, $expected );
|
||||||
|
|
||||||
|
remove_filter( 'map_meta_cap', array( $this, 'grant_unfiltered_html_cap' ) );
|
||||||
|
add_filter( 'map_meta_cap', array( $this, 'revoke_unfiltered_html_cap' ), 10, 2 );
|
||||||
|
$this->assertFalse( current_user_can( 'unfiltered_html' ) );
|
||||||
|
$instance['text'] = '<script>alert( "Howdy!" );</script>';
|
||||||
|
$expected['text'] = wp_kses_post( $instance['text'] );
|
||||||
|
$result = $widget->update( $instance, array() );
|
||||||
|
$this->assertEquals( $result, $expected );
|
||||||
|
remove_filter( 'map_meta_cap', array( $this, 'revoke_unfiltered_html_cap' ), 10, 2 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Grant unfiltered_html cap via map_meta_cap.
|
||||||
|
*
|
||||||
|
* @param array $caps Returns the user's actual capabilities.
|
||||||
|
* @param string $cap Capability name.
|
||||||
|
* @return array Caps.
|
||||||
|
*/
|
||||||
|
function grant_unfiltered_html_cap( $caps, $cap ) {
|
||||||
|
if ( 'unfiltered_html' === $cap ) {
|
||||||
|
$caps = array_diff( $caps, array( 'do_not_allow' ) );
|
||||||
|
$caps[] = 'unfiltered_html';
|
||||||
|
}
|
||||||
|
return $caps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Revoke unfiltered_html cap via map_meta_cap.
|
||||||
|
*
|
||||||
|
* @param array $caps Returns the user's actual capabilities.
|
||||||
|
* @param string $cap Capability name.
|
||||||
|
* @return array Caps.
|
||||||
|
*/
|
||||||
|
function revoke_unfiltered_html_cap( $caps, $cap ) {
|
||||||
|
if ( 'unfiltered_html' === $cap ) {
|
||||||
|
$caps = array_diff( $caps, array( 'unfiltered_html' ) );
|
||||||
|
$caps[] = 'do_not_allow';
|
||||||
|
}
|
||||||
|
return $caps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test enqueue_admin_scripts method.
|
||||||
|
*
|
||||||
|
* @covers WP_Widget_Text::enqueue_admin_scripts
|
||||||
|
*/
|
||||||
|
function test_enqueue_admin_scripts() {
|
||||||
|
set_current_screen( 'widgets.php' );
|
||||||
|
$widget = new WP_Widget_Text();
|
||||||
|
$widget->enqueue_admin_scripts();
|
||||||
|
|
||||||
|
$this->assertTrue( wp_script_is( 'text-widgets' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test render_control_template_scripts method.
|
||||||
|
*
|
||||||
|
* @covers WP_Widget_Text::render_control_template_scripts
|
||||||
|
*/
|
||||||
|
function test_render_control_template_scripts() {
|
||||||
|
$widget = new WP_Widget_Text();
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
$widget->render_control_template_scripts();
|
||||||
|
$output = ob_get_clean();
|
||||||
|
|
||||||
|
$this->assertContains( '<script type="text/html" id="tmpl-widget-text-control-fields">', $output );
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user