<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[40631] trunk: Widgets: Extend the Text widget with TinyMCE.</title>
</head>
<body>
<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; }
#msg dl a { font-weight: bold}
#msg dl a:link { color:#fc3; }
#msg dl a:active { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="https://core.trac.wordpress.org/changeset/40631">40631</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"https://core.trac.wordpress.org/changeset/40631","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>westonruter</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2017-05-11 18:54:24 +0000 (Thu, 11 May 2017)</dd>
</dl>
<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>Widgets: Extend the Text widget with TinyMCE.
Introduces rich text formatting: bold, italic, lists, links.
Props westonruter, azaozz, timmydcrawford, obenland, melchoyce.
See <a href="https://core.trac.wordpress.org/ticket/35760">#35760</a>.
Fixes <a href="https://core.trac.wordpress.org/ticket/35243">#35243</a>.</pre>
<h3>Modified Paths</h3>
<ul>
<li><a href="#trunkGruntfilejs">trunk/Gruntfile.js</a></li>
<li><a href="#trunksrcwpadmincsscustomizewidgetscss">trunk/src/wp-admin/css/customize-widgets.css</a></li>
<li><a href="#trunksrcwpincludesdefaultfiltersphp">trunk/src/wp-includes/default-filters.php</a></li>
<li><a href="#trunksrcwpincludesscriptloaderphp">trunk/src/wp-includes/script-loader.php</a></li>
<li><a href="#trunksrcwpincludeswidgetsclasswpwidgettextphp">trunk/src/wp-includes/widgets/class-wp-widget-text.php</a></li>
</ul>
<h3>Added Paths</h3>
<ul>
<li>trunk/src/wp-admin/js/widgets/</li>
<li><a href="#trunksrcwpadminjswidgetstextwidgetsjs">trunk/src/wp-admin/js/widgets/text-widgets.js</a></li>
<li>trunk/tests/phpunit/tests/widgets/</li>
<li><a href="#trunktestsphpunittestswidgetstextwidgetphp">trunk/tests/phpunit/tests/widgets/text-widget.php</a></li>
</ul>
</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunkGruntfilejs"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/Gruntfile.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/Gruntfile.js 2017-05-11 18:40:17 UTC (rev 40630)
+++ trunk/Gruntfile.js 2017-05-11 18:54:24 UTC (rev 40631)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -456,7 +456,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> dest: BUILD_DIR,
</span><span class="cx" style="display: block; padding: 0 10px"> ext: '.min.js',
</span><span class="cx" style="display: block; padding: 0 10px"> src: [
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- 'wp-admin/js/*.js',
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ 'wp-admin/js/**/*.js',
</ins><span class="cx" style="display: block; padding: 0 10px"> 'wp-includes/js/*.js',
</span><span class="cx" style="display: block; padding: 0 10px"> 'wp-includes/js/mediaelement/wp-mediaelement.js',
</span><span class="cx" style="display: block; padding: 0 10px"> 'wp-includes/js/mediaelement/wp-playlist.js',
</span></span></pre></div>
<a id="trunksrcwpadmincsscustomizewidgetscss"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-admin/css/customize-widgets.css</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/css/customize-widgets.css 2017-05-11 18:40:17 UTC (rev 40630)
+++ trunk/src/wp-admin/css/customize-widgets.css 2017-05-11 18:54:24 UTC (rev 40631)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -213,6 +213,21 @@
</span><span class="cx" style="display: block; padding: 0 10px"> display: block;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+/* 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 */
+}
+
</ins><span class="cx" style="display: block; padding: 0 10px"> /**
</span><span class="cx" style="display: block; padding: 0 10px"> * Styles for new widget addition panel
</span><span class="cx" style="display: block; padding: 0 10px"> */
</span></span></pre></div>
<a id="trunksrcwpadminjswidgetstextwidgetsjs"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/src/wp-admin/js/widgets/text-widgets.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/js/widgets/text-widgets.js (rev 0)
+++ trunk/src/wp-admin/js/widgets/text-widgets.js 2017-05-11 18:54:24 UTC (rev 40631)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,326 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+/* 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 );
</ins></span></pre></div>
<a id="trunksrcwpincludesdefaultfiltersphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/default-filters.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/default-filters.php 2017-05-11 18:40:17 UTC (rev 40630)
+++ trunk/src/wp-includes/default-filters.php 2017-05-11 18:54:24 UTC (rev 40631)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -164,7 +164,11 @@
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> add_filter( 'wp_sprintf', 'wp_sprintf_l', 10, 2 );
</span><span class="cx" style="display: block; padding: 0 10px">
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-add_filter( 'widget_text', 'balanceTags' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+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' );
</ins><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> add_filter( 'date_i18n', 'wp_maybe_decline_date' );
</span><span class="cx" style="display: block; padding: 0 10px">
</span></span></pre></div>
<a id="trunksrcwpincludesscriptloaderphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/script-loader.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/script-loader.php 2017-05-11 18:40:17 UTC (rev 40630)
+++ trunk/src/wp-includes/script-loader.php 2017-05-11 18:54:24 UTC (rev 40631)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -602,6 +602,8 @@
</span><span class="cx" style="display: block; padding: 0 10px"> $scripts->add( 'admin-gallery', "/wp-admin/js/gallery$suffix.js", array( 'jquery-ui-sortable' ) );
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> $scripts->add( 'admin-widgets', "/wp-admin/js/widgets$suffix.js", array( 'jquery-ui-sortable', 'jquery-ui-draggable', 'jquery-ui-droppable' ), false, 1 );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $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' );
</ins><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> $scripts->add( 'theme', "/wp-admin/js/theme$suffix.js", array( 'wp-backbone', 'wp-a11y' ), false, 1 );
</span><span class="cx" style="display: block; padding: 0 10px">
</span></span></pre></div>
<a id="trunksrcwpincludeswidgetsclasswpwidgettextphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/widgets/class-wp-widget-text.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/widgets/class-wp-widget-text.php 2017-05-11 18:40:17 UTC (rev 40630)
+++ trunk/src/wp-includes/widgets/class-wp-widget-text.php 2017-05-11 18:54:24 UTC (rev 40631)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -28,11 +28,31 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 'description' => __( 'Arbitrary text or HTML.' ),
</span><span class="cx" style="display: block; padding: 0 10px"> 'customize_selective_refresh' => true,
</span><span class="cx" style="display: block; padding: 0 10px"> );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- $control_ops = array( 'width' => 400, 'height' => 350 );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $control_ops = array(
+ 'width' => 400,
+ 'height' => 350,
+ );
</ins><span class="cx" style="display: block; padding: 0 10px"> parent::__construct( 'text', __( 'Text' ), $widget_ops, $control_ops );
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * 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();
+ }
+
+ /**
</ins><span class="cx" style="display: block; padding: 0 10px"> * Outputs the content for the current Text widget instance.
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="cx" style="display: block; padding: 0 10px"> * @since 2.8.0
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -61,11 +81,34 @@
</span><span class="cx" style="display: block; padding: 0 10px"> */
</span><span class="cx" style="display: block; padding: 0 10px"> $text = apply_filters( 'widget_text', $widget_text, $instance, $this );
</span><span class="cx" style="display: block; padding: 0 10px">
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ 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.
+ }
+ }
+
</ins><span class="cx" style="display: block; padding: 0 10px"> echo $args['before_widget'];
</span><span class="cx" style="display: block; padding: 0 10px"> if ( ! empty( $title ) ) {
</span><span class="cx" style="display: block; padding: 0 10px"> echo $args['before_title'] . $title . $args['after_title'];
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- } ?>
- <div class="textwidget"><?php echo !empty( $instance['filter'] ) ? wpautop( $text ) : $text; ?></div>
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ }
+
+ ?>
+ <div class="textwidget"><?php echo $text; ?></div>
</ins><span class="cx" style="display: block; padding: 0 10px"> <?php
</span><span class="cx" style="display: block; padding: 0 10px"> echo $args['after_widget'];
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -89,30 +132,73 @@
</span><span class="cx" style="display: block; padding: 0 10px"> } else {
</span><span class="cx" style="display: block; padding: 0 10px"> $instance['text'] = wp_kses_post( $new_instance['text'] );
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- $instance['filter'] = ! empty( $new_instance['filter'] );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+ /*
+ * 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';
+
</ins><span class="cx" style="display: block; padding: 0 10px"> return $instance;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * 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' );
+ }
+
+ /**
</ins><span class="cx" style="display: block; padding: 0 10px"> * Outputs the Text widget settings form.
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="cx" style="display: block; padding: 0 10px"> * @since 2.8.0
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * @since 4.8.0 Form only contains hidden inputs which are synced with JS template.
</ins><span class="cx" style="display: block; padding: 0 10px"> * @access public
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * @see WP_Widget_Visual_Text::render_control_template_scripts()
</ins><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="cx" style="display: block; padding: 0 10px"> * @param array $instance Current settings.
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * @return void
</ins><span class="cx" style="display: block; padding: 0 10px"> */
</span><span class="cx" style="display: block; padding: 0 10px"> public function form( $instance ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- $instance = wp_parse_args( (array) $instance, array( 'title' => '', 'text' => '' ) );
- $filter = isset( $instance['filter'] ) ? $instance['filter'] : 0;
- $title = sanitize_text_field( $instance['title'] );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $instance = wp_parse_args(
+ (array) $instance,
+ array(
+ 'title' => '',
+ 'text' => '',
+ )
+ );
</ins><span class="cx" style="display: block; padding: 0 10px"> ?>
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- <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($title); ?>" /></p>
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ <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 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
+ }
</ins><span class="cx" style="display: block; padding: 0 10px">
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- <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>
-
- <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>
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ /**
+ * Render form template scripts.
+ *
+ * @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>
</ins><span class="cx" style="display: block; padding: 0 10px"> <?php
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span></span></pre></div>
<a id="trunktestsphpunittestswidgetstextwidgetphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/widgets/text-widget.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/widgets/text-widget.php (rev 0)
+++ trunk/tests/phpunit/tests/widgets/text-widget.php 2017-05-11 18:54:24 UTC (rev 40631)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,249 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?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 );
+ }
+}
</ins></span></pre>
</div>
</div>
</body>
</html>