Widgets: Add legacy mode for Text widget and add usage pointers to default visual mode.

The Text widget in legacy mode omits TinyMCE and retains old behavior for matching pre-existing Text widgets. Usage pointers added to default visual mode appear when attempting to paste HTML code into the Visual tab and when clicking on the Text tab, informing users of the new Custom HTML widget.

Merges [41050] to 4.8 branch.
Props westonruter, melchoyce, gitlost for testing, obenland for testing, dougal for testing, afercia for testing.
See #35243.
Fixes #40951 for 4.8.1.

Built from https://develop.svn.wordpress.org/branches/4.8@41053


git-svn-id: http://core.svn.wordpress.org/branches/4.8@40903 1a063a9b-81f0-0310-95a4-ce76da25c4cd
This commit is contained in:
Weston Ruter 2017-07-14 17:28:32 +00:00
parent e5f6b9078e
commit c4ac9d0ade
8 changed files with 381 additions and 11 deletions

View File

@ -619,6 +619,29 @@ div#widgets-right .widget-top:hover,
cursor: move;
}
/* =Specific widget styling
-------------------------------------------------------------- */
.text-widget-fields {
position: relative;
}
.text-widget-fields [hidden] {
display: none;
}
.text-widget-fields .wp-pointer.wp-pointer-top {
position: absolute;
z-index: 3;
top: 100px;
left: 10px;
right: 10px;
}
.text-widget-fields .wp-pointer .wp-pointer-arrow {
right: auto;
left: 15px;
}
.text-widget-fields .wp-pointer .wp-pointer-buttons {
line-height: 1.4em;
}
/* =Media Queries
-------------------------------------------------------------- */

File diff suppressed because one or more lines are too long

View File

@ -619,6 +619,29 @@ div#widgets-right .widget-top:hover,
cursor: move;
}
/* =Specific widget styling
-------------------------------------------------------------- */
.text-widget-fields {
position: relative;
}
.text-widget-fields [hidden] {
display: none;
}
.text-widget-fields .wp-pointer.wp-pointer-top {
position: absolute;
z-index: 3;
top: 100px;
right: 10px;
left: 10px;
}
.text-widget-fields .wp-pointer .wp-pointer-arrow {
left: auto;
right: 15px;
}
.text-widget-fields .wp-pointer .wp-pointer-buttons {
line-height: 1.4em;
}
/* =Media Queries
-------------------------------------------------------------- */

File diff suppressed because one or more lines are too long

View File

@ -3,7 +3,9 @@
wp.textWidgets = ( function( $ ) {
'use strict';
var component = {};
var component = {
dismissedPointers: []
};
/**
* Text widget control.
@ -45,6 +47,31 @@ wp.textWidgets = ( function( $ ) {
control.$el.addClass( 'text-widget-fields' );
control.$el.html( wp.template( 'widget-text-control-fields' ) );
control.customHtmlWidgetPointer = control.$el.find( '.wp-pointer.custom-html-widget-pointer' );
if ( control.customHtmlWidgetPointer.length ) {
control.customHtmlWidgetPointer.find( '.close' ).on( 'click', function( event ) {
event.preventDefault();
control.customHtmlWidgetPointer.hide();
$( '#' + control.fields.text.attr( 'id' ) + '-html' ).focus();
control.dismissPointers( [ 'text_widget_custom_html' ] );
});
control.customHtmlWidgetPointer.find( '.add-widget' ).on( 'click', function( event ) {
event.preventDefault();
control.customHtmlWidgetPointer.hide();
control.openAvailableWidgetsPanel();
});
}
control.pasteHtmlPointer = control.$el.find( '.wp-pointer.paste-html-pointer' );
if ( control.pasteHtmlPointer.length ) {
control.pasteHtmlPointer.find( '.close' ).on( 'click', function( event ) {
event.preventDefault();
control.pasteHtmlPointer.hide();
control.editor.focus();
control.dismissPointers( [ 'text_widget_custom_html', 'text_widget_paste_html' ] );
});
}
control.fields = {
title: control.$el.find( '.title' ),
text: control.$el.find( '.text' )
@ -65,6 +92,45 @@ wp.textWidgets = ( function( $ ) {
});
},
/**
* Dismiss pointers for Custom HTML widget.
*
* @since 4.8.1
*
* @param {Array} pointers Pointer IDs to dismiss.
* @returns {void}
*/
dismissPointers: function dismissPointers( pointers ) {
_.each( pointers, function( pointer ) {
wp.ajax.post( 'dismiss-wp-pointer', {
pointer: pointer
});
component.dismissedPointers.push( pointer );
});
},
/**
* Open available widgets panel.
*
* @since 4.8.1
* @returns {void}
*/
openAvailableWidgetsPanel: function openAvailableWidgetsPanel() {
var sidebarControl;
wp.customize.section.each( function( section ) {
if ( section.extended( wp.customize.Widgets.SidebarSection ) && section.expanded() ) {
sidebarControl = wp.customize.control( 'sidebars_widgets[' + section.params.sidebarId + ']' );
}
});
if ( ! sidebarControl ) {
return;
}
setTimeout( function() { // Timeout to prevent click event from causing panel to immediately collapse.
wp.customize.Widgets.availableWidgetsPanel.open( sidebarControl );
wp.customize.Widgets.availableWidgetsPanel.$search.val( 'HTML' ).trigger( 'keyup' );
});
},
/**
* Update input fields from the sync fields.
*
@ -108,7 +174,7 @@ wp.textWidgets = ( function( $ ) {
* @returns {void}
*/
function buildEditor() {
var editor, triggerChangeIfDirty, onInit;
var editor, triggerChangeIfDirty, onInit, showPointerElement;
// Abort building if the textarea is gone, likely due to the widget having been deleted entirely.
if ( ! document.getElementById( id ) ) {
@ -128,6 +194,20 @@ wp.textWidgets = ( function( $ ) {
quicktags: true
});
/**
* Show a pointer, focus on dismiss, and speak the contents for a11y.
*
* @param {jQuery} pointerElement Pointer element.
* @returns {void}
*/
showPointerElement = function( pointerElement ) {
pointerElement.show();
pointerElement.find( '.close' ).focus();
wp.a11y.speak( pointerElement.find( 'h3, p' ).map( function() {
return $( this ).text();
} ).get().join( '\n\n' ) );
};
editor = window.tinymce.get( id );
if ( ! editor ) {
throw new Error( 'Failed to initialize editor' );
@ -143,6 +223,34 @@ wp.textWidgets = ( function( $ ) {
if ( restoreTextMode ) {
switchEditors.go( id, 'toggle' );
}
// Show the pointer.
$( '#' + id + '-html' ).on( 'click', function() {
control.pasteHtmlPointer.hide(); // Hide the HTML pasting pointer.
if ( -1 !== component.dismissedPointers.indexOf( 'text_widget_custom_html' ) ) {
return;
}
showPointerElement( control.customHtmlWidgetPointer );
});
// Hide the pointer when switching tabs.
$( '#' + id + '-tmce' ).on( 'click', function() {
control.customHtmlWidgetPointer.hide();
});
// Show pointer when pasting HTML.
editor.on( 'pastepreprocess', function( event ) {
var content = event.content;
if ( -1 !== component.dismissedPointers.indexOf( 'text_widget_paste_html' ) || ! content || ! /<\w+.*?>/.test( content ) ) {
return;
}
// Show the pointer after a slight delay so the user sees what they pasted.
_.delay( function() {
showPointerElement( control.pasteHtmlPointer );
}, 250 );
});
};
if ( editor.initialized ) {
@ -224,6 +332,11 @@ wp.textWidgets = ( function( $ ) {
return;
}
// Bypass using TinyMCE when widget is in legacy mode.
if ( widgetForm.find( '.legacy' ).length > 0 ) {
return;
}
/*
* Create a container element for the widget control fields.
* This is inserted into the DOM immediately before the the .widget-content
@ -280,6 +393,11 @@ wp.textWidgets = ( function( $ ) {
return;
}
// Bypass using TinyMCE when widget is in legacy mode.
if ( widgetForm.find( '.legacy' ).length > 0 ) {
return;
}
fieldContainer = $( '<div></div>' );
syncContainer = widgetForm.find( '> .widget-inside' );
syncContainer.before( fieldContainer );

File diff suppressed because one or more lines are too long

View File

@ -608,7 +608,7 @@ function wp_default_scripts( &$scripts ) {
$scripts->add( 'media-audio-widget', "/wp-admin/js/widgets/media-audio-widget$suffix.js", array( 'media-widgets', 'media-audiovideo' ) );
$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' ) );
$scripts->add( 'text-widgets', "/wp-admin/js/widgets/text-widgets$suffix.js", array( 'jquery', 'backbone', 'editor', 'wp-util' ) );
$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( 'theme', "/wp-admin/js/theme$suffix.js", array( 'wp-backbone', 'wp-a11y' ), false, 1 );
@ -845,7 +845,7 @@ function wp_default_styles( &$styles ) {
$styles->add( 'themes', "/wp-admin/css/themes$suffix.css" );
$styles->add( 'about', "/wp-admin/css/about$suffix.css" );
$styles->add( 'nav-menus', "/wp-admin/css/nav-menus$suffix.css" );
$styles->add( 'widgets', "/wp-admin/css/widgets$suffix.css" );
$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" );

View File

@ -63,6 +63,129 @@ class WP_Widget_Text extends WP_Widget {
add_action( 'admin_footer-widgets.php', array( $this, 'render_control_template_scripts' ) );
}
/**
* Determines whether a given instance is legacy and should bypass using TinyMCE.
*
* @since 4.8.1
*
* @param array $instance {
* Instance data.
*
* @type string $text Content.
* @type bool|string $filter Whether autop or content filters should apply.
* @type bool $legacy Whether widget is in legacy mode.
* }
* @return bool Whether Text widget instance contains legacy data.
*/
public function is_legacy_instance( $instance ) {
// If the widget has been updated while in legacy mode, it stays in legacy mode.
if ( ! empty( $instance['legacy'] ) ) {
return true;
}
// If the widget has been added/updated in 4.8 then filter prop is 'content' and it is no longer legacy.
if ( isset( $instance['filter'] ) && 'content' === $instance['filter'] ) {
return false;
}
// If the text is empty, then nothing is preventing migration to TinyMCE.
if ( empty( $instance['text'] ) ) {
return false;
}
$wpautop = ! empty( $instance['filter'] );
$has_line_breaks = ( false !== strpos( $instance['text'], "\n" ) );
// If auto-paragraphs are not enabled and there are line breaks, then ensure legacy mode.
if ( ! $wpautop && $has_line_breaks ) {
return true;
}
// If an HTML comment is present, assume legacy mode.
if ( false !== strpos( $instance['text'], '<!--' ) ) {
return true;
}
/*
* If a shortcode is present (with support added by a plugin), assume legacy mode
* since shortcodes would apply at the widget_text filter and thus be applied
* before wpautop runs at the widget_text_content filter.
*/
if ( preg_match( '/' . get_shortcode_regex() . '/', $instance['text'] ) ) {
return true;
}
// In the rare case that DOMDocument is not available we cannot reliably sniff content and so we assume legacy.
if ( ! class_exists( 'DOMDocument' ) ) {
// @codeCoverageIgnoreStart
return true;
// @codeCoverageIgnoreEnd
}
$doc = new DOMDocument();
$doc->loadHTML( sprintf(
'<html><head><meta charset="%s"></head><body>%s</body></html>',
esc_attr( get_bloginfo( 'charset' ) ),
$instance['text']
) );
$body = $doc->getElementsByTagName( 'body' )->item( 0 );
// See $allowedposttags.
$safe_elements_attributes = array(
'strong' => array(),
'em' => array(),
'b' => array(),
'i' => array(),
'u' => array(),
's' => array(),
'ul' => array(),
'ol' => array(),
'li' => array(),
'hr' => array(),
'abbr' => array(),
'acronym' => array(),
'code' => array(),
'dfn' => array(),
'a' => array(
'href' => true,
),
'img' => array(
'src' => true,
'alt' => true,
),
);
$safe_empty_elements = array( 'img', 'hr', 'iframe' );
foreach ( $body->getElementsByTagName( '*' ) as $element ) {
/** @var DOMElement $element */
$tag_name = strtolower( $element->nodeName );
// If the element is not safe, then the instance is legacy.
if ( ! isset( $safe_elements_attributes[ $tag_name ] ) ) {
return true;
}
// If the element is not safely empty and it has empty contents, then legacy mode.
if ( ! in_array( $tag_name, $safe_empty_elements, true ) && '' === trim( $element->textContent ) ) {
return true;
}
// If an attribute is not recognized as safe, then the instance is legacy.
foreach ( $element->attributes as $attribute ) {
/** @var DOMAttr $attribute */
$attribute_name = strtolower( $attribute->nodeName );
if ( ! isset( $safe_elements_attributes[ $tag_name ][ $attribute_name ] ) ) {
return true;
}
}
}
// Otherwise, the text contains no elements/attributes that TinyMCE could drop, and therefore the widget does not need legacy mode.
return false;
}
/**
* Outputs the content for the current Text widget instance.
*
@ -79,6 +202,20 @@ class WP_Widget_Text extends WP_Widget {
$title = apply_filters( 'widget_title', empty( $instance['title'] ) ? '' : $instance['title'], $instance, $this->id_base );
$text = ! empty( $instance['text'] ) ? $instance['text'] : '';
$is_visual_text_widget = ( isset( $instance['filter'] ) && 'content' === $instance['filter'] );
/*
* Just-in-time temporarily upgrade Visual Text widget shortcode handling
* (with support added by plugin) from the widget_text filter to
* widget_text_content:11 to prevent wpautop from corrupting HTML output
* added by the shortcode.
*/
$widget_text_do_shortcode_priority = has_filter( 'widget_text', 'do_shortcode' );
$should_upgrade_shortcode_handling = ( $is_visual_text_widget && false !== $widget_text_do_shortcode_priority );
if ( $should_upgrade_shortcode_handling ) {
remove_filter( 'widget_text', 'do_shortcode', $widget_text_do_shortcode_priority );
add_filter( 'widget_text_content', 'do_shortcode', 11 );
}
/**
* Filters the content of the Text widget.
@ -113,6 +250,12 @@ class WP_Widget_Text extends WP_Widget {
}
}
// Undo temporary upgrade of the plugin-supplied shortcode handling.
if ( $should_upgrade_shortcode_handling ) {
remove_filter( 'widget_text_content', 'do_shortcode', 11 );
add_filter( 'widget_text', 'do_shortcode', $widget_text_do_shortcode_priority );
}
echo $args['before_widget'];
if ( ! empty( $title ) ) {
echo $args['before_title'] . $title . $args['after_title'];
@ -145,12 +288,20 @@ class WP_Widget_Text extends WP_Widget {
}
/*
* Re-use legacy 'filter' (wpautop) property to now indicate content filters will always apply.
* If the Text widget is in legacy mode, then a hidden input will indicate this
* and the new content value for the filter prop will by bypassed. Otherwise,
* 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';
if ( isset( $new_instance['legacy'] ) || isset( $old_instance['legacy'] ) || ( isset( $new_instance['filter'] ) && 'content' !== $new_instance['filter'] ) ) {
$instance['filter'] = ! empty( $new_instance['filter'] );
$instance['legacy'] = true;
} else {
$instance['filter'] = 'content';
unset( $instance['legacy'] );
}
return $instance;
}
@ -171,6 +322,7 @@ class WP_Widget_Text extends WP_Widget {
*
* @since 2.8.0
* @since 4.8.0 Form only contains hidden inputs which are synced with JS template.
* @since 4.8.1 Restored original form to be displayed when in legacy mode.
* @access public
* @see WP_Widget_Visual_Text::render_control_template_scripts()
*
@ -186,9 +338,27 @@ class WP_Widget_Text extends WP_Widget {
)
);
?>
<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 if ( ! $this->is_legacy_instance( $instance ) ) : ?>
<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 else : ?>
<input name="<?php echo $this->get_field_name( 'legacy' ); ?>" type="hidden" class="legacy" value="true">
<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'] ); ?>"/>
</p>
<div class="notice inline notice-info notice-alt">
<p><?php _e( 'This widget contains code that may work better in the new &#8220;Custom HTML&#8221; widget. How about trying that widget instead?' ); ?></p>
</div>
<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( ! empty( $instance['filter'] ) ); ?> />&nbsp;<label for="<?php echo $this->get_field_id( 'filter' ); ?>"><?php _e( 'Automatically add paragraphs' ); ?></label>
</p>
<?php
endif;
}
/**
@ -198,6 +368,7 @@ class WP_Widget_Text extends WP_Widget {
* @access public
*/
public function render_control_template_scripts() {
$dismissed_pointers = explode( ',', (string) get_user_meta( get_current_user_id(), 'dismissed_wp_pointers', true ) );
?>
<script type="text/html" id="tmpl-widget-text-control-fields">
<# var elementIdPrefix = 'el' + String( Math.random() ).replace( /\D/g, '' ) + '_' #>
@ -205,6 +376,41 @@ class WP_Widget_Text extends WP_Widget {
<label for="{{ elementIdPrefix }}title"><?php esc_html_e( 'Title:' ); ?></label>
<input id="{{ elementIdPrefix }}title" type="text" class="widefat title">
</p>
<?php if ( ! in_array( 'text_widget_custom_html', $dismissed_pointers, true ) ) : ?>
<div hidden class="wp-pointer custom-html-widget-pointer wp-pointer-top">
<div class="wp-pointer-content">
<h3><?php _e( 'New Custom HTML Widget' ); ?></h3>
<?php if ( is_customize_preview() ) : ?>
<p><?php _e( 'Hey, did you hear we have a &#8220;Custom HTML&#8221; widget now? You can find it by pressing the &#8220;<a class="add-widget" href="#">Add a Widget</a>&#8221; button and searching for &#8220;HTML&#8221;. Check it out to add some custom code to your site!' ); ?></p>
<?php else : ?>
<p><?php _e( 'Hey, did you hear we have a &#8220;Custom HTML&#8221; widget now? You can find it by scanning the list of available widgets on this screen. Check it out to add some custom code to your site!' ); ?></p>
<?php endif; ?>
<div class="wp-pointer-buttons">
<a class="close" href="#"><?php _e( 'Dismiss' ); ?></a>
</div>
</div>
<div class="wp-pointer-arrow">
<div class="wp-pointer-arrow-inner"></div>
</div>
</div>
<?php endif; ?>
<?php if ( ! in_array( 'text_widget_paste_html', $dismissed_pointers, true ) ) : ?>
<div hidden class="wp-pointer paste-html-pointer wp-pointer-top">
<div class="wp-pointer-content">
<h3><?php _e( 'Did you just paste HTML?' ); ?></h3>
<p><?php _e( 'Hey there, looks like you just pasted HTML into the &#8220;Visual&#8221; tab of the Text widget. You may want to paste your code into the &#8220;Text&#8221; tab instead. Alternately, try out the new &#8220;Custom HTML&#8221; widget!' ); ?></p>
<div class="wp-pointer-buttons">
<a class="close" href="#"><?php _e( 'Dismiss' ); ?></a>
</div>
</div>
<div class="wp-pointer-arrow">
<div class="wp-pointer-arrow-inner"></div>
</div>
</div>
<?php endif; ?>
<p>
<label for="{{ elementIdPrefix }}text" class="screen-reader-text"><?php esc_html_e( 'Content:' ); ?></label>
<textarea id="{{ elementIdPrefix }}text" class="widefat text wp-editor-area" style="height: 200px" rows="16" cols="20"></textarea>