From c02bdd66a7a2eadadfa5e05dedd1b4c52a774164 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 11 Jan 2017 23:24:08 -0800 Subject: [PATCH 01/92] Add widgets initially as Shortcake Post Elements (without Form integration) --- php/class-js-widget-shortcode-controller.php | 89 ++++++++++++++++++++ php/class-js-widgets-plugin.php | 27 +++++- 2 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 php/class-js-widget-shortcode-controller.php diff --git a/php/class-js-widget-shortcode-controller.php b/php/class-js-widget-shortcode-controller.php new file mode 100644 index 0000000..c03e947 --- /dev/null +++ b/php/class-js-widget-shortcode-controller.php @@ -0,0 +1,89 @@ +plugin = $plugin; + $this->widget = $widget; + } + + /** + * Get shortcode tag. + * + * @return string Shortcode tag. + */ + public function get_shortcode_tag() { + return sprintf( 'widget_%s', $this->widget->id_base ); + } + + /** + * Register shortcodes for widgets. + * + * @global WP_Widget_Factory $wp_widget_factory + */ + public function register_shortcode() { + add_shortcode( $this->get_shortcode_tag(), array( $this, 'render_widget_shortcode' ) ); + } + + /** + * Render widget shortcode. + * + * @global array $wp_registered_sidebars + * + * @param array $atts Shortcode attributes. + * @return string Rendered shortcode. + */ + public function render_widget_shortcode( $atts ) { + global $wp_registered_sidebars; + reset( $wp_registered_sidebars ); + $args = current( $wp_registered_sidebars ); + + $atts = shortcode_atts( array( 'title' => '' ), $atts, $this->get_shortcode_tag() ); + $instance = $atts; // @todo There should be the JSON instance encoded in one attribute. + ob_start(); + $this->widget->render( $args, $instance ); + return ob_get_clean(); + } + + /** + * Register shortcode UI for widget shortcodes. + */ + public function register_shortcode_ui() { + shortcode_ui_register_for_shortcode( + $this->get_shortcode_tag(), + array( + 'label' => $this->widget->name, + ) + ); + } +} diff --git a/php/class-js-widgets-plugin.php b/php/class-js-widgets-plugin.php index 068f34f..26cc9f2 100644 --- a/php/class-js-widgets-plugin.php +++ b/php/class-js-widgets-plugin.php @@ -26,6 +26,13 @@ class JS_Widgets_Plugin { */ public $rest_api_namespace = 'js-widgets/v1'; + /** + * Registered controllers for widget shortcodes. + * + * @var JS_Widget_Shortcode_Controller[] + */ + public $widget_shortcodes = array(); + /** * Instances of JS_Widgets_REST_Controller for each widget type. * @@ -104,8 +111,9 @@ public function init() { add_action( 'customize_controls_print_footer_scripts', array( $this, 'render_widget_form_template_scripts' ) ); add_action( 'admin_footer-widgets.php', array( $this, 'render_widget_form_template_scripts' ) ); add_action( 'customize_controls_init', array( $this, 'upgrade_customize_widget_controls' ) ); - add_action( 'widgets_init', array( $this, 'capture_original_instances' ), 94 ); add_action( 'widgets_init', array( $this, 'upgrade_core_widgets' ) ); + add_action( 'widgets_init', array( $this, 'register_widget_shortcodes' ), 90 ); + add_action( 'widgets_init', array( $this, 'capture_original_instances' ), 94 ); add_action( 'in_widget_form', array( $this, 'start_capturing_in_widget_form' ), 0, 3 ); add_action( 'in_widget_form', array( $this, 'stop_capturing_in_widget_form' ), 1000, 3 ); @@ -337,6 +345,23 @@ public function upgrade_core_widgets() { } } + /** + * Register widget shortcodes. + * + * @global WP_Widget_Factory $wp_widget_factory + */ + function register_widget_shortcodes() { + global $wp_widget_factory; + require_once __DIR__ . '/class-js-widget-shortcode-controller.php'; + foreach ( $wp_widget_factory->widgets as $widget ) { + if ( $widget instanceof WP_JS_Widget ) { + $widget_shortcode = new JS_Widget_Shortcode_Controller( $this, $widget ); + $widget_shortcode->register_shortcode(); + add_action( 'register_shortcode_ui', array( $widget_shortcode, 'register_shortcode_ui' ) ); + } + } + } + /** * Replace instances of `WP_Widget_Form_Customize_Control` for JS Widgets to exclude PHP-generated content. * From da55eefd6aca169062bcab9362f9644bb2a5c278 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 11 Jan 2017 23:30:15 -0800 Subject: [PATCH 02/92] Allow shortcode attributes for the widget's instance properties --- php/class-js-widget-shortcode-controller.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/php/class-js-widget-shortcode-controller.php b/php/class-js-widget-shortcode-controller.php index c03e947..3e822e7 100644 --- a/php/class-js-widget-shortcode-controller.php +++ b/php/class-js-widget-shortcode-controller.php @@ -68,7 +68,11 @@ public function render_widget_shortcode( $atts ) { reset( $wp_registered_sidebars ); $args = current( $wp_registered_sidebars ); - $atts = shortcode_atts( array( 'title' => '' ), $atts, $this->get_shortcode_tag() ); + $atts = shortcode_atts( + $this->widget->get_default_instance(), + $atts, + $this->get_shortcode_tag() + ); $instance = $atts; // @todo There should be the JSON instance encoded in one attribute. ob_start(); $this->widget->render( $args, $instance ); From 0f9b8edb99fc2a30ed2c6356991795abafb7a239 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 11 Jan 2017 23:54:22 -0800 Subject: [PATCH 03/92] Supply proper args to render method via first-registered sidebar --- php/class-js-widget-shortcode-controller.php | 52 ++++++++++++++++++-- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/php/class-js-widget-shortcode-controller.php b/php/class-js-widget-shortcode-controller.php index 3e822e7..98dfa42 100644 --- a/php/class-js-widget-shortcode-controller.php +++ b/php/class-js-widget-shortcode-controller.php @@ -55,6 +55,52 @@ public function register_shortcode() { add_shortcode( $this->get_shortcode_tag(), array( $this, 'render_widget_shortcode' ) ); } + /** + * Get sidebar args needed for rendering a widget. + * + * This will by default use the args from the first registered sidebar. + * + * @see WP_JS_Widget::render() + * + * @return array { + * Sidebar args. + * + * @type string $name Name of the sidebar the widget is assigned to. + * @type string $id ID of the sidebar the widget is assigned to. + * @type string $description The sidebar description. + * @type string $class CSS class applied to the sidebar container. + * @type string $before_widget HTML markup to prepend to each widget in the sidebar. + * @type string $after_widget HTML markup to append to each widget in the sidebar. + * @type string $before_title HTML markup to prepend to the widget title when displayed. + * @type string $after_title HTML markup to append to the widget title when displayed. + * @type string $widget_id ID of the widget. + * @type string $widget_name Name of the widget. + * } + */ + public function get_sidebar_args() { + global $wp_registered_sidebars; + reset( $wp_registered_sidebars ); + + $sidebar = current( $wp_registered_sidebars ); + + $widget_id = sprintf( '%s-%d', $this->widget->id_base, -rand() ); + $args = array_merge( + $sidebar, + array( + 'widget_id' => $widget_id, + 'widget_name' => $this->widget->name, + ) + ); + + // Substitute HTML id and class attributes into before_widget. + $args['before_widget'] = sprintf( $args['before_widget'], $widget_id, $this->widget->widget_options['classname'] ); + + /** This filter is documented in wp-includes/widgets.php */ + $params = apply_filters( 'dynamic_sidebar_params', array( $args, array( 'number' => null ) ) ); + + return $params[0]; + } + /** * Render widget shortcode. * @@ -64,10 +110,6 @@ public function register_shortcode() { * @return string Rendered shortcode. */ public function render_widget_shortcode( $atts ) { - global $wp_registered_sidebars; - reset( $wp_registered_sidebars ); - $args = current( $wp_registered_sidebars ); - $atts = shortcode_atts( $this->widget->get_default_instance(), $atts, @@ -75,7 +117,7 @@ public function render_widget_shortcode( $atts ) { ); $instance = $atts; // @todo There should be the JSON instance encoded in one attribute. ob_start(); - $this->widget->render( $args, $instance ); + $this->widget->render( $this->get_sidebar_args(), $instance ); return ob_get_clean(); } From 2b885317d0a9e05de27e6d8c120460eb42808ddd Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Thu, 12 Jan 2017 16:20:28 +0100 Subject: [PATCH 04/92] Re-use the dashicon associated with a given widget in the customizer to also be used for the Post Element browser in the editor. --- core-adapter-widgets/archives/class.php | 7 +++++++ core-adapter-widgets/calendar/class.php | 7 +++++++ core-adapter-widgets/categories/class.php | 7 +++++++ core-adapter-widgets/meta/class.php | 8 +++++++- core-adapter-widgets/nav_menu/class.php | 7 +++++++ core-adapter-widgets/pages/class.php | 7 +++++++ core-adapter-widgets/recent-comments/class.php | 7 +++++++ core-adapter-widgets/recent-posts/class.php | 7 +++++++ core-adapter-widgets/rss/class.php | 7 +++++++ core-adapter-widgets/search/class.php | 7 +++++++ core-adapter-widgets/tag_cloud/class.php | 7 +++++++ core-adapter-widgets/text/class.php | 7 +++++++ php/class-js-widget-shortcode-controller.php | 12 ++++++++++++ post-collection-widget/class-widget.php | 7 +++++++ 14 files changed, 103 insertions(+), 1 deletion(-) diff --git a/core-adapter-widgets/archives/class.php b/core-adapter-widgets/archives/class.php index 70e4a9d..362db76 100644 --- a/core-adapter-widgets/archives/class.php +++ b/core-adapter-widgets/archives/class.php @@ -12,6 +12,13 @@ */ class WP_JS_Widget_Archives extends WP_Adapter_JS_Widget { + /** + * Icon name. + * + * @var string + */ + public $icon_name = 'dashicons-archive'; + /** * WP_JS_Widget_Archives constructor. * diff --git a/core-adapter-widgets/calendar/class.php b/core-adapter-widgets/calendar/class.php index 1579979..0daa738 100644 --- a/core-adapter-widgets/calendar/class.php +++ b/core-adapter-widgets/calendar/class.php @@ -12,6 +12,13 @@ */ class WP_JS_Widget_Calendar extends WP_Adapter_JS_Widget { + /** + * Icon name. + * + * @var string + */ + public $icon_name = 'dashicons-calendar'; + /** * WP_JS_Widget_Calendar constructor. * diff --git a/core-adapter-widgets/categories/class.php b/core-adapter-widgets/categories/class.php index 156db5d..61d269e 100644 --- a/core-adapter-widgets/categories/class.php +++ b/core-adapter-widgets/categories/class.php @@ -12,6 +12,13 @@ */ class WP_JS_Widget_Categories extends WP_Adapter_JS_Widget { + /** + * Icon name. + * + * @var string + */ + public $icon_name = 'dashicons-category'; + /** * WP_JS_Widget_Categories constructor. * diff --git a/core-adapter-widgets/meta/class.php b/core-adapter-widgets/meta/class.php index 04ba2c6..8bdd42a 100644 --- a/core-adapter-widgets/meta/class.php +++ b/core-adapter-widgets/meta/class.php @@ -12,6 +12,13 @@ */ class WP_JS_Widget_Meta extends WP_Adapter_JS_Widget { + /** + * Icon name. + * + * @var string + */ + public $icon_name = 'dashicons-wordpress'; + /** * WP_JS_Widget_Meta constructor. * @@ -22,7 +29,6 @@ public function __construct( JS_Widgets_Plugin $plugin, WP_Widget_Meta $adapted_ parent::__construct( $plugin, $adapted_widget ); } - /** * Get instance schema properties. * diff --git a/core-adapter-widgets/nav_menu/class.php b/core-adapter-widgets/nav_menu/class.php index dcd02bb..cf857ce 100644 --- a/core-adapter-widgets/nav_menu/class.php +++ b/core-adapter-widgets/nav_menu/class.php @@ -14,6 +14,13 @@ */ class WP_JS_Widget_Nav_Menu extends WP_Adapter_JS_Widget { + /** + * Icon name. + * + * @var string + */ + public $icon_name = 'dashicons-menu'; + /** * WP_JS_Widget_Nav_Menu constructor. * diff --git a/core-adapter-widgets/pages/class.php b/core-adapter-widgets/pages/class.php index e09c3cf..477ba6d 100644 --- a/core-adapter-widgets/pages/class.php +++ b/core-adapter-widgets/pages/class.php @@ -14,6 +14,13 @@ class WP_JS_Widget_Pages extends WP_Adapter_JS_Widget { const ID_LIST_PATTERN = '\d+(,\s*\d+)*'; + /** + * Icon name. + * + * @var string + */ + public $icon_name = 'dashicons-admin-page'; + /** * WP_JS_Widget_Pages constructor. * diff --git a/core-adapter-widgets/recent-comments/class.php b/core-adapter-widgets/recent-comments/class.php index 8361326..47829ad 100644 --- a/core-adapter-widgets/recent-comments/class.php +++ b/core-adapter-widgets/recent-comments/class.php @@ -12,6 +12,13 @@ */ class WP_JS_Widget_Recent_Comments extends WP_Adapter_JS_Widget { + /** + * Icon name. + * + * @var string + */ + public $icon_name = 'dashicons-admin-comments'; + /** * WP_JS_Widget_Recent_Comments constructor. * diff --git a/core-adapter-widgets/recent-posts/class.php b/core-adapter-widgets/recent-posts/class.php index 74732c7..2825a14 100644 --- a/core-adapter-widgets/recent-posts/class.php +++ b/core-adapter-widgets/recent-posts/class.php @@ -12,6 +12,13 @@ */ class WP_JS_Widget_Recent_Posts extends WP_Adapter_JS_Widget { + /** + * Icon name. + * + * @var string + */ + public $icon_name = 'dashicons-admin-post'; + /** * WP_JS_Widget_Recent_Posts constructor. * diff --git a/core-adapter-widgets/rss/class.php b/core-adapter-widgets/rss/class.php index e984949..3ba22ea 100644 --- a/core-adapter-widgets/rss/class.php +++ b/core-adapter-widgets/rss/class.php @@ -12,6 +12,13 @@ */ class WP_JS_Widget_RSS extends WP_Adapter_JS_Widget { + /** + * Icon name. + * + * @var string + */ + public $icon_name = 'dashicons-rss'; + /** * WP_JS_Widget_RSS constructor. * diff --git a/core-adapter-widgets/search/class.php b/core-adapter-widgets/search/class.php index 7c32e2f..a37efaf 100644 --- a/core-adapter-widgets/search/class.php +++ b/core-adapter-widgets/search/class.php @@ -12,6 +12,13 @@ */ class WP_JS_Widget_Search extends WP_Adapter_JS_Widget { + /** + * Icon name. + * + * @var string + */ + public $icon_name = 'dashicons-search'; + /** * WP_JS_Widget_Search constructor. * diff --git a/core-adapter-widgets/tag_cloud/class.php b/core-adapter-widgets/tag_cloud/class.php index 8651547..a1ecd15 100644 --- a/core-adapter-widgets/tag_cloud/class.php +++ b/core-adapter-widgets/tag_cloud/class.php @@ -12,6 +12,13 @@ */ class WP_JS_Widget_Tag_Cloud extends WP_Adapter_JS_Widget { + /** + * Icon name. + * + * @var string + */ + public $icon_name = 'dashicons-tagcloud'; + /** * WP_JS_Widget_Tag_Cloud constructor. * diff --git a/core-adapter-widgets/text/class.php b/core-adapter-widgets/text/class.php index dc8c0c1..2b68e95 100644 --- a/core-adapter-widgets/text/class.php +++ b/core-adapter-widgets/text/class.php @@ -19,6 +19,13 @@ class WP_JS_Widget_Text extends WP_Adapter_JS_Widget { */ public $adapted_widget; + /** + * Icon name. + * + * @var string + */ + public $icon_name = 'dashicons-text'; + /** * Get instance schema properties. * diff --git a/php/class-js-widget-shortcode-controller.php b/php/class-js-widget-shortcode-controller.php index 98dfa42..feb14cb 100644 --- a/php/class-js-widget-shortcode-controller.php +++ b/php/class-js-widget-shortcode-controller.php @@ -26,6 +26,13 @@ class JS_Widget_Shortcode_Controller { */ public $widget = array(); + /** + * Default icon name. + * + * @var string + */ + public $default_icon_name = 'dashicons-format-aside'; + /** * Constructor. * @@ -125,10 +132,15 @@ public function render_widget_shortcode( $atts ) { * Register shortcode UI for widget shortcodes. */ public function register_shortcode_ui() { + $icon_name = $this->default_icon_name; + if ( isset( $this->widget->icon_name ) ) { + $icon_name = $this->widget->icon_name; + } shortcode_ui_register_for_shortcode( $this->get_shortcode_tag(), array( 'label' => $this->widget->name, + 'listItemImage' => $icon_name, ) ); } diff --git a/post-collection-widget/class-widget.php b/post-collection-widget/class-widget.php index ed39ebe..fabe96e 100644 --- a/post-collection-widget/class-widget.php +++ b/post-collection-widget/class-widget.php @@ -26,6 +26,13 @@ class WP_JS_Widget_Post_Collection extends WP_JS_Widget { */ public $id_base = 'post-collection'; + /** + * Icon name. + * + * @var string + */ + public $icon_name = 'dashicons-admin-post'; + /** * Base query vars used in post lookup. * From 9dc0700a346a43104db5fd4714dbcdd313b3f76c Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 12 Jan 2017 11:16:02 -0800 Subject: [PATCH 05/92] Define icon_name on base WP_JS_Widget class --- php/class-js-widget-shortcode-controller.php | 13 +------------ php/class-wp-js-widget.php | 7 +++++++ 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/php/class-js-widget-shortcode-controller.php b/php/class-js-widget-shortcode-controller.php index feb14cb..6aa1c52 100644 --- a/php/class-js-widget-shortcode-controller.php +++ b/php/class-js-widget-shortcode-controller.php @@ -26,13 +26,6 @@ class JS_Widget_Shortcode_Controller { */ public $widget = array(); - /** - * Default icon name. - * - * @var string - */ - public $default_icon_name = 'dashicons-format-aside'; - /** * Constructor. * @@ -132,15 +125,11 @@ public function render_widget_shortcode( $atts ) { * Register shortcode UI for widget shortcodes. */ public function register_shortcode_ui() { - $icon_name = $this->default_icon_name; - if ( isset( $this->widget->icon_name ) ) { - $icon_name = $this->widget->icon_name; - } shortcode_ui_register_for_shortcode( $this->get_shortcode_tag(), array( 'label' => $this->widget->name, - 'listItemImage' => $icon_name, + 'listItemImage' => $this->widget->icon_name, ) ); } diff --git a/php/class-wp-js-widget.php b/php/class-wp-js-widget.php index e926fcc..a3a8adf 100644 --- a/php/class-wp-js-widget.php +++ b/php/class-wp-js-widget.php @@ -12,6 +12,13 @@ */ abstract class WP_JS_Widget extends WP_Widget { + /** + * Icon name. + * + * @var string + */ + public $icon_name = 'dashicons-format-aside'; + /** * REST controller class that should be used for this widget. * From 2b35e8020a0e37db8f12622a78929f4012152596 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 12 Jan 2017 11:46:40 -0800 Subject: [PATCH 06/92] Add skeleton for widget_form shortcake field type --- php/class-js-widget-shortcode-controller.php | 8 +++++ php/class-js-widgets-plugin.php | 32 ++++++++++++++++++-- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/php/class-js-widget-shortcode-controller.php b/php/class-js-widget-shortcode-controller.php index 6aa1c52..9419fc6 100644 --- a/php/class-js-widget-shortcode-controller.php +++ b/php/class-js-widget-shortcode-controller.php @@ -130,6 +130,14 @@ public function register_shortcode_ui() { array( 'label' => $this->widget->name, 'listItemImage' => $this->widget->icon_name, + 'attrs' => array( + array( + 'label' => __( 'JSON Widget Instance Data', 'js-widgets' ), + 'attr' => 'instance_data', + 'type' => 'widget_form', + 'encode' => true, + ), + ), ) ); } diff --git a/php/class-js-widgets-plugin.php b/php/class-js-widgets-plugin.php index 26cc9f2..eca91ea 100644 --- a/php/class-js-widgets-plugin.php +++ b/php/class-js-widgets-plugin.php @@ -112,12 +112,16 @@ public function init() { add_action( 'admin_footer-widgets.php', array( $this, 'render_widget_form_template_scripts' ) ); add_action( 'customize_controls_init', array( $this, 'upgrade_customize_widget_controls' ) ); add_action( 'widgets_init', array( $this, 'upgrade_core_widgets' ) ); - add_action( 'widgets_init', array( $this, 'register_widget_shortcodes' ), 90 ); add_action( 'widgets_init', array( $this, 'capture_original_instances' ), 94 ); add_action( 'in_widget_form', array( $this, 'start_capturing_in_widget_form' ), 0, 3 ); add_action( 'in_widget_form', array( $this, 'stop_capturing_in_widget_form' ), 1000, 3 ); + // Shortcake integration. + add_action( 'widgets_init', array( $this, 'register_widget_shortcodes' ), 90 ); + add_filter( 'shortcode_ui_fields', array( $this, 'filter_shortcode_ui_fields' ) ); + add_action( 'print_shortcode_ui_templates', array( $this, 'print_shortcode_ui_templates' ) ); + // @todo Add widget REST endpoint for getting the rendered value of widgets. Note originating context URL will need to be supplied when rendering some widgets. } @@ -350,7 +354,7 @@ public function upgrade_core_widgets() { * * @global WP_Widget_Factory $wp_widget_factory */ - function register_widget_shortcodes() { + public function register_widget_shortcodes() { global $wp_widget_factory; require_once __DIR__ . '/class-js-widget-shortcode-controller.php'; foreach ( $wp_widget_factory->widgets as $widget ) { @@ -362,6 +366,30 @@ function register_widget_shortcodes() { } } + /** + * Add widget_form as a new shortcode UI field. + * + * @param array $fields Shortcode fields. + * @return array Fields. + */ + public function filter_shortcode_ui_fields( $fields ) { + $fields['widget_form'] = array( + 'template' => 'shortcode-ui-field-widget_form', + ); + return $fields; + } + + /** + * Print shortcode UI templates. + */ + public function print_shortcode_ui_templates() { + ?> + + Date: Thu, 12 Jan 2017 11:57:16 -0800 Subject: [PATCH 07/92] Add skeleton for shortcode UI js-hooks integration --- js/shortcode-ui-js-widgets.js | 11 +++++++++++ php/class-js-widgets-plugin.php | 13 +++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 js/shortcode-ui-js-widgets.js diff --git a/js/shortcode-ui-js-widgets.js b/js/shortcode-ui-js-widgets.js new file mode 100644 index 0000000..edb967b --- /dev/null +++ b/js/shortcode-ui-js-widgets.js @@ -0,0 +1,11 @@ +/* global console, wp */ + +wp.shortcake.hooks.addAction( 'shortcode-ui.render_edit', function() { + console.info( 'shortcode-ui.render_edit', this, arguments ); +} ); +wp.shortcake.hooks.addAction( 'shortcode-ui.render_new', function() { + console.info( 'shortcode-ui.render_new', this, arguments ); +} ); +wp.shortcake.hooks.addAction( 'shortcode-ui.render_destroy', function() { + console.info( 'shortcode-ui.render_destroy', this, arguments ); +} ); diff --git a/php/class-js-widgets-plugin.php b/php/class-js-widgets-plugin.php index eca91ea..5302eeb 100644 --- a/php/class-js-widgets-plugin.php +++ b/php/class-js-widgets-plugin.php @@ -121,6 +121,7 @@ public function init() { add_action( 'widgets_init', array( $this, 'register_widget_shortcodes' ), 90 ); add_filter( 'shortcode_ui_fields', array( $this, 'filter_shortcode_ui_fields' ) ); add_action( 'print_shortcode_ui_templates', array( $this, 'print_shortcode_ui_templates' ) ); + add_action( 'enqueue_shortcode_ui', array( $this, 'enqueue_shortcode_ui' ) ); // @todo Add widget REST endpoint for getting the rendered value of widgets. Note originating context URL will need to be supplied when rendering some widgets. } @@ -163,6 +164,11 @@ public function register_scripts( WP_Scripts $wp_scripts ) { $deps = array( 'admin-widgets', $this->script_handles['form'] ); $wp_scripts->add( $this->script_handles['admin-js-widgets'], $src, $deps, $this->version ); + $this->script_handles['shortcode-ui-js-widgets'] = 'shortcode-ui-js-widgets'; + $src = $plugin_dir_url . 'js/shortcode-ui-js-widgets.js'; + $deps = array( 'shortcode-ui' ); + $wp_scripts->add( $this->script_handles['shortcode-ui-js-widgets'], $src, $deps, $this->version ); + $this->script_handles['trac-39389-controls'] = 'js-widgets-trac-39389-controls'; $src = $plugin_dir_url . 'js/trac-39389-controls.js'; $deps = array( 'customize-widgets' ); @@ -284,6 +290,13 @@ function enqueue_widgets_admin_scripts( $hook_suffix ) { } } + /** + * Enqueue scripts for shortcode UI. + */ + function enqueue_shortcode_ui() { + wp_enqueue_script( $this->script_handles['shortcode-ui-js-widgets'] ); + } + /** * Enqueue scripts on the frontend. * From 6e581f6e24e58369f2fdbd9e05a0764be35247cd Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 12 Jan 2017 12:01:42 -0800 Subject: [PATCH 08/92] Add missing js-widget dependnecies for shortcode-ui --- php/class-js-widgets-plugin.php | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/php/class-js-widgets-plugin.php b/php/class-js-widgets-plugin.php index 5302eeb..07be747 100644 --- a/php/class-js-widgets-plugin.php +++ b/php/class-js-widgets-plugin.php @@ -166,7 +166,7 @@ public function register_scripts( WP_Scripts $wp_scripts ) { $this->script_handles['shortcode-ui-js-widgets'] = 'shortcode-ui-js-widgets'; $src = $plugin_dir_url . 'js/shortcode-ui-js-widgets.js'; - $deps = array( 'shortcode-ui' ); + $deps = array( 'shortcode-ui', $this->script_handles['form'] ); $wp_scripts->add( $this->script_handles['shortcode-ui-js-widgets'], $src, $deps, $this->version ); $this->script_handles['trac-39389-controls'] = 'js-widgets-trac-39389-controls'; @@ -292,9 +292,19 @@ function enqueue_widgets_admin_scripts( $hook_suffix ) { /** * Enqueue scripts for shortcode UI. + * + * @global WP_Widget_Factory $wp_widget_factory */ function enqueue_shortcode_ui() { + global $wp_widget_factory; + wp_enqueue_script( $this->script_handles['shortcode-ui-js-widgets'] ); + + foreach ( $wp_widget_factory->widgets as $widget ) { + if ( $widget instanceof WP_JS_Widget ) { + $widget->enqueue_control_scripts(); + } + } } /** @@ -396,6 +406,8 @@ public function filter_shortcode_ui_fields( $fields ) { * Print shortcode UI templates. */ public function print_shortcode_ui_templates() { + $this->render_widget_form_template_scripts(); + ?> Date: Fri, 13 Jan 2017 14:16:54 +0100 Subject: [PATCH 10/92] Include widget base styles in `widget-form.css` so that forms look as expected inside Shortcode UI window. --- css/widget-form.css | 45 +++++++++++++++++++++++++++++++++++ js/shortcode-ui-js-widgets.js | 4 +++- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/css/widget-form.css b/css/widget-form.css index 28aa0c6..ce3ec96 100644 --- a/css/widget-form.css +++ b/css/widget-form.css @@ -60,3 +60,48 @@ .js-widget-form-notifications-container .notice-warning.notice-alt { background-color: #fff8e5; } + +.js-widget-form-shortcode-ui input[type="text"], +.js-widget-form-shortcode-ui input[type="password"], +.js-widget-form-shortcode-ui input[type="color"], +.js-widget-form-shortcode-ui input[type="date"], +.js-widget-form-shortcode-ui input[type="datetime"], +.js-widget-form-shortcode-ui input[type="datetime-local"], +.js-widget-form-shortcode-ui input[type="email"], +.js-widget-form-shortcode-ui input[type="month"], +.js-widget-form-shortcode-ui input[type="number"], +.js-widget-form-shortcode-ui input[type="search"], +.js-widget-form-shortcode-ui input[type="tel"], +.js-widget-form-shortcode-ui input[type="text"], +.js-widget-form-shortcode-ui input[type="time"], +.js-widget-form-shortcode-ui input[type="url"], +.js-widget-form-shortcode-ui input[type="week"], +.js-widget-form-shortcode-ui select, +.js-widget-form-shortcode-ui textarea { + width: 25em; + padding: 3px 5px; + font-size: 14px; +} + +.js-widget-form-shortcode-ui input[type="checkbox"], +.js-widget-form-shortcode-ui input[type="radio"] { + border: 1px solid #b4b9be; + color: #555; +} +.js-widget-form-shortcode-ui input[type="checkbox"]:focus, +.js-widget-form-shortcode-ui input[type="radio"]:focus { + border-color: #5b9dd9; +} +.js-widget-form-shortcode-ui input[type="checkbox"] + label, +.js-widget-form-shortcode-ui input[type="radio"] + label { + display: inline; + clear: none; +} + +.js-widget-form-shortcode-ui label { + color: #555d66; +} + +.js-widget-form-shortcode-ui .select2-container { + max-width: 26.92em; /* 25em * 14px = 350px => 350px / 13px = 26.92em */ +} diff --git a/js/shortcode-ui-js-widgets.js b/js/shortcode-ui-js-widgets.js index 6cc198c..5a035d3 100644 --- a/js/shortcode-ui-js-widgets.js +++ b/js/shortcode-ui-js-widgets.js @@ -37,7 +37,9 @@ wp.shortcake.JSWidgets = (function( $ ) { // eslint-disable-line no-unused-vars // @todo syncInput is unexpectedly persisting a value after closing the lightbox without pressing Update, even though the shortcode attributes remain unchanged until Update pressed. instanceValue = new wp.customize.Value( syncInput.val() ? JSON.parse( syncInput.val() ) : {} ); - container = $( '
' ); + container = $( '
', { + 'class': 'js-widget-form-shortcode-ui' + } ); syncInput.after( container ); form = new FormConstructor( { model: instanceValue, From f800691a0e1c78e11ad3823196e157fbf5a303fc Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 17 Jan 2017 17:12:16 -0800 Subject: [PATCH 11/92] Override display:block style for labels in shortcake widget forms --- css/widget-form.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/css/widget-form.css b/css/widget-form.css index ce3ec96..19448e5 100644 --- a/css/widget-form.css +++ b/css/widget-form.css @@ -105,3 +105,9 @@ .js-widget-form-shortcode-ui .select2-container { max-width: 26.92em; /* 25em * 14px = 350px => 350px / 13px = 26.92em */ } + +/* Shortcake style fixes */ +.edit-shortcode-form .js-widget-form-notifications-container label { + clear: none; + display: inline; +} From 0aa05a5bdede35d52f1577c0c103ca3d5a766037 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 17 Jan 2017 23:07:06 -0800 Subject: [PATCH 12/92] Hide Edit Menu button when no menu is selected --- core-adapter-widgets/nav_menu/class.php | 2 +- core-adapter-widgets/nav_menu/form.js | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/core-adapter-widgets/nav_menu/class.php b/core-adapter-widgets/nav_menu/class.php index cf857ce..c514735 100644 --- a/core-adapter-widgets/nav_menu/class.php +++ b/core-adapter-widgets/nav_menu/class.php @@ -108,7 +108,7 @@ public function render_form_template() { ), ) ); ?> -

+

diff --git a/core-adapter-widgets/nav_menu/form.js b/core-adapter-widgets/nav_menu/form.js index e7301a5..c4020c7 100644 --- a/core-adapter-widgets/nav_menu/form.js +++ b/core-adapter-widgets/nav_menu/form.js @@ -101,7 +101,7 @@ wp.widgets.formConstructor.nav_menu = (function( api, $ ) { initialize: function initialize( properties ) { var form = this; wp.widgets.Form.prototype.initialize.call( form, properties ); - _.bindAll( form, 'updateForm', 'handleEditButtonClick' ); + _.bindAll( form, 'updateForm', 'handleEditButtonClick', 'updateEditButtonVisibility' ); if ( _.isObject( form.config.nav_menus ) && 0 === classProps.navMenuCollection.length ) { _.each( form.config.nav_menus, function( name, id ) { @@ -120,10 +120,12 @@ wp.widgets.formConstructor.nav_menu = (function( api, $ ) { var form = this; wp.widgets.Form.prototype.render.call( form ); NavMenuWidgetForm.navMenuCollection.on( 'update change', form.updateForm ); + form.model.bind( form.updateEditButtonVisibility ); form.container.find( 'button.edit' ).on( 'click', form.handleEditButtonClick ); form.noMenusMessage = form.container.find( '.no-menus-message' ); form.menuSelection = form.container.find( '.menu-selection' ); form.updateForm(); + form.updateEditButtonVisibility(); }, /** @@ -135,6 +137,7 @@ wp.widgets.formConstructor.nav_menu = (function( api, $ ) { var form = this; form.container.find( 'button.edit' ).off( 'click', form.handleEditButtonClick ); NavMenuWidgetForm.navMenuCollection.off( 'update change', form.updateForm ); + form.model.unbind( form.updateEditButtonVisibility ); form.noMenusMessage = null; form.menuSelection = null; wp.widgets.Form.prototype.destruct.call( form ); @@ -182,6 +185,17 @@ wp.widgets.formConstructor.nav_menu = (function( api, $ ) { select.val( NavMenuWidgetForm.navMenuCollection.has( currentValue.nav_menu ) ? currentValue.nav_menu : 0 ); form.noMenusMessage.toggle( 0 === NavMenuWidgetForm.navMenuCollection.length ); form.menuSelection.toggle( 0 !== NavMenuWidgetForm.navMenuCollection.length ); + }, + + /** + * Update the visibility of the edit button based on whether a menu is selected. + * + * @returns {void} + */ + updateEditButtonVisibility: function updateEditButtonVisibility() { + var form = this, button; + button = form.container.find( '.edit-menu' ); + button.toggle( NavMenuWidgetForm.navMenuCollection.length > 0 && form.getValue().nav_menu > 0 ); } }, classProps ); From 20e24301f314addc6209ad4de19d772fb7009cce Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Fri, 20 Jan 2017 13:36:02 -0500 Subject: [PATCH 13/92] Add initial basic test --- package.json | 19 +++++++++++++--- tests/js/customize-js-widgets.js | 38 ++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 tests/js/customize-js-widgets.js diff --git a/package.json b/package.json index 8ab0a6a..ac42036 100644 --- a/package.json +++ b/package.json @@ -4,12 +4,13 @@ "homepage": "https://github.com/xwp/wp-js-widgets", "repository": { "type": "git", - "url": "https://github.com/xwp/wp-js-widgets.git" + "url": "git+https://github.com/xwp/wp-js-widgets.git" }, "version": "0.3.0", "license": "GPL-2.0+", "private": true, "devDependencies": { + "chai": "^3.5.0", "eslint": "^3.13.1", "grunt": "~0.4.5", "grunt-contrib-clean": "~1.0.0", @@ -17,7 +18,19 @@ "grunt-contrib-jshint": "~1.0.0", "grunt-contrib-watch": "^1.0.0", "grunt-shell": "^1.3.1", - "grunt-wp-deploy": "^1.2.1" + "grunt-wp-deploy": "^1.2.1", + "mocha": "^3.2.0" }, - "author": "XWP" + "author": "XWP", + "description": "The next generation of widgets in core, embracing JS for UI and powering the Widgets REST API.", + "bugs": { + "url": "https://github.com/xwp/wp-js-widgets/issues" + }, + "main": "Gruntfile.js", + "directories": { + "test": "tests" + }, + "scripts": { + "test": "mocha tests/js" + } } diff --git a/tests/js/customize-js-widgets.js b/tests/js/customize-js-widgets.js new file mode 100644 index 0000000..a427d60 --- /dev/null +++ b/tests/js/customize-js-widgets.js @@ -0,0 +1,38 @@ +/* globals global, require, describe, it */ +/* eslint-disable no-unused-expressions */ + +const expect = require( 'chai' ).expect; + +const noop = () => null; + +global.wp = { + widgets: { + formConstructor: {}, + }, + customize: { + Widgets: { + WidgetControl: { + extend: Object.assign, + prototype: { + initialize: noop, + }, + }, + }, + }, +}; +global.jQuery = {}; + +const JSWidgets = require( '../../js/customize-js-widgets' ); + +describe( 'wp.customize.JSWidgets', function() { + describe( '#isJsWidgetControl()', function() { + it( 'returns false if the passed Control is not a JSWidget', function() { + const mockWidget = { + extended: () => false, + params: {}, + }; + const result = JSWidgets.isJsWidgetControl( mockWidget ); + expect( result ).to.be.false; + } ); + } ); +} ); From d64c61f537360f2f7b11e754e042f64de9fb8662 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Fri, 20 Jan 2017 13:50:11 -0500 Subject: [PATCH 14/92] Alter extendWidgetControl to a (mostly) pure function Changes the function to accept an object, alter that object's prototype, and return it. The caller is then responsible for passing the constructor function to change. This allows the function to be more easily tested since it does not directly mutate global variables (although of course changing the prototype of a global variable will have a a global effect). Also adds `wp` and `_` as parameters to the IIFE to make their usage explicit. --- js/customize-js-widgets.js | 14 ++++++---- package.json | 3 +- tests/js/customize-js-widgets.js | 47 ++++++++++++++++++++++---------- 3 files changed, 42 insertions(+), 22 deletions(-) diff --git a/js/customize-js-widgets.js b/js/customize-js-widgets.js index 45d7c80..fb66691 100644 --- a/js/customize-js-widgets.js +++ b/js/customize-js-widgets.js @@ -3,7 +3,7 @@ /* eslint-disable complexity */ /* eslint consistent-this: [ "error", "control" ] */ -wp.customize.JSWidgets = (function( api, $ ) { // eslint-disable-line no-unused-vars +wp.customize.JSWidgets = (function( wp, api, $, _ ) { // eslint-disable-line no-unused-vars 'use strict'; var component = {}, originalInitialize; @@ -16,7 +16,7 @@ wp.customize.JSWidgets = (function( api, $ ) { // eslint-disable-line no-unused- * @returns {void} */ component.init = function initComponent() { - component.extendWidgetControl(); + component.extendWidgetControl( api.Widgets.WidgetControl ); // Handle (re-)adding a (previously-removed) control. api.control.bind( 'add', function( addedControl ) { @@ -46,9 +46,10 @@ wp.customize.JSWidgets = (function( api, $ ) { // eslint-disable-line no-unused- /** * Inject WidgetControl instances with our component.WidgetControl method overrides. * - * @returns {void} + * @param {wp.Customize.Widgets.WidgetControl} WidgetControl The constructor function to modify + * @returns {WidgetControl} The constructor function with a modified prototype */ - component.extendWidgetControl = function extendWidgetControl() { + component.extendWidgetControl = function extendWidgetControl( WidgetControl ) { /** * Initialize JS widget control. @@ -57,7 +58,7 @@ wp.customize.JSWidgets = (function( api, $ ) { // eslint-disable-line no-unused- * @param {object} options Control options. * @returns {void} */ - api.Widgets.WidgetControl.prototype.initialize = function initializeWidgetControl( id, options ) { + WidgetControl.prototype.initialize = function initializeWidgetControl( id, options ) { var control = this, isJsWidget; isJsWidget = options.params.widget_id_base && 'undefined' !== typeof wp.widgets.formConstructor[ options.params.widget_id_base ]; if ( isJsWidget ) { @@ -67,6 +68,7 @@ wp.customize.JSWidgets = (function( api, $ ) { // eslint-disable-line no-unused- originalInitialize.call( control, id, options ); } }; + return WidgetControl; }; /** @@ -310,4 +312,4 @@ wp.customize.JSWidgets = (function( api, $ ) { // eslint-disable-line no-unused- return component; -})( wp.customize, jQuery ); +})( wp, wp.customize, jQuery, _ ); diff --git a/package.json b/package.json index ac42036..8413124 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "grunt-contrib-watch": "^1.0.0", "grunt-shell": "^1.3.1", "grunt-wp-deploy": "^1.2.1", - "mocha": "^3.2.0" + "mocha": "^3.2.0", + "underscore": "^1.8.3" }, "author": "XWP", "description": "The next generation of widgets in core, embracing JS for UI and powering the Widgets REST API.", diff --git a/tests/js/customize-js-widgets.js b/tests/js/customize-js-widgets.js index a427d60..4a91e4d 100644 --- a/tests/js/customize-js-widgets.js +++ b/tests/js/customize-js-widgets.js @@ -1,31 +1,40 @@ -/* globals global, require, describe, it */ +/* globals global, require, describe, it, beforeEach */ /* eslint-disable no-unused-expressions */ const expect = require( 'chai' ).expect; +const _ = require( 'underscore' ); const noop = () => null; -global.wp = { - widgets: { - formConstructor: {}, - }, - customize: { - Widgets: { - WidgetControl: { - extend: Object.assign, - prototype: { - initialize: noop, +function resetGlobals() { + global.wp = { + widgets: { + formConstructor: {}, + }, + customize: { + Widgets: { + WidgetControl: { + extend: Object.assign, + prototype: { + initialize: noop, + }, }, }, }, - }, -}; -global.jQuery = {}; + }; + global.jQuery = {}; + global._ = _; +} +resetGlobals(); const JSWidgets = require( '../../js/customize-js-widgets' ); describe( 'wp.customize.JSWidgets', function() { - describe( '#isJsWidgetControl()', function() { + beforeEach( function() { + resetGlobals(); + } ); + + describe( '.isJsWidgetControl()', function() { it( 'returns false if the passed Control is not a JSWidget', function() { const mockWidget = { extended: () => false, @@ -35,4 +44,12 @@ describe( 'wp.customize.JSWidgets', function() { expect( result ).to.be.false; } ); } ); + + describe( '.extendWidgetControl()', function() { + it( 'overrides the initialize prototype method of the passed constructor function', function() { + const Obj = function() {}; + JSWidgets.extendWidgetControl( Obj ); + expect( Obj.prototype ).to.have.property( 'initialize' ); + } ); + } ); } ); From 1fe1906507f99d35e161e1e6c0b85fb54d21ee44 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Fri, 20 Jan 2017 18:29:59 -0500 Subject: [PATCH 15/92] Add truthy test for isJsWidgetControl Not currently using real Controls, but it's something. --- tests/js/customize-js-widgets.js | 33 ++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/tests/js/customize-js-widgets.js b/tests/js/customize-js-widgets.js index 4a91e4d..4114976 100644 --- a/tests/js/customize-js-widgets.js +++ b/tests/js/customize-js-widgets.js @@ -9,7 +9,9 @@ const noop = () => null; function resetGlobals() { global.wp = { widgets: { - formConstructor: {}, + formConstructor: { + text: noop, + }, }, customize: { Widgets: { @@ -35,7 +37,7 @@ describe( 'wp.customize.JSWidgets', function() { } ); describe( '.isJsWidgetControl()', function() { - it( 'returns false if the passed Control is not a JSWidget', function() { + it( 'returns false if the passed Control is not a WidgetControl and has no id_base', function() { const mockWidget = { extended: () => false, params: {}, @@ -43,6 +45,33 @@ describe( 'wp.customize.JSWidgets', function() { const result = JSWidgets.isJsWidgetControl( mockWidget ); expect( result ).to.be.false; } ); + + it( 'returns false if the passed Control has a known formConstructor for its id_base but is not a WidgetControl', function() { + const mockWidget = { + extended: () => false, + params: { widget_id_base: 'text' }, + }; + const result = JSWidgets.isJsWidgetControl( mockWidget ); + expect( result ).to.be.false; + } ); + + it( 'returns false if the passed Control is a WidgetControl and has an unknown formConstructor for its id_base', function() { + const mockWidget = { + extended: () => true, + params: { widget_id_base: 'foo' }, + }; + const result = JSWidgets.isJsWidgetControl( mockWidget ); + expect( result ).to.be.false; + } ); + + it( 'returns true if the passed Control is a WidgetControl and has a known formConstructor for its id_base', function() { + const mockWidget = { + extended: () => true, + params: { widget_id_base: 'text' }, + }; + const result = JSWidgets.isJsWidgetControl( mockWidget ); + expect( result ).to.be.true; + } ); } ); describe( '.extendWidgetControl()', function() { From e2dcb3385da90d0d2ae0fb30e2045273dcaab399 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Fri, 20 Jan 2017 18:46:06 -0500 Subject: [PATCH 16/92] Fix missing jsdoc rules to satisfy eslint --- js/customize-js-widgets.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/js/customize-js-widgets.js b/js/customize-js-widgets.js index fb66691..2122332 100644 --- a/js/customize-js-widgets.js +++ b/js/customize-js-widgets.js @@ -85,11 +85,11 @@ wp.customize.JSWidgets = (function( wp, api, $, _ ) { // eslint-disable-line no- /** * Initialize. * - * @param {string} id - * @param {object} options - * @param {object} options.params - * @param {string} options.params.widget_id - * @param {string} options.params.widget_id_base + * @param {string} id The widget id + * @param {object} options The options (see below) + * @param {object} options.params The params (see below) + * @param {string} options.params.widget_id The widget id + * @param {string} options.params.widget_id_base The widget id_base * @param {string} [options.params.type] - Must be 'widget_form'. * @param {string} [options.params.content] - This may be supplied by addWidget, but it will not be read since the form is constructed dynamically. * @param {string} [options.params.widget_control] - Handled internally, if supplied, an error will be thrown. @@ -234,7 +234,7 @@ wp.customize.JSWidgets = (function( wp, api, $, _ ) { // eslint-disable-line no- * Submit the widget form via Ajax and get back the updated instance, * along with the new widget control form to render. * - * @param {object} [args] + * @param {object} [args] The args to update the widget (see below) * @param {Object|null} [args.instance=null] When the model changes, the instance is sent here; otherwise, the inputs from the form are used * @param {Function|null} [args.complete=null] Function which is called when the request finishes. Context is bound to the control. First argument is any error. Following arguments are for success. * @returns {void} @@ -261,6 +261,7 @@ wp.customize.JSWidgets = (function( wp, api, $, _ ) { // eslint-disable-line no- * * @deprecated * @private + * @return {void} */ _getInputs: function _getInputs() { throw new Error( 'The _getInputs method should not be called for customize widget instances.' ); @@ -273,6 +274,7 @@ wp.customize.JSWidgets = (function( wp, api, $, _ ) { // eslint-disable-line no- * * @deprecated * @private + * @return {void} */ _getInputsSignature: function _getInputsSignature() { throw new Error( 'The _getInputsSignature method should not be called for customize widget instances.' ); @@ -285,6 +287,7 @@ wp.customize.JSWidgets = (function( wp, api, $, _ ) { // eslint-disable-line no- * * @deprecated * @private + * @return {void} */ _getInputState: function _getInputState() { throw new Error( 'The _getInputState method should not be called for customize widget instances.' ); @@ -297,6 +300,7 @@ wp.customize.JSWidgets = (function( wp, api, $, _ ) { // eslint-disable-line no- * * @deprecated * @private + * @return {void} */ _setInputState: function _setInputState() { throw new Error( 'The _setInputState method should not be called for customize widget instances.' ); From 3c33e2bf38e482831ecffaa53cc54c3be2a8b1de Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 22 Jan 2017 09:58:18 -0800 Subject: [PATCH 17/92] Prevent enqueueing trac-39389-preview script when user cannot manage widgets Fixes JS error due to customize-preview-widgets getting enqueued as well erroneously --- php/class-js-widgets-plugin.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/php/class-js-widgets-plugin.php b/php/class-js-widgets-plugin.php index 46aaec9..20237a4 100644 --- a/php/class-js-widgets-plugin.php +++ b/php/class-js-widgets-plugin.php @@ -313,16 +313,17 @@ function enqueue_shortcode_ui() { * * @access public * @global WP_Widget_Factory $wp_widget_factory + * @global WP_Customize_Manager $wp_customize */ function enqueue_frontend_scripts() { - global $wp_widget_factory; + global $wp_widget_factory, $wp_customize; foreach ( $wp_widget_factory->widgets as $widget ) { if ( $widget instanceof WP_JS_Widget && ( is_active_widget( false, false, $widget->id_base ) || is_customize_preview() ) ) { $widget->enqueue_frontend_scripts(); } } - if ( is_customize_preview() ) { + if ( is_customize_preview() && ! empty( $wp_customize->widgets ) && current_user_can( 'edit_theme_options' ) ) { wp_enqueue_script( $this->script_handles['trac-39389-preview'] ); } } From 26915f7ed989aefd194a9323be0771e33c03a309 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Mon, 23 Jan 2017 16:24:03 -0500 Subject: [PATCH 18/92] Explicitly pass underscore to the IIFE IIFEs are good tools for encapsulating functionality and to maintain that encapsulation, all inputs and outputs (the exported API) need to be explicit. To simply alias variables, variable assignment is a less cumbersome tool. --- js/widget-form.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/widget-form.js b/js/widget-form.js index 5630f8d..3554694 100644 --- a/js/widget-form.js +++ b/js/widget-form.js @@ -10,7 +10,7 @@ if ( ! wp.widgets.formConstructor ) { wp.widgets.formConstructor = {}; } -wp.widgets.Form = (function( api, $ ) { +wp.widgets.Form = (function( api, $, _ ) { 'use strict'; /** @@ -395,4 +395,4 @@ wp.widgets.Form = (function( api, $ ) { } }); -} )( wp.customize, jQuery ); +} )( wp.customize, jQuery, _ ); From 8c34cfe9b91241dc551f5d6ec0ed9b00f472c0f6 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Mon, 23 Jan 2017 16:39:12 -0500 Subject: [PATCH 19/92] Remove unnecessary use of `_.clone` `_.clone()` only performs a shallow copy and `_.extend()` is already doing that here. --- js/widget-form.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/widget-form.js b/js/widget-form.js index 3554694..8dfb233 100644 --- a/js/widget-form.js +++ b/js/widget-form.js @@ -51,7 +51,7 @@ wp.widgets.Form = (function( api, $, _ ) { default_instance: {} } }, - properties ? _.clone( properties ) : {} + properties || {} ); if ( ! args.model || ! args.model.extended || ! args.model.extended( api.Value ) ) { From 9cc522923d2ce6d0d8686e986fac0dcbf6e26bdc Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Mon, 23 Jan 2017 18:49:44 -0500 Subject: [PATCH 20/92] Export Form as a module when within CommonJS --- js/widget-form.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/js/widget-form.js b/js/widget-form.js index 8dfb233..4022dc9 100644 --- a/js/widget-form.js +++ b/js/widget-form.js @@ -1,4 +1,4 @@ -/* global wp, console */ +/* global wp, console, module */ /* eslint-disable strict */ /* eslint consistent-this: [ "error", "form" ] */ /* eslint-disable complexity */ @@ -396,3 +396,7 @@ wp.widgets.Form = (function( api, $, _ ) { }); } )( wp.customize, jQuery, _ ); + +if ( 'undefined' !== typeof module ) { + module.exports = wp.widgets.Form; +} From 739150e26d5187e3fd338ae0a94aa0b0e58a0b95 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Tue, 24 Jan 2017 11:57:01 -0500 Subject: [PATCH 21/92] Add customize-base for tests --- tests/js/lib/customize-base.js | 853 +++++++++++++++++++++++++++++++++ 1 file changed, 853 insertions(+) create mode 100644 tests/js/lib/customize-base.js diff --git a/tests/js/lib/customize-base.js b/tests/js/lib/customize-base.js new file mode 100644 index 0000000..b754857 --- /dev/null +++ b/tests/js/lib/customize-base.js @@ -0,0 +1,853 @@ +window.wp = window.wp || {}; + +(function( exports, $ ){ + var api = {}, ctor, inherits, + slice = Array.prototype.slice; + + // Shared empty constructor function to aid in prototype-chain creation. + ctor = function() {}; + + /** + * Helper function to correctly set up the prototype chain, for subclasses. + * Similar to `goog.inherits`, but uses a hash of prototype properties and + * class properties to be extended. + * + * @param object parent Parent class constructor to inherit from. + * @param object protoProps Properties to apply to the prototype for use as class instance properties. + * @param object staticProps Properties to apply directly to the class constructor. + * @return child The subclassed constructor. + */ + inherits = function( parent, protoProps, staticProps ) { + var child; + + // The constructor function for the new subclass is either defined by you + // (the "constructor" property in your `extend` definition), or defaulted + // by us to simply call `super()`. + if ( protoProps && protoProps.hasOwnProperty( 'constructor' ) ) { + child = protoProps.constructor; + } else { + child = function() { + // Storing the result `super()` before returning the value + // prevents a bug in Opera where, if the constructor returns + // a function, Opera will reject the return value in favor of + // the original object. This causes all sorts of trouble. + var result = parent.apply( this, arguments ); + return result; + }; + } + + // Inherit class (static) properties from parent. + $.extend( child, parent ); + + // Set the prototype chain to inherit from `parent`, without calling + // `parent`'s constructor function. + ctor.prototype = parent.prototype; + child.prototype = new ctor(); + + // Add prototype properties (instance properties) to the subclass, + // if supplied. + if ( protoProps ) + $.extend( child.prototype, protoProps ); + + // Add static properties to the constructor function, if supplied. + if ( staticProps ) + $.extend( child, staticProps ); + + // Correctly set child's `prototype.constructor`. + child.prototype.constructor = child; + + // Set a convenience property in case the parent's prototype is needed later. + child.__super__ = parent.prototype; + + return child; + }; + + /** + * Base class for object inheritance. + */ + api.Class = function( applicator, argsArray, options ) { + var magic, args = arguments; + + if ( applicator && argsArray && api.Class.applicator === applicator ) { + args = argsArray; + $.extend( this, options || {} ); + } + + magic = this; + + /* + * If the class has a method called "instance", + * the return value from the class' constructor will be a function that + * calls the "instance" method. + * + * It is also an object that has properties and methods inside it. + */ + if ( this.instance ) { + magic = function() { + return magic.instance.apply( magic, arguments ); + }; + + $.extend( magic, this ); + } + + magic.initialize.apply( magic, args ); + return magic; + }; + + /** + * Creates a subclass of the class. + * + * @param object protoProps Properties to apply to the prototype. + * @param object staticProps Properties to apply directly to the class. + * @return child The subclass. + */ + api.Class.extend = function( protoProps, classProps ) { + var child = inherits( this, protoProps, classProps ); + child.extend = this.extend; + return child; + }; + + api.Class.applicator = {}; + + /** + * Initialize a class instance. + * + * Override this function in a subclass as needed. + */ + api.Class.prototype.initialize = function() {}; + + /* + * Checks whether a given instance extended a constructor. + * + * The magic surrounding the instance parameter causes the instanceof + * keyword to return inaccurate results; it defaults to the function's + * prototype instead of the constructor chain. Hence this function. + */ + api.Class.prototype.extended = function( constructor ) { + var proto = this; + + while ( typeof proto.constructor !== 'undefined' ) { + if ( proto.constructor === constructor ) + return true; + if ( typeof proto.constructor.__super__ === 'undefined' ) + return false; + proto = proto.constructor.__super__; + } + return false; + }; + + /** + * An events manager object, offering the ability to bind to and trigger events. + * + * Used as a mixin. + */ + api.Events = { + trigger: function( id ) { + if ( this.topics && this.topics[ id ] ) + this.topics[ id ].fireWith( this, slice.call( arguments, 1 ) ); + return this; + }, + + bind: function( id ) { + this.topics = this.topics || {}; + this.topics[ id ] = this.topics[ id ] || $.Callbacks(); + this.topics[ id ].add.apply( this.topics[ id ], slice.call( arguments, 1 ) ); + return this; + }, + + unbind: function( id ) { + if ( this.topics && this.topics[ id ] ) + this.topics[ id ].remove.apply( this.topics[ id ], slice.call( arguments, 1 ) ); + return this; + } + }; + + /** + * Observable values that support two-way binding. + * + * @constructor + */ + api.Value = api.Class.extend({ + /** + * @param {mixed} initial The initial value. + * @param {object} options + */ + initialize: function( initial, options ) { + this._value = initial; // @todo: potentially change this to a this.set() call. + this.callbacks = $.Callbacks(); + this._dirty = false; + + $.extend( this, options || {} ); + + this.set = $.proxy( this.set, this ); + }, + + /* + * Magic. Returns a function that will become the instance. + * Set to null to prevent the instance from extending a function. + */ + instance: function() { + return arguments.length ? this.set.apply( this, arguments ) : this.get(); + }, + + /** + * Get the value. + * + * @return {mixed} + */ + get: function() { + return this._value; + }, + + /** + * Set the value and trigger all bound callbacks. + * + * @param {object} to New value. + */ + set: function( to ) { + var from = this._value; + + to = this._setter.apply( this, arguments ); + to = this.validate( to ); + + // Bail if the sanitized value is null or unchanged. + if ( null === to || _.isEqual( from, to ) ) { + return this; + } + + this._value = to; + this._dirty = true; + + this.callbacks.fireWith( this, [ to, from ] ); + + return this; + }, + + _setter: function( to ) { + return to; + }, + + setter: function( callback ) { + var from = this.get(); + this._setter = callback; + // Temporarily clear value so setter can decide if it's valid. + this._value = null; + this.set( from ); + return this; + }, + + resetSetter: function() { + this._setter = this.constructor.prototype._setter; + this.set( this.get() ); + return this; + }, + + validate: function( value ) { + return value; + }, + + /** + * Bind a function to be invoked whenever the value changes. + * + * @param {...Function} A function, or multiple functions, to add to the callback stack. + */ + bind: function() { + this.callbacks.add.apply( this.callbacks, arguments ); + return this; + }, + + /** + * Unbind a previously bound function. + * + * @param {...Function} A function, or multiple functions, to remove from the callback stack. + */ + unbind: function() { + this.callbacks.remove.apply( this.callbacks, arguments ); + return this; + }, + + link: function() { // values* + var set = this.set; + $.each( arguments, function() { + this.bind( set ); + }); + return this; + }, + + unlink: function() { // values* + var set = this.set; + $.each( arguments, function() { + this.unbind( set ); + }); + return this; + }, + + sync: function() { // values* + var that = this; + $.each( arguments, function() { + that.link( this ); + this.link( that ); + }); + return this; + }, + + unsync: function() { // values* + var that = this; + $.each( arguments, function() { + that.unlink( this ); + this.unlink( that ); + }); + return this; + } + }); + + /** + * A collection of observable values. + * + * @constructor + * @augments wp.customize.Class + * @mixes wp.customize.Events + */ + api.Values = api.Class.extend({ + + /** + * The default constructor for items of the collection. + * + * @type {object} + */ + defaultConstructor: api.Value, + + initialize: function( options ) { + $.extend( this, options || {} ); + + this._value = {}; + this._deferreds = {}; + }, + + /** + * Get the instance of an item from the collection if only ID is specified. + * + * If more than one argument is supplied, all are expected to be IDs and + * the last to be a function callback that will be invoked when the requested + * items are available. + * + * @see {api.Values.when} + * + * @param {string} id ID of the item. + * @param {...} Zero or more IDs of items to wait for and a callback + * function to invoke when they're available. Optional. + * @return {mixed} The item instance if only one ID was supplied. + * A Deferred Promise object if a callback function is supplied. + */ + instance: function( id ) { + if ( arguments.length === 1 ) + return this.value( id ); + + return this.when.apply( this, arguments ); + }, + + /** + * Get the instance of an item. + * + * @param {string} id The ID of the item. + * @return {[type]} [description] + */ + value: function( id ) { + return this._value[ id ]; + }, + + /** + * Whether the collection has an item with the given ID. + * + * @param {string} id The ID of the item to look for. + * @return {Boolean} + */ + has: function( id ) { + return typeof this._value[ id ] !== 'undefined'; + }, + + /** + * Add an item to the collection. + * + * @param {string} id The ID of the item. + * @param {mixed} value The item instance. + * @return {mixed} The new item's instance. + */ + add: function( id, value ) { + if ( this.has( id ) ) + return this.value( id ); + + this._value[ id ] = value; + value.parent = this; + + // Propagate a 'change' event on an item up to the collection. + if ( value.extended( api.Value ) ) + value.bind( this._change ); + + this.trigger( 'add', value ); + + // If a deferred object exists for this item, + // resolve it. + if ( this._deferreds[ id ] ) + this._deferreds[ id ].resolve(); + + return this._value[ id ]; + }, + + /** + * Create a new item of the collection using the collection's default constructor + * and store it in the collection. + * + * @param {string} id The ID of the item. + * @param {mixed} value Any extra arguments are passed into the item's initialize method. + * @return {mixed} The new item's instance. + */ + create: function( id ) { + return this.add( id, new this.defaultConstructor( api.Class.applicator, slice.call( arguments, 1 ) ) ); + }, + + /** + * Iterate over all items in the collection invoking the provided callback. + * + * @param {Function} callback Function to invoke. + * @param {object} context Object context to invoke the function with. Optional. + */ + each: function( callback, context ) { + context = typeof context === 'undefined' ? this : context; + + $.each( this._value, function( key, obj ) { + callback.call( context, obj, key ); + }); + }, + + /** + * Remove an item from the collection. + * + * @param {string} id The ID of the item to remove. + */ + remove: function( id ) { + var value; + + if ( this.has( id ) ) { + value = this.value( id ); + this.trigger( 'remove', value ); + if ( value.extended( api.Value ) ) + value.unbind( this._change ); + delete value.parent; + } + + delete this._value[ id ]; + delete this._deferreds[ id ]; + }, + + /** + * Runs a callback once all requested values exist. + * + * when( ids*, [callback] ); + * + * For example: + * when( id1, id2, id3, function( value1, value2, value3 ) {} ); + * + * @returns $.Deferred.promise(); + */ + when: function() { + var self = this, + ids = slice.call( arguments ), + dfd = $.Deferred(); + + // If the last argument is a callback, bind it to .done() + if ( $.isFunction( ids[ ids.length - 1 ] ) ) + dfd.done( ids.pop() ); + + /* + * Create a stack of deferred objects for each item that is not + * yet available, and invoke the supplied callback when they are. + */ + $.when.apply( $, $.map( ids, function( id ) { + if ( self.has( id ) ) + return; + + /* + * The requested item is not available yet, create a deferred + * object to resolve when it becomes available. + */ + return self._deferreds[ id ] = self._deferreds[ id ] || $.Deferred(); + })).done( function() { + var values = $.map( ids, function( id ) { + return self( id ); + }); + + // If a value is missing, we've used at least one expired deferred. + // Call Values.when again to generate a new deferred. + if ( values.length !== ids.length ) { + // ids.push( callback ); + self.when.apply( self, ids ).done( function() { + dfd.resolveWith( self, values ); + }); + return; + } + + dfd.resolveWith( self, values ); + }); + + return dfd.promise(); + }, + + /** + * A helper function to propagate a 'change' event from an item + * to the collection itself. + */ + _change: function() { + this.parent.trigger( 'change', this ); + } + }); + + // Create a global events bus on the Customizer. + $.extend( api.Values.prototype, api.Events ); + + + /** + * Cast a string to a jQuery collection if it isn't already. + * + * @param {string|jQuery collection} element + */ + api.ensure = function( element ) { + return typeof element == 'string' ? $( element ) : element; + }; + + /** + * An observable value that syncs with an element. + * + * Handles inputs, selects, and textareas by default. + * + * @constructor + * @augments wp.customize.Value + * @augments wp.customize.Class + */ + api.Element = api.Value.extend({ + initialize: function( element, options ) { + var self = this, + synchronizer = api.Element.synchronizer.html, + type, update, refresh; + + this.element = api.ensure( element ); + this.events = ''; + + if ( this.element.is('input, select, textarea') ) { + this.events += 'change'; + synchronizer = api.Element.synchronizer.val; + + if ( this.element.is('input') ) { + type = this.element.prop('type'); + if ( api.Element.synchronizer[ type ] ) { + synchronizer = api.Element.synchronizer[ type ]; + } + if ( 'text' === type || 'password' === type ) { + this.events += ' keyup'; + } else if ( 'range' === type ) { + this.events += ' input propertychange'; + } + } else if ( this.element.is('textarea') ) { + this.events += ' keyup'; + } + } + + api.Value.prototype.initialize.call( this, null, $.extend( options || {}, synchronizer ) ); + this._value = this.get(); + + update = this.update; + refresh = this.refresh; + + this.update = function( to ) { + if ( to !== refresh.call( self ) ) + update.apply( this, arguments ); + }; + this.refresh = function() { + self.set( refresh.call( self ) ); + }; + + this.bind( this.update ); + this.element.bind( this.events, this.refresh ); + }, + + find: function( selector ) { + return $( selector, this.element ); + }, + + refresh: function() {}, + + update: function() {} + }); + + api.Element.synchronizer = {}; + + $.each( [ 'html', 'val' ], function( index, method ) { + api.Element.synchronizer[ method ] = { + update: function( to ) { + this.element[ method ]( to ); + }, + refresh: function() { + return this.element[ method ](); + } + }; + }); + + api.Element.synchronizer.checkbox = { + update: function( to ) { + this.element.prop( 'checked', to ); + }, + refresh: function() { + return this.element.prop( 'checked' ); + } + }; + + api.Element.synchronizer.radio = { + update: function( to ) { + this.element.filter( function() { + return this.value === to; + }).prop( 'checked', true ); + }, + refresh: function() { + return this.element.filter( ':checked' ).val(); + } + }; + + $.support.postMessage = !! window.postMessage; + + /** + * A communicator for sending data from one window to another over postMessage. + * + * @constructor + * @augments wp.customize.Class + * @mixes wp.customize.Events + */ + api.Messenger = api.Class.extend({ + /** + * Create a new Value. + * + * @param {string} key Unique identifier. + * @param {mixed} initial Initial value. + * @param {mixed} options Options hash. Optional. + * @return {Value} Class instance of the Value. + */ + add: function( key, initial, options ) { + return this[ key ] = new api.Value( initial, options ); + }, + + /** + * Initialize Messenger. + * + * @param {object} params - Parameters to configure the messenger. + * {string} params.url - The URL to communicate with. + * {window} params.targetWindow - The window instance to communicate with. Default window.parent. + * {string} params.channel - If provided, will send the channel with each message and only accept messages a matching channel. + * @param {object} options - Extend any instance parameter or method with this object. + */ + initialize: function( params, options ) { + // Target the parent frame by default, but only if a parent frame exists. + var defaultTarget = window.parent === window ? null : window.parent; + + $.extend( this, options || {} ); + + this.add( 'channel', params.channel ); + this.add( 'url', params.url || '' ); + this.add( 'origin', this.url() ).link( this.url ).setter( function( to ) { + var urlParser = document.createElement( 'a' ); + urlParser.href = to; + // Port stripping needed by IE since it adds to host but not to event.origin. + return urlParser.protocol + '//' + urlParser.host.replace( /:80$/, '' ); + }); + + // first add with no value + this.add( 'targetWindow', null ); + // This avoids SecurityErrors when setting a window object in x-origin iframe'd scenarios. + this.targetWindow.set = function( to ) { + var from = this._value; + + to = this._setter.apply( this, arguments ); + to = this.validate( to ); + + if ( null === to || from === to ) { + return this; + } + + this._value = to; + this._dirty = true; + + this.callbacks.fireWith( this, [ to, from ] ); + + return this; + }; + // now set it + this.targetWindow( params.targetWindow || defaultTarget ); + + + // Since we want jQuery to treat the receive function as unique + // to this instance, we give the function a new guid. + // + // This will prevent every Messenger's receive function from being + // unbound when calling $.off( 'message', this.receive ); + this.receive = $.proxy( this.receive, this ); + this.receive.guid = $.guid++; + + $( window ).on( 'message', this.receive ); + }, + + destroy: function() { + $( window ).off( 'message', this.receive ); + }, + + /** + * Receive data from the other window. + * + * @param {jQuery.Event} event Event with embedded data. + */ + receive: function( event ) { + var message; + + event = event.originalEvent; + + if ( ! this.targetWindow || ! this.targetWindow() ) { + return; + } + + // Check to make sure the origin is valid. + if ( this.origin() && event.origin !== this.origin() ) + return; + + // Ensure we have a string that's JSON.parse-able + if ( typeof event.data !== 'string' || event.data[0] !== '{' ) { + return; + } + + message = JSON.parse( event.data ); + + // Check required message properties. + if ( ! message || ! message.id || typeof message.data === 'undefined' ) + return; + + // Check if channel names match. + if ( ( message.channel || this.channel() ) && this.channel() !== message.channel ) + return; + + this.trigger( message.id, message.data ); + }, + + /** + * Send data to the other window. + * + * @param {string} id The event name. + * @param {object} data Data. + */ + send: function( id, data ) { + var message; + + data = typeof data === 'undefined' ? null : data; + + if ( ! this.url() || ! this.targetWindow() ) + return; + + message = { id: id, data: data }; + if ( this.channel() ) + message.channel = this.channel(); + + this.targetWindow().postMessage( JSON.stringify( message ), this.origin() ); + } + }); + + // Add the Events mixin to api.Messenger. + $.extend( api.Messenger.prototype, api.Events ); + + /** + * Notification. + * + * @class + * @augments wp.customize.Class + * @since 4.6.0 + * + * @param {string} code - The error code. + * @param {object} params - Params. + * @param {string} params.message=null - The error message. + * @param {string} [params.type=error] - The notification type. + * @param {boolean} [params.fromServer=false] - Whether the notification was server-sent. + * @param {string} [params.setting=null] - The setting ID that the notification is related to. + * @param {*} [params.data=null] - Any additional data. + */ + api.Notification = api.Class.extend({ + initialize: function( code, params ) { + var _params; + this.code = code; + _params = _.extend( + { + message: null, + type: 'error', + fromServer: false, + data: null, + setting: null + }, + params + ); + delete _params.code; + _.extend( this, _params ); + } + }); + + // The main API object is also a collection of all customizer settings. + api = $.extend( new api.Values(), api ); + + /** + * Get all customize settings. + * + * @return {object} + */ + api.get = function() { + var result = {}; + + this.each( function( obj, key ) { + result[ key ] = obj.get(); + }); + + return result; + }; + + /** + * Utility function namespace + */ + api.utils = {}; + + /** + * Parse query string. + * + * @since 4.7.0 + * @access public + * + * @param {string} queryString Query string. + * @returns {object} Parsed query string. + */ + api.utils.parseQueryString = function parseQueryString( queryString ) { + var queryParams = {}; + _.each( queryString.split( '&' ), function( pair ) { + var parts, key, value; + parts = pair.split( '=', 2 ); + if ( ! parts[0] ) { + return; + } + key = decodeURIComponent( parts[0].replace( /\+/g, ' ' ) ); + key = key.replace( / /g, '_' ); // What PHP does. + if ( _.isUndefined( parts[1] ) ) { + value = null; + } else { + value = decodeURIComponent( parts[1].replace( /\+/g, ' ' ) ); + } + queryParams[ key ] = value; + } ); + return queryParams; + }; + + // Expose the API publicly on window.wp.customize + exports.customize = api; +})( wp, jQuery ); + +if ( 'undefined' !== typeof module ) { + module.exports = wp.customize; +} From 9e66457c96d0bf0f5f55536cf4cdda3f28fd2145 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Tue, 24 Jan 2017 11:56:47 -0500 Subject: [PATCH 22/92] Add initial tests for Form --- package.json | 2 + tests/js/widget-form.js | 371 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 373 insertions(+) create mode 100644 tests/js/widget-form.js diff --git a/package.json b/package.json index 8413124..5e42228 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,8 @@ "grunt-shell": "^1.3.1", "grunt-wp-deploy": "^1.2.1", "mocha": "^3.2.0", + "sinon": "^1.17.7", + "sinon-chai": "^2.8.0", "underscore": "^1.8.3" }, "author": "XWP", diff --git a/tests/js/widget-form.js b/tests/js/widget-form.js new file mode 100644 index 0000000..1d28e15 --- /dev/null +++ b/tests/js/widget-form.js @@ -0,0 +1,371 @@ +/* globals global, require, describe, it, beforeEach */ +/* eslint-disable no-unused-expressions, no-new, no-magic-numbers */ + +const sinon = require( 'sinon' ); +const sinonChai = require( 'sinon-chai' ); +const chai = require( 'chai' ); +const _ = require( 'underscore' ); + +chai.use( sinonChai ); +const expect = chai.expect; + +function getMockDom() { + return { + 'findme': true, + 'findit': true, + }; +} + +function mockJQuery() { + const jQuery = ( selector ) => { + const jQueryResult = { + selector: '', + length: 0, + find: jQuery, + is: () => mockDom[ selector ], + html: markup => markup, + each: () => null, + }; + if ( ! mockDom[ selector ] ) { + return jQueryResult; + } + jQueryResult.selector = selector; + jQueryResult.length = 1; + jQueryResult.each = iterator => iterator.call( jQueryResult ); + return jQueryResult; + }; + jQuery.extend = _.extend; + jQuery.each = ( elements, callback ) => { + if ( Array.isArray( elements ) ) { + return elements.map( ( element, index ) => callback( index, element ) ); + } + return Object.keys( elements ).map( key => callback( key, elements[ key ] ) ); + }; + jQuery.trim = str => str.trim(); + jQuery.support = {}; + jQuery.Callbacks = () => ( { fireWith: () => null, add: () => null } ); + jQuery.proxy = _.bind; + return jQuery; +} + +function resetGlobals() { + const jQuery = mockJQuery(); + global.wp.customize = {}; + global.wp.template = () => null; + global.jQuery = jQuery; + global.$ = jQuery; + global._ = _; + global.window = { + jQuery, + }; +} + +let mockDom = getMockDom(); +resetGlobals(); +const { Value, Notification } = require( './lib/customize-base' ); +const Form = require( '../../js/widget-form' ); + +describe( 'wp.widgets.Form', function() { + let templateSpy; + + beforeEach( function() { + mockDom = getMockDom(); + templateSpy = sinon.stub().returns( () => 'pastries' ); + global.wp.template = templateSpy; + } ); + + describe( '.initialize() (constructor)', function() { + let model; + let container; + + beforeEach( function() { + model = new Value( 'test' ); + container = 'findme'; + } ); + + it( 'assigns arbitrary properties on the argument object as properties on the form object', function() { + const params = { foo: 'bar', model, container }; + const form = new Form( params ); + expect( form.foo ).to.eql( 'bar' ); + } ); + + it( 'throws an error if the model property is missing', function( done ) { + const params = { foo: 'bar', container }; + try { + new Form( params ); + } catch ( err ) { + done(); + } + throw new Error( 'Expected initialize to throw an Error but it did not' ); + } ); + + it( 'throws an error if the model property is not an instance of Value', function( done ) { + const params = { foo: 'bar', model: { hello: 'world' }, container }; + try { + new Form( params ); + } catch ( err ) { + done(); + } + throw new Error( 'Expected initialize to throw an Error but it did not' ); + } ); + + it( 'assigns a default config property if it has not already been set on the form object', function() { + const params = { foo: 'bar', model, container }; + const form = new Form( params ); + const expected = { + form_template_id: '', + notifications_template_id: '', + l10n: {}, + default_instance: {}, + }; + expect( form.config ).to.eql( expected ); + } ); + + it( 'keeps the current config property if it is already set on the form object', function() { + const MyForm = Form.extend( { + config: { subclass: 'yes' }, + } ); + const params = { foo: 'bar', model, container }; + const form = new MyForm( params ); + const expected = { + form_template_id: '', + notifications_template_id: '', + l10n: {}, + default_instance: {}, + subclass: 'yes', + }; + expect( form.config ).to.eql( expected ); + } ); + + it( 'allows overriding the default property values with the properties of the argument', function() { + const params = { foo: 'bar', model, config: { form_template_id: 'hello' }, container }; + const form = new Form( params ); + const expected = { + form_template_id: 'hello', + notifications_template_id: '', + l10n: {}, + default_instance: {}, + }; + expect( form.config ).to.eql( expected ); + } ); + + it( 'assigns form.setting as an alias to form.model', function() { + const params = { foo: 'bar', model, container }; + const form = new Form( params ); + expect( form.setting ).to.equal( model ); + } ); + + it( 'assigns form.notifications as an alias for form.model.notifications if it exists', function() { + model.notifications = { hello: 'world' }; + const params = { foo: 'bar', model, container }; + const form = new Form( params ); + expect( form.notifications ).to.equal( model.notifications ); + } ); + + it( 'assigns form.notifications to a new Values with the Notification defaultConstructor if form.model.notifications does not exist', function() { + const params = { foo: 'bar', model, container }; + const form = new Form( params ); + expect( form.notifications.defaultConstructor ).to.equal( Notification ); + } ); + + it( 'assigns form.container to a jQuery DOM object for the selector previously in form.container', function() { + const params = { foo: 'bar', model, container: 'findit' }; + const form = new Form( params ); + expect( form.container.selector ).to.eql( 'findit' ); + } ); + + it( 'throws an error if the form.container selector does not match a DOM node', function( done ) { + const params = { foo: 'bar', model, container: 'notfound' }; + try { + new Form( params ); + } catch( err ) { + done(); + } + throw new Error( 'Expected initialize to throw an Error but it did not' ); + } ); + + it( 'mutates the model to override the `validate` method', function() { + const params = { foo: 'bar', model, container }; + const previousValidate = model.validate; + new Form( params ); + expect( model.validate ).to.not.equal( previousValidate ); + } ); + } ); + + describe( '.getNotificationsContainerElement()', function() { + let form; + + beforeEach( function() { + const model = new Value( 'test' ); + form = new Form( { model, container: 'findme' } ); + } ); + + it( 'returns a jQuery object for the first instance of .js-widget-form-notifications-container in the container', function() { + mockDom[ '.js-widget-form-notifications-container:first' ] = true; + const actual = form.getNotificationsContainerElement(); + expect( actual.length ).to.eql( 1 ); + } ); + + it( 'returns an empty jQuery object if there are no instances of .js-widget-form-notifications-container in the container', function() { + const actual = form.getNotificationsContainerElement(); + expect( actual.length ).to.eql( 0 ); + } ); + } ); + + describe( '.sanitize()', function() { + let form; + + beforeEach( function() { + const model = new Value( 'test' ); + form = new Form( { model, container: 'findme' } ); + } ); + + it( 'throws an Error if oldInstance is not set', function( done ) { + try { + form.sanitize( {} ); + } catch( err ) { + done(); + } + throw new Error( 'Expected sanitize to throw an Error but it did not' ); + } ); + + it( 'does not throw an error if oldInstance is null', function() { + form.sanitize( {}, null ); + } ); + + it( 'returns an object with the same properties as the original object', function() { + const actual = form.sanitize( { foo: 'bar' }, {} ); + expect( actual.foo ).to.eql( 'bar' ); + } ); + + it( 'returns an object with the `title` property added if it does not exist on the original', function() { + const actual = form.sanitize( { foo: 'bar' }, {} ); + expect( actual ).to.have.property( 'title' ); + } ); + + it( 'returns an object with the `title` property trimmed', function() { + const actual = form.sanitize( { title: ' hello world ' }, {} ); + expect( actual.title ).to.eql( 'hello world' ); + } ); + + it( 'adds a markupTitleInvalid notification to form.notifications if there is markup in the `title` property', function() { + form.sanitize( { title: 'this is cool' }, {} ); + expect( form.notifications.has( 'markupTitleInvalid' ) ).to.be.true; + } ); + + it( 'does not add a markupTitleInvalid notification from form.notifications if there is no markup in the `title` property', function() { + form.sanitize( { title: 'this is cool' }, {} ); + expect( form.notifications.has( 'markupTitleInvalid' ) ).to.be.false; + } ); + + it( 'removes any existing markupTitleInvalid notification from form.notifications if there is no markup in the `title` property', function() { + form.sanitize( { title: 'this is cool' }, {} ); + form.sanitize( { title: 'this is cool' }, {} ); + expect( form.notifications.has( 'markupTitleInvalid' ) ).to.be.false; + } ); + } ); + + describe( '.getValue()', function() { + let form; + + beforeEach( function() { + const model = new Value( { hello: 'world' } ); + form = new Form( { model, container: 'findme', config: { default_instance: { foo: 'bar' } } } ); + } ); + + it( 'returns an object with the properties of the form\'s config.default_instance', function() { + const actual = form.getValue(); + expect( actual.foo ).to.eql( 'bar' ); + } ); + + it( 'returns an object with the properties of the form\'s model.get()', function() { + const actual = form.getValue(); + expect( actual.hello ).to.eql( 'world' ); + } ); + } ); + + describe( '.setState()', function() { + let form, model; + + beforeEach( function() { + model = new Value( { hello: 'world' } ); + form = new Form( { model, container: 'findme', config: { default_instance: { foo: 'bar' } } } ); + } ); + + it( 'updates the model by merging its value with the passed object properties', function() { + form.setState( { beep: 'boop' } ); + expect( model.get().beep ).to.eql( 'boop' ); + } ); + + it( 'updates the model by merging its value with the properties of config.default_instance', function() { + form.setState( { beep: 'boop' } ); + expect( model.get().foo ).to.eql( 'bar' ); + } ); + } ); + + describe( '.getTemplate()', function() { + let form, model; + + beforeEach( function() { + model = new Value( { hello: 'world' } ); + mockDom[ '#tmpl-my-template' ] = true; + form = new Form( { model, container: 'findme', config: { form_template_id: 'my-template' } } ); + } ); + + it( 'calls wp.template() on the config.form_template_id if no template is cached', function() { + form.getTemplate(); + expect( templateSpy ).to.have.been.calledWith( 'my-template' ); + } ); + + it( 'returns the result of wp.template() if no template is cached', function() { + const actual = form.getTemplate(); + expect( actual() ).to.eql( 'pastries' ); + } ); + + it( 'throws an error if the template does not exist in the DOM', function( done ) { + form = new Form( { model, container: 'findme', config: { form_template_id: 'notfound' } } ); + try { + form.getTemplate(); + } catch( err ) { + done(); + } + throw new Error( 'Expected getTemplate to throw an Error but it did not' ); + } ); + + it( 'returns the cached template if one is saved', function() { + form.getTemplate(); + const actual = form.getTemplate(); + expect( actual() ).to.eql( 'pastries' ); + } ); + + it( 'does not search the DOM for a template if one is cached', function() { + form.getTemplate(); + form.getTemplate(); + expect( templateSpy ).to.have.been.calledOnce; + } ); + } ); + + describe( '.render()', function() { + let form, model; + + beforeEach( function() { + model = new Value( { hello: 'world' } ); + mockDom[ '#tmpl-my-template' ] = true; + form = new Form( { model, container: 'findme', config: { form_template_id: 'my-template' } } ); + form.container.html = sinon.spy(); + } ); + + it( 'replaces the html of form.container with the interpolated value of form.getTemplate()', function() { + form.render(); + expect( form.container.html ).to.have.been.calledWith( 'pastries' ); + } ); + + it( 'calls the template function passing the Form object itself', function() { + const interpolateSpy = sinon.spy(); + templateSpy = sinon.stub().returns( interpolateSpy ); + global.wp.template = templateSpy; + form.render(); + expect( interpolateSpy ).to.have.been.calledWith( form ); + } ); + } ); +} ); From 99c9ae22e00a77fae11e61871d26480ce8eba3f1 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Tue, 24 Jan 2017 18:10:41 -0500 Subject: [PATCH 23/92] Update Form.initialize to deep merge config with defaults --- js/widget-form.js | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/js/widget-form.js b/js/widget-form.js index 4022dc9..53a9c8f 100644 --- a/js/widget-form.js +++ b/js/widget-form.js @@ -38,21 +38,25 @@ wp.widgets.Form = (function( api, $, _ ) { * @return {void} */ initialize: function initialize( properties ) { - var form = this, args, previousValidate; - - args = _.extend( - { - model: null, - container: null, - config: ! _.isEmpty( form.config ) ? _.clone( form.config ) : { - form_template_id: '', - notifications_template_id: '', - l10n: {}, - default_instance: {} - } - }, - properties || {} - ); + var form = this, args, previousValidate, defaultConfig, defaultProperties, config; + + defaultConfig = { + form_template_id: '', + notifications_template_id: '', + l10n: {}, + default_instance: {} + }; + + defaultProperties = { + model: null, + container: null, + config: {}, + }; + + config = properties ? properties.config : {}; + + args = _.extend( {}, defaultProperties, properties || {} ); + args.config = _.extend( {}, defaultConfig, form.config, config || {} ); if ( ! args.model || ! args.model.extended || ! args.model.extended( api.Value ) ) { throw new Error( 'Missing model property which must be a Value or Setting instance.' ); From ba2700d0ee64f1cb694018bcfc237dc9f0c9ab58 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Wed, 25 Jan 2017 16:48:13 -0500 Subject: [PATCH 24/92] Update tests to prove setState does not unexpectedly mutate the model --- tests/js/widget-form.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/js/widget-form.js b/tests/js/widget-form.js index 1d28e15..8f5afaa 100644 --- a/tests/js/widget-form.js +++ b/tests/js/widget-form.js @@ -297,9 +297,9 @@ describe( 'wp.widgets.Form', function() { expect( model.get().beep ).to.eql( 'boop' ); } ); - it( 'updates the model by merging its value with the properties of config.default_instance', function() { - form.setState( { beep: 'boop' } ); - expect( model.get().foo ).to.eql( 'bar' ); + it( 'does not change the model if no properties are passed', function() { + form.setState( {} ); + expect( model.get() ).to.eql( { hello: 'world' } ); } ); } ); From 1d2ad13e46d0b7cca6301ed0ce4e5fa030271a19 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Wed, 25 Jan 2017 19:31:57 -0500 Subject: [PATCH 25/92] Add tests for Form.model.validate() --- tests/js/widget-form.js | 117 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/tests/js/widget-form.js b/tests/js/widget-form.js index 8f5afaa..a9407bc 100644 --- a/tests/js/widget-form.js +++ b/tests/js/widget-form.js @@ -192,6 +192,123 @@ describe( 'wp.widgets.Form', function() { } ); } ); + describe( '.model.validate()', function() { + let model; + + beforeEach( function() { + model = new Value( { hello: 'world' } ); + } ); + + it( 'returns an object with the properties of the object passed in', function() { + new Form( { model, container: 'findme' } ); + const actual = model.validate( { foo: 'bar' } ); + expect( actual.foo ).to.eql( 'bar' ); + } ); + + it( 'also calls the model\'s previous validate method', function() { + model.validate = values => Object.assign( {}, values, { red: 'green' } ); + new Form( { model, container: 'findme' } ); + const actual = model.validate( { foo: 'bar' } ); + expect( actual.red ).to.eql( 'green' ); + } ); + + it( 'does not remove any Notifications from form.notifications which do not have the `viaWidgetFormSanitizeReturn` property', function() { + const form = new Form( { model, container: '.findme' } ); + form.notifications.add( 'other-notification', new Notification( 'other-notification', { message: 'test', type: 'warning' } ) ); + model.validate( { foo: 'bar' } ); + expect( form.notifications.has( 'other-notification' ) ).to.be.true; + } ); + + it( 'removes any Notifications from form.notifications which have the `viaWidgetFormSanitizeReturn` property', function() { + const form = new Form( { model, container: '.findme' } ); + const notification = new Notification( 'other-notification', { message: 'test', type: 'warning' } ); + notification.viaWidgetFormSanitizeReturn = true; + form.notifications.add( 'other-notification', notification ); + model.validate( { foo: 'bar' } ); + expect( form.notifications.has( 'other-notification' ) ).to.be.false; + } ); + + describe( 'if the sanitize function returns a Notification', function() { + let MyForm, form; + + beforeEach( function() { + MyForm = Form.extend( { + sanitize: input => MyForm.returnNotification ? new Notification( 'testcode', { message: 'test', type: 'warning' } ) : input, + } ); + form = new MyForm( { model, container: 'findme' } ); + MyForm.returnNotification = true; + } ); + + it( 'returns null', function() { + const actual = model.validate( { foo: 'bar' } ); + expect( actual ).to.be.null; + } ); + + it( 'adds any Notification returned from the sanitize method to form.notifications', function() { + model.validate( { foo: 'bar' } ); + expect( form.notifications.has( 'testcode' ) ).to.be.true; + } ); + + it( 'adds `viaWidgetFormSanitizeReturn` property to any Notification returned from the sanitize method', function() { + model.validate( { foo: 'bar' } ); + expect( form.notifications.value( 'testcode' ).viaWidgetFormSanitizeReturn ).to.be.true; + } ); + + it( 'adds `viaWidgetFormSanitizeReturn` property to any Notification returned from the sanitize method', function() { + model.validate( { foo: 'bar' } ); + expect( form.notifications.value( 'testcode' ).viaWidgetFormSanitizeReturn ).to.be.true; + } ); + + it( 'causes subsequent calls to remove any Notifications from form.notifications which have the `viaWidgetFormSanitizeReturn` property', function() { + model.validate( { foo: 'bar' } ); + MyForm.returnNotification = false; + model.validate( { foo: 'bar' } ); + expect( form.notifications.has( 'testcode' ) ).to.be.false; + } ); + + it( 'does not remove any Notifications from form.notifications which have the `viaWidgetFormSanitizeReturn` property and whose code matches the new Notification', function() { + const notification = new Notification( 'testcode', { message: 'test', type: 'warning' } ); + notification.firstOne = true; + notification.viaWidgetFormSanitizeReturn = true; + form.notifications.add( 'testcode', notification ); + model.validate( { foo: 'bar' } ); + expect( form.notifications.value( 'testcode' ).firstOne ).to.be.true; + } ); + } ); + + describe( 'if the sanitize function returns an Error', function() { + let MyForm, form; + + beforeEach( function() { + MyForm = Form.extend( { + sanitize: input => MyForm.returnError ? new Error( 'test error' ) : input, + } ); + form = new MyForm( { model, container: 'findme' } ); + MyForm.returnError = true; + } ); + + it( 'returns null', function() { + const actual = model.validate( { foo: 'bar' } ); + expect( actual ).to.be.null; + } ); + + it( 'adds a Notification with the code `invalidValue`', function() { + model.validate( { foo: 'bar' } ); + expect( form.notifications.has( 'invalidValue' ) ).to.be.true; + } ); + + it( 'adds a Notification with the type `error`', function() { + model.validate( { foo: 'bar' } ); + expect( form.notifications.value( 'invalidValue' ).type ).to.eql( 'error' ); + } ); + + it( 'adds a Notification with the Error message in the message', function() { + model.validate( { foo: 'bar' } ); + expect( form.notifications.value( 'invalidValue' ).message ).to.eql( 'test error' ); + } ); + } ); + } ); + describe( '.getNotificationsContainerElement()', function() { let form; From 187ac67312fb44cbaf10da853cf09ed517bccd21 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Wed, 25 Jan 2017 19:32:57 -0500 Subject: [PATCH 26/92] Add additional test to verify merging properties for setState() --- tests/js/widget-form.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/js/widget-form.js b/tests/js/widget-form.js index a9407bc..4c9d151 100644 --- a/tests/js/widget-form.js +++ b/tests/js/widget-form.js @@ -414,6 +414,11 @@ describe( 'wp.widgets.Form', function() { expect( model.get().beep ).to.eql( 'boop' ); } ); + it( 'retains the current values on the model if not specifically overwritten', function() { + form.setState( { beep: 'boop' } ); + expect( model.get().hello ).to.eql( 'world' ); + } ); + it( 'does not change the model if no properties are passed', function() { form.setState( {} ); expect( model.get() ).to.eql( { hello: 'world' } ); From e296998838908b5731771929a261a6583f78f88b Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Wed, 25 Jan 2017 19:33:37 -0500 Subject: [PATCH 27/92] Remove test for sanitize adding a title property --- tests/js/widget-form.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/js/widget-form.js b/tests/js/widget-form.js index 4c9d151..b25d1a6 100644 --- a/tests/js/widget-form.js +++ b/tests/js/widget-form.js @@ -355,11 +355,6 @@ describe( 'wp.widgets.Form', function() { expect( actual.foo ).to.eql( 'bar' ); } ); - it( 'returns an object with the `title` property added if it does not exist on the original', function() { - const actual = form.sanitize( { foo: 'bar' }, {} ); - expect( actual ).to.have.property( 'title' ); - } ); - it( 'returns an object with the `title` property trimmed', function() { const actual = form.sanitize( { title: ' hello world ' }, {} ); expect( actual.title ).to.eql( 'hello world' ); From 5d3e100cb8990c274108c4964eddc1c3e744eb26 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Wed, 25 Jan 2017 19:34:57 -0500 Subject: [PATCH 28/92] Remove merging default_instance in validate --- js/widget-form.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/js/widget-form.js b/js/widget-form.js index 53a9c8f..21bd9f4 100644 --- a/js/widget-form.js +++ b/js/widget-form.js @@ -89,10 +89,9 @@ wp.widgets.Form = (function( api, $, _ ) { */ form.model.validate = function validate( value ) { var setting = this, newValue, oldValue, error, code, notification; // eslint-disable-line consistent-this - newValue = _.extend( {}, form.config.default_instance, value ); oldValue = _.extend( {}, setting() ); - newValue = previousValidate.call( setting, newValue ); + newValue = previousValidate.call( setting, value ); newValue = form.sanitize( newValue, oldValue ); if ( newValue instanceof Error ) { @@ -106,7 +105,7 @@ wp.widgets.Form = (function( api, $, _ ) { notification = newValue; } - // If sanitize method returned an error/notification, block setting u0date. + // If sanitize method returned an error/notification, block setting update. if ( notification ) { newValue = null; } From c9cab135ce3f18dc5115fde123b2b2a3c562b041 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Tue, 31 Jan 2017 11:07:04 -0500 Subject: [PATCH 29/92] Remove merging default_instance in sanitize --- js/widget-form.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/widget-form.js b/js/widget-form.js index 21bd9f4..c9ba052 100644 --- a/js/widget-form.js +++ b/js/widget-form.js @@ -204,7 +204,7 @@ wp.widgets.Form = (function( api, $, _ ) { if ( _.isUndefined( oldInstance ) ) { throw new Error( 'Expected oldInstance' ); } - instance = _.extend( {}, form.config.default_instance, newInstance ); + instance = _.extend( {}, newInstance ); if ( ! instance.title ) { instance.title = ''; From ce79ba43f09bf6d8420f89da5aaa2b830ff0d2fb Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Tue, 31 Jan 2017 11:07:50 -0500 Subject: [PATCH 30/92] Remove adding missing title property to model in sanitize This does not seem necessary. --- js/widget-form.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/js/widget-form.js b/js/widget-form.js index c9ba052..c2d3f93 100644 --- a/js/widget-form.js +++ b/js/widget-form.js @@ -206,10 +206,6 @@ wp.widgets.Form = (function( api, $, _ ) { } instance = _.extend( {}, newInstance ); - if ( ! instance.title ) { - instance.title = ''; - } - // Warn about markup in title. code = 'markupTitleInvalid'; if ( /<\/?\w+[^>]*>/.test( instance.title ) ) { @@ -226,7 +222,9 @@ wp.widgets.Form = (function( api, $, _ ) { * Trim per sanitize_text_field(). * Protip: This prevents the widget partial from refreshing after adding a space or adding a new paragraph. */ - instance.title = $.trim( instance.title ); + if ( instance.title ) { + instance.title = $.trim( instance.title ); + } return instance; }, From 3c991865c13cf2160b28723b41f24469cd05e555 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Wed, 25 Jan 2017 19:36:54 -0500 Subject: [PATCH 31/92] Remove merging default_instance in setState() --- js/widget-form.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/widget-form.js b/js/widget-form.js index c2d3f93..ea93fcd 100644 --- a/js/widget-form.js +++ b/js/widget-form.js @@ -255,7 +255,7 @@ wp.widgets.Form = (function( api, $, _ ) { */ setState: function setState( props ) { var form = this, value; - value = _.extend( form.getValue(), props || {} ); + value = _.extend( {}, form.model.get(), props || {} ); form.model.set( value ); }, From 778bc95e1190ffdb87b1f965b5e41a560003df29 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Fri, 27 Jan 2017 12:06:57 -0500 Subject: [PATCH 32/92] Add jsdom to use real jQuery for tests --- package.json | 3 ++ tests/js/widget-form.js | 80 +++++++++++------------------------------ 2 files changed, 24 insertions(+), 59 deletions(-) diff --git a/package.json b/package.json index 5e42228..1a4f677 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,9 @@ "grunt-contrib-watch": "^1.0.0", "grunt-shell": "^1.3.1", "grunt-wp-deploy": "^1.2.1", + "jquery": "^3.1.1", + "jsdom": "^9.9.1", + "jsdom-global": "^2.1.1", "mocha": "^3.2.0", "sinon": "^1.17.7", "sinon-chai": "^2.8.0", diff --git a/tests/js/widget-form.js b/tests/js/widget-form.js index b25d1a6..32c83ba 100644 --- a/tests/js/widget-form.js +++ b/tests/js/widget-form.js @@ -5,51 +5,14 @@ const sinon = require( 'sinon' ); const sinonChai = require( 'sinon-chai' ); const chai = require( 'chai' ); const _ = require( 'underscore' ); +const markup = 'findme'; +require( 'jsdom-global' )( '
' ); +const jQuery = require( 'jquery' ); chai.use( sinonChai ); const expect = chai.expect; -function getMockDom() { - return { - 'findme': true, - 'findit': true, - }; -} - -function mockJQuery() { - const jQuery = ( selector ) => { - const jQueryResult = { - selector: '', - length: 0, - find: jQuery, - is: () => mockDom[ selector ], - html: markup => markup, - each: () => null, - }; - if ( ! mockDom[ selector ] ) { - return jQueryResult; - } - jQueryResult.selector = selector; - jQueryResult.length = 1; - jQueryResult.each = iterator => iterator.call( jQueryResult ); - return jQueryResult; - }; - jQuery.extend = _.extend; - jQuery.each = ( elements, callback ) => { - if ( Array.isArray( elements ) ) { - return elements.map( ( element, index ) => callback( index, element ) ); - } - return Object.keys( elements ).map( key => callback( key, elements[ key ] ) ); - }; - jQuery.trim = str => str.trim(); - jQuery.support = {}; - jQuery.Callbacks = () => ( { fireWith: () => null, add: () => null } ); - jQuery.proxy = _.bind; - return jQuery; -} - function resetGlobals() { - const jQuery = mockJQuery(); global.wp.customize = {}; global.wp.template = () => null; global.jQuery = jQuery; @@ -60,7 +23,6 @@ function resetGlobals() { }; } -let mockDom = getMockDom(); resetGlobals(); const { Value, Notification } = require( './lib/customize-base' ); const Form = require( '../../js/widget-form' ); @@ -69,7 +31,7 @@ describe( 'wp.widgets.Form', function() { let templateSpy; beforeEach( function() { - mockDom = getMockDom(); + jQuery( '.root' ).html( markup ); templateSpy = sinon.stub().returns( () => 'pastries' ); global.wp.template = templateSpy; } ); @@ -80,7 +42,7 @@ describe( 'wp.widgets.Form', function() { beforeEach( function() { model = new Value( 'test' ); - container = 'findme'; + container = '.findme'; } ); it( 'assigns arbitrary properties on the argument object as properties on the form object', function() { @@ -169,9 +131,9 @@ describe( 'wp.widgets.Form', function() { } ); it( 'assigns form.container to a jQuery DOM object for the selector previously in form.container', function() { - const params = { foo: 'bar', model, container: 'findit' }; + const params = { foo: 'bar', model, container: '.findme' }; const form = new Form( params ); - expect( form.container.selector ).to.eql( 'findit' ); + expect( form.container[ 0 ].className ).to.eql( 'findme' ); } ); it( 'throws an error if the form.container selector does not match a DOM node', function( done ) { @@ -200,14 +162,14 @@ describe( 'wp.widgets.Form', function() { } ); it( 'returns an object with the properties of the object passed in', function() { - new Form( { model, container: 'findme' } ); + new Form( { model, container: '.findme' } ); const actual = model.validate( { foo: 'bar' } ); expect( actual.foo ).to.eql( 'bar' ); } ); it( 'also calls the model\'s previous validate method', function() { model.validate = values => Object.assign( {}, values, { red: 'green' } ); - new Form( { model, container: 'findme' } ); + new Form( { model, container: '.findme' } ); const actual = model.validate( { foo: 'bar' } ); expect( actual.red ).to.eql( 'green' ); } ); @@ -235,7 +197,7 @@ describe( 'wp.widgets.Form', function() { MyForm = Form.extend( { sanitize: input => MyForm.returnNotification ? new Notification( 'testcode', { message: 'test', type: 'warning' } ) : input, } ); - form = new MyForm( { model, container: 'findme' } ); + form = new MyForm( { model, container: '.findme' } ); MyForm.returnNotification = true; } ); @@ -283,7 +245,7 @@ describe( 'wp.widgets.Form', function() { MyForm = Form.extend( { sanitize: input => MyForm.returnError ? new Error( 'test error' ) : input, } ); - form = new MyForm( { model, container: 'findme' } ); + form = new MyForm( { model, container: '.findme' } ); MyForm.returnError = true; } ); @@ -314,11 +276,11 @@ describe( 'wp.widgets.Form', function() { beforeEach( function() { const model = new Value( 'test' ); - form = new Form( { model, container: 'findme' } ); + form = new Form( { model, container: '.findme' } ); } ); it( 'returns a jQuery object for the first instance of .js-widget-form-notifications-container in the container', function() { - mockDom[ '.js-widget-form-notifications-container:first' ] = true; + jQuery( '.findme' ).append( 'notifications' ); const actual = form.getNotificationsContainerElement(); expect( actual.length ).to.eql( 1 ); } ); @@ -334,7 +296,7 @@ describe( 'wp.widgets.Form', function() { beforeEach( function() { const model = new Value( 'test' ); - form = new Form( { model, container: 'findme' } ); + form = new Form( { model, container: '.findme' } ); } ); it( 'throws an Error if oldInstance is not set', function( done ) { @@ -382,7 +344,7 @@ describe( 'wp.widgets.Form', function() { beforeEach( function() { const model = new Value( { hello: 'world' } ); - form = new Form( { model, container: 'findme', config: { default_instance: { foo: 'bar' } } } ); + form = new Form( { model, container: '.findme', config: { default_instance: { foo: 'bar' } } } ); } ); it( 'returns an object with the properties of the form\'s config.default_instance', function() { @@ -401,7 +363,7 @@ describe( 'wp.widgets.Form', function() { beforeEach( function() { model = new Value( { hello: 'world' } ); - form = new Form( { model, container: 'findme', config: { default_instance: { foo: 'bar' } } } ); + form = new Form( { model, container: '.findme', config: { default_instance: { foo: 'bar' } } } ); } ); it( 'updates the model by merging its value with the passed object properties', function() { @@ -425,8 +387,8 @@ describe( 'wp.widgets.Form', function() { beforeEach( function() { model = new Value( { hello: 'world' } ); - mockDom[ '#tmpl-my-template' ] = true; - form = new Form( { model, container: 'findme', config: { form_template_id: 'my-template' } } ); + jQuery( '.root' ).append( '' ) + form = new Form( { model, container: '.findme', config: { form_template_id: 'my-template' } } ); } ); it( 'calls wp.template() on the config.form_template_id if no template is cached', function() { @@ -440,7 +402,7 @@ describe( 'wp.widgets.Form', function() { } ); it( 'throws an error if the template does not exist in the DOM', function( done ) { - form = new Form( { model, container: 'findme', config: { form_template_id: 'notfound' } } ); + form = new Form( { model, container: '.findme', config: { form_template_id: 'notfound' } } ); try { form.getTemplate(); } catch( err ) { @@ -467,8 +429,8 @@ describe( 'wp.widgets.Form', function() { beforeEach( function() { model = new Value( { hello: 'world' } ); - mockDom[ '#tmpl-my-template' ] = true; - form = new Form( { model, container: 'findme', config: { form_template_id: 'my-template' } } ); + jQuery( '.root' ).append( '' ) + form = new Form( { model, container: '.findme', config: { form_template_id: 'my-template' } } ); form.container.html = sinon.spy(); } ); From f531a006ff27d26a3726704fbbd5e0b9eaca0623 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Fri, 27 Jan 2017 13:47:52 -0500 Subject: [PATCH 33/92] Add test for two-way binding form and model data --- tests/js/widget-form.js | 37 +++++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/tests/js/widget-form.js b/tests/js/widget-form.js index 32c83ba..62f544c 100644 --- a/tests/js/widget-form.js +++ b/tests/js/widget-form.js @@ -14,7 +14,7 @@ const expect = chai.expect; function resetGlobals() { global.wp.customize = {}; - global.wp.template = () => null; + global.wp.template = id => _.template( jQuery( '#tmpl-' + id ).html(), { evaluate: /<#([\s\S]+?)#>/g, interpolate: /\{\{\{([\s\S]+?)\}\}\}/g, escape: /\{\{([^\}]+?)\}\}(?!\})/g, variable: 'data' } ); global.jQuery = jQuery; global.$ = jQuery; global._ = _; @@ -28,12 +28,9 @@ const { Value, Notification } = require( './lib/customize-base' ); const Form = require( '../../js/widget-form' ); describe( 'wp.widgets.Form', function() { - let templateSpy; - beforeEach( function() { + resetGlobals(); jQuery( '.root' ).html( markup ); - templateSpy = sinon.stub().returns( () => 'pastries' ); - global.wp.template = templateSpy; } ); describe( '.initialize() (constructor)', function() { @@ -383,9 +380,11 @@ describe( 'wp.widgets.Form', function() { } ); describe( '.getTemplate()', function() { - let form, model; + let form, model, templateSpy; beforeEach( function() { + templateSpy = sinon.stub().returns( () => 'pastries' ); + global.wp.template = templateSpy; model = new Value( { hello: 'world' } ); jQuery( '.root' ).append( '' ) form = new Form( { model, container: '.findme', config: { form_template_id: 'my-template' } } ); @@ -425,11 +424,11 @@ describe( 'wp.widgets.Form', function() { } ); describe( '.render()', function() { - let form, model; + let form, model, templateSpy; beforeEach( function() { model = new Value( { hello: 'world' } ); - jQuery( '.root' ).append( '' ) + jQuery( '.root' ).append( '' ) form = new Form( { model, container: '.findme', config: { form_template_id: 'my-template' } } ); form.container.html = sinon.spy(); } ); @@ -447,4 +446,26 @@ describe( 'wp.widgets.Form', function() { expect( interpolateSpy ).to.have.been.calledWith( form ); } ); } ); + + describe( 'when rendered with a form element whose `data-field` attribute matches a model property', function() { + let model; + + beforeEach( function() { + model = new Value( { hello: 'world' } ); + jQuery( '.root' ).append( '' ); + const form = new Form( { model, container: '.findme', config: { form_template_id: 'my-template' } } ); + form.render(); + } ); + + it( 'updates the associated model property when the form element changes', function() { + jQuery( '.hello-field' ).val( 'computer' ); + jQuery( '.hello-field' ).trigger( 'change' ); + expect( model.get().hello ).to.eql( 'computer' ); + } ); + + it( 'updates the form field value when the model property changes', function() { + model.set( { hello: 'computer' } ); + expect( jQuery( '.hello-field' ).val() ).to.eql( 'computer' ); + } ); + } ); } ); From 235cf1d087c18a64590392056d04681729f2a6e0 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Mon, 30 Jan 2017 17:28:10 -0500 Subject: [PATCH 34/92] Simplify the form.model.validate method --- js/widget-form.js | 96 +++++++++++++++++++++++------------------------ 1 file changed, 46 insertions(+), 50 deletions(-) diff --git a/js/widget-form.js b/js/widget-form.js index ea93fcd..cd637d5 100644 --- a/js/widget-form.js +++ b/js/widget-form.js @@ -1,6 +1,6 @@ /* global wp, console, module */ /* eslint-disable strict */ -/* eslint consistent-this: [ "error", "form" ] */ +/* eslint consistent-this: [ "error", "form", "setting" ] */ /* eslint-disable complexity */ if ( ! wp.widgets ) { @@ -13,6 +13,49 @@ if ( ! wp.widgets.formConstructor ) { wp.widgets.Form = (function( api, $, _ ) { 'use strict'; + function removeSanitizeNotifications( notifications ) { + notifications.each( function iterateNotifications( notification ) { + if ( notification.viaWidgetFormSanitizeReturn ) { + notifications.remove( notification.code ); + } + } ); + } + + function addSanitizeNotification( widgetForm, notification ) { + notification.viaWidgetFormSanitizeReturn = true; + widgetForm.notifications.add( notification.code, notification ); + } + + function getValidateWidget( widgetForm, previousValidate ) { + /** + * Validate the instance data. + * + * @todo In order for returning an error/notification to work properly, api._handleSettingValidities needs to only remove notification errors that are no longer valid which are fromServer: + * + * @param {object} value Instance value. + * @returns {object|Error|wp.customize.Notification} Sanitized instance value or error/notification. + */ + return function validate( value ) { + var setting = this, newValue, oldValue; + oldValue = setting(); + newValue = widgetForm.sanitize( previousValidate.call( setting, value ), oldValue ); + + // If sanitize method returned an error/notification, block setting update and add a notification + if ( newValue instanceof Error ) { + newValue = new api.Notification( 'invalidValue', { message: newValue.message, type: 'error' } ); + } + if ( newValue instanceof api.Notification ) { + addSanitizeNotification( widgetForm, newValue ); + return null; + } + + // Remove all existing notifications added via sanitization since only one can be returned. + removeSanitizeNotifications( widgetForm.notifications ); + + return newValue; + } + } + /** * Customize Widget Form. * @@ -38,7 +81,7 @@ wp.widgets.Form = (function( api, $, _ ) { * @return {void} */ initialize: function initialize( properties ) { - var form = this, args, previousValidate, defaultConfig, defaultProperties, config; + var form = this, args, defaultConfig, defaultProperties, config; defaultConfig = { form_template_id: '', @@ -77,54 +120,7 @@ wp.widgets.Form = (function( api, $, _ ) { throw new Error( 'Missing container property as Element or jQuery.' ); } - previousValidate = form.model.validate; - - /** - * Validate the instance data. - * - * @todo In order for returning an error/notification to work properly, api._handleSettingValidities needs to only remove notification errors that are no longer valid which are fromServer: - * - * @param {object} value Instance value. - * @returns {object|Error|wp.customize.Notification} Sanitized instance value or error/notification. - */ - form.model.validate = function validate( value ) { - var setting = this, newValue, oldValue, error, code, notification; // eslint-disable-line consistent-this - oldValue = _.extend( {}, setting() ); - - newValue = previousValidate.call( setting, value ); - - newValue = form.sanitize( newValue, oldValue ); - if ( newValue instanceof Error ) { - error = newValue; - code = 'invalidValue'; - notification = new api.Notification( code, { - message: error.message, - type: 'error' - } ); - } else if ( newValue instanceof api.Notification ) { - notification = newValue; - } - - // If sanitize method returned an error/notification, block setting update. - if ( notification ) { - newValue = null; - } - - // Remove all existing notifications added via sanitization since only one can be returned. - form.notifications.each( function iterateNotifications( iteratedNotification ) { - if ( iteratedNotification.viaWidgetFormSanitizeReturn && ( ! notification || notification.code !== iteratedNotification.code ) ) { - form.notifications.remove( iteratedNotification.code ); - } - } ); - - // Add the new notification. - if ( notification ) { - notification.viaWidgetFormSanitizeReturn = true; - form.notifications.add( notification.code, notification ); - } - - return newValue; - }; + form.model.validate = getValidateWidget( form, form.model.validate ); }, /** From b19fba3812ea5ba0d9467120a59a791147215e9c Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Mon, 30 Jan 2017 17:53:10 -0500 Subject: [PATCH 35/92] Simplify Form constructor --- js/widget-form.js | 69 ++++++++++++++++++++++++----------------------- 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/js/widget-form.js b/js/widget-form.js index cd637d5..b0f27e6 100644 --- a/js/widget-form.js +++ b/js/widget-form.js @@ -56,6 +56,35 @@ wp.widgets.Form = (function( api, $, _ ) { } } + function validateForm( widgetForm ) { + if ( ! widgetForm.model || ! widgetForm.model.extended || ! widgetForm.model.extended( api.Value ) ) { + throw new Error( 'Widget Form is missing model property which must be a Value or Setting instance.' ); + } + if ( 0 === widgetForm.container.length ) { + throw new Error( 'Widget Form is missing container property as Element or jQuery.' ); + } + } + + function getValidatedFormProperties( config, properties ) { + var defaultConfig = { + form_template_id: '', + notifications_template_id: '', + l10n: {}, + default_instance: {} + }; + + var defaultProperties = { + model: null, + container: null, + config: {}, + }; + + var args = _.extend( {}, defaultProperties, properties || {} ); + var propertiesConfig = properties ? properties.config : {}; + args.config = _.extend( {}, defaultConfig, config, propertiesConfig || {} ); + return args; + } + /** * Customize Widget Form. * @@ -81,44 +110,16 @@ wp.widgets.Form = (function( api, $, _ ) { * @return {void} */ initialize: function initialize( properties ) { - var form = this, args, defaultConfig, defaultProperties, config; - - defaultConfig = { - form_template_id: '', - notifications_template_id: '', - l10n: {}, - default_instance: {} - }; - - defaultProperties = { - model: null, - container: null, - config: {}, - }; - - config = properties ? properties.config : {}; - - args = _.extend( {}, defaultProperties, properties || {} ); - args.config = _.extend( {}, defaultConfig, form.config, config || {} ); - - if ( ! args.model || ! args.model.extended || ! args.model.extended( api.Value ) ) { - throw new Error( 'Missing model property which must be a Value or Setting instance.' ); - } + var form = this; - _.extend( form, args ); - form.setting = args.model; // @todo Deprecate 'setting' name in favor of 'model'? + _.extend( form, getValidatedFormProperties( form.config, properties ) ); - if ( form.model.notifications ) { - form.notifications = form.model.notifications; - } else { - form.notifications = new api.Values({ defaultConstructor: api.Notification }); - } + form.setting = form.model; // @todo Deprecate 'setting' name in favor of 'model'? + form.notifications = form.model.notifications || new api.Values( { defaultConstructor: api.Notification } ); form.renderNotifications = _.bind( form.renderNotifications, form ); - form.container = $( form.container ); - if ( 0 === form.container.length ) { - throw new Error( 'Missing container property as Element or jQuery.' ); - } + + validateForm( form ); form.model.validate = getValidateWidget( form, form.model.validate ); }, From 25b27ae11191bbcce6fa06326811977296e117fb Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Tue, 31 Jan 2017 13:01:43 -0500 Subject: [PATCH 36/92] Add Form.renderNotificationsToContainer to allow testing Debounced functions are async by nature and harder to test. This rewrites `renderNotifications` as a debounced proxy for `renderNotificationsToContainer`. --- js/widget-form.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/js/widget-form.js b/js/widget-form.js index b0f27e6..c030daa 100644 --- a/js/widget-form.js +++ b/js/widget-form.js @@ -138,6 +138,10 @@ wp.widgets.Form = (function( api, $, _ ) { * @returns {void} */ renderNotifications: _.debounce( function renderNotifications() { + this.renderNotificationsToContainer(); + } ), + + renderNotificationsToContainer: function renderNotificationsToContainer() { var form = this, container, notifications, hasError = false; container = form.getNotificationsContainerElement(); if ( ! container || ! container.length ) { @@ -175,7 +179,7 @@ wp.widgets.Form = (function( api, $, _ ) { container.empty().append( $.trim( form._notificationsTemplate( { notifications: notifications, altNotice: Boolean( form.altNotice ) } ) ) ); - } ), + }, /** * Get the element inside of a form's container that contains the notifications. From 7f3d014c27d4b5c6ec1e494ed4f00dc9f1ecc551 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Tue, 31 Jan 2017 13:10:38 -0500 Subject: [PATCH 37/92] Add tests for renderNotificationsToContainer --- tests/js/widget-form.js | 119 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/tests/js/widget-form.js b/tests/js/widget-form.js index 62f544c..6698990 100644 --- a/tests/js/widget-form.js +++ b/tests/js/widget-form.js @@ -14,6 +14,7 @@ const expect = chai.expect; function resetGlobals() { global.wp.customize = {}; + global.wp.a11y = { speak: () => null }; global.wp.template = id => _.template( jQuery( '#tmpl-' + id ).html(), { evaluate: /<#([\s\S]+?)#>/g, interpolate: /\{\{\{([\s\S]+?)\}\}\}/g, escape: /\{\{([^\}]+?)\}\}(?!\})/g, variable: 'data' } ); global.jQuery = jQuery; global.$ = jQuery; @@ -423,6 +424,124 @@ describe( 'wp.widgets.Form', function() { } ); } ); + describe( '.renderNotificationsToContainer()', function() { + let form, model; + + beforeEach( function() { + model = new Value( { hello: 'world' } ); + jQuery( '.root' ).append( '' ) + jQuery( '.root' ).append( '' ) + jQuery( '.findme' ).append( '' ); + form = new Form( { model, container: '.findme', config: { form_template_id: 'my-template', notifications_template_id: 'my-notifications' } } ); + } ); + + it( 'throws an error if notifications_template_id is undefined', function( done ) { + form = new Form( { model, container: '.findme', config: { form_template_id: 'my-template' } } ); + try { + form.renderNotificationsToContainer(); + } catch( err ) { + done(); + } + throw new Error( 'Expected renderNotificationsToContainer to throw an Error but it did not' ); + } ); + + it( 'does not throw any errors if getNotificationsContainerElement returns null', function() { + form.getNotificationsContainerElement = () => null; + expect( form.renderNotificationsToContainer() ).to.be.empty; + } ); + + describe( 'when form.notifications is empty', function() { + it( 'hides the notifications area', function() { + const notificationsContainer = jQuery( '.js-widget-form-notifications-container' ); + notificationsContainer.slideUp = sinon.spy(); + form.getNotificationsContainerElement = () => notificationsContainer; + form.renderNotificationsToContainer(); + expect( notificationsContainer.slideUp ).to.have.been.called; + } ); + + it( 'removes the `has-notifications` class on form.container', function() { + jQuery( '.findme' ).addClass( 'has-notifications' ); + form.renderNotificationsToContainer(); + expect( jQuery( '.findme' ).hasClass( 'has-notifications' ) ).to.be.false; + } ); + + it( 'removes the `has-error` class on form.container', function() { + jQuery( '.findme' ).addClass( 'has-error' ); + form.renderNotificationsToContainer(); + expect( jQuery( '.findme' ).hasClass( 'has-error' ) ).to.be.false; + } ); + + it( 'replaces the content of the notifications area with the contents of the notifications template', function() { + jQuery( '.js-widget-form-notifications-container' ).text( 'nothing here' ); + form.renderNotificationsToContainer(); + expect( jQuery( '.js-widget-form-notifications-container' ).text() ).to.eql( 'notifications template' ); + } ); + + it( 'replaces the content of the notifications area with the trimmed contents of the notifications template', function() { + jQuery( '#tmpl-my-notifications' ).html( ' notifications template ' ) + jQuery( '.js-widget-form-notifications-container' ).text( 'nothing here' ); + form.renderNotificationsToContainer(); + expect( jQuery( '.js-widget-form-notifications-container' ).text() ).to.eql( 'notifications template' ); + } ); + + it( 'does not call wp.a11y.speak', function() { + wp.a11y.speak = sinon.spy(); + form.renderNotificationsToContainer(); + expect( wp.a11y.speak ).to.not.have.been.called; + } ); + } ); + + describe( 'when there are notifications', function() { + let myNotification; + + beforeEach( function() { + myNotification = new Notification( 'other-notification', { message: 'test', type: 'warning' } ); + form.notifications.add( 'other-notification', myNotification ); + } ); + + it( 'shows the notifications area', function() { + const notificationsContainer = jQuery( '.js-widget-form-notifications-container' ); + notificationsContainer.slideDown = sinon.spy(); + form.getNotificationsContainerElement = () => notificationsContainer; + form.renderNotificationsToContainer(); + expect( notificationsContainer.slideDown ).to.have.been.called; + } ); + + it( 'adds the `has-notifications` class on form.container', function() { + jQuery( '.findme' ).removeClass( 'has-notifications' ); + form.renderNotificationsToContainer(); + expect( jQuery( '.findme' ).hasClass( 'has-notifications' ) ).to.be.true; + } ); + + it( 'adds the `has-error` class on form.container if a notification is an error', function() { + jQuery( '.findme' ).removeClass( 'has-error' ); + form.notifications.add( 'error-notification', new Notification( 'error-notification', { message: 'test', type: 'error' } ) ); + form.renderNotificationsToContainer(); + expect( jQuery( '.findme' ).hasClass( 'has-error' ) ).to.be.true; + } ); + + it( 'removes the `has-error` class on form.container if no notifications are errors', function() { + jQuery( '.findme' ).addClass( 'has-error' ); + form.renderNotificationsToContainer(); + expect( jQuery( '.findme' ).hasClass( 'has-error' ) ).to.be.false; + } ); + + it( 'calls wp.a11y.speak for each notification', function() { + wp.a11y.speak = sinon.spy(); + form.renderNotificationsToContainer(); + expect( wp.a11y.speak ).to.have.been.calledWith( 'test', 'assertive' ); + } ); + + it( 'calls the notifications template function with the notifications as interpolated values', function() { + const interpolateSpy = sinon.spy(); + const templateSpy = sinon.stub().returns( interpolateSpy ); + global.wp.template = templateSpy; + form.renderNotificationsToContainer(); + expect( interpolateSpy ).to.have.been.calledWith( { notifications: [ myNotification ], altNotice: false } ); + } ); + } ); + } ); + describe( '.render()', function() { let form, model, templateSpy; From ff9ff0930ce9560b6767b2e8a3aa4c8d8e3f7e12 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Tue, 31 Jan 2017 15:37:32 -0500 Subject: [PATCH 38/92] Update validate tests to prove notifications are always removed --- tests/js/widget-form.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/js/widget-form.js b/tests/js/widget-form.js index 6698990..db7fbf9 100644 --- a/tests/js/widget-form.js +++ b/tests/js/widget-form.js @@ -226,13 +226,13 @@ describe( 'wp.widgets.Form', function() { expect( form.notifications.has( 'testcode' ) ).to.be.false; } ); - it( 'does not remove any Notifications from form.notifications which have the `viaWidgetFormSanitizeReturn` property and whose code matches the new Notification', function() { + it( 'removes any Notifications from form.notifications which have the `viaWidgetFormSanitizeReturn` property and whose code matches the new Notification', function() { const notification = new Notification( 'testcode', { message: 'test', type: 'warning' } ); notification.firstOne = true; notification.viaWidgetFormSanitizeReturn = true; form.notifications.add( 'testcode', notification ); model.validate( { foo: 'bar' } ); - expect( form.notifications.value( 'testcode' ).firstOne ).to.be.true; + expect( form.notifications.value( 'testcode' ) ).to.not.have.property( 'firstOne' ); } ); } ); From 4dc63498a0087f8798b0996f81660f405a8829e5 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Tue, 31 Jan 2017 15:37:48 -0500 Subject: [PATCH 39/92] Always remove all sanitize-added notifications in validate --- js/widget-form.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/js/widget-form.js b/js/widget-form.js index c030daa..821753e 100644 --- a/js/widget-form.js +++ b/js/widget-form.js @@ -40,6 +40,9 @@ wp.widgets.Form = (function( api, $, _ ) { oldValue = setting(); newValue = widgetForm.sanitize( previousValidate.call( setting, value ), oldValue ); + // Remove all existing notifications added via sanitization since only one can be returned. + removeSanitizeNotifications( widgetForm.notifications ); + // If sanitize method returned an error/notification, block setting update and add a notification if ( newValue instanceof Error ) { newValue = new api.Notification( 'invalidValue', { message: newValue.message, type: 'error' } ); @@ -49,9 +52,6 @@ wp.widgets.Form = (function( api, $, _ ) { return null; } - // Remove all existing notifications added via sanitization since only one can be returned. - removeSanitizeNotifications( widgetForm.notifications ); - return newValue; } } From ce7b650266e7b647891f30d967b55f8a89ad9072 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Tue, 31 Jan 2017 15:50:29 -0500 Subject: [PATCH 40/92] Add missing semicolon to validate function --- js/widget-form.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/widget-form.js b/js/widget-form.js index 821753e..fef2da1 100644 --- a/js/widget-form.js +++ b/js/widget-form.js @@ -53,7 +53,7 @@ wp.widgets.Form = (function( api, $, _ ) { } return newValue; - } + }; } function validateForm( widgetForm ) { From 19f752a6b6449b4d02b75f02018ef2b18c5ca9b1 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Tue, 31 Jan 2017 15:51:30 -0500 Subject: [PATCH 41/92] Ignore js tests in jshintignore --- .jshintignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.jshintignore b/.jshintignore index b84ca0d..0260fcb 100644 --- a/.jshintignore +++ b/.jshintignore @@ -3,3 +3,4 @@ **/vendor/** **/*.jsx /bower_components/** +tests/js/** From 5976cc778cddf87403ad280fa406b60214db48a7 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Tue, 31 Jan 2017 16:11:12 -0500 Subject: [PATCH 42/92] Add `npm test` to travis config --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 39a9473..c16692a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,9 +18,11 @@ install: - export DEV_LIB_PATH=dev-lib - if [ ! -e "$DEV_LIB_PATH" ] && [ -L .travis.yml ]; then export DEV_LIB_PATH=$( dirname $( readlink .travis.yml ) ); fi - if [ ! -e "$DEV_LIB_PATH" ]; then git clone https://github.com/xwp/wp-dev-lib.git $DEV_LIB_PATH; fi + - npm install - source $DEV_LIB_PATH/travis.install.sh script: + - npm test - source $DEV_LIB_PATH/travis.script.sh after_script: From cad5b6a198c18d1d6e6800988166fde288c47082 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Tue, 31 Jan 2017 16:13:42 -0500 Subject: [PATCH 43/92] Remove trailing comma to placate jshint --- js/widget-form.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/widget-form.js b/js/widget-form.js index fef2da1..344e1e3 100644 --- a/js/widget-form.js +++ b/js/widget-form.js @@ -76,7 +76,7 @@ wp.widgets.Form = (function( api, $, _ ) { var defaultProperties = { model: null, container: null, - config: {}, + config: {} }; var args = _.extend( {}, defaultProperties, properties || {} ); From cb7bf731ea04708fd9d9210eeb20233088d08a81 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 31 Jan 2017 13:18:28 -0800 Subject: [PATCH 44/92] Add root slash for .jshintignore --- .jshintignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.jshintignore b/.jshintignore index 0260fcb..6a041e1 100644 --- a/.jshintignore +++ b/.jshintignore @@ -3,4 +3,4 @@ **/vendor/** **/*.jsx /bower_components/** -tests/js/** +/tests/js/** From 7be56412eb4f98ea5803d44c3ef44261a6927e41 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Tue, 31 Jan 2017 16:54:29 -0500 Subject: [PATCH 45/92] Add tests dir to jscsrc ignore --- .jscsrc | 1 + 1 file changed, 1 insertion(+) diff --git a/.jscsrc b/.jscsrc index 29ae93e..55aab8a 100644 --- a/.jscsrc +++ b/.jscsrc @@ -5,6 +5,7 @@ "**/*.jsx", "**/node_modules/**", "**/vendor/**", + "**/tests/**", "bower_components/**" ] } From 1b08fdfa78e4aa1498e7fa2b2f4cab3a42d97337 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Tue, 31 Jan 2017 17:01:47 -0500 Subject: [PATCH 46/92] Add tests/js/lib to eslintignore --- .eslintignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.eslintignore b/.eslintignore index 6b5adc8..0cbe75b 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,5 +1,6 @@ **/*.min.js **/node_modules/** **/vendor/** +/tests/js/lib/** **/*.browserified.js /bower_components/** From 6361e7c41fb1ddb85a8d80f9d01f006ff8812eb9 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Tue, 31 Jan 2017 17:58:22 -0500 Subject: [PATCH 47/92] Remove duplicate "npm install" --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index c16692a..999efab 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,7 +18,6 @@ install: - export DEV_LIB_PATH=dev-lib - if [ ! -e "$DEV_LIB_PATH" ] && [ -L .travis.yml ]; then export DEV_LIB_PATH=$( dirname $( readlink .travis.yml ) ); fi - if [ ! -e "$DEV_LIB_PATH" ]; then git clone https://github.com/xwp/wp-dev-lib.git $DEV_LIB_PATH; fi - - npm install - source $DEV_LIB_PATH/travis.install.sh script: From 5bbceca2d9a2f008e983e560394520e30f62a937 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Wed, 1 Feb 2017 19:57:30 -0500 Subject: [PATCH 48/92] Simplify renderNotificationsToContainer --- js/widget-form.js | 85 +++++++++++++++++++++++++++++++---------------- 1 file changed, 56 insertions(+), 29 deletions(-) diff --git a/js/widget-form.js b/js/widget-form.js index 344e1e3..c86b18e 100644 --- a/js/widget-form.js +++ b/js/widget-form.js @@ -13,6 +13,50 @@ if ( ! wp.widgets.formConstructor ) { wp.widgets.Form = (function( api, $, _ ) { 'use strict'; + function getArrayFromValues( values ) { + var ary = []; + values.each( function( value ) { + ary.push( value ); + } ); + return ary; + } + + function isNotificationError( notification ) { + return 'error' === notification.type; + } + + function toggleContainer( container, showContainer ) { + var deferred = $.Deferred(); + if ( showContainer ) { + container.stop().slideDown( 'fast', null, deferred.resolve ); + } else { + container.stop().slideUp( 'fast', null, deferred.resolve ); + } + return deferred; + } + + function speakNotification( notification ) { + if ( ! notification.hasA11ySpoken ) { + + // @todo In the context of the Customizer, this presently will end up getting spoken twice due to wp.customize.Control also rendering it. + wp.a11y.speak( notification.message, 'assertive' ); + notification.hasA11ySpoken = true; + } + } + + function getNotificationsTemplate( widgetForm ) { + if ( ! widgetForm._notificationsTemplate ) { + widgetForm._notificationsTemplate = wp.template( widgetForm.config.notifications_template_id ); + } + return widgetForm._notificationsTemplate; + } + + function renderNotificationsTemplate( container, templateFunction, notifications, altNotice ) { + container.empty().append( $.trim( + templateFunction( { notifications: notifications, altNotice: Boolean( altNotice ) } ) + ) ); + } + function removeSanitizeNotifications( notifications ) { notifications.each( function iterateNotifications( notification ) { if ( notification.viaWidgetFormSanitizeReturn ) { @@ -142,43 +186,26 @@ wp.widgets.Form = (function( api, $, _ ) { } ), renderNotificationsToContainer: function renderNotificationsToContainer() { - var form = this, container, notifications, hasError = false; + var form = this, container, notifications, templateFunction; container = form.getNotificationsContainerElement(); if ( ! container || ! container.length ) { return; } - notifications = []; - form.notifications.each( function( notification ) { - notifications.push( notification ); - if ( 'error' === notification.type ) { - hasError = true; - } - - if ( ! notification.hasA11ySpoken ) { + notifications = getArrayFromValues( form.notifications ); - // @todo In the context of the Customizer, this presently will end up getting spoken twice due to wp.customize.Control also rendering it. - wp.a11y.speak( notification.message, 'assertive' ); - notification.hasA11ySpoken = true; - } - } ); - - if ( 0 === notifications.length ) { - container.stop().slideUp( 'fast' ); - } else { - container.stop().slideDown( 'fast', null, function() { - $( this ).css( 'height', 'auto' ); + toggleContainer( container, notifications.length > 0 ) + .then( function() { + if ( notifications.length > 0 ) { + $( form ).css( 'height', 'auto' ); + } } ); - } + form.container.toggleClass( 'has-error', notifications.filter( isNotificationError ).length > 0 ); + form.container.toggleClass( 'has-notifications', notifications.length > 0 ); - if ( ! form._notificationsTemplate ) { - form._notificationsTemplate = wp.template( form.config.notifications_template_id ); - } + notifications.map( speakNotification ); - form.container.toggleClass( 'has-notifications', 0 !== notifications.length ); - form.container.toggleClass( 'has-error', hasError ); - container.empty().append( $.trim( - form._notificationsTemplate( { notifications: notifications, altNotice: Boolean( form.altNotice ) } ) - ) ); + templateFunction = getNotificationsTemplate( form ); + renderNotificationsTemplate( container, templateFunction, notifications, form.altNotice ); }, /** From bafd1b907792336b1e5dd348e306dd91fd4cec6e Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Thu, 2 Feb 2017 11:47:24 -0500 Subject: [PATCH 49/92] Add jsdoc to each helper method --- js/widget-form.js | 85 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/js/widget-form.js b/js/widget-form.js index c86b18e..52d6e30 100644 --- a/js/widget-form.js +++ b/js/widget-form.js @@ -13,6 +13,12 @@ if ( ! wp.widgets.formConstructor ) { wp.widgets.Form = (function( api, $, _ ) { 'use strict'; + /** + * Return an Array of an api.Values object's values + * + * @param {wp.customize.Values} values An instance of api.Values + * @return {Array} An array of api.Value objects + */ function getArrayFromValues( values ) { var ary = []; values.each( function( value ) { @@ -21,10 +27,23 @@ wp.widgets.Form = (function( api, $, _ ) { return ary; } + /** + * Return true if the Notification is an error + * + * @param {wp.customize.Notification} notification An instance of api.Notification + * @return {Boolean} True if the `type` of the Notification is 'error' + */ function isNotificationError( notification ) { return 'error' === notification.type; } + /** + * Hide or show a DOM node using jQuery animation + * + * @param {jQuery} container The jQuery object to hide or show + * @param {Boolean} showContainer True to show the node, or false to hide it + * @return {Deferred} A promise that is resolved when the animation is complete + */ function toggleContainer( container, showContainer ) { var deferred = $.Deferred(); if ( showContainer ) { @@ -35,6 +54,14 @@ wp.widgets.Form = (function( api, $, _ ) { return deferred; } + /** + * Speak a Notification using wp.a11y + * + * Will only speak a Notification once, so if passed a Notification that has already been spoken, this is a noop. + * + * @param {Notification} notification The Notification to speak + * @return {void} + */ function speakNotification( notification ) { if ( ! notification.hasA11ySpoken ) { @@ -44,6 +71,12 @@ wp.widgets.Form = (function( api, $, _ ) { } } + /** + * Return the template function for rendering Notifications + * + * @param {Form} widgetForm The instance of the Form whose template to fetch + * @return {Function} The template function + */ function getNotificationsTemplate( widgetForm ) { if ( ! widgetForm._notificationsTemplate ) { widgetForm._notificationsTemplate = wp.template( widgetForm.config.notifications_template_id ); @@ -51,12 +84,31 @@ wp.widgets.Form = (function( api, $, _ ) { return widgetForm._notificationsTemplate; } + /** + * Apply and render a Notifications area template function + * + * @todo: simplify these arguments + * + * @param {jQuery} container The DOM node which will be replaced by the template + * @param {Function} templateFunction The template function to use + * @param {wp.customize.Values} notifications An instance of api.Values to pass to the template function + * @param {Boolean} altNotice An argument to pass to the altNotice param of the template function + * @return {void} + */ function renderNotificationsTemplate( container, templateFunction, notifications, altNotice ) { container.empty().append( $.trim( templateFunction( { notifications: notifications, altNotice: Boolean( altNotice ) } ) ) ); } + /** + * Removes Notification objects which have been added by `addSanitizeNotification` + * + * Note: this mutates the object itself! + * + * @param {wp.customize.Values} notifications An instance of api.Values containing Notification objects + * @return {void} + */ function removeSanitizeNotifications( notifications ) { notifications.each( function iterateNotifications( notification ) { if ( notification.viaWidgetFormSanitizeReturn ) { @@ -65,11 +117,27 @@ wp.widgets.Form = (function( api, $, _ ) { } ); } + /** + * Adds a Notification to a Form from the form's `sanitize` method + * + * @param {Form} widgetForm The instance of the Form to modify + * @param {wp.customize.Values} notification An instance of api.Notification to add + * @return {void} + */ function addSanitizeNotification( widgetForm, notification ) { notification.viaWidgetFormSanitizeReturn = true; widgetForm.notifications.add( notification.code, notification ); } + /** + * Return a new validation function for a Form's model + * + * The new method will call the old method first. + * + * @param {Form} widgetForm The instance of the Form to modify + * @param {Function} previousValidate The old version of the model's validate method + * @return {Function} The new validate method + */ function getValidateWidget( widgetForm, previousValidate ) { /** * Validate the instance data. @@ -100,6 +168,14 @@ wp.widgets.Form = (function( api, $, _ ) { }; } + /** + * Validate the properties of a Form + * + * Throws an Error if the properties are invalid. + * + * @param {Form} widgetForm The instance of the Form to modify + * @return {void} + */ function validateForm( widgetForm ) { if ( ! widgetForm.model || ! widgetForm.model.extended || ! widgetForm.model.extended( api.Value ) ) { throw new Error( 'Widget Form is missing model property which must be a Value or Setting instance.' ); @@ -109,6 +185,15 @@ wp.widgets.Form = (function( api, $, _ ) { } } + /** + * Merges properties for a Form with the defaults + * + * The passed properties override the Form's config property which overrides the default values. + * + * @param {object} config The Form's current config property + * @param {object} properties The passed-in properties to the Form + * @return {object} The merged properties object + */ function getValidatedFormProperties( config, properties ) { var defaultConfig = { form_template_id: '', From c0e778e0bd28c06f08e81772d465f00f82d07ead Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Thu, 2 Feb 2017 11:51:49 -0500 Subject: [PATCH 50/92] Rename renderNotificationsTemplate to renderMarkupToContainer The previous helper had too many arguments --- js/widget-form.js | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/js/widget-form.js b/js/widget-form.js index 52d6e30..27a29ea 100644 --- a/js/widget-form.js +++ b/js/widget-form.js @@ -85,20 +85,14 @@ wp.widgets.Form = (function( api, $, _ ) { } /** - * Apply and render a Notifications area template function + * Replace the markup of a DOM node container * - * @todo: simplify these arguments - * - * @param {jQuery} container The DOM node which will be replaced by the template - * @param {Function} templateFunction The template function to use - * @param {wp.customize.Values} notifications An instance of api.Values to pass to the template function - * @param {Boolean} altNotice An argument to pass to the altNotice param of the template function + * @param {jQuery} container The DOM node which will be replaced by the markup + * @param {string} markup The markup to apply to the container * @return {void} */ - function renderNotificationsTemplate( container, templateFunction, notifications, altNotice ) { - container.empty().append( $.trim( - templateFunction( { notifications: notifications, altNotice: Boolean( altNotice ) } ) - ) ); + function renderMarkupToContainer( container, markup ) { + container.empty().append( $.trim( markup ) ); } /** @@ -290,7 +284,7 @@ wp.widgets.Form = (function( api, $, _ ) { notifications.map( speakNotification ); templateFunction = getNotificationsTemplate( form ); - renderNotificationsTemplate( container, templateFunction, notifications, form.altNotice ); + renderMarkupToContainer( container, templateFunction( { notifications: notifications, altNotice: Boolean( form.altNotice ) } ) ); }, /** From 0602b2f21b210452669a89de85a90090116ee26c Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Fri, 3 Feb 2017 19:40:42 -0500 Subject: [PATCH 51/92] Change tests so only model and container can be Form arguments --- tests/js/widget-form.js | 73 +++++++++++++++++++++++------------------ 1 file changed, 41 insertions(+), 32 deletions(-) diff --git a/tests/js/widget-form.js b/tests/js/widget-form.js index db7fbf9..a8b72c1 100644 --- a/tests/js/widget-form.js +++ b/tests/js/widget-form.js @@ -43,14 +43,14 @@ describe( 'wp.widgets.Form', function() { container = '.findme'; } ); - it( 'assigns arbitrary properties on the argument object as properties on the form object', function() { + it( 'ignores arbitrary properties on the argument object', function() { const params = { foo: 'bar', model, container }; const form = new Form( params ); - expect( form.foo ).to.eql( 'bar' ); + expect( form.foo ).to.not.exist; } ); it( 'throws an error if the model property is missing', function( done ) { - const params = { foo: 'bar', container }; + const params = { container }; try { new Form( params ); } catch ( err ) { @@ -60,7 +60,7 @@ describe( 'wp.widgets.Form', function() { } ); it( 'throws an error if the model property is not an instance of Value', function( done ) { - const params = { foo: 'bar', model: { hello: 'world' }, container }; + const params = { model: { hello: 'world' }, container }; try { new Form( params ); } catch ( err ) { @@ -70,7 +70,7 @@ describe( 'wp.widgets.Form', function() { } ); it( 'assigns a default config property if it has not already been set on the form object', function() { - const params = { foo: 'bar', model, container }; + const params = { model, container }; const form = new Form( params ); const expected = { form_template_id: '', @@ -85,7 +85,7 @@ describe( 'wp.widgets.Form', function() { const MyForm = Form.extend( { config: { subclass: 'yes' }, } ); - const params = { foo: 'bar', model, container }; + const params = { model, container }; const form = new MyForm( params ); const expected = { form_template_id: '', @@ -97,45 +97,33 @@ describe( 'wp.widgets.Form', function() { expect( form.config ).to.eql( expected ); } ); - it( 'allows overriding the default property values with the properties of the argument', function() { - const params = { foo: 'bar', model, config: { form_template_id: 'hello' }, container }; - const form = new Form( params ); - const expected = { - form_template_id: 'hello', - notifications_template_id: '', - l10n: {}, - default_instance: {}, - }; - expect( form.config ).to.eql( expected ); - } ); - it( 'assigns form.setting as an alias to form.model', function() { - const params = { foo: 'bar', model, container }; + const params = { model, container }; const form = new Form( params ); expect( form.setting ).to.equal( model ); } ); it( 'assigns form.notifications as an alias for form.model.notifications if it exists', function() { model.notifications = { hello: 'world' }; - const params = { foo: 'bar', model, container }; + const params = { model, container }; const form = new Form( params ); expect( form.notifications ).to.equal( model.notifications ); } ); it( 'assigns form.notifications to a new Values with the Notification defaultConstructor if form.model.notifications does not exist', function() { - const params = { foo: 'bar', model, container }; + const params = { model, container }; const form = new Form( params ); expect( form.notifications.defaultConstructor ).to.equal( Notification ); } ); it( 'assigns form.container to a jQuery DOM object for the selector previously in form.container', function() { - const params = { foo: 'bar', model, container: '.findme' }; + const params = { model, container: '.findme' }; const form = new Form( params ); expect( form.container[ 0 ].className ).to.eql( 'findme' ); } ); it( 'throws an error if the form.container selector does not match a DOM node', function( done ) { - const params = { foo: 'bar', model, container: 'notfound' }; + const params = { model, container: 'notfound' }; try { new Form( params ); } catch( err ) { @@ -145,7 +133,7 @@ describe( 'wp.widgets.Form', function() { } ); it( 'mutates the model to override the `validate` method', function() { - const params = { foo: 'bar', model, container }; + const params = { model, container }; const previousValidate = model.validate; new Form( params ); expect( model.validate ).to.not.equal( previousValidate ); @@ -342,7 +330,10 @@ describe( 'wp.widgets.Form', function() { beforeEach( function() { const model = new Value( { hello: 'world' } ); - form = new Form( { model, container: '.findme', config: { default_instance: { foo: 'bar' } } } ); + const MyForm = Form.extend( { + config: { default_instance: { foo: 'bar' } }, + } ); + form = new MyForm( { model, container: '.findme' } ); } ); it( 'returns an object with the properties of the form\'s config.default_instance', function() { @@ -361,7 +352,7 @@ describe( 'wp.widgets.Form', function() { beforeEach( function() { model = new Value( { hello: 'world' } ); - form = new Form( { model, container: '.findme', config: { default_instance: { foo: 'bar' } } } ); + form = new Form( { model, container: '.findme' } ); } ); it( 'updates the model by merging its value with the passed object properties', function() { @@ -388,7 +379,10 @@ describe( 'wp.widgets.Form', function() { global.wp.template = templateSpy; model = new Value( { hello: 'world' } ); jQuery( '.root' ).append( '' ) - form = new Form( { model, container: '.findme', config: { form_template_id: 'my-template' } } ); + const MyForm = Form.extend( { + config: { form_template_id: 'my-template' }, + } ); + form = new MyForm( { model, container: '.findme' } ); } ); it( 'calls wp.template() on the config.form_template_id if no template is cached', function() { @@ -402,7 +396,10 @@ describe( 'wp.widgets.Form', function() { } ); it( 'throws an error if the template does not exist in the DOM', function( done ) { - form = new Form( { model, container: '.findme', config: { form_template_id: 'notfound' } } ); + const MyForm = Form.extend( { + config: { form_template_id: 'notfound' }, + } ); + form = new MyForm( { model, container: '.findme' } ); try { form.getTemplate(); } catch( err ) { @@ -432,11 +429,17 @@ describe( 'wp.widgets.Form', function() { jQuery( '.root' ).append( '' ) jQuery( '.root' ).append( '' ) jQuery( '.findme' ).append( '' ); - form = new Form( { model, container: '.findme', config: { form_template_id: 'my-template', notifications_template_id: 'my-notifications' } } ); + const MyForm = Form.extend( { + config: { form_template_id: 'my-template', notifications_template_id: 'my-notifications' }, + } ); + form = new MyForm( { model, container: '.findme' } ); } ); it( 'throws an error if notifications_template_id is undefined', function( done ) { - form = new Form( { model, container: '.findme', config: { form_template_id: 'my-template' } } ); + const MyForm = Form.extend( { + config: { form_template_id: 'my-template' }, + } ); + form = new MyForm( { model, container: '.findme' } ); try { form.renderNotificationsToContainer(); } catch( err ) { @@ -548,7 +551,10 @@ describe( 'wp.widgets.Form', function() { beforeEach( function() { model = new Value( { hello: 'world' } ); jQuery( '.root' ).append( '' ) - form = new Form( { model, container: '.findme', config: { form_template_id: 'my-template' } } ); + const MyForm = Form.extend( { + config: { form_template_id: 'my-template' }, + } ); + form = new MyForm( { model, container: '.findme' } ); form.container.html = sinon.spy(); } ); @@ -572,7 +578,10 @@ describe( 'wp.widgets.Form', function() { beforeEach( function() { model = new Value( { hello: 'world' } ); jQuery( '.root' ).append( '' ); - const form = new Form( { model, container: '.findme', config: { form_template_id: 'my-template' } } ); + const MyForm = Form.extend( { + config: { form_template_id: 'my-template' }, + } ); + const form = new MyForm( { model, container: '.findme' } ); form.render(); } ); From 6f707e1bf694188b2bc0ac395e34f5c887375cdd Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Fri, 3 Feb 2017 19:44:43 -0500 Subject: [PATCH 52/92] Explicitly pluck Form arguments when assigning Only allows `model` and `container`, ignoring all other properties. --- js/widget-form.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/js/widget-form.js b/js/widget-form.js index 27a29ea..0284676 100644 --- a/js/widget-form.js +++ b/js/widget-form.js @@ -202,10 +202,10 @@ wp.widgets.Form = (function( api, $, _ ) { config: {} }; - var args = _.extend( {}, defaultProperties, properties || {} ); - var propertiesConfig = properties ? properties.config : {}; - args.config = _.extend( {}, defaultConfig, config, propertiesConfig || {} ); - return args; + var formArguments = properties ? { model: properties.model, container: properties.container } : {}; + var validProperties = _.extend( {}, defaultProperties, formArguments ); + validProperties.config = _.extend( {}, defaultConfig, config ); + return validProperties; } /** From b06d22682927d82005827428261655a2774842f6 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Fri, 3 Feb 2017 19:56:35 -0500 Subject: [PATCH 53/92] Change tests so Form constructor requires id_base and default_instance --- tests/js/widget-form.js | 94 ++++++++++++++++++++++++----------------- 1 file changed, 55 insertions(+), 39 deletions(-) diff --git a/tests/js/widget-form.js b/tests/js/widget-form.js index a8b72c1..dfc7419 100644 --- a/tests/js/widget-form.js +++ b/tests/js/widget-form.js @@ -29,9 +29,15 @@ const { Value, Notification } = require( './lib/customize-base' ); const Form = require( '../../js/widget-form' ); describe( 'wp.widgets.Form', function() { + let CustomForm; + beforeEach( function() { resetGlobals(); jQuery( '.root' ).html( markup ); + CustomForm = Form.extend( { + id_base: 'custom', + config: { default_instance: { red: '#f00' } }, + } ); } ); describe( '.initialize() (constructor)', function() { @@ -44,15 +50,13 @@ describe( 'wp.widgets.Form', function() { } ); it( 'ignores arbitrary properties on the argument object', function() { - const params = { foo: 'bar', model, container }; - const form = new Form( params ); + const form = new CustomForm( { foo: 'bar', model, container } ); expect( form.foo ).to.not.exist; } ); it( 'throws an error if the model property is missing', function( done ) { - const params = { container }; try { - new Form( params ); + new CustomForm( { container } ); } catch ( err ) { done(); } @@ -60,9 +64,28 @@ describe( 'wp.widgets.Form', function() { } ); it( 'throws an error if the model property is not an instance of Value', function( done ) { - const params = { model: { hello: 'world' }, container }; try { - new Form( params ); + new CustomForm( { model: { hello: 'world' }, container } ); + } catch ( err ) { + done(); + } + throw new Error( 'Expected initialize to throw an Error but it did not' ); + } ); + + it( 'throws an error if the id_base property is not set on the Form prototype', function( done ) { + CustomForm.prototype.id_base = null; + try { + new CustomForm( { model, container } ); + } catch ( err ) { + done(); + } + throw new Error( 'Expected initialize to throw an Error but it did not' ); + } ); + + it( 'throws an error if the config.default_instance property is not set on the Form prototype', function( done ) { + CustomForm.prototype.config.default_instance = null; + try { + new CustomForm( { model, container } ); } catch ( err ) { done(); } @@ -70,8 +93,8 @@ describe( 'wp.widgets.Form', function() { } ); it( 'assigns a default config property if it has not already been set on the form object', function() { - const params = { model, container }; - const form = new Form( params ); + CustomForm.prototype.config = null; + const form = new CustomForm( { model, container } ); const expected = { form_template_id: '', notifications_template_id: '', @@ -82,11 +105,10 @@ describe( 'wp.widgets.Form', function() { } ); it( 'keeps the current config property if it is already set on the form object', function() { - const MyForm = Form.extend( { + const MyForm = CustomForm.extend( { config: { subclass: 'yes' }, } ); - const params = { model, container }; - const form = new MyForm( params ); + const form = new MyForm( { model, container } ); const expected = { form_template_id: '', notifications_template_id: '', @@ -98,34 +120,29 @@ describe( 'wp.widgets.Form', function() { } ); it( 'assigns form.setting as an alias to form.model', function() { - const params = { model, container }; - const form = new Form( params ); + const form = new CustomForm( { model, container } ); expect( form.setting ).to.equal( model ); } ); it( 'assigns form.notifications as an alias for form.model.notifications if it exists', function() { model.notifications = { hello: 'world' }; - const params = { model, container }; - const form = new Form( params ); + const form = new CustomForm( { model, container } ); expect( form.notifications ).to.equal( model.notifications ); } ); it( 'assigns form.notifications to a new Values with the Notification defaultConstructor if form.model.notifications does not exist', function() { - const params = { model, container }; - const form = new Form( params ); + const form = new CustomForm( { model, container } ); expect( form.notifications.defaultConstructor ).to.equal( Notification ); } ); it( 'assigns form.container to a jQuery DOM object for the selector previously in form.container', function() { - const params = { model, container: '.findme' }; - const form = new Form( params ); + const form = new CustomForm( { model, container: '.findme' } ); expect( form.container[ 0 ].className ).to.eql( 'findme' ); } ); it( 'throws an error if the form.container selector does not match a DOM node', function( done ) { - const params = { model, container: 'notfound' }; try { - new Form( params ); + new CustomForm( { model, container: 'notfound' } ); } catch( err ) { done(); } @@ -133,9 +150,8 @@ describe( 'wp.widgets.Form', function() { } ); it( 'mutates the model to override the `validate` method', function() { - const params = { model, container }; const previousValidate = model.validate; - new Form( params ); + new CustomForm( { model, container } ); expect( model.validate ).to.not.equal( previousValidate ); } ); } ); @@ -148,27 +164,27 @@ describe( 'wp.widgets.Form', function() { } ); it( 'returns an object with the properties of the object passed in', function() { - new Form( { model, container: '.findme' } ); + new CustomForm( { model, container: '.findme' } ); const actual = model.validate( { foo: 'bar' } ); expect( actual.foo ).to.eql( 'bar' ); } ); it( 'also calls the model\'s previous validate method', function() { model.validate = values => Object.assign( {}, values, { red: 'green' } ); - new Form( { model, container: '.findme' } ); + new CustomForm( { model, container: '.findme' } ); const actual = model.validate( { foo: 'bar' } ); expect( actual.red ).to.eql( 'green' ); } ); it( 'does not remove any Notifications from form.notifications which do not have the `viaWidgetFormSanitizeReturn` property', function() { - const form = new Form( { model, container: '.findme' } ); + const form = new CustomForm( { model, container: '.findme' } ); form.notifications.add( 'other-notification', new Notification( 'other-notification', { message: 'test', type: 'warning' } ) ); model.validate( { foo: 'bar' } ); expect( form.notifications.has( 'other-notification' ) ).to.be.true; } ); it( 'removes any Notifications from form.notifications which have the `viaWidgetFormSanitizeReturn` property', function() { - const form = new Form( { model, container: '.findme' } ); + const form = new CustomForm( { model, container: '.findme' } ); const notification = new Notification( 'other-notification', { message: 'test', type: 'warning' } ); notification.viaWidgetFormSanitizeReturn = true; form.notifications.add( 'other-notification', notification ); @@ -180,7 +196,7 @@ describe( 'wp.widgets.Form', function() { let MyForm, form; beforeEach( function() { - MyForm = Form.extend( { + MyForm = CustomForm.extend( { sanitize: input => MyForm.returnNotification ? new Notification( 'testcode', { message: 'test', type: 'warning' } ) : input, } ); form = new MyForm( { model, container: '.findme' } ); @@ -228,7 +244,7 @@ describe( 'wp.widgets.Form', function() { let MyForm, form; beforeEach( function() { - MyForm = Form.extend( { + MyForm = CustomForm.extend( { sanitize: input => MyForm.returnError ? new Error( 'test error' ) : input, } ); form = new MyForm( { model, container: '.findme' } ); @@ -262,7 +278,7 @@ describe( 'wp.widgets.Form', function() { beforeEach( function() { const model = new Value( 'test' ); - form = new Form( { model, container: '.findme' } ); + form = new CustomForm( { model, container: '.findme' } ); } ); it( 'returns a jQuery object for the first instance of .js-widget-form-notifications-container in the container', function() { @@ -282,7 +298,7 @@ describe( 'wp.widgets.Form', function() { beforeEach( function() { const model = new Value( 'test' ); - form = new Form( { model, container: '.findme' } ); + form = new CustomForm( { model, container: '.findme' } ); } ); it( 'throws an Error if oldInstance is not set', function( done ) { @@ -330,7 +346,7 @@ describe( 'wp.widgets.Form', function() { beforeEach( function() { const model = new Value( { hello: 'world' } ); - const MyForm = Form.extend( { + const MyForm = CustomForm.extend( { config: { default_instance: { foo: 'bar' } }, } ); form = new MyForm( { model, container: '.findme' } ); @@ -352,7 +368,7 @@ describe( 'wp.widgets.Form', function() { beforeEach( function() { model = new Value( { hello: 'world' } ); - form = new Form( { model, container: '.findme' } ); + form = new CustomForm( { model, container: '.findme' } ); } ); it( 'updates the model by merging its value with the passed object properties', function() { @@ -379,7 +395,7 @@ describe( 'wp.widgets.Form', function() { global.wp.template = templateSpy; model = new Value( { hello: 'world' } ); jQuery( '.root' ).append( '' ) - const MyForm = Form.extend( { + const MyForm = CustomForm.extend( { config: { form_template_id: 'my-template' }, } ); form = new MyForm( { model, container: '.findme' } ); @@ -396,7 +412,7 @@ describe( 'wp.widgets.Form', function() { } ); it( 'throws an error if the template does not exist in the DOM', function( done ) { - const MyForm = Form.extend( { + const MyForm = CustomForm.extend( { config: { form_template_id: 'notfound' }, } ); form = new MyForm( { model, container: '.findme' } ); @@ -429,14 +445,14 @@ describe( 'wp.widgets.Form', function() { jQuery( '.root' ).append( '' ) jQuery( '.root' ).append( '' ) jQuery( '.findme' ).append( '' ); - const MyForm = Form.extend( { + const MyForm = CustomForm.extend( { config: { form_template_id: 'my-template', notifications_template_id: 'my-notifications' }, } ); form = new MyForm( { model, container: '.findme' } ); } ); it( 'throws an error if notifications_template_id is undefined', function( done ) { - const MyForm = Form.extend( { + const MyForm = CustomForm.extend( { config: { form_template_id: 'my-template' }, } ); form = new MyForm( { model, container: '.findme' } ); @@ -551,7 +567,7 @@ describe( 'wp.widgets.Form', function() { beforeEach( function() { model = new Value( { hello: 'world' } ); jQuery( '.root' ).append( '' ) - const MyForm = Form.extend( { + const MyForm = CustomForm.extend( { config: { form_template_id: 'my-template' }, } ); form = new MyForm( { model, container: '.findme' } ); @@ -578,7 +594,7 @@ describe( 'wp.widgets.Form', function() { beforeEach( function() { model = new Value( { hello: 'world' } ); jQuery( '.root' ).append( '' ); - const MyForm = Form.extend( { + const MyForm = CustomForm.extend( { config: { form_template_id: 'my-template' }, } ); const form = new MyForm( { model, container: '.findme' } ); From c2e5a0dedac3d668a46c7ab0bda59439020a8539 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Fri, 3 Feb 2017 19:58:26 -0500 Subject: [PATCH 54/92] Require id_base and config.default_instance in Form constructor --- js/widget-form.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/js/widget-form.js b/js/widget-form.js index 0284676..78967aa 100644 --- a/js/widget-form.js +++ b/js/widget-form.js @@ -177,6 +177,12 @@ wp.widgets.Form = (function( api, $, _ ) { if ( 0 === widgetForm.container.length ) { throw new Error( 'Widget Form is missing container property as Element or jQuery.' ); } + if ( ! widgetForm.id_base ) { + throw new Error( 'Widget Form class is missing id_base' ); + } + if ( ! widgetForm.config || ! widgetForm.config.default_instance ) { + throw new Error( 'Widget Form class is missing config.default_instance' ); + } } /** From ff48f637844e35efb64e4dd7db587c55928b7497 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Sat, 4 Feb 2017 13:11:10 -0500 Subject: [PATCH 55/92] Add test for memory leak in Form.model.validate --- tests/js/widget-form.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/js/widget-form.js b/tests/js/widget-form.js index dfc7419..f4a4bf5 100644 --- a/tests/js/widget-form.js +++ b/tests/js/widget-form.js @@ -192,6 +192,16 @@ describe( 'wp.widgets.Form', function() { expect( form.notifications.has( 'other-notification' ) ).to.be.false; } ); + it( 'retains a reference to the Form through the validate closure even after the Form is destroyed', function() { + let form = new CustomForm( { model, container: '.findme' } ); + const sanitizeSpy = sinon.spy(); + form.sanitize = sanitizeSpy; + form.destruct(); + form = null; + model.validate( { foo: 'bar' } ); + expect( sanitizeSpy ).to.have.been.called; + } ); + describe( 'if the sanitize function returns a Notification', function() { let MyForm, form; From 52552911e0f6c53efcb8cd3c7821cc6fc0accd89 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Mon, 6 Feb 2017 16:34:15 -0500 Subject: [PATCH 56/92] Move Form private functions to bottom of file It's easier to read the purpose of the file if the class comes first. --- js/widget-form.js | 402 +++++++++++++++++++++++----------------------- 1 file changed, 201 insertions(+), 201 deletions(-) diff --git a/js/widget-form.js b/js/widget-form.js index 78967aa..aaaba00 100644 --- a/js/widget-form.js +++ b/js/widget-form.js @@ -13,207 +13,6 @@ if ( ! wp.widgets.formConstructor ) { wp.widgets.Form = (function( api, $, _ ) { 'use strict'; - /** - * Return an Array of an api.Values object's values - * - * @param {wp.customize.Values} values An instance of api.Values - * @return {Array} An array of api.Value objects - */ - function getArrayFromValues( values ) { - var ary = []; - values.each( function( value ) { - ary.push( value ); - } ); - return ary; - } - - /** - * Return true if the Notification is an error - * - * @param {wp.customize.Notification} notification An instance of api.Notification - * @return {Boolean} True if the `type` of the Notification is 'error' - */ - function isNotificationError( notification ) { - return 'error' === notification.type; - } - - /** - * Hide or show a DOM node using jQuery animation - * - * @param {jQuery} container The jQuery object to hide or show - * @param {Boolean} showContainer True to show the node, or false to hide it - * @return {Deferred} A promise that is resolved when the animation is complete - */ - function toggleContainer( container, showContainer ) { - var deferred = $.Deferred(); - if ( showContainer ) { - container.stop().slideDown( 'fast', null, deferred.resolve ); - } else { - container.stop().slideUp( 'fast', null, deferred.resolve ); - } - return deferred; - } - - /** - * Speak a Notification using wp.a11y - * - * Will only speak a Notification once, so if passed a Notification that has already been spoken, this is a noop. - * - * @param {Notification} notification The Notification to speak - * @return {void} - */ - function speakNotification( notification ) { - if ( ! notification.hasA11ySpoken ) { - - // @todo In the context of the Customizer, this presently will end up getting spoken twice due to wp.customize.Control also rendering it. - wp.a11y.speak( notification.message, 'assertive' ); - notification.hasA11ySpoken = true; - } - } - - /** - * Return the template function for rendering Notifications - * - * @param {Form} widgetForm The instance of the Form whose template to fetch - * @return {Function} The template function - */ - function getNotificationsTemplate( widgetForm ) { - if ( ! widgetForm._notificationsTemplate ) { - widgetForm._notificationsTemplate = wp.template( widgetForm.config.notifications_template_id ); - } - return widgetForm._notificationsTemplate; - } - - /** - * Replace the markup of a DOM node container - * - * @param {jQuery} container The DOM node which will be replaced by the markup - * @param {string} markup The markup to apply to the container - * @return {void} - */ - function renderMarkupToContainer( container, markup ) { - container.empty().append( $.trim( markup ) ); - } - - /** - * Removes Notification objects which have been added by `addSanitizeNotification` - * - * Note: this mutates the object itself! - * - * @param {wp.customize.Values} notifications An instance of api.Values containing Notification objects - * @return {void} - */ - function removeSanitizeNotifications( notifications ) { - notifications.each( function iterateNotifications( notification ) { - if ( notification.viaWidgetFormSanitizeReturn ) { - notifications.remove( notification.code ); - } - } ); - } - - /** - * Adds a Notification to a Form from the form's `sanitize` method - * - * @param {Form} widgetForm The instance of the Form to modify - * @param {wp.customize.Values} notification An instance of api.Notification to add - * @return {void} - */ - function addSanitizeNotification( widgetForm, notification ) { - notification.viaWidgetFormSanitizeReturn = true; - widgetForm.notifications.add( notification.code, notification ); - } - - /** - * Return a new validation function for a Form's model - * - * The new method will call the old method first. - * - * @param {Form} widgetForm The instance of the Form to modify - * @param {Function} previousValidate The old version of the model's validate method - * @return {Function} The new validate method - */ - function getValidateWidget( widgetForm, previousValidate ) { - /** - * Validate the instance data. - * - * @todo In order for returning an error/notification to work properly, api._handleSettingValidities needs to only remove notification errors that are no longer valid which are fromServer: - * - * @param {object} value Instance value. - * @returns {object|Error|wp.customize.Notification} Sanitized instance value or error/notification. - */ - return function validate( value ) { - var setting = this, newValue, oldValue; - oldValue = setting(); - newValue = widgetForm.sanitize( previousValidate.call( setting, value ), oldValue ); - - // Remove all existing notifications added via sanitization since only one can be returned. - removeSanitizeNotifications( widgetForm.notifications ); - - // If sanitize method returned an error/notification, block setting update and add a notification - if ( newValue instanceof Error ) { - newValue = new api.Notification( 'invalidValue', { message: newValue.message, type: 'error' } ); - } - if ( newValue instanceof api.Notification ) { - addSanitizeNotification( widgetForm, newValue ); - return null; - } - - return newValue; - }; - } - - /** - * Validate the properties of a Form - * - * Throws an Error if the properties are invalid. - * - * @param {Form} widgetForm The instance of the Form to modify - * @return {void} - */ - function validateForm( widgetForm ) { - if ( ! widgetForm.model || ! widgetForm.model.extended || ! widgetForm.model.extended( api.Value ) ) { - throw new Error( 'Widget Form is missing model property which must be a Value or Setting instance.' ); - } - if ( 0 === widgetForm.container.length ) { - throw new Error( 'Widget Form is missing container property as Element or jQuery.' ); - } - if ( ! widgetForm.id_base ) { - throw new Error( 'Widget Form class is missing id_base' ); - } - if ( ! widgetForm.config || ! widgetForm.config.default_instance ) { - throw new Error( 'Widget Form class is missing config.default_instance' ); - } - } - - /** - * Merges properties for a Form with the defaults - * - * The passed properties override the Form's config property which overrides the default values. - * - * @param {object} config The Form's current config property - * @param {object} properties The passed-in properties to the Form - * @return {object} The merged properties object - */ - function getValidatedFormProperties( config, properties ) { - var defaultConfig = { - form_template_id: '', - notifications_template_id: '', - l10n: {}, - default_instance: {} - }; - - var defaultProperties = { - model: null, - container: null, - config: {} - }; - - var formArguments = properties ? { model: properties.model, container: properties.container } : {}; - var validProperties = _.extend( {}, defaultProperties, formArguments ); - validProperties.config = _.extend( {}, defaultConfig, config ); - return validProperties; - } - /** * Customize Widget Form. * @@ -509,6 +308,207 @@ wp.widgets.Form = (function( api, $, _ ) { } }); + /** + * Return an Array of an api.Values object's values + * + * @param {wp.customize.Values} values An instance of api.Values + * @return {Array} An array of api.Value objects + */ + function getArrayFromValues( values ) { + var ary = []; + values.each( function( value ) { + ary.push( value ); + } ); + return ary; + } + + /** + * Return true if the Notification is an error + * + * @param {wp.customize.Notification} notification An instance of api.Notification + * @return {Boolean} True if the `type` of the Notification is 'error' + */ + function isNotificationError( notification ) { + return 'error' === notification.type; + } + + /** + * Hide or show a DOM node using jQuery animation + * + * @param {jQuery} container The jQuery object to hide or show + * @param {Boolean} showContainer True to show the node, or false to hide it + * @return {Deferred} A promise that is resolved when the animation is complete + */ + function toggleContainer( container, showContainer ) { + var deferred = $.Deferred(); + if ( showContainer ) { + container.stop().slideDown( 'fast', null, deferred.resolve ); + } else { + container.stop().slideUp( 'fast', null, deferred.resolve ); + } + return deferred; + } + + /** + * Speak a Notification using wp.a11y + * + * Will only speak a Notification once, so if passed a Notification that has already been spoken, this is a noop. + * + * @param {Notification} notification The Notification to speak + * @return {void} + */ + function speakNotification( notification ) { + if ( ! notification.hasA11ySpoken ) { + + // @todo In the context of the Customizer, this presently will end up getting spoken twice due to wp.customize.Control also rendering it. + wp.a11y.speak( notification.message, 'assertive' ); + notification.hasA11ySpoken = true; + } + } + + /** + * Return the template function for rendering Notifications + * + * @param {Form} widgetForm The instance of the Form whose template to fetch + * @return {Function} The template function + */ + function getNotificationsTemplate( widgetForm ) { + if ( ! widgetForm._notificationsTemplate ) { + widgetForm._notificationsTemplate = wp.template( widgetForm.config.notifications_template_id ); + } + return widgetForm._notificationsTemplate; + } + + /** + * Replace the markup of a DOM node container + * + * @param {jQuery} container The DOM node which will be replaced by the markup + * @param {string} markup The markup to apply to the container + * @return {void} + */ + function renderMarkupToContainer( container, markup ) { + container.empty().append( $.trim( markup ) ); + } + + /** + * Removes Notification objects which have been added by `addSanitizeNotification` + * + * Note: this mutates the object itself! + * + * @param {wp.customize.Values} notifications An instance of api.Values containing Notification objects + * @return {void} + */ + function removeSanitizeNotifications( notifications ) { + notifications.each( function iterateNotifications( notification ) { + if ( notification.viaWidgetFormSanitizeReturn ) { + notifications.remove( notification.code ); + } + } ); + } + + /** + * Adds a Notification to a Form from the form's `sanitize` method + * + * @param {Form} widgetForm The instance of the Form to modify + * @param {wp.customize.Values} notification An instance of api.Notification to add + * @return {void} + */ + function addSanitizeNotification( widgetForm, notification ) { + notification.viaWidgetFormSanitizeReturn = true; + widgetForm.notifications.add( notification.code, notification ); + } + + /** + * Return a new validation function for a Form's model + * + * The new method will call the old method first. + * + * @param {Form} widgetForm The instance of the Form to modify + * @param {Function} previousValidate The old version of the model's validate method + * @return {Function} The new validate method + */ + function getValidateWidget( widgetForm, previousValidate ) { + /** + * Validate the instance data. + * + * @todo In order for returning an error/notification to work properly, api._handleSettingValidities needs to only remove notification errors that are no longer valid which are fromServer: + * + * @param {object} value Instance value. + * @returns {object|Error|wp.customize.Notification} Sanitized instance value or error/notification. + */ + return function validate( value ) { + var setting = this, newValue, oldValue; + oldValue = setting(); + newValue = widgetForm.sanitize( previousValidate.call( setting, value ), oldValue ); + + // Remove all existing notifications added via sanitization since only one can be returned. + removeSanitizeNotifications( widgetForm.notifications ); + + // If sanitize method returned an error/notification, block setting update and add a notification + if ( newValue instanceof Error ) { + newValue = new api.Notification( 'invalidValue', { message: newValue.message, type: 'error' } ); + } + if ( newValue instanceof api.Notification ) { + addSanitizeNotification( widgetForm, newValue ); + return null; + } + + return newValue; + }; + } + + /** + * Validate the properties of a Form + * + * Throws an Error if the properties are invalid. + * + * @param {Form} widgetForm The instance of the Form to modify + * @return {void} + */ + function validateForm( widgetForm ) { + if ( ! widgetForm.model || ! widgetForm.model.extended || ! widgetForm.model.extended( api.Value ) ) { + throw new Error( 'Widget Form is missing model property which must be a Value or Setting instance.' ); + } + if ( 0 === widgetForm.container.length ) { + throw new Error( 'Widget Form is missing container property as Element or jQuery.' ); + } + if ( ! widgetForm.id_base ) { + throw new Error( 'Widget Form class is missing id_base' ); + } + if ( ! widgetForm.config || ! widgetForm.config.default_instance ) { + throw new Error( 'Widget Form class is missing config.default_instance' ); + } + } + + /** + * Merges properties for a Form with the defaults + * + * The passed properties override the Form's config property which overrides the default values. + * + * @param {object} config The Form's current config property + * @param {object} properties The passed-in properties to the Form + * @return {object} The merged properties object + */ + function getValidatedFormProperties( config, properties ) { + var defaultConfig = { + form_template_id: '', + notifications_template_id: '', + l10n: {}, + default_instance: {} + }; + + var defaultProperties = { + model: null, + container: null, + config: {} + }; + + var formArguments = properties ? { model: properties.model, container: properties.container } : {}; + var validProperties = _.extend( {}, defaultProperties, formArguments ); + validProperties.config = _.extend( {}, defaultConfig, config ); + return validProperties; + } + } )( wp.customize, jQuery, _ ); if ( 'undefined' !== typeof module ) { From 2145662f7d142c8c391907bef3e2496c54abc2d5 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Mon, 6 Feb 2017 17:03:35 -0500 Subject: [PATCH 57/92] Add test for changing notification area height on show --- tests/js/widget-form.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/js/widget-form.js b/tests/js/widget-form.js index f4a4bf5..97c36a1 100644 --- a/tests/js/widget-form.js +++ b/tests/js/widget-form.js @@ -488,6 +488,18 @@ describe( 'wp.widgets.Form', function() { expect( notificationsContainer.slideUp ).to.have.been.called; } ); + it( 'does not change the height css of the notifications area', function( done ) { + const notificationsContainer = jQuery( '.js-widget-form-notifications-container' ); + notificationsContainer.css = sinon.spy(); + form.getNotificationsContainerElement = () => notificationsContainer; + form.renderNotificationsToContainer(); + // The height change happens async so we defer this assertion + setTimeout( function() { + expect( notificationsContainer.css ).to.not.have.been.called; + done(); + }, 0 ); + } ); + it( 'removes the `has-notifications` class on form.container', function() { jQuery( '.findme' ).addClass( 'has-notifications' ); form.renderNotificationsToContainer(); @@ -536,6 +548,18 @@ describe( 'wp.widgets.Form', function() { expect( notificationsContainer.slideDown ).to.have.been.called; } ); + it( 'changes the css height property on the notification container to `auto`', function( done ) { + const notificationsContainer = jQuery( '.js-widget-form-notifications-container' ); + notificationsContainer.css = sinon.spy(); + form.getNotificationsContainerElement = () => notificationsContainer; + form.renderNotificationsToContainer(); + // The height change happens async so we defer this assertion + setTimeout( function() { + expect( notificationsContainer.css ).to.have.been.calledWith( 'height', 'auto' ); + done(); + }, 0 ); + } ); + it( 'adds the `has-notifications` class on form.container', function() { jQuery( '.findme' ).removeClass( 'has-notifications' ); form.renderNotificationsToContainer(); From 379af12b05cd423e8e5ba87799ae99e57b68c64e Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Mon, 6 Feb 2017 17:06:01 -0500 Subject: [PATCH 58/92] Fix changing height of Form notifications area --- js/widget-form.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/js/widget-form.js b/js/widget-form.js index aaaba00..952bc29 100644 --- a/js/widget-form.js +++ b/js/widget-form.js @@ -80,7 +80,7 @@ wp.widgets.Form = (function( api, $, _ ) { toggleContainer( container, notifications.length > 0 ) .then( function() { if ( notifications.length > 0 ) { - $( form ).css( 'height', 'auto' ); + container.css( 'height', 'auto' ); } } ); form.container.toggleClass( 'has-error', notifications.filter( isNotificationError ).length > 0 ); @@ -342,7 +342,9 @@ wp.widgets.Form = (function( api, $, _ ) { function toggleContainer( container, showContainer ) { var deferred = $.Deferred(); if ( showContainer ) { - container.stop().slideDown( 'fast', null, deferred.resolve ); + container.stop().slideDown( 'fast', null, function() { + deferred.resolve(); + } ); } else { container.stop().slideUp( 'fast', null, deferred.resolve ); } From dae04c6e2dd67976291473d11582986c174f7fbb Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Fri, 10 Feb 2017 17:48:55 -0500 Subject: [PATCH 59/92] Change tests so that validate becomes a method on Form This replaces overriding the `model.validate` method by requiring using `form.setState()` to update the model --- tests/js/widget-form.js | 101 +++++++++++++++++++++------------------- 1 file changed, 52 insertions(+), 49 deletions(-) diff --git a/tests/js/widget-form.js b/tests/js/widget-form.js index 97c36a1..dd92d87 100644 --- a/tests/js/widget-form.js +++ b/tests/js/widget-form.js @@ -1,5 +1,5 @@ /* globals global, require, describe, it, beforeEach */ -/* eslint-disable no-unused-expressions, no-new, no-magic-numbers */ +/* eslint-disable no-unused-expressions, no-new, no-magic-numbers, max-nested-callbacks */ const sinon = require( 'sinon' ); const sinonChai = require( 'sinon-chai' ); @@ -148,95 +148,69 @@ describe( 'wp.widgets.Form', function() { } throw new Error( 'Expected initialize to throw an Error but it did not' ); } ); - - it( 'mutates the model to override the `validate` method', function() { - const previousValidate = model.validate; - new CustomForm( { model, container } ); - expect( model.validate ).to.not.equal( previousValidate ); - } ); } ); - describe( '.model.validate()', function() { - let model; + describe( '.validate()', function() { + let model, form; beforeEach( function() { model = new Value( { hello: 'world' } ); + form = new CustomForm( { model, container: '.findme' } ); } ); it( 'returns an object with the properties of the object passed in', function() { - new CustomForm( { model, container: '.findme' } ); - const actual = model.validate( { foo: 'bar' } ); + const actual = form.validate( { foo: 'bar' } ); expect( actual.foo ).to.eql( 'bar' ); } ); - it( 'also calls the model\'s previous validate method', function() { - model.validate = values => Object.assign( {}, values, { red: 'green' } ); - new CustomForm( { model, container: '.findme' } ); - const actual = model.validate( { foo: 'bar' } ); - expect( actual.red ).to.eql( 'green' ); - } ); - it( 'does not remove any Notifications from form.notifications which do not have the `viaWidgetFormSanitizeReturn` property', function() { - const form = new CustomForm( { model, container: '.findme' } ); form.notifications.add( 'other-notification', new Notification( 'other-notification', { message: 'test', type: 'warning' } ) ); - model.validate( { foo: 'bar' } ); + form.validate( { foo: 'bar' } ); expect( form.notifications.has( 'other-notification' ) ).to.be.true; } ); it( 'removes any Notifications from form.notifications which have the `viaWidgetFormSanitizeReturn` property', function() { - const form = new CustomForm( { model, container: '.findme' } ); const notification = new Notification( 'other-notification', { message: 'test', type: 'warning' } ); notification.viaWidgetFormSanitizeReturn = true; form.notifications.add( 'other-notification', notification ); - model.validate( { foo: 'bar' } ); + form.validate( { foo: 'bar' } ); expect( form.notifications.has( 'other-notification' ) ).to.be.false; } ); - it( 'retains a reference to the Form through the validate closure even after the Form is destroyed', function() { - let form = new CustomForm( { model, container: '.findme' } ); - const sanitizeSpy = sinon.spy(); - form.sanitize = sanitizeSpy; - form.destruct(); - form = null; - model.validate( { foo: 'bar' } ); - expect( sanitizeSpy ).to.have.been.called; - } ); - describe( 'if the sanitize function returns a Notification', function() { - let MyForm, form; + let MyForm; beforeEach( function() { - MyForm = CustomForm.extend( { - sanitize: input => MyForm.returnNotification ? new Notification( 'testcode', { message: 'test', type: 'warning' } ) : input, - } ); + const sanitize = input => MyForm.returnNotification ? new Notification( 'testcode', { message: 'test', type: 'warning' } ) : input; + MyForm = CustomForm.extend( { sanitize } ); form = new MyForm( { model, container: '.findme' } ); MyForm.returnNotification = true; } ); it( 'returns null', function() { - const actual = model.validate( { foo: 'bar' } ); + const actual = form.validate( { foo: 'bar' } ); expect( actual ).to.be.null; } ); it( 'adds any Notification returned from the sanitize method to form.notifications', function() { - model.validate( { foo: 'bar' } ); + form.validate( { foo: 'bar' } ); expect( form.notifications.has( 'testcode' ) ).to.be.true; } ); it( 'adds `viaWidgetFormSanitizeReturn` property to any Notification returned from the sanitize method', function() { - model.validate( { foo: 'bar' } ); + form.validate( { foo: 'bar' } ); expect( form.notifications.value( 'testcode' ).viaWidgetFormSanitizeReturn ).to.be.true; } ); it( 'adds `viaWidgetFormSanitizeReturn` property to any Notification returned from the sanitize method', function() { - model.validate( { foo: 'bar' } ); + form.validate( { foo: 'bar' } ); expect( form.notifications.value( 'testcode' ).viaWidgetFormSanitizeReturn ).to.be.true; } ); it( 'causes subsequent calls to remove any Notifications from form.notifications which have the `viaWidgetFormSanitizeReturn` property', function() { - model.validate( { foo: 'bar' } ); + form.validate( { foo: 'bar' } ); MyForm.returnNotification = false; - model.validate( { foo: 'bar' } ); + form.validate( { foo: 'bar' } ); expect( form.notifications.has( 'testcode' ) ).to.be.false; } ); @@ -245,13 +219,13 @@ describe( 'wp.widgets.Form', function() { notification.firstOne = true; notification.viaWidgetFormSanitizeReturn = true; form.notifications.add( 'testcode', notification ); - model.validate( { foo: 'bar' } ); + form.validate( { foo: 'bar' } ); expect( form.notifications.value( 'testcode' ) ).to.not.have.property( 'firstOne' ); } ); } ); describe( 'if the sanitize function returns an Error', function() { - let MyForm, form; + let MyForm; beforeEach( function() { MyForm = CustomForm.extend( { @@ -262,22 +236,22 @@ describe( 'wp.widgets.Form', function() { } ); it( 'returns null', function() { - const actual = model.validate( { foo: 'bar' } ); + const actual = form.validate( { foo: 'bar' } ); expect( actual ).to.be.null; } ); it( 'adds a Notification with the code `invalidValue`', function() { - model.validate( { foo: 'bar' } ); + form.validate( { foo: 'bar' } ); expect( form.notifications.has( 'invalidValue' ) ).to.be.true; } ); it( 'adds a Notification with the type `error`', function() { - model.validate( { foo: 'bar' } ); + form.validate( { foo: 'bar' } ); expect( form.notifications.value( 'invalidValue' ).type ).to.eql( 'error' ); } ); it( 'adds a Notification with the Error message in the message', function() { - model.validate( { foo: 'bar' } ); + form.validate( { foo: 'bar' } ); expect( form.notifications.value( 'invalidValue' ).message ).to.eql( 'test error' ); } ); } ); @@ -381,11 +355,16 @@ describe( 'wp.widgets.Form', function() { form = new CustomForm( { model, container: '.findme' } ); } ); - it( 'updates the model by merging its value with the passed object properties', function() { + it( 'updates the model by merging its value with the passed object properties to add values', function() { form.setState( { beep: 'boop' } ); expect( model.get().beep ).to.eql( 'boop' ); } ); + it( 'updates the model by merging its value with the passed object properties to replace values', function() { + form.setState( { hello: 'boop' } ); + expect( model.get().hello ).to.eql( 'boop' ); + } ); + it( 'retains the current values on the model if not specifically overwritten', function() { form.setState( { beep: 'boop' } ); expect( model.get().hello ).to.eql( 'world' ); @@ -395,6 +374,30 @@ describe( 'wp.widgets.Form', function() { form.setState( {} ); expect( model.get() ).to.eql( { hello: 'world' } ); } ); + + it( 'calls form.validate() on its value before updating the model', function() { + form.validate = () => ( { valid: 'good' } ); + form.setState( { invalid: 'bad' } ); + expect( model.get() ).to.eql( { hello: 'world', valid: 'good' } ); + } ); + + it( 'if form.validate returns an Error, the model is left unchanged', function() { + form.validate = () => new Error( 'test error' ); + form.setState( { invalid: 'bad' } ); + expect( model.get() ).to.eql( { hello: 'world' } ); + } ); + + it( 'if form.validate returns null, the model is left unchanged', function() { + form.validate = () => null; + form.setState( { invalid: 'bad' } ); + expect( model.get() ).to.eql( { hello: 'world' } ); + } ); + + it( 'if form.validate returns a Notification, the model is left unchanged', function() { + form.validate = () => new Notification( 'other-notification', { message: 'test', type: 'warning' } ); + form.setState( { invalid: 'bad' } ); + expect( model.get() ).to.eql( { hello: 'world' } ); + } ); } ); describe( '.getTemplate()', function() { From c63043c7016229d41bffc6feff1e53de823d817f Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Fri, 10 Feb 2017 17:50:38 -0500 Subject: [PATCH 60/92] Move model.validate to Form.validate Form.validate is now called by Form.setState. This avoids mutating the model. --- js/widget-form.js | 77 +++++++++++++++++++++-------------------------- 1 file changed, 34 insertions(+), 43 deletions(-) diff --git a/js/widget-form.js b/js/widget-form.js index 952bc29..1e9ffca 100644 --- a/js/widget-form.js +++ b/js/widget-form.js @@ -48,8 +48,6 @@ wp.widgets.Form = (function( api, $, _ ) { form.container = $( form.container ); validateForm( form ); - - form.model.validate = getValidateWidget( form, form.model.validate ); }, /** @@ -104,6 +102,34 @@ wp.widgets.Form = (function( api, $, _ ) { return form.container.find( '.js-widget-form-notifications-container:first' ); }, + /** + * Validate the instance data. + * + * @todo In order for returning an error/notification to work properly, api._handleSettingValidities needs to only remove notification errors that are no longer valid which are fromServer: + * + * @param {object} value Instance value. + * @returns {object|Error|wp.customize.Notification} Sanitized instance value or error/notification. + */ + validate: function validate( value ) { + var form = this, newValue, oldValue; + oldValue = form.model.get(); + newValue = form.sanitize( value, oldValue ); + + // Remove all existing notifications added via sanitization since only one can be returned. + removeSanitizeNotifications( form.notifications ); + + // If sanitize method returned an error/notification, block setting update and add a notification + if ( newValue instanceof Error ) { + newValue = new api.Notification( 'invalidValue', { message: newValue.message, type: 'error' } ); + } + if ( newValue instanceof api.Notification ) { + addSanitizeNotification( form, newValue ); + return null; + } + + return newValue; + }, + /** * Sanitize the instance data. * @@ -166,8 +192,12 @@ wp.widgets.Form = (function( api, $, _ ) { * @returns {void} */ setState: function setState( props ) { - var form = this, value; - value = _.extend( {}, form.model.get(), props || {} ); + var form = this, value, validated; + validated = form.validate( props || {} ); + if ( ! validated || validated instanceof Error || validated instanceof api.Notification ) { + return; + } + value = _.extend( {}, form.model.get(), validated ); form.model.set( value ); }, @@ -420,45 +450,6 @@ wp.widgets.Form = (function( api, $, _ ) { widgetForm.notifications.add( notification.code, notification ); } - /** - * Return a new validation function for a Form's model - * - * The new method will call the old method first. - * - * @param {Form} widgetForm The instance of the Form to modify - * @param {Function} previousValidate The old version of the model's validate method - * @return {Function} The new validate method - */ - function getValidateWidget( widgetForm, previousValidate ) { - /** - * Validate the instance data. - * - * @todo In order for returning an error/notification to work properly, api._handleSettingValidities needs to only remove notification errors that are no longer valid which are fromServer: - * - * @param {object} value Instance value. - * @returns {object|Error|wp.customize.Notification} Sanitized instance value or error/notification. - */ - return function validate( value ) { - var setting = this, newValue, oldValue; - oldValue = setting(); - newValue = widgetForm.sanitize( previousValidate.call( setting, value ), oldValue ); - - // Remove all existing notifications added via sanitization since only one can be returned. - removeSanitizeNotifications( widgetForm.notifications ); - - // If sanitize method returned an error/notification, block setting update and add a notification - if ( newValue instanceof Error ) { - newValue = new api.Notification( 'invalidValue', { message: newValue.message, type: 'error' } ); - } - if ( newValue instanceof api.Notification ) { - addSanitizeNotification( widgetForm, newValue ); - return null; - } - - return newValue; - }; - } - /** * Validate the properties of a Form * From 8ba065a9b183fedc54cd4aa2671332cb230f9425 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Fri, 10 Feb 2017 17:51:27 -0500 Subject: [PATCH 61/92] Make model.get always explicit --- js/widget-form.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/widget-form.js b/js/widget-form.js index 1e9ffca..f48a821 100644 --- a/js/widget-form.js +++ b/js/widget-form.js @@ -179,7 +179,7 @@ wp.widgets.Form = (function( api, $, _ ) { return _.extend( {}, form.config.default_instance, - form.model() || {} + form.model.get() || {} ); }, From 33ed37879d7ac45d7931ec8a3b7f2525cdf2721d Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Fri, 10 Feb 2017 17:55:47 -0500 Subject: [PATCH 62/92] Add tests to be sure two-way binding uses validate --- tests/js/widget-form.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/tests/js/widget-form.js b/tests/js/widget-form.js index dd92d87..3f30d51 100644 --- a/tests/js/widget-form.js +++ b/tests/js/widget-form.js @@ -626,7 +626,7 @@ describe( 'wp.widgets.Form', function() { } ); describe( 'when rendered with a form element whose `data-field` attribute matches a model property', function() { - let model; + let model, form; beforeEach( function() { model = new Value( { hello: 'world' } ); @@ -634,7 +634,7 @@ describe( 'wp.widgets.Form', function() { const MyForm = CustomForm.extend( { config: { form_template_id: 'my-template' }, } ); - const form = new MyForm( { model, container: '.findme' } ); + form = new MyForm( { model, container: '.findme' } ); form.render(); } ); @@ -644,6 +644,20 @@ describe( 'wp.widgets.Form', function() { expect( model.get().hello ).to.eql( 'computer' ); } ); + it( 'updates the associated model property with a validated version when the form element changes', function() { + form.validate = () => ( { hello: 'valid' } ); + jQuery( '.hello-field' ).val( 'invalid' ); + jQuery( '.hello-field' ).trigger( 'change' ); + expect( model.get().hello ).to.eql( 'valid' ); + } ); + + it( 'does not update the associated model property when the form element changes and validation fails', function() { + form.validate = () => null; + jQuery( '.hello-field' ).val( 'invalid' ); + jQuery( '.hello-field' ).trigger( 'change' ); + expect( model.get().hello ).to.eql( 'world' ); + } ); + it( 'updates the form field value when the model property changes', function() { model.set( { hello: 'computer' } ); expect( jQuery( '.hello-field' ).val() ).to.eql( 'computer' ); From 5f8f62bcc2137a48c7eadefc52278eebf66e305b Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Fri, 10 Feb 2017 17:57:31 -0500 Subject: [PATCH 63/92] Add placeholder for using setState in linkPropertyElements --- js/widget-form.js | 1 + 1 file changed, 1 insertion(+) diff --git a/js/widget-form.js b/js/widget-form.js index f48a821..02e14ec 100644 --- a/js/widget-form.js +++ b/js/widget-form.js @@ -257,6 +257,7 @@ wp.widgets.Form = (function( api, $, _ ) { return; } + // @todo: use setState instead of updating the model directly syncedProperty = form.createSyncedPropertyValue( form.model, field ); syncedProperty.element = new api.Element( input ); syncedProperty.element.set( initialInstanceData[ field ] ); From 02b0a1c8acf605a99cace3a7de5e66fd62cce978 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Fri, 10 Feb 2017 19:16:35 -0500 Subject: [PATCH 64/92] Add tests for destruct and field references --- tests/js/widget-form.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/js/widget-form.js b/tests/js/widget-form.js index 3f30d51..ecb01ba 100644 --- a/tests/js/widget-form.js +++ b/tests/js/widget-form.js @@ -663,4 +663,26 @@ describe( 'wp.widgets.Form', function() { expect( jQuery( '.hello-field' ).val() ).to.eql( 'computer' ); } ); } ); + + describe( 'when rendered and then destroyed', function() { + let model, form; + + beforeEach( function() { + model = new Value( { hello: 'world' } ); + jQuery( '.root' ).append( '' ); + const MyForm = CustomForm.extend( { + config: { form_template_id: 'my-template' }, + } ); + form = new MyForm( { model, container: '.findme' } ); + form.render(); + } ); + + it( 'removes event bindings between the Form and the DOM', function() { + // This is hard to test; keep the DOM around to see if it retains references + form.container.empty = () => null; + form.destruct(); + model.set( { hello: 'computer' } ); + expect( jQuery( '.hello-field' ).val() ).to.not.eql( 'computer' ); + } ); + } ); } ); From 6bc2e6609306e720f2e687b87d8f055ba940c50d Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Sat, 11 Feb 2017 17:36:22 -0500 Subject: [PATCH 65/92] Modify createSyncedPropertyValue to use setState This makes sure that data from the DOM is validated before changing the model. --- js/widget-form.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/js/widget-form.js b/js/widget-form.js index 02e14ec..d538617 100644 --- a/js/widget-form.js +++ b/js/widget-form.js @@ -215,15 +215,15 @@ wp.widgets.Form = (function( api, $, _ ) { * @returns {object} Property value instance. */ createSyncedPropertyValue: function createSyncedPropertyValue( root, property ) { - var propertyValue, rootChangeListener, propertyChangeListener; + var form = this, propertyValue, rootChangeListener, propertyChangeListener; propertyValue = new api.Value( root.get()[ property ] ); // Sync changes to the property back to the root value. propertyChangeListener = function( newPropertyValue ) { - var rootValue = _.clone( root.get() ); - rootValue[ property ] = newPropertyValue; - root.set( rootValue ); + var newState = {}; + newState[ property ] = newPropertyValue; + form.setState( newState ); }; propertyValue.bind( propertyChangeListener ); From 4f5aa12fc0dd5c95697d0076515c27c81b6e941f Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Mon, 13 Feb 2017 12:13:11 -0500 Subject: [PATCH 66/92] Remove fixed todo --- js/widget-form.js | 1 - 1 file changed, 1 deletion(-) diff --git a/js/widget-form.js b/js/widget-form.js index d538617..6e3770a 100644 --- a/js/widget-form.js +++ b/js/widget-form.js @@ -257,7 +257,6 @@ wp.widgets.Form = (function( api, $, _ ) { return; } - // @todo: use setState instead of updating the model directly syncedProperty = form.createSyncedPropertyValue( form.model, field ); syncedProperty.element = new api.Element( input ); syncedProperty.element.set( initialInstanceData[ field ] ); From a0cb61faa364880bd8108ab053cab0cf7ba6efe6 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Mon, 13 Feb 2017 15:43:50 -0500 Subject: [PATCH 67/92] Fix `properties.container` jsdoc for Form.initialize --- js/widget-form.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/widget-form.js b/js/widget-form.js index 6e3770a..c6d7100 100644 --- a/js/widget-form.js +++ b/js/widget-form.js @@ -33,7 +33,7 @@ wp.widgets.Form = (function( api, $, _ ) { * @param {object} properties Properties. * @param {string} properties.id_base The widget ID base (aka type). * @param {wp.customize.Value} properties.model The Value or Setting instance containing the widget instance data object. - * @param {Element|jQuery} properties.container The Value or Setting instance containing the widget instance data object. + * @param {string|Element|jQuery} properties.container The selector string or DOM element in which to render this Form. * @param {object} properties.config Form config. * @return {void} */ From 267498c4ff2b6f8229dd6e8f41c96fca593f5589 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 13 Feb 2017 23:21:48 -0800 Subject: [PATCH 68/92] Extend editAttributeField View instead of hacking hidden input to inject component --- .jscsrc | 4 ++ js/shortcode-ui-js-widgets.js | 62 -------------------- js/shortcode-ui-view-widget-form-field.js | 47 +++++++++++++++ php/class-js-widget-shortcode-controller.php | 11 +++- php/class-js-widgets-plugin.php | 16 ++--- 5 files changed, 65 insertions(+), 75 deletions(-) delete mode 100644 js/shortcode-ui-js-widgets.js create mode 100644 js/shortcode-ui-view-widget-form-field.js diff --git a/.jscsrc b/.jscsrc index 29ae93e..55dc417 100644 --- a/.jscsrc +++ b/.jscsrc @@ -1,5 +1,9 @@ { "preset": "wordpress", + "requireCamelCaseOrUpperCaseIdentifiers": { + "ignoreProperties": true, + "allExcept": [ "Shortcode_UI" ] + }, "excludeFiles": [ "**/*.min.js", "**/*.jsx", diff --git a/js/shortcode-ui-js-widgets.js b/js/shortcode-ui-js-widgets.js deleted file mode 100644 index 5a035d3..0000000 --- a/js/shortcode-ui-js-widgets.js +++ /dev/null @@ -1,62 +0,0 @@ -/* global wp, jQuery, JSON, module */ -/* eslint-disable strict */ -/* eslint-disable complexity */ - -wp.shortcake.JSWidgets = (function( $ ) { // eslint-disable-line no-unused-vars - 'use strict'; - - var component = {}; - - component.init = function initShortcakeJSWidgets() { - wp.shortcake.hooks.addAction( 'shortcode-ui.render_new', component.embedForm ); - wp.shortcake.hooks.addAction( 'shortcode-ui.render_edit', component.embedForm ); - - // @todo Call form.destuct() on shortcode-ui.render_destroy? - }; - - /** - * Embed widget form. - * - * @param {Backbone.Model} shortcakeModel Shortcake model. - * @returns {void} - */ - component.embedForm = function embedForm( shortcakeModel ) { - var FormConstructor, form, syncInput, container, instanceValue; - - if ( ! shortcakeModel.get( 'widgetType' ) || ! wp.widgets.formConstructor[ shortcakeModel.get( 'widgetType' ) ] ) { - return; - } - - FormConstructor = wp.widgets.formConstructor[ shortcakeModel.get( 'widgetType' ) ]; - - syncInput = $( '.edit-shortcode-form .shortcode-ui-edit-' + shortcakeModel.get( 'shortcode_tag' ) ).find( 'input[name="encoded_json_instance"]' ); - if ( ! syncInput.length ) { - return; - } - - // @todo syncInput is unexpectedly persisting a value after closing the lightbox without pressing Update, even though the shortcode attributes remain unchanged until Update pressed. - instanceValue = new wp.customize.Value( syncInput.val() ? JSON.parse( syncInput.val() ) : {} ); - - container = $( '
', { - 'class': 'js-widget-form-shortcode-ui' - } ); - syncInput.after( container ); - form = new FormConstructor( { - model: instanceValue, - container: container - } ); - form.render(); - - instanceValue.bind( function( instanceData ) { - syncInput.val( JSON.stringify( instanceData ) ); - syncInput.trigger( 'input' ); - } ); - }; - - if ( 'undefined' !== typeof module ) { - module.exports = component; - } - - return component; - -})( jQuery ); diff --git a/js/shortcode-ui-view-widget-form-field.js b/js/shortcode-ui-view-widget-form-field.js new file mode 100644 index 0000000..a02153e --- /dev/null +++ b/js/shortcode-ui-view-widget-form-field.js @@ -0,0 +1,47 @@ +/* global wp, JSON, Shortcode_UI */ +/* eslint-disable strict */ +/* eslint consistent-this: [ "error", "view" ] */ +/* eslint-disable complexity */ + +Shortcode_UI.views.widgetFormField = (function( sui ) { + 'use strict'; + + /** + * Widget form. + * + * @class + */ + return sui.views.editAttributeField.extend( { + + events: {}, + + /** + * Render. + * + * @return {sui.views.editAttributeField} View. + */ + render: function() { + var view = this, FormConstructor, instanceValue, form; + + if ( ! view.shortcode.get( 'widgetType' ) || ! wp.widgets.formConstructor[ view.shortcode.get( 'widgetType' ) ] ) { + throw new Error( 'Unable to determine the widget type.' ); + } + + view.$el.addClass( 'js-widget-form-shortcode-ui' ); + instanceValue = new wp.customize.Value( view.getValue() ? JSON.parse( view.getValue() ) : {} ); + FormConstructor = wp.widgets.formConstructor[ view.shortcode.get( 'widgetType' ) ]; + form = new FormConstructor( { + model: instanceValue, + container: view.$el + } ); + instanceValue.bind( function( instanceData ) { + view.setValue( JSON.stringify( instanceData ) ); + } ); + form.render(); + + view.triggerCallbacks(); + return view; + } + } ); + +})( Shortcode_UI ); diff --git a/php/class-js-widget-shortcode-controller.php b/php/class-js-widget-shortcode-controller.php index 315fb93..e0761bd 100644 --- a/php/class-js-widget-shortcode-controller.php +++ b/php/class-js-widget-shortcode-controller.php @@ -96,7 +96,12 @@ public function get_sidebar_args() { $args['before_widget'] = sprintf( $args['before_widget'], $widget_id, $this->widget->widget_options['classname'] ); /** This filter is documented in wp-includes/widgets.php */ - $params = apply_filters( 'dynamic_sidebar_params', array( $args, array( 'number' => null ) ) ); + $params = apply_filters( 'dynamic_sidebar_params', array( + $args, + array( + 'number' => null, + ), + ) ); return $params[0]; } @@ -111,7 +116,9 @@ public function get_sidebar_args() { */ public function render_widget_shortcode( $atts ) { $atts = shortcode_atts( - array( 'encoded_json_instance' => '' ), + array( + 'encoded_json_instance' => '', + ), $atts, $this->get_shortcode_tag() ); diff --git a/php/class-js-widgets-plugin.php b/php/class-js-widgets-plugin.php index 20237a4..c59a733 100644 --- a/php/class-js-widgets-plugin.php +++ b/php/class-js-widgets-plugin.php @@ -164,10 +164,10 @@ public function register_scripts( WP_Scripts $wp_scripts ) { $deps = array( 'admin-widgets', $this->script_handles['form'] ); $wp_scripts->add( $this->script_handles['admin-js-widgets'], $src, $deps, $this->version ); - $this->script_handles['shortcode-ui-js-widgets'] = 'shortcode-ui-js-widgets'; - $src = $plugin_dir_url . 'js/shortcode-ui-js-widgets.js'; + $this->script_handles['shortcode-ui-view-widget-form-field'] = 'shortcode-ui-view-widget-form-field'; + $src = $plugin_dir_url . 'js/shortcode-ui-view-widget-form-field.js'; $deps = array( 'shortcode-ui', $this->script_handles['form'] ); - $wp_scripts->add( $this->script_handles['shortcode-ui-js-widgets'], $src, $deps, $this->version ); + $wp_scripts->add( $this->script_handles['shortcode-ui-view-widget-form-field'], $src, $deps, $this->version ); $this->script_handles['trac-39389-controls'] = 'js-widgets-trac-39389-controls'; $src = $plugin_dir_url . 'js/trac-39389-controls.js'; @@ -298,8 +298,7 @@ function enqueue_widgets_admin_scripts( $hook_suffix ) { function enqueue_shortcode_ui() { global $wp_widget_factory; - wp_enqueue_script( $this->script_handles['shortcode-ui-js-widgets'] ); - wp_add_inline_script( $this->script_handles['shortcode-ui-js-widgets'], 'wp.shortcake.JSWidgets.init()' ); + wp_enqueue_script( $this->script_handles['shortcode-ui-view-widget-form-field'] ); foreach ( $wp_widget_factory->widgets as $widget ) { if ( $widget instanceof WP_JS_Widget ) { @@ -400,6 +399,7 @@ public function register_widget_shortcodes() { public function filter_shortcode_ui_fields( $fields ) { $fields['widget_form'] = array( 'template' => 'shortcode-ui-field-widget_form', + 'view' => 'widgetFormField', ); return $fields; } @@ -409,12 +409,6 @@ public function filter_shortcode_ui_fields( $fields ) { */ public function print_shortcode_ui_templates() { $this->render_widget_form_template_scripts(); - - ?> - - Date: Mon, 13 Feb 2017 23:39:13 -0800 Subject: [PATCH 69/92] Replace deprecated name param with field param --- core-adapter-widgets/tag_cloud/class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core-adapter-widgets/tag_cloud/class.php b/core-adapter-widgets/tag_cloud/class.php index 8651547..743d51d 100644 --- a/core-adapter-widgets/tag_cloud/class.php +++ b/core-adapter-widgets/tag_cloud/class.php @@ -184,7 +184,7 @@ public function render_form_template() { $taxonomy_choices[ $taxonomy->name ] = $taxonomy->label; } $this->render_form_field_template( array( - 'name' => 'taxonomy', + 'field' => 'taxonomy', 'label' => __( 'Taxonomy:', 'default' ), 'type' => 'select', 'choices' => $taxonomy_choices, From 5d7ccb03db5909d95cb3e917ad8ce4e0863a704d Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 13 Feb 2017 23:39:37 -0800 Subject: [PATCH 70/92] Ensure that id_base is defined on widget form prototypes --- js/widget-form.js | 4 ++++ php/class-wp-adapter-js-widget.php | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/js/widget-form.js b/js/widget-form.js index c6d7100..08c3c14 100644 --- a/js/widget-form.js +++ b/js/widget-form.js @@ -40,6 +40,10 @@ wp.widgets.Form = (function( api, $, _ ) { initialize: function initialize( properties ) { var form = this; + if ( ! form.id_base ) { + throw new Error( 'Missing id_base' ); + } + _.extend( form, getValidatedFormProperties( form.config, properties ) ); form.setting = form.model; // @todo Deprecate 'setting' name in favor of 'model'? diff --git a/php/class-wp-adapter-js-widget.php b/php/class-wp-adapter-js-widget.php index 596510f..200ce68 100644 --- a/php/class-wp-adapter-js-widget.php +++ b/php/class-wp-adapter-js-widget.php @@ -87,6 +87,11 @@ public function enqueue_control_scripts() { wp_json_encode( $this->id_base ), wp_json_encode( $this->get_form_config() ) ) ); + wp_add_inline_script( $handle, sprintf( + 'wp.widgets.formConstructor[ %s ].prototype.id_base = %s;', + wp_json_encode( $this->id_base ), + wp_json_encode( $this->id_base ) + ) ); } /** From 268824486768ed4728994832a32013ed39a13d9d Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 14 Feb 2017 00:08:18 -0800 Subject: [PATCH 71/92] Merge props to validate on top of existing model --- js/widget-form.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/js/widget-form.js b/js/widget-form.js index 08c3c14..5e203a5 100644 --- a/js/widget-form.js +++ b/js/widget-form.js @@ -197,12 +197,11 @@ wp.widgets.Form = (function( api, $, _ ) { */ setState: function setState( props ) { var form = this, value, validated; - validated = form.validate( props || {} ); + validated = form.validate( _.extend( {}, form.model.get(), props ) ); if ( ! validated || validated instanceof Error || validated instanceof api.Notification ) { return; } - value = _.extend( {}, form.model.get(), validated ); - form.model.set( value ); + form.model.set( validated ); }, /** @@ -221,7 +220,7 @@ wp.widgets.Form = (function( api, $, _ ) { createSyncedPropertyValue: function createSyncedPropertyValue( root, property ) { var form = this, propertyValue, rootChangeListener, propertyChangeListener; - propertyValue = new api.Value( root.get()[ property ] ); + propertyValue = new api.Value( form.getValue()[ property ] ); // Sync changes to the property back to the root value. propertyChangeListener = function( newPropertyValue ) { From e796fed73844777c5568af3198a95d1df30e1f49 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Tue, 14 Feb 2017 12:37:26 -0500 Subject: [PATCH 72/92] Update setState test to better prove validate --- tests/js/widget-form.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/js/widget-form.js b/tests/js/widget-form.js index ecb01ba..a86996a 100644 --- a/tests/js/widget-form.js +++ b/tests/js/widget-form.js @@ -376,7 +376,9 @@ describe( 'wp.widgets.Form', function() { } ); it( 'calls form.validate() on its value before updating the model', function() { - form.validate = () => ( { valid: 'good' } ); + form.validate = original => { + return Object.assign( {}, _.omit( original, [ 'invalid' ] ), { valid: 'good' } ); + }; form.setState( { invalid: 'bad' } ); expect( model.get() ).to.eql( { hello: 'world', valid: 'good' } ); } ); From 29af2254734a1e7541c81bc4e26e260298c98371 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Tue, 14 Feb 2017 12:40:12 -0500 Subject: [PATCH 73/92] Add test for early merge of model properties in setState --- tests/js/widget-form.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/js/widget-form.js b/tests/js/widget-form.js index a86996a..c13de25 100644 --- a/tests/js/widget-form.js +++ b/tests/js/widget-form.js @@ -383,6 +383,12 @@ describe( 'wp.widgets.Form', function() { expect( model.get() ).to.eql( { hello: 'world', valid: 'good' } ); } ); + it( 'calls form.validate() with the new value merged with the model\'s current value', function() { + form.validate = sinon.spy(); + form.setState( { foo: 'bar' } ); + expect( form.validate ).to.be.calledWith( { hello: 'world', foo: 'bar' } ); + } ); + it( 'if form.validate returns an Error, the model is left unchanged', function() { form.validate = () => new Error( 'test error' ); form.setState( { invalid: 'bad' } ); From 2b495c99010667d5ae2938b18e4b1f7ece35a6c1 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Tue, 14 Feb 2017 12:42:01 -0500 Subject: [PATCH 74/92] Remove unused variable in setState --- js/widget-form.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/widget-form.js b/js/widget-form.js index 5e203a5..4a1aa87 100644 --- a/js/widget-form.js +++ b/js/widget-form.js @@ -196,7 +196,7 @@ wp.widgets.Form = (function( api, $, _ ) { * @returns {void} */ setState: function setState( props ) { - var form = this, value, validated; + var form = this, validated; validated = form.validate( _.extend( {}, form.model.get(), props ) ); if ( ! validated || validated instanceof Error || validated instanceof api.Notification ) { return; From a1c95338dffe22e7e936cf1da25383e680d2b459 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Tue, 14 Feb 2017 19:13:00 -0500 Subject: [PATCH 75/92] Remove id_base from Form tests --- tests/js/widget-form.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/tests/js/widget-form.js b/tests/js/widget-form.js index c13de25..753a26d 100644 --- a/tests/js/widget-form.js +++ b/tests/js/widget-form.js @@ -35,7 +35,6 @@ describe( 'wp.widgets.Form', function() { resetGlobals(); jQuery( '.root' ).html( markup ); CustomForm = Form.extend( { - id_base: 'custom', config: { default_instance: { red: '#f00' } }, } ); } ); @@ -72,16 +71,6 @@ describe( 'wp.widgets.Form', function() { throw new Error( 'Expected initialize to throw an Error but it did not' ); } ); - it( 'throws an error if the id_base property is not set on the Form prototype', function( done ) { - CustomForm.prototype.id_base = null; - try { - new CustomForm( { model, container } ); - } catch ( err ) { - done(); - } - throw new Error( 'Expected initialize to throw an Error but it did not' ); - } ); - it( 'throws an error if the config.default_instance property is not set on the Form prototype', function( done ) { CustomForm.prototype.config.default_instance = null; try { From caa5b49c568611e888ad9450401b473df2ccb071 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Tue, 14 Feb 2017 19:13:18 -0500 Subject: [PATCH 76/92] Remove id_base property from Form --- js/widget-form.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/js/widget-form.js b/js/widget-form.js index 4a1aa87..07e4372 100644 --- a/js/widget-form.js +++ b/js/widget-form.js @@ -31,7 +31,6 @@ wp.widgets.Form = (function( api, $, _ ) { * Initialize. * * @param {object} properties Properties. - * @param {string} properties.id_base The widget ID base (aka type). * @param {wp.customize.Value} properties.model The Value or Setting instance containing the widget instance data object. * @param {string|Element|jQuery} properties.container The selector string or DOM element in which to render this Form. * @param {object} properties.config Form config. @@ -40,10 +39,6 @@ wp.widgets.Form = (function( api, $, _ ) { initialize: function initialize( properties ) { var form = this; - if ( ! form.id_base ) { - throw new Error( 'Missing id_base' ); - } - _.extend( form, getValidatedFormProperties( form.config, properties ) ); form.setting = form.model; // @todo Deprecate 'setting' name in favor of 'model'? @@ -468,9 +463,6 @@ wp.widgets.Form = (function( api, $, _ ) { if ( 0 === widgetForm.container.length ) { throw new Error( 'Widget Form is missing container property as Element or jQuery.' ); } - if ( ! widgetForm.id_base ) { - throw new Error( 'Widget Form class is missing id_base' ); - } if ( ! widgetForm.config || ! widgetForm.config.default_instance ) { throw new Error( 'Widget Form class is missing config.default_instance' ); } From a7f8d0778f031dc94d47b245c43c05b5031f7adf Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Tue, 14 Feb 2017 19:14:33 -0500 Subject: [PATCH 77/92] Remove id_base from core-adapter-widgets --- core-adapter-widgets/archives/form.js | 4 +--- core-adapter-widgets/categories/form.js | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/core-adapter-widgets/archives/form.js b/core-adapter-widgets/archives/form.js index fb24a29..0621a96 100644 --- a/core-adapter-widgets/archives/form.js +++ b/core-adapter-widgets/archives/form.js @@ -13,9 +13,7 @@ wp.widgets.formConstructor.archives = (function() { * * @constructor */ - ArchivesWidgetForm = wp.widgets.Form.extend( { - id_base: 'archives' - } ); + ArchivesWidgetForm = wp.widgets.Form.extend( {} ); if ( 'undefined' !== typeof module ) { module.exports = ArchivesWidgetForm; diff --git a/core-adapter-widgets/categories/form.js b/core-adapter-widgets/categories/form.js index a852d4e..d198c97 100644 --- a/core-adapter-widgets/categories/form.js +++ b/core-adapter-widgets/categories/form.js @@ -13,9 +13,7 @@ wp.widgets.formConstructor.categories = (function() { * * @constructor */ - CategoriesWidgetForm = wp.widgets.Form.extend( { - id_base: 'categories' - } ); + CategoriesWidgetForm = wp.widgets.Form.extend( {} ); if ( 'undefined' !== typeof module ) { module.exports = CategoriesWidgetForm; From 519411542b1b8b1ccc71ebb6c6a4c1c17a50550e Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 15 Feb 2017 14:43:21 -0800 Subject: [PATCH 78/92] Rename validateForm to assertValidForm --- js/widget-form.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/widget-form.js b/js/widget-form.js index 07e4372..2dee2f6 100644 --- a/js/widget-form.js +++ b/js/widget-form.js @@ -46,7 +46,7 @@ wp.widgets.Form = (function( api, $, _ ) { form.renderNotifications = _.bind( form.renderNotifications, form ); form.container = $( form.container ); - validateForm( form ); + assertValidForm( form ); }, /** @@ -456,7 +456,7 @@ wp.widgets.Form = (function( api, $, _ ) { * @param {Form} widgetForm The instance of the Form to modify * @return {void} */ - function validateForm( widgetForm ) { + function assertValidForm( widgetForm ) { if ( ! widgetForm.model || ! widgetForm.model.extended || ! widgetForm.model.extended( api.Value ) ) { throw new Error( 'Widget Form is missing model property which must be a Value or Setting instance.' ); } From 8562511c4c9439395593d1e421a14617c19b6596 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 15 Feb 2017 21:22:03 -0800 Subject: [PATCH 79/92] Update readme, bump version, add props [ci skip] --- js-widgets.php | 4 ++-- post-collection-widget.php | 4 ++-- readme.md | 5 ++++- readme.txt | 6 +++++- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/js-widgets.php b/js-widgets.php index 13b446a..d5a811e 100644 --- a/js-widgets.php +++ b/js-widgets.php @@ -3,8 +3,8 @@ * Plugin Name: JS Widgets * Description: The next generation of widgets in core, embracing JS for UI and powering the Widgets REST API. * Plugin URI: https://github.com/xwp/wp-js-widgets/ - * Version: 0.3.0 - * Author: Weston Ruter, XWP + * Version: 0.3.0-beta + * Author: XWP * Author URI: https://make.xwp.co/ * License: GPLv2+ * diff --git a/post-collection-widget.php b/post-collection-widget.php index d8c571e..085a9b0 100644 --- a/post-collection-widget.php +++ b/post-collection-widget.php @@ -3,8 +3,8 @@ * Plugin Name: JS Widgets: Post Collection Widget * Description: A widget allowing for featuring a curated list of posts. * Plugin URI: https://github.com/xwp/wp-js-widgets/ - * Version: 0.3.0-alpha - * Author: Weston Ruter, XWP + * Version: 0.3.0-beta + * Author: XWP * Author URI: https://make.xwp.co/ * License: GPLv2+ * diff --git a/readme.md b/readme.md index 7343d91..597602f 100644 --- a/readme.md +++ b/readme.md @@ -3,7 +3,7 @@ The next generation of widgets in core, embracing JS for UI and powering the Widgets REST API. -**Contributors:** [xwp](https://profiles.wordpress.org/xwp), [westonruter](https://profiles.wordpress.org/westonruter) +**Contributors:** [xwp](https://profiles.wordpress.org/xwp), [westonruter](https://profiles.wordpress.org/westonruter), [sirbrillig](https://profiles.wordpress.org/sirbrillig) **Tags:** [customizer](https://wordpress.org/plugins/tags/customizer), [widgets](https://wordpress.org/plugins/tags/widgets), [rest-api](https://wordpress.org/plugins/tags/rest-api) **Requires at least:** 4.7.0 **Tested up to:** 4.7.0 @@ -83,6 +83,9 @@ See [issues and PRs in milestone](https://github.com/xwp/wp-js-widgets/milestone See also updated [Customizer Object Selector](https://wordpress.org/plugins/customize-object-selector/) and [Next Recent Posts Widget](https://github.com/xwp/wp-next-recent-posts-widget) plugins. +### 0.3.0 - 2017-02-?? (Unreleased) ### +* Refactor of `Form` along with introduction of JS unit tests. See [#35](https://github.com/xwp/wp-js-widgets/pull/35). Props [sirbrillig](https://profiles.wordpress.org/sirbrillig)! + ### 0.2.0 - 2017-01-02 ### * Important: Update minimum WordPress core version to 4.7.0. * Eliminate `Form#embed` JS method in favor of just `Form#render`. Introduce `Form#destruct` to handle unmounting a rendered form. diff --git a/readme.txt b/readme.txt index d343b0b..3672bfc 100644 --- a/readme.txt +++ b/readme.txt @@ -1,5 +1,5 @@ === JS Widgets === -Contributors: xwp, westonruter +Contributors: xwp, westonruter, sirbrillig Tags: customizer, widgets, rest-api Requires at least: 4.7.0 Tested up to: 4.7.0 @@ -81,6 +81,10 @@ See [issues and PRs in milestone](https://github.com/xwp/wp-js-widgets/milestone See also updated [Customizer Object Selector](https://wordpress.org/plugins/customize-object-selector/) and [Next Recent Posts Widget](https://github.com/xwp/wp-next-recent-posts-widget) plugins. += 0.3.0 - 2017-02-?? (Unreleased) = + +* Refactor of `Form` along with introduction of JS unit tests. See [#35](https://github.com/xwp/wp-js-widgets/pull/35). Props [sirbrillig](https://profiles.wordpress.org/sirbrillig)! + = 0.2.0 - 2017-01-02 = * Important: Update minimum WordPress core version to 4.7.0. From 192f7b57f5f1c75e0d331a7b846ef13d5d7084e8 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 16 Feb 2017 10:59:33 -0800 Subject: [PATCH 80/92] Fix excluding pages in the Pages widget --- core-adapter-widgets/pages/class.php | 17 +++++++++++++++++ core-adapter-widgets/pages/form.js | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/core-adapter-widgets/pages/class.php b/core-adapter-widgets/pages/class.php index 477ba6d..335c497 100644 --- a/core-adapter-widgets/pages/class.php +++ b/core-adapter-widgets/pages/class.php @@ -90,6 +90,23 @@ public function sanitize_exclude( $value, $request, $param ) { return join( ',', wp_parse_id_list( $value ) ); // String as needed by WP_Widget_Pages. } + /** + * Render widget. + * + * @param array $args Widget args. + * @param array $instance Widget instance. + * @return void + */ + public function render( $args, $instance ) { + + // Convert an array exclude into a string exclude since wp_list_pages() requires it. + if ( isset( $instance['exclude'] ) && is_array( $instance['exclude'] ) ) { + $instance['exclude'] = join( ',', $instance['exclude'] ); + } + + $this->adapted_widget->widget( $args, $instance ); + } + /** * Sanitize instance data. * diff --git a/core-adapter-widgets/pages/form.js b/core-adapter-widgets/pages/form.js index 5a63922..6f344da 100644 --- a/core-adapter-widgets/pages/form.js +++ b/core-adapter-widgets/pages/form.js @@ -81,8 +81,8 @@ wp.widgets.formConstructor.pages = (function( api ) { excludeIds.push( id ); } } ); + form.model._value.exclude = excludeIds; } - form.model._value.exclude = excludeIds; form.syncedProperties.exclude = form.createSyncedPropertyValue( form.model, 'exclude' ); } From 3a64d1a757b018c5f3162e12224d65182316bc5e Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 16 Feb 2017 15:35:13 -0800 Subject: [PATCH 81/92] Ensure attributes passed to shortcodes get validated and sanitized --- core-adapter-widgets/pages/class.php | 17 ----------------- php/class-js-widget-shortcode-controller.php | 7 +++++-- 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/core-adapter-widgets/pages/class.php b/core-adapter-widgets/pages/class.php index 335c497..477ba6d 100644 --- a/core-adapter-widgets/pages/class.php +++ b/core-adapter-widgets/pages/class.php @@ -90,23 +90,6 @@ public function sanitize_exclude( $value, $request, $param ) { return join( ',', wp_parse_id_list( $value ) ); // String as needed by WP_Widget_Pages. } - /** - * Render widget. - * - * @param array $args Widget args. - * @param array $instance Widget instance. - * @return void - */ - public function render( $args, $instance ) { - - // Convert an array exclude into a string exclude since wp_list_pages() requires it. - if ( isset( $instance['exclude'] ) && is_array( $instance['exclude'] ) ) { - $instance['exclude'] = join( ',', $instance['exclude'] ); - } - - $this->adapted_widget->widget( $args, $instance ); - } - /** * Sanitize instance data. * diff --git a/php/class-js-widget-shortcode-controller.php b/php/class-js-widget-shortcode-controller.php index e0761bd..a4986d3 100644 --- a/php/class-js-widget-shortcode-controller.php +++ b/php/class-js-widget-shortcode-controller.php @@ -126,8 +126,11 @@ public function render_widget_shortcode( $atts ) { $instance_data = array(); if ( ! empty( $atts['encoded_json_instance'] ) ) { $decoded_instance_data = json_decode( urldecode( $atts['encoded_json_instance'] ), true ); - if ( is_array( $decoded_instance_data ) ) { - $instance_data = $decoded_instance_data; + if ( is_array( $decoded_instance_data ) && true === $this->widget->validate( $decoded_instance_data ) ) { + $instance_data = $this->widget->sanitize( $decoded_instance_data, array() ); + if ( is_wp_error( $instance_data ) ) { + $instance_data = array(); + } } } From d703fd234c59f475110e1af254526a1a6869358c Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 16 Feb 2017 16:40:02 -0800 Subject: [PATCH 82/92] Separate out shortcode UI logic into separate class --- php/class-js-widgets-plugin.php | 78 +++------------------- php/class-js-widgets-shortcode-ui.php | 96 +++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 68 deletions(-) create mode 100644 php/class-js-widgets-shortcode-ui.php diff --git a/php/class-js-widgets-plugin.php b/php/class-js-widgets-plugin.php index c59a733..246863d 100644 --- a/php/class-js-widgets-plugin.php +++ b/php/class-js-widgets-plugin.php @@ -26,13 +26,6 @@ class JS_Widgets_Plugin { */ public $rest_api_namespace = 'js-widgets/v1'; - /** - * Registered controllers for widget shortcodes. - * - * @var JS_Widget_Shortcode_Controller[] - */ - public $widget_shortcodes = array(); - /** * Instances of JS_Widgets_REST_Controller for each widget type. * @@ -78,6 +71,13 @@ class JS_Widgets_Plugin { */ public $script_handles = array(); + /** + * Shortcode UI (Shortcake) integration. + * + * @var Shortcode_UI + */ + public $shortcode_ui; + /** * Plugin constructor. */ @@ -118,12 +118,9 @@ public function init() { add_action( 'in_widget_form', array( $this, 'stop_capturing_in_widget_form' ), 1000, 3 ); // Shortcake integration. - add_action( 'widgets_init', array( $this, 'register_widget_shortcodes' ), 90 ); - add_filter( 'shortcode_ui_fields', array( $this, 'filter_shortcode_ui_fields' ) ); - add_action( 'print_shortcode_ui_templates', array( $this, 'print_shortcode_ui_templates' ) ); - add_action( 'enqueue_shortcode_ui', array( $this, 'enqueue_shortcode_ui' ) ); - - // @todo Add widget REST endpoint for getting the rendered value of widgets. Note originating context URL will need to be supplied when rendering some widgets. + require_once __DIR__ . '/class-js-widgets-shortcode-ui.php'; + $this->shortcode_ui = new JS_Widgets_Shortcode_UI( $this ); + $this->shortcode_ui->add_hooks(); } /** @@ -290,23 +287,6 @@ function enqueue_widgets_admin_scripts( $hook_suffix ) { } } - /** - * Enqueue scripts for shortcode UI. - * - * @global WP_Widget_Factory $wp_widget_factory - */ - function enqueue_shortcode_ui() { - global $wp_widget_factory; - - wp_enqueue_script( $this->script_handles['shortcode-ui-view-widget-form-field'] ); - - foreach ( $wp_widget_factory->widgets as $widget ) { - if ( $widget instanceof WP_JS_Widget ) { - $widget->enqueue_control_scripts(); - } - } - } - /** * Enqueue scripts on the frontend. * @@ -373,44 +353,6 @@ public function upgrade_core_widgets() { } } - /** - * Register widget shortcodes. - * - * @global WP_Widget_Factory $wp_widget_factory - */ - public function register_widget_shortcodes() { - global $wp_widget_factory; - require_once __DIR__ . '/class-js-widget-shortcode-controller.php'; - foreach ( $wp_widget_factory->widgets as $widget ) { - if ( $widget instanceof WP_JS_Widget ) { - $widget_shortcode = new JS_Widget_Shortcode_Controller( $this, $widget ); - $widget_shortcode->register_shortcode(); - add_action( 'register_shortcode_ui', array( $widget_shortcode, 'register_shortcode_ui' ) ); - } - } - } - - /** - * Add widget_form as a new shortcode UI field. - * - * @param array $fields Shortcode fields. - * @return array Fields. - */ - public function filter_shortcode_ui_fields( $fields ) { - $fields['widget_form'] = array( - 'template' => 'shortcode-ui-field-widget_form', - 'view' => 'widgetFormField', - ); - return $fields; - } - - /** - * Print shortcode UI templates. - */ - public function print_shortcode_ui_templates() { - $this->render_widget_form_template_scripts(); - } - /** * Replace instances of `WP_Widget_Form_Customize_Control` for JS Widgets to exclude PHP-generated content. * diff --git a/php/class-js-widgets-shortcode-ui.php b/php/class-js-widgets-shortcode-ui.php new file mode 100644 index 0000000..fbc0898 --- /dev/null +++ b/php/class-js-widgets-shortcode-ui.php @@ -0,0 +1,96 @@ +plugin = $plugin; + } + + /** + * Add hooks. + */ + public function add_hooks() { + add_action( 'widgets_init', array( $this, 'register_widget_shortcodes' ), 90 ); + add_filter( 'shortcode_ui_fields', array( $this, 'filter_shortcode_ui_fields' ) ); + add_action( 'print_shortcode_ui_templates', array( $this, 'print_shortcode_ui_templates' ) ); + add_action( 'enqueue_shortcode_ui', array( $this, 'enqueue_shortcode_ui' ) ); + } + + /** + * Enqueue scripts for shortcode UI. + * + * @global WP_Widget_Factory $wp_widget_factory + */ + function enqueue_shortcode_ui() { + global $wp_widget_factory; + + wp_enqueue_script( $this->plugin->script_handles['shortcode-ui-view-widget-form-field'] ); + + foreach ( $wp_widget_factory->widgets as $widget ) { + if ( $widget instanceof WP_JS_Widget ) { + $widget->enqueue_control_scripts(); + } + } + } + + /** + * Register widget shortcodes. + * + * @global WP_Widget_Factory $wp_widget_factory + */ + public function register_widget_shortcodes() { + global $wp_widget_factory; + require_once __DIR__ . '/class-js-widget-shortcode-controller.php'; + foreach ( $wp_widget_factory->widgets as $widget ) { + if ( $widget instanceof WP_JS_Widget ) { + $widget_shortcode = new JS_Widget_Shortcode_Controller( $this->plugin, $widget ); + $widget_shortcode->register_shortcode(); + add_action( 'register_shortcode_ui', array( $widget_shortcode, 'register_shortcode_ui' ) ); + } + } + } + + /** + * Add widget_form as a new shortcode UI field. + * + * @param array $fields Shortcode fields. + * @return array Fields. + */ + public function filter_shortcode_ui_fields( $fields ) { + $fields['widget_form'] = array( + 'template' => 'shortcode-ui-field-widget_form', + 'view' => 'widgetFormField', + ); + return $fields; + } + + /** + * Print shortcode UI templates. + */ + public function print_shortcode_ui_templates() { + $this->plugin->render_widget_form_template_scripts(); + } + +} From 69f68a09a6960c9e8962d16c94a0037ca1554dac Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 16 Feb 2017 16:44:09 -0800 Subject: [PATCH 83/92] Fix phpcs issues --- php/class-js-widgets-rest-controller.php | 37 ++++++++++++++++++------ php/class-wp-js-widget.php | 6 ++-- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/php/class-js-widgets-rest-controller.php b/php/class-js-widgets-rest-controller.php index 355833b..4e61201 100644 --- a/php/class-js-widgets-rest-controller.php +++ b/php/class-js-widgets-rest-controller.php @@ -181,7 +181,9 @@ public function register_routes() { 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_item_permissions_check' ), 'args' => array( - 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + 'context' => $this->get_context_param( array( + 'default' => 'view', + ) ), ), ), array( @@ -267,7 +269,10 @@ public function get_endpoint_args_for_item_schema( $method = WP_REST_Server::CRE // Only use required / default from arg_options on CREATABLE/EDITABLE endpoints. if ( ! $is_create_or_edit ) { - $params['arg_options'] = array_diff_key( $params['arg_options'], array( 'required' => '', 'default' => '' ) ); + $params['arg_options'] = array_diff_key( $params['arg_options'], array( + 'required' => '', + 'default' => '', + ) ); } $endpoint_args[ $field_id ] = array_merge( $endpoint_args[ $field_id ], $params['arg_options'] ); @@ -397,7 +402,9 @@ public function get_item_permissions_check( $request ) { public function get_items_permissions_check( $request ) { if ( 'edit' === $request['context'] && ! $this->current_user_can_manage_widgets() ) { - return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to edit widgets.', 'js-widgets' ), array( 'status' => rest_authorization_required_code() ) ); + return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to edit widgets.', 'js-widgets' ), array( + 'status' => rest_authorization_required_code(), + ) ); } return true; @@ -445,7 +452,9 @@ public function delete_item_permissions_check( $request ) { public function get_item( $request ) { $instances = $this->widget->get_settings(); if ( ! array_key_exists( $request['widget_number'], $instances ) ) { - return new WP_Error( 'rest_widget_invalid_number', __( 'Unknown widget.', 'js-widgets' ), array( 'status' => 404 ) ); + return new WP_Error( 'rest_widget_invalid_number', __( 'Unknown widget.', 'js-widgets' ), array( + 'status' => 404, + ) ); } $instance = $instances[ $request['widget_number'] ]; @@ -464,16 +473,22 @@ public function get_item( $request ) { public function update_item( $request ) { $instances = $this->widget->get_settings(); if ( ! array_key_exists( $request['widget_number'], $instances ) ) { - return new WP_Error( 'rest_widget_invalid_number', __( 'Unknown widget.', 'js-widgets' ), array( 'status' => 404 ) ); + return new WP_Error( 'rest_widget_invalid_number', __( 'Unknown widget.', 'js-widgets' ), array( + 'status' => 404, + ) ); } $old_instance = $instances[ $request['widget_number'] ]; $expected_id = $this->get_object_id( $request['widget_number'] ); if ( ! empty( $request['id'] ) && $expected_id !== $request['id'] ) { - return new WP_Error( 'rest_widget_unexpected_id', __( 'Widget ID mismatch.', 'js-widgets' ), array( 'status' => 400 ) ); + return new WP_Error( 'rest_widget_unexpected_id', __( 'Widget ID mismatch.', 'js-widgets' ), array( + 'status' => 400, + ) ); } if ( ! empty( $request['type'] ) && $this->get_object_type() !== $request['type'] ) { - return new WP_Error( 'rest_widget_unexpected_type', __( 'Widget type mismatch.', 'js-widgets' ), array( 'status' => 400 ) ); + return new WP_Error( 'rest_widget_unexpected_type', __( 'Widget type mismatch.', 'js-widgets' ), array( + 'status' => 400, + ) ); } // Note that $new_instance has gone through the validate and sanitize callbacks defined on the instance schema. @@ -485,7 +500,9 @@ public function update_item( $request ) { return $instance; } if ( ! is_array( $instance ) ) { - return new WP_Error( 'rest_widget_sanitize_failed', __( 'Sanitization failed.', 'js-widgets' ), array( 'status' => 400 ) ); + return new WP_Error( 'rest_widget_sanitize_failed', __( 'Sanitization failed.', 'js-widgets' ), array( + 'status' => 400, + ) ); } $instances[ $request['widget_number'] ] = $instance; @@ -530,7 +547,9 @@ public function prepare_item_for_response( $instance, $request, $widget_number = $widget_number = $request['widget_number']; } if ( empty( $widget_number ) ) { - return new WP_Error( 'rest_widget_unavailable_widget_number', __( 'Unknown widget number.', 'js-widgets' ), array( 'status' => 500 ) ); + return new WP_Error( 'rest_widget_unavailable_widget_number', __( 'Unknown widget number.', 'js-widgets' ), array( + 'status' => 500, + ) ); } // Just in case. diff --git a/php/class-wp-js-widget.php b/php/class-wp-js-widget.php index a3a8adf..8498a79 100644 --- a/php/class-wp-js-widget.php +++ b/php/class-wp-js-widget.php @@ -613,12 +613,13 @@ protected function render_form_field_template( $args = array() ) { } elseif ( 'integer' === $schema_type || 'number' === $schema_type ) { $default_input_attrs['type'] = 'number'; } elseif ( 'string' === $schema_type && isset( $field_schema['format'] ) ) { + + // @todo Support date-time format. if ( 'uri' === $field_schema['format'] ) { $default_input_attrs['type'] = 'url'; } elseif ( 'email' === $field_schema['format'] ) { $default_input_attrs['type'] = 'email'; } - // @todo Support date-time format. } if ( 'integer' === $schema_type ) { @@ -673,9 +674,10 @@ protected function render_form_field_template( $args = array() ) { - + From fda18fe02eeab5ede5b4aa74013c3e92ea9c044c Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 16 Feb 2017 23:21:16 -0800 Subject: [PATCH 84/92] Remove leftover id_base exporting --- php/class-wp-adapter-js-widget.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/php/class-wp-adapter-js-widget.php b/php/class-wp-adapter-js-widget.php index 200ce68..596510f 100644 --- a/php/class-wp-adapter-js-widget.php +++ b/php/class-wp-adapter-js-widget.php @@ -87,11 +87,6 @@ public function enqueue_control_scripts() { wp_json_encode( $this->id_base ), wp_json_encode( $this->get_form_config() ) ) ); - wp_add_inline_script( $handle, sprintf( - 'wp.widgets.formConstructor[ %s ].prototype.id_base = %s;', - wp_json_encode( $this->id_base ), - wp_json_encode( $this->id_base ) - ) ); } /** From a4c06ae363eb13cd453578828eafba56a77daf6f Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 16 Feb 2017 23:22:30 -0800 Subject: [PATCH 85/92] Ensure frontend assets are enqueued for widgets as shortcodes --- php/class-js-widget-shortcode-controller.php | 1 + php/class-js-widgets-shortcode-ui.php | 93 ++++++++++++++++++++ php/class-wp-js-widget.php | 6 ++ 3 files changed, 100 insertions(+) diff --git a/php/class-js-widget-shortcode-controller.php b/php/class-js-widget-shortcode-controller.php index a4986d3..2dcff28 100644 --- a/php/class-js-widget-shortcode-controller.php +++ b/php/class-js-widget-shortcode-controller.php @@ -135,6 +135,7 @@ public function render_widget_shortcode( $atts ) { } ob_start(); + $this->widget->enqueue_frontend_scripts(); $this->widget->render( $this->get_sidebar_args(), $instance_data ); return ob_get_clean(); } diff --git a/php/class-js-widgets-shortcode-ui.php b/php/class-js-widgets-shortcode-ui.php index fbc0898..67c7e5e 100644 --- a/php/class-js-widgets-shortcode-ui.php +++ b/php/class-js-widgets-shortcode-ui.php @@ -32,10 +32,45 @@ public function __construct( JS_Widgets_Plugin $plugin ) { * Add hooks. */ public function add_hooks() { + add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_the_loop_shortcode_dependencies' ) ); add_action( 'widgets_init', array( $this, 'register_widget_shortcodes' ), 90 ); add_filter( 'shortcode_ui_fields', array( $this, 'filter_shortcode_ui_fields' ) ); add_action( 'print_shortcode_ui_templates', array( $this, 'print_shortcode_ui_templates' ) ); add_action( 'enqueue_shortcode_ui', array( $this, 'enqueue_shortcode_ui' ) ); + + add_action( 'shortcode_ui_before_do_shortcode', array( $this, 'before_do_shortcode' ) ); + add_action( 'shortcode_ui_after_do_shortcode', array( $this, 'after_do_shortcode' ) ); + } + + /** + * Enqueue scripts and styles for widgets that appear as shortcodes. + * + * @global WP_Widget_Factory $wp_widget_factory + * @global WP_Query $the_wp_query + */ + function enqueue_the_loop_shortcode_dependencies() { + global $wp_the_query, $wp_query, $wp_widget_factory; + + if ( empty( $wp_the_query ) ) { + return; + } + + $widgets_by_id_base = array(); + foreach ( $wp_widget_factory->widgets as $widget ) { + if ( $widget instanceof WP_JS_Widget ) { + $widgets_by_id_base[ $widget->id_base ] = $widget; + } + } + + $pattern = '#' . sprintf( '\[widget_(%s)', join( '|', array_keys( $widgets_by_id_base ) ) ) . '#'; + $all_content = join( ' ', wp_list_pluck( $wp_query->posts, 'post_content' ) ); + if ( ! preg_match_all( $pattern, $all_content, $matches ) ) { + return; + } + foreach ( $matches[1] as $matched_id_base ) { + $widget = $widgets_by_id_base[ $matched_id_base ]; + $widget->enqueue_frontend_scripts(); + } } /** @@ -93,4 +128,62 @@ public function print_shortcode_ui_templates() { $this->plugin->render_widget_form_template_scripts(); } + /** + * Whether footer scripts should be printed. + * + * @var bool + */ + protected $should_print_footer_scripts = false; + + /** + * Backup of suspended WP_Scripts. + * + * @var WP_Scripts + */ + protected $suspended_wp_scripts; + + /** + * Backup of suspended WP_Styles. + * + * @var WP_Styles + */ + protected $suspended_wp_styles; + + /** + * Handle printing before shortcode. + * + * @param string $shortcode Shortcode. + * @global WP_Widget_Factory $wp_widget_factory + */ + public function before_do_shortcode( $shortcode ) { + global $wp_scripts, $wp_styles; + + $this->should_print_footer_scripts = (bool) preg_match( '#^\[widget_(?P.+?)\s#', $shortcode ); + if ( ! $this->should_print_footer_scripts ) { + return; + } + + // Reset enqueued assets so that only the widget's specific assets will be enqueued. + $this->suspended_wp_scripts = $wp_scripts; + $this->suspended_wp_styles = $wp_styles; + $wp_scripts = null; + $wp_styles = null; + } + + /** + * Print scripts and styles that the widget depends on. + */ + public function after_do_shortcode() { + global $wp_scripts, $wp_styles; + if ( ! $this->should_print_footer_scripts ) { + return; + } + + // Prints head scripts and styles as well as footer scripts and required templates. + wp_print_footer_scripts(); + + // Restore enqueued scripts and styles. + $wp_scripts = $this->suspended_wp_scripts; + $wp_styles = $this->suspended_wp_styles; + } } diff --git a/php/class-wp-js-widget.php b/php/class-wp-js-widget.php index 8498a79..81a475d 100644 --- a/php/class-wp-js-widget.php +++ b/php/class-wp-js-widget.php @@ -453,6 +453,12 @@ public function validate( $value ) { * @param array $instance The settings for the particular instance of the widget. */ final public function widget( $args, $instance ) { + /* + * Make sure frontend scripts and styles get enqueued if not already done. + * This is particularly important in the case of a widget used in a shortcode. + */ + $this->enqueue_frontend_scripts(); + $this->render( $args, $instance ); } From ae6ba7895a5b4607289c3058af172897e09bd83f Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 16 Feb 2017 23:58:10 -0800 Subject: [PATCH 86/92] Ensure dashicon related to a JS Widget gets applied in the available widgets panel --- php/class-js-widgets-plugin.php | 26 ++++++++++++++++++++++++++ php/class-js-widgets-shortcode-ui.php | 6 +++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/php/class-js-widgets-plugin.php b/php/class-js-widgets-plugin.php index 246863d..f83311f 100644 --- a/php/class-js-widgets-plugin.php +++ b/php/class-js-widgets-plugin.php @@ -107,6 +107,7 @@ public function init() { add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_frontend_scripts' ) ); add_action( 'rest_api_init', array( $this, 'rest_api_init' ), 100 ); add_action( 'customize_controls_enqueue_scripts', array( $this, 'enqueue_customize_controls_scripts' ) ); + add_action( 'customize_controls_print_scripts', array( $this, 'print_available_widget_icon_styles' ), 100 ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_widgets_admin_scripts' ) ); add_action( 'customize_controls_print_footer_scripts', array( $this, 'render_widget_form_template_scripts' ) ); add_action( 'admin_footer-widgets.php', array( $this, 'render_widget_form_template_scripts' ) ); @@ -250,6 +251,31 @@ function enqueue_customize_controls_scripts() { wp_enqueue_script( $this->script_handles['trac-39389-controls'] ); } + /** + * Print the CSS styles to ensure the defined Dashicon $icon_name in the available JS widgets panel. + * + * This is somewhat hacky to parse the Dashicons CSS to obtain the necessary CSS properties + * to output into the page + * + * @global WP_Widget_Factory $wp_widget_factory + */ + function print_available_widget_icon_styles() { + global $wp_widget_factory; + + $dashicons_content = file_get_contents( ABSPATH . WPINC . '/css/dashicons.css' ); + + echo ''; + } + /** * Enqueue scripts for the widgets admin screen. * diff --git a/php/class-js-widgets-shortcode-ui.php b/php/class-js-widgets-shortcode-ui.php index 67c7e5e..b4cefc2 100644 --- a/php/class-js-widgets-shortcode-ui.php +++ b/php/class-js-widgets-shortcode-ui.php @@ -153,7 +153,8 @@ public function print_shortcode_ui_templates() { * Handle printing before shortcode. * * @param string $shortcode Shortcode. - * @global WP_Widget_Factory $wp_widget_factory + * @global WP_Scripts $wp_scripts + * @global WP_Styles $wp_styles */ public function before_do_shortcode( $shortcode ) { global $wp_scripts, $wp_styles; @@ -172,6 +173,9 @@ public function before_do_shortcode( $shortcode ) { /** * Print scripts and styles that the widget depends on. + * + * @global WP_Scripts $wp_scripts + * @global WP_Styles $wp_styles */ public function after_do_shortcode() { global $wp_scripts, $wp_styles; From c1c892acbf71a3603cd2dba891546f8556b9ed5e Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 17 Feb 2017 09:47:11 -0800 Subject: [PATCH 87/92] Update readme and bump versions --- composer.json | 2 +- js-widgets.php | 2 +- package.json | 2 +- post-collection-widget.php | 2 +- readme.md | 13 ++++++++----- readme.txt | 15 +++++++++------ 6 files changed, 21 insertions(+), 15 deletions(-) diff --git a/composer.json b/composer.json index f17f22f..d854e77 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "xwp/wp-js-widgets", "description": "The next generation of widgets in Core (Widgets 3.0), embracing JS for UI and powering the Widgets REST API.", - "version": "0.3.0", + "version": "0.4.0", "type": "wordpress-plugin", "keywords": [ "customizer", "widgets", "rest-api" ], "homepage": "https://github.com/xwp/wp-js-widgets/", diff --git a/js-widgets.php b/js-widgets.php index d5a811e..9172d2e 100644 --- a/js-widgets.php +++ b/js-widgets.php @@ -3,7 +3,7 @@ * Plugin Name: JS Widgets * Description: The next generation of widgets in core, embracing JS for UI and powering the Widgets REST API. * Plugin URI: https://github.com/xwp/wp-js-widgets/ - * Version: 0.3.0-beta + * Version: 0.4.0 * Author: XWP * Author URI: https://make.xwp.co/ * License: GPLv2+ diff --git a/package.json b/package.json index 1a4f677..b0cf1b2 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git+https://github.com/xwp/wp-js-widgets.git" }, - "version": "0.3.0", + "version": "0.4.0", "license": "GPL-2.0+", "private": true, "devDependencies": { diff --git a/post-collection-widget.php b/post-collection-widget.php index 085a9b0..48de69d 100644 --- a/post-collection-widget.php +++ b/post-collection-widget.php @@ -3,7 +3,7 @@ * Plugin Name: JS Widgets: Post Collection Widget * Description: A widget allowing for featuring a curated list of posts. * Plugin URI: https://github.com/xwp/wp-js-widgets/ - * Version: 0.3.0-beta + * Version: 0.4.0 * Author: XWP * Author URI: https://make.xwp.co/ * License: GPLv2+ diff --git a/readme.md b/readme.md index 597602f..a8746cd 100644 --- a/readme.md +++ b/readme.md @@ -6,8 +6,8 @@ The next generation of widgets in core, embracing JS for UI and powering the Wid **Contributors:** [xwp](https://profiles.wordpress.org/xwp), [westonruter](https://profiles.wordpress.org/westonruter), [sirbrillig](https://profiles.wordpress.org/sirbrillig) **Tags:** [customizer](https://wordpress.org/plugins/tags/customizer), [widgets](https://wordpress.org/plugins/tags/widgets), [rest-api](https://wordpress.org/plugins/tags/rest-api) **Requires at least:** 4.7.0 -**Tested up to:** 4.7.0 -**Stable tag:** 0.3.0 +**Tested up to:** 4.8-alpha +**Stable tag:** 0.4.0 **License:** [GPLv2 or later](http://www.gnu.org/licenses/gpl-2.0.html) [![Build Status](https://travis-ci.org/xwp/wp-js-widgets.svg?branch=master)](https://travis-ci.org/xwp/wp-js-widgets) [![Coverage Status](https://coveralls.io/repos/xwp/wp-js-widgets/badge.svg?branch=master)](https://coveralls.io/github/xwp/wp-js-widgets) [![Built with Grunt](https://cdn.gruntjs.com/builtwith.svg)](http://gruntjs.com) [![devDependency Status](https://david-dm.org/xwp/wp-js-widgets/dev-status.svg)](https://david-dm.org/xwp/wp-js-widgets#info=devDependencies) @@ -26,7 +26,9 @@ This plugin implements: Features: +* Integrates with [Shortcake (Shortcode UI)](https://wordpress.org/plugins/shortcode-ui/) to allow all JS widgets to be made available as Post Elements in the editor. * Widget instance settings in the Customizer are exported from PHP as regular JSON without any PHP-serialized base64-encoded `encoded_serialized_instance` anywhere to be seen. +* Previewing widget changes in the customizer is faster since the `update-widget` Ajax request can be eliminated since the JS control can directly manipulate the widget instance data. * Widgets control forms use JS content templates instead of PHP to render the markup for each control, reducing the weight of the customizer load, especially when there are a lot of widgets in use. * Widgets that extend `WP_JS_Widget` will editable from both the customizer and the widgets admin page using the same `Form` JS interface. This `Form` is also able to be embedded in other contexts, like on the frontend and as a Shortcake (Shortcode UI) form. See [#11](https://github.com/xwp/wp-js-widgets/issues/11). * Widgets employ the JSON Schema from the REST API to define an instance with validation and sanitization of the instance properties, beyond also providing `validate` and `sanitize` methods that work on the instance array as a whole. @@ -50,6 +52,10 @@ Limitations/Caveats: ## Changelog ## +### 0.4.0 - 2017-02-17 ### +* Integrate with [Shortcake (Shortcode UI)](https://wordpress.org/plugins/shortcode-ui/) to allow any JS widget to be used inside the editor as a Post Element. See [#11](https://github.com/xwp/wp-js-widgets/issues/11), [#32](https://github.com/xwp/wp-js-widgets/pull/32). +* Refactor of `Form` along with introduction of JS unit tests. See [#35](https://github.com/xwp/wp-js-widgets/pull/35). Props [sirbrillig](https://profiles.wordpress.org/sirbrillig)! + ### 0.3.0 - 2017-01-11 ### Added: @@ -83,9 +89,6 @@ See [issues and PRs in milestone](https://github.com/xwp/wp-js-widgets/milestone See also updated [Customizer Object Selector](https://wordpress.org/plugins/customize-object-selector/) and [Next Recent Posts Widget](https://github.com/xwp/wp-next-recent-posts-widget) plugins. -### 0.3.0 - 2017-02-?? (Unreleased) ### -* Refactor of `Form` along with introduction of JS unit tests. See [#35](https://github.com/xwp/wp-js-widgets/pull/35). Props [sirbrillig](https://profiles.wordpress.org/sirbrillig)! - ### 0.2.0 - 2017-01-02 ### * Important: Update minimum WordPress core version to 4.7.0. * Eliminate `Form#embed` JS method in favor of just `Form#render`. Introduce `Form#destruct` to handle unmounting a rendered form. diff --git a/readme.txt b/readme.txt index 3672bfc..6b1919c 100644 --- a/readme.txt +++ b/readme.txt @@ -2,8 +2,8 @@ Contributors: xwp, westonruter, sirbrillig Tags: customizer, widgets, rest-api Requires at least: 4.7.0 -Tested up to: 4.7.0 -Stable tag: 0.3.0 +Tested up to: 4.8-alpha +Stable tag: 0.4.0 License: GPLv2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html @@ -23,7 +23,9 @@ This plugin implements: Features: +* Integrates with [Shortcake (Shortcode UI)](https://wordpress.org/plugins/shortcode-ui/) to allow all JS widgets to be made available as Post Elements in the editor. * Widget instance settings in the Customizer are exported from PHP as regular JSON without any PHP-serialized base64-encoded `encoded_serialized_instance` anywhere to be seen. +* Previewing widget changes in the customizer is faster since the `update-widget` Ajax request can be eliminated since the JS control can directly manipulate the widget instance data. * Widgets control forms use JS content templates instead of PHP to render the markup for each control, reducing the weight of the customizer load, especially when there are a lot of widgets in use. * Widgets that extend `WP_JS_Widget` will editable from both the customizer and the widgets admin page using the same `Form` JS interface. This `Form` is also able to be embedded in other contexts, like on the frontend and as a Shortcake (Shortcode UI) form. See [#11](https://github.com/xwp/wp-js-widgets/issues/11). * Widgets employ the JSON Schema from the REST API to define an instance with validation and sanitization of the instance properties, beyond also providing `validate` and `sanitize` methods that work on the instance array as a whole. @@ -47,6 +49,11 @@ Limitations/Caveats: == Changelog == += 0.4.0 - 2017-02-17 = + +* Integrate with [Shortcake (Shortcode UI)](https://wordpress.org/plugins/shortcode-ui/) to allow any JS widget to be used inside the editor as a Post Element. See [#11](https://github.com/xwp/wp-js-widgets/issues/11), [#32](https://github.com/xwp/wp-js-widgets/pull/32). +* Refactor of `Form` along with introduction of JS unit tests. See [#35](https://github.com/xwp/wp-js-widgets/pull/35). Props [sirbrillig](https://profiles.wordpress.org/sirbrillig)! + = 0.3.0 - 2017-01-11 = Added: @@ -81,10 +88,6 @@ See [issues and PRs in milestone](https://github.com/xwp/wp-js-widgets/milestone See also updated [Customizer Object Selector](https://wordpress.org/plugins/customize-object-selector/) and [Next Recent Posts Widget](https://github.com/xwp/wp-next-recent-posts-widget) plugins. -= 0.3.0 - 2017-02-?? (Unreleased) = - -* Refactor of `Form` along with introduction of JS unit tests. See [#35](https://github.com/xwp/wp-js-widgets/pull/35). Props [sirbrillig](https://profiles.wordpress.org/sirbrillig)! - = 0.2.0 - 2017-01-02 = * Important: Update minimum WordPress core version to 4.7.0. From e97d7c1737f171b50911cfe6b1be6d61d871c8fa Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 17 Feb 2017 09:49:10 -0800 Subject: [PATCH 88/92] Reuse standard item link relation instead of custom wp:post, wp:page, and wp:comment relations --- core-adapter-widgets/categories/class.php | 4 ++-- core-adapter-widgets/pages/class.php | 4 ++-- core-adapter-widgets/recent-comments/class.php | 4 ++-- core-adapter-widgets/recent-posts/class.php | 4 ++-- post-collection-widget/class-widget.php | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/core-adapter-widgets/categories/class.php b/core-adapter-widgets/categories/class.php index 61d269e..e26b73a 100644 --- a/core-adapter-widgets/categories/class.php +++ b/core-adapter-widgets/categories/class.php @@ -171,7 +171,7 @@ public function get_categories_list( $args ) { public function get_rest_response_links( $response, $request, $controller ) { $links = array(); - $links['wp:term'] = array(); + $links['item'] = array(); foreach ( $response->data['terms'] as $term_id ) { $term = get_term( (int) $term_id ); if ( empty( $term ) || is_wp_error( $term ) ) { @@ -185,7 +185,7 @@ public function get_rest_response_links( $response, $request, $controller ) { $rest_base = ! empty( $obj->rest_base ) ? $obj->rest_base : $obj->name; $base = sprintf( '/wp/v2/%s', $rest_base ); - $links['wp:term'][] = array( + $links['item'][] = array( 'href' => rest_url( trailingslashit( $base ) . $term_id ), 'embeddable' => true, 'taxonomy' => $term->taxonomy, diff --git a/core-adapter-widgets/pages/class.php b/core-adapter-widgets/pages/class.php index 477ba6d..3f33c86 100644 --- a/core-adapter-widgets/pages/class.php +++ b/core-adapter-widgets/pages/class.php @@ -187,7 +187,7 @@ public function prepare_item_for_response( $instance, $request ) { public function get_rest_response_links( $response, $request, $controller ) { $links = array(); - $links['wp:page'] = array(); + $links['item'] = array(); foreach ( $response->data['pages'] as $post_id ) { $post = get_post( $post_id ); if ( empty( $post ) ) { @@ -201,7 +201,7 @@ public function get_rest_response_links( $response, $request, $controller ) { $rest_base = ! empty( $obj->rest_base ) ? $obj->rest_base : $obj->name; $base = sprintf( '/wp/v2/%s', $rest_base ); - $links['wp:page'][] = array( + $links['item'][] = array( 'href' => rest_url( trailingslashit( $base ) . $post_id ), 'embeddable' => true, 'post_type' => $post->post_type, diff --git a/core-adapter-widgets/recent-comments/class.php b/core-adapter-widgets/recent-comments/class.php index 47829ad..d7ed745 100644 --- a/core-adapter-widgets/recent-comments/class.php +++ b/core-adapter-widgets/recent-comments/class.php @@ -101,9 +101,9 @@ public function prepare_item_for_response( $instance, $request ) { public function get_rest_response_links( $response, $request, $controller ) { $links = array(); - $links['wp:comment'] = array(); + $links['item'] = array(); foreach ( $response->data['comments'] as $comment_id ) { - $links['wp:comment'][] = array( + $links['item'][] = array( 'href' => rest_url( "/wp/v2/comments/$comment_id" ), 'embeddable' => true, ); diff --git a/core-adapter-widgets/recent-posts/class.php b/core-adapter-widgets/recent-posts/class.php index 2825a14..92f29e5 100644 --- a/core-adapter-widgets/recent-posts/class.php +++ b/core-adapter-widgets/recent-posts/class.php @@ -111,7 +111,7 @@ public function prepare_item_for_response( $instance, $request ) { public function get_rest_response_links( $response, $request, $controller ) { $links = array(); - $links['wp:post'] = array(); + $links['item'] = array(); foreach ( $response->data['posts'] as $post_id ) { $post = get_post( $post_id ); if ( empty( $post ) ) { @@ -125,7 +125,7 @@ public function get_rest_response_links( $response, $request, $controller ) { $rest_base = ! empty( $obj->rest_base ) ? $obj->rest_base : $obj->name; $base = sprintf( '/wp/v2/%s', $rest_base ); - $links['wp:post'][] = array( + $links['item'][] = array( 'href' => rest_url( trailingslashit( $base ) . $post_id ), 'embeddable' => true, 'post_type' => $post->post_type, diff --git a/post-collection-widget/class-widget.php b/post-collection-widget/class-widget.php index fabe96e..4f4abc7 100644 --- a/post-collection-widget/class-widget.php +++ b/post-collection-widget/class-widget.php @@ -237,7 +237,7 @@ public function get_rest_response_links( $response, $request, $controller ) { unset( $request, $controller ); $links = array(); - $links['wp:post'] = array(); + $links['item'] = array(); foreach ( $response->data['posts'] as $post_id ) { $post = get_post( $post_id ); if ( empty( $post ) ) { @@ -251,7 +251,7 @@ public function get_rest_response_links( $response, $request, $controller ) { $rest_base = ! empty( $obj->rest_base ) ? $obj->rest_base : $obj->name; $base = sprintf( '/wp/v2/%s', $rest_base ); - $links['wp:post'][] = array( + $links['item'][] = array( 'href' => rest_url( trailingslashit( $base ) . $post_id ), 'embeddable' => true, 'post_type' => $post->post_type, From f058d7a23b9fd5c16ffba0f95f90908a490c26f1 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 17 Feb 2017 10:38:16 -0800 Subject: [PATCH 89/92] Fix phpcs issues --- core-adapter-widgets/tag_cloud/class.php | 17 +++++++++++++---- phpcs.ruleset.xml | 5 +++++ readme.md | 1 + readme.txt | 1 + 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/core-adapter-widgets/tag_cloud/class.php b/core-adapter-widgets/tag_cloud/class.php index 895d8eb..81bc6d1 100644 --- a/core-adapter-widgets/tag_cloud/class.php +++ b/core-adapter-widgets/tag_cloud/class.php @@ -35,7 +35,9 @@ public function __construct( JS_Widgets_Plugin $plugin, WP_Widget_Tag_Cloud $ada * @return array Schema. */ public function get_item_schema() { - $taxonomies = get_taxonomies( array( 'show_tagcloud' => true ), 'names' ); + $taxonomies = get_taxonomies( array( + 'show_tagcloud' => true, + ), 'names' ); if ( ! get_option( 'link_manager_enabled' ) ) { unset( $taxonomies['link_category'] ); } @@ -87,7 +89,9 @@ public function validate_taxonomy( $taxonomy, $request, $param ) { if ( true !== $validity ) { return $validity; } - if ( 0 === count( get_taxonomies( array( 'show_tagcloud' => true ), 'names' ) ) ) { + if ( 0 === count( get_taxonomies( array( + 'show_tagcloud' => true, + ), 'names' ) ) ) { return new WP_Error( 'no_tagcloud_taxonomies', __( 'The tag cloud will not be displayed since there are no taxonomies that support the tag cloud widget.', 'default' ) ); } return true; @@ -140,7 +144,10 @@ public function prepare_item_for_response( $instance, $request ) { $tags = get_terms( $tag_cloud_args['taxonomy'], - array_merge( $tag_cloud_args, array( 'orderby' => 'count', 'order' => 'DESC' ) ) // Always query top tags. + array_merge( $tag_cloud_args, array( + 'orderby' => 'count', + 'order' => 'DESC', + ) ) // Always query top tags. ); // @todo should these not be _links? @@ -181,7 +188,9 @@ public function render_form_template() { $this->render_title_form_field_template( array( 'placeholder' => $item_schema['title']['properties']['raw']['default'], ) ); - $taxonomies = get_taxonomies( array( 'show_tagcloud' => true ), 'object' ); + $taxonomies = get_taxonomies( array( + 'show_tagcloud' => true, + ), 'object' ); if ( ! get_option( 'link_manager_enabled' ) ) { unset( $taxonomies['link_category'] ); } diff --git a/phpcs.ruleset.xml b/phpcs.ruleset.xml index b2a57ab..317c2fe 100644 --- a/phpcs.ruleset.xml +++ b/phpcs.ruleset.xml @@ -18,6 +18,11 @@ 0 + + + 0 + + */dev-lib/* */node_modules/* */vendor/* diff --git a/readme.md b/readme.md index a8746cd..2be53a0 100644 --- a/readme.md +++ b/readme.md @@ -55,6 +55,7 @@ Limitations/Caveats: ### 0.4.0 - 2017-02-17 ### * Integrate with [Shortcake (Shortcode UI)](https://wordpress.org/plugins/shortcode-ui/) to allow any JS widget to be used inside the editor as a Post Element. See [#11](https://github.com/xwp/wp-js-widgets/issues/11), [#32](https://github.com/xwp/wp-js-widgets/pull/32). * Refactor of `Form` along with introduction of JS unit tests. See [#35](https://github.com/xwp/wp-js-widgets/pull/35). Props [sirbrillig](https://profiles.wordpress.org/sirbrillig)! +* Use `item` relation in resource links instead of ad hoc `wp:post`, `wp:page`, and `wp:comment` relations. See [#36](https://github.com/xwp/wp-js-widgets/issues/36), [#38](https://github.com/xwp/wp-js-widgets/pull/38). ### 0.3.0 - 2017-01-11 ### Added: diff --git a/readme.txt b/readme.txt index 6b1919c..00ad81b 100644 --- a/readme.txt +++ b/readme.txt @@ -53,6 +53,7 @@ Limitations/Caveats: * Integrate with [Shortcake (Shortcode UI)](https://wordpress.org/plugins/shortcode-ui/) to allow any JS widget to be used inside the editor as a Post Element. See [#11](https://github.com/xwp/wp-js-widgets/issues/11), [#32](https://github.com/xwp/wp-js-widgets/pull/32). * Refactor of `Form` along with introduction of JS unit tests. See [#35](https://github.com/xwp/wp-js-widgets/pull/35). Props [sirbrillig](https://profiles.wordpress.org/sirbrillig)! +* Use `item` relation in resource links instead of ad hoc `wp:post`, `wp:page`, and `wp:comment` relations. See [#36](https://github.com/xwp/wp-js-widgets/issues/36), [#38](https://github.com/xwp/wp-js-widgets/pull/38). = 0.3.0 - 2017-01-11 = From 94d4e6b43a841af4241113b7328f614bd3fc89ae Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 17 Feb 2017 10:43:59 -0800 Subject: [PATCH 90/92] Add props --- readme.md | 4 ++++ readme.txt | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/readme.md b/readme.md index 2be53a0..3ccc98b 100644 --- a/readme.md +++ b/readme.md @@ -57,6 +57,10 @@ Limitations/Caveats: * Refactor of `Form` along with introduction of JS unit tests. See [#35](https://github.com/xwp/wp-js-widgets/pull/35). Props [sirbrillig](https://profiles.wordpress.org/sirbrillig)! * Use `item` relation in resource links instead of ad hoc `wp:post`, `wp:page`, and `wp:comment` relations. See [#36](https://github.com/xwp/wp-js-widgets/issues/36), [#38](https://github.com/xwp/wp-js-widgets/pull/38). +See issues and PRs in milestone and full release commit log. + +Props Payton Swick (@sirbrillig), Weston Ruter (@westonruter), Piotr Delawski (@delawski. + ### 0.3.0 - 2017-01-11 ### Added: diff --git a/readme.txt b/readme.txt index 00ad81b..15f7bcf 100644 --- a/readme.txt +++ b/readme.txt @@ -55,6 +55,10 @@ Limitations/Caveats: * Refactor of `Form` along with introduction of JS unit tests. See [#35](https://github.com/xwp/wp-js-widgets/pull/35). Props [sirbrillig](https://profiles.wordpress.org/sirbrillig)! * Use `item` relation in resource links instead of ad hoc `wp:post`, `wp:page`, and `wp:comment` relations. See [#36](https://github.com/xwp/wp-js-widgets/issues/36), [#38](https://github.com/xwp/wp-js-widgets/pull/38). +See issues and PRs in milestone and full release commit log. + +Props Payton Swick (@sirbrillig), Weston Ruter (@westonruter), Piotr Delawski (@delawski. + = 0.3.0 - 2017-01-11 = Added: From 40349eb14fee799c138e14c190bde7bb23960729 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 17 Feb 2017 10:44:27 -0800 Subject: [PATCH 91/92] Update wp-dev-lib c061303...4bc43e4: Merge pull request #214 from rmccue/patch-1 https://github.com/xwp/wp-dev-lib/compare/c061303...4bc43e4 --- dev-lib | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-lib b/dev-lib index c061303..4bc43e4 160000 --- a/dev-lib +++ b/dev-lib @@ -1 +1 @@ -Subproject commit c061303c696a327861604fc781c7585c76d24ade +Subproject commit 4bc43e4b00a3f5f2d1dd54cb61538ba445438c03 From d7e388a2fe56d6b0ea611dc815286106b5976311 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 17 Feb 2017 10:54:52 -0800 Subject: [PATCH 92/92] Fix readme typo [ci skip] --- readme.md | 2 +- readme.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index 3ccc98b..f3dcc09 100644 --- a/readme.md +++ b/readme.md @@ -59,7 +59,7 @@ Limitations/Caveats: See issues and PRs in milestone and full release commit log. -Props Payton Swick (@sirbrillig), Weston Ruter (@westonruter), Piotr Delawski (@delawski. +Props Payton Swick (@sirbrillig), Weston Ruter (@westonruter), Piotr Delawski (@delawski). ### 0.3.0 - 2017-01-11 ### Added: diff --git a/readme.txt b/readme.txt index 15f7bcf..d398639 100644 --- a/readme.txt +++ b/readme.txt @@ -57,7 +57,7 @@ Limitations/Caveats: See issues and PRs in milestone and full release commit log. -Props Payton Swick (@sirbrillig), Weston Ruter (@westonruter), Piotr Delawski (@delawski. +Props Payton Swick (@sirbrillig), Weston Ruter (@westonruter), Piotr Delawski (@delawski). = 0.3.0 - 2017-01-11 =