Changeset 36586
- Timestamp:
- 02/19/2016 06:40:06 PM (9 years ago)
- Location:
- trunk
- Files:
-
- 6 added
- 17 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/src/wp-admin/js/customize-controls.js
r36532 r36586 3787 3787 }); 3788 3788 3789 // Focus on the control that is associated with the given setting. 3790 api.previewer.bind( 'focus-control-for-setting', function( settingId ) { 3791 var matchedControl; 3792 api.control.each( function( control ) { 3793 var settingIds = _.pluck( control.settings, 'id' ); 3794 if ( -1 !== _.indexOf( settingIds, settingId ) ) { 3795 matchedControl = control; 3796 } 3797 } ); 3798 3799 if ( matchedControl ) { 3800 matchedControl.focus(); 3801 } 3802 } ); 3803 3804 // Refresh the preview when it requests. 3805 api.previewer.bind( 'refresh', function() { 3806 api.previewer.refresh(); 3807 }); 3808 3789 3809 api.trigger( 'ready' ); 3790 3810 -
trunk/src/wp-admin/js/customize-nav-menus.js
r36573 r36586 20 20 itemTypes: [], 21 21 l10n: {}, 22 menuItemTransport: 'postMessage',22 settingTransport: 'refresh', 23 23 phpIntMax: 0, 24 24 defaultSettingValues: { … … 2311 2311 settingArgs = { 2312 2312 type: 'nav_menu_item', 2313 transport: 'postMessage',2313 transport: api.Menus.data.settingTransport, 2314 2314 previewer: api.previewer 2315 2315 }; … … 2400 2400 api.create( customizeId, customizeId, {}, { 2401 2401 type: 'nav_menu', 2402 transport: 'postMessage',2402 transport: api.Menus.data.settingTransport, 2403 2403 previewer: api.previewer 2404 2404 } ); … … 2487 2487 } ); 2488 2488 2489 api.previewer.bind( 'refresh', function() {2490 api.previewer.refresh();2491 });2492 2493 2489 // Open and focus menu control. 2494 2490 api.previewer.bind( 'focus-nav-menu-item-control', api.Menus.focusMenuItemControl ); … … 2536 2532 newSetting = api.create( newCustomizeId, newCustomizeId, settingValue, { 2537 2533 type: 'nav_menu', 2538 transport: 'postMessage',2534 transport: api.Menus.data.settingTransport, 2539 2535 previewer: api.previewer 2540 2536 } ); … … 2684 2680 newSetting = api.create( newCustomizeId, newCustomizeId, settingValue, { 2685 2681 type: 'nav_menu_item', 2686 transport: 'postMessage',2682 transport: api.Menus.data.settingTransport, 2687 2683 previewer: api.previewer 2688 2684 } ); -
trunk/src/wp-admin/js/customize-widgets.js
r36546 r36586 35 35 name: null, 36 36 id_base: null, 37 transport: 'refresh',37 transport: api.Widgets.data.selectiveRefresh ? 'postMessage' : 'refresh', 38 38 params: [], 39 39 width: null, … … 1983 1983 if ( ! isExistingWidget ) { 1984 1984 settingArgs = { 1985 transport: 'refresh',1985 transport: api.Widgets.data.selectiveRefresh ? 'postMessage' : 'refresh', 1986 1986 previewer: this.setting.previewer 1987 1987 }; -
trunk/src/wp-content/themes/twentythirteen/js/theme-customizer.js
r30482 r36586 39 39 } ); 40 40 } ); 41 42 if ( wp.customize.selectiveRefresh ) { 43 wp.customize.selectiveRefresh.bind( 'sidebar-updated', function( sidebarPartial ) { 44 var widgetArea; 45 if ( 'sidebar-1' === sidebarPartial.sidebarId && $.isFunction( $.fn.masonry ) ) { 46 widgetArea = $( '#secondary .widget-area' ); 47 widgetArea.masonry( 'destroy' ); 48 widgetArea.masonry(); 49 } 50 } ); 51 } 52 41 53 } )( jQuery ); -
trunk/src/wp-includes/class-wp-customize-manager.php
r36532 r36586 68 68 69 69 /** 70 * Methods and properties dealing with selective refresh in the Customizer preview. 71 * 72 * @since 4.5.0 73 * @access public 74 * @var WP_Customize_Selective_Refresh 75 */ 76 public $selective_refresh; 77 78 /** 70 79 * Registered instances of WP_Customize_Setting. 71 80 * … … 101 110 * @var array 102 111 */ 103 protected $components = array( 'widgets', 'nav_menus' );112 protected $components = array( 'widgets', 'nav_menus', 'selective_refresh' ); 104 113 105 114 /** … … 250 259 $components = apply_filters( 'customize_loaded_components', $this->components, $this ); 251 260 252 if ( in_array( 'widgets', $components ) ) {261 if ( in_array( 'widgets', $components, true ) ) { 253 262 require_once( ABSPATH . WPINC . '/class-wp-customize-widgets.php' ); 254 263 $this->widgets = new WP_Customize_Widgets( $this ); 255 264 } 256 if ( in_array( 'nav_menus', $components ) ) { 265 266 if ( in_array( 'nav_menus', $components, true ) ) { 257 267 require_once( ABSPATH . WPINC . '/class-wp-customize-nav-menus.php' ); 258 268 $this->nav_menus = new WP_Customize_Nav_Menus( $this ); 269 } 270 271 if ( in_array( 'selective_refresh', $components, true ) ) { 272 require_once( ABSPATH . WPINC . '/customize/class-wp-customize-selective-refresh.php' ); 273 $this->selective_refresh = new WP_Customize_Selective_Refresh( $this ); 259 274 } 260 275 … … 1712 1727 'documentTitleTmpl' => $this->get_document_title_template(), 1713 1728 'previewableDevices' => $this->get_previewable_devices(), 1729 'selectiveRefreshEnabled' => isset( $this->selective_refresh ), 1714 1730 ); 1715 1731 -
trunk/src/wp-includes/class-wp-customize-nav-menus.php
r36573 r36586 62 62 add_action( 'customize_controls_print_footer_scripts', array( $this, 'available_items_template' ) ); 63 63 add_action( 'customize_preview_init', array( $this, 'customize_preview_init' ) ); 64 65 // Selective Refresh partials. 66 add_filter( 'customize_dynamic_partial_args', array( $this, 'customize_dynamic_partial_args' ), 10, 2 ); 64 67 } 65 68 … … 376 379 'reorderLabelOff' => esc_attr__( 'Close reorder mode' ), 377 380 ), 378 ' menuItemTransport' => 'postMessage',381 'settingTransport' => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh', 379 382 'phpIntMax' => PHP_INT_MAX, 380 383 'defaultSettingValues' => array( … … 427 430 if ( preg_match( WP_Customize_Nav_Menu_Setting::ID_PATTERN, $setting_id ) ) { 428 431 $setting_args = array( 429 'type' => WP_Customize_Nav_Menu_Setting::TYPE, 432 'type' => WP_Customize_Nav_Menu_Setting::TYPE, 433 'transport' => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh', 430 434 ); 431 435 } elseif ( preg_match( WP_Customize_Nav_Menu_Item_Setting::ID_PATTERN, $setting_id ) ) { 432 436 $setting_args = array( 433 'type' => WP_Customize_Nav_Menu_Item_Setting::TYPE, 437 'type' => WP_Customize_Nav_Menu_Item_Setting::TYPE, 438 'transport' => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh', 434 439 ); 435 440 } … … 516 521 $setting = $this->manager->get_setting( $setting_id ); 517 522 if ( $setting ) { 518 $setting->transport = 'postMessage';523 $setting->transport = isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh'; 519 524 remove_filter( "customize_sanitize_{$setting_id}", 'absint' ); 520 525 add_filter( "customize_sanitize_{$setting_id}", array( $this, 'intval_base10' ) ); … … 524 529 'theme_supports' => 'menus', 525 530 'type' => 'theme_mod', 526 'transport' => 'postMessage',531 'transport' => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh', 527 532 'default' => 0, 528 533 ) ); … … 550 555 551 556 $nav_menu_setting_id = 'nav_menu[' . $menu_id . ']'; 552 $this->manager->add_setting( new WP_Customize_Nav_Menu_Setting( $this->manager, $nav_menu_setting_id ) ); 557 $this->manager->add_setting( new WP_Customize_Nav_Menu_Setting( $this->manager, $nav_menu_setting_id, array( 558 'transport' => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh', 559 ) ) ); 553 560 554 561 // Add the menu contents. … … 563 570 $value['nav_menu_term_id'] = $menu_id; 564 571 $this->manager->add_setting( new WP_Customize_Nav_Menu_Item_Setting( $this->manager, $menu_item_setting_id, array( 565 'value' => $value, 572 'value' => $value, 573 'transport' => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh', 566 574 ) ) ); 567 575 … … 587 595 'type' => 'new_menu', 588 596 'default' => '', 589 'transport' => 'postMessage',597 'transport' => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh', 590 598 ) ); 591 599 … … 803 811 } 804 812 813 // 805 814 // Start functionality specific to partial-refresh of menu changes in Customizer preview. 806 const RENDER_AJAX_ACTION = 'customize_render_menu_partial'; 807 const RENDER_NONCE_POST_KEY = 'render-menu-nonce'; 808 const RENDER_QUERY_VAR = 'wp_customize_menu_render'; 809 810 /** 811 * The number of wp_nav_menu() calls which have happened in the preview. 812 * 813 * @since 4.3.0 814 * @access public 815 * @var int 816 */ 817 public $preview_nav_menu_instance_number = 0; 818 819 /** 820 * Nav menu args used for each instance. 821 * 822 * @since 4.3.0 823 * @access public 824 * @var array 825 */ 826 public $preview_nav_menu_instance_args = array(); 815 // 816 817 /** 818 * Filters arguments for dynamic nav_menu selective refresh partials. 819 * 820 * @since 4.5.0 821 * @access public 822 * 823 * @param array|false $partial_args Partial args. 824 * @param string $partial_id Partial ID. 825 * @return array Partial args 826 */ 827 public function customize_dynamic_partial_args( $partial_args, $partial_id ) { 828 829 if ( preg_match( '/^nav_menu_instance\[[0-9a-f]{32}\]$/', $partial_id ) ) { 830 if ( false === $partial_args ) { 831 $partial_args = array(); 832 } 833 $partial_args = array_merge( 834 $partial_args, 835 array( 836 'type' => 'nav_menu_instance', 837 'render_callback' => array( $this, 'render_nav_menu_partial' ), 838 'container_inclusive' => true, 839 ) 840 ); 841 } 842 843 return $partial_args; 844 } 827 845 828 846 /** … … 833 851 */ 834 852 public function customize_preview_init() { 835 add_action( 'template_redirect', array( $this, 'render_menu' ) );836 853 add_action( 'wp_enqueue_scripts', array( $this, 'customize_preview_enqueue_deps' ) ); 837 838 if ( ! isset( $_REQUEST[ self::RENDER_QUERY_VAR ] ) ) { 839 add_filter( 'wp_nav_menu_args', array( $this, 'filter_wp_nav_menu_args' ), 1000 ); 840 add_filter( 'wp_nav_menu', array( $this, 'filter_wp_nav_menu' ), 10, 2 ); 841 } 854 add_filter( 'wp_nav_menu_args', array( $this, 'filter_wp_nav_menu_args' ), 1000 ); 855 add_filter( 'wp_nav_menu', array( $this, 'filter_wp_nav_menu' ), 10, 2 ); 842 856 } 843 857 … … 847 861 * @since 4.3.0 848 862 * @access public 849 *850 863 * @see wp_nav_menu() 864 * @see WP_Customize_Widgets_Partial_Refresh::filter_dynamic_sidebar_params() 851 865 * 852 866 * @param array $args An array containing wp_nav_menu() arguments. … … 854 868 */ 855 869 public function filter_wp_nav_menu_args( $args ) { 856 $this->preview_nav_menu_instance_number += 1; 857 $args['instance_number'] = $this->preview_nav_menu_instance_number; 858 859 $can_partial_refresh = ( 870 /* 871 * The following conditions determine whether or not this instance of 872 * wp_nav_menu() can use selective refreshed. A wp_nav_menu() can be 873 * selective refreshed if... 874 */ 875 $can_selective_refresh = ( 876 // ...if wp_nav_menu() is directly echoing out the menu (and thus isn't manipulating the string after generated), 860 877 ! empty( $args['echo'] ) 861 878 && 879 // ...and if the fallback_cb can be serialized to JSON, since it will be included in the placement context data, 862 880 ( empty( $args['fallback_cb'] ) || is_string( $args['fallback_cb'] ) ) 863 881 && 882 // ...and if the walker can also be serialized to JSON, since it will be included in the placement context data as well, 864 883 ( empty( $args['walker'] ) || is_string( $args['walker'] ) ) 865 &&866 (884 // ...and if it has a theme location assigned or an assigned menu to display, 885 && ( 867 886 ! empty( $args['theme_location'] ) 868 887 || 869 888 ( ! empty( $args['menu'] ) && ( is_numeric( $args['menu'] ) || is_object( $args['menu'] ) ) ) 870 889 ) 890 && 891 // ...and if the nav menu would be rendered with a wrapper container element (upon which to attach data-* attributes). 892 ( 893 ! empty( $args['container'] ) 894 || 895 ( isset( $args['items_wrap'] ) && '<' === substr( $args['items_wrap'], 0, 1 ) ) 896 ) 871 897 ); 872 $args['can_partial_refresh'] = $can_partial_refresh; 873 874 $hashed_args = $args; 875 876 if ( ! $can_partial_refresh ) { 877 $hashed_args['fallback_cb'] = ''; 878 $hashed_args['walker'] = ''; 879 } 880 881 // Replace object menu arg with a term_id menu arg, as this exports better to JS and is easier to compare hashes. 882 if ( ! empty( $hashed_args['menu'] ) && is_object( $hashed_args['menu'] ) ) { 883 $hashed_args['menu'] = $hashed_args['menu']->term_id; 884 } 885 886 ksort( $hashed_args ); 887 $hashed_args['args_hash'] = $this->hash_nav_menu_args( $hashed_args ); 888 889 $this->preview_nav_menu_instance_args[ $this->preview_nav_menu_instance_number ] = $hashed_args; 898 899 if ( ! $can_selective_refresh ) { 900 return $args; 901 } 902 903 $exported_args = $args; 904 905 /* 906 * Replace object menu arg with a term_id menu arg, as this exports better 907 * to JS and is easier to compare hashes. 908 */ 909 if ( ! empty( $exported_args['menu'] ) && is_object( $exported_args['menu'] ) ) { 910 $exported_args['menu'] = $exported_args['menu']->term_id; 911 } 912 913 ksort( $exported_args ); 914 $exported_args['args_hmac'] = $this->hash_nav_menu_args( $exported_args ); 915 916 $args['customize_preview_nav_menus_args'] = $exported_args; 917 890 918 return $args; 891 919 } 892 920 893 921 /** 894 * Prepare wp_nav_menu() calls for partial refresh. Wraps output in container for refreshing. 922 * Prepares wp_nav_menu() calls for partial refresh. 923 * 924 * Injects attributes into container element. 895 925 * 896 926 * @since 4.3.0 … … 904 934 */ 905 935 public function filter_wp_nav_menu( $nav_menu_content, $args ) { 906 if ( ! empty( $args->can_partial_refresh ) && ! empty( $args->instance_number ) ) { 907 $nav_menu_content = preg_replace( 908 '/(?<=class=")/', 909 sprintf( 'partial-refreshable-nav-menu partial-refreshable-nav-menu-%1$d ', $args->instance_number ), 910 $nav_menu_content, 911 1 // Only update the class on the first element found, the menu container. 912 ); 936 if ( ! empty( $args->customize_preview_nav_menus_args ) ) { 937 $attributes = sprintf( ' data-customize-partial-id="%s"', esc_attr( 'nav_menu_instance[' . $args->customize_preview_nav_menus_args['args_hmac'] . ']' ) ); 938 $attributes .= ' data-customize-partial-type="nav_menu_instance"'; 939 $attributes .= sprintf( ' data-customize-partial-placement-context="%s"', esc_attr( wp_json_encode( $args->customize_preview_nav_menus_args ) ) ); 940 $nav_menu_content = preg_replace( '#^(<\w+)#', '$1 ' . $attributes, $nav_menu_content, 1 ); 913 941 } 914 942 return $nav_menu_content; … … 916 944 917 945 /** 918 * Hash (hmac) the arguments with the nonce and secret auth key to ensure they 919 * are not tampered with when submitted in the Ajax request. 946 * Hashes (hmac) the nav menu arguments to ensure they are not tampered with when 947 * submitted in the Ajax request. 948 * 949 * Note that the array is expected to be pre-sorted. 920 950 * 921 951 * @since 4.3.0 … … 923 953 * 924 954 * @param array $args The arguments to hash. 925 * @return string 955 * @return string Hashed nav menu arguments. 926 956 */ 927 957 public function hash_nav_menu_args( $args ) { 928 return wp_hash( wp_create_nonce( self::RENDER_AJAX_ACTION ) .serialize( $args ) );958 return wp_hash( serialize( $args ) ); 929 959 } 930 960 … … 936 966 */ 937 967 public function customize_preview_enqueue_deps() { 938 wp_enqueue_script( 'customize-preview-nav-menus' ); 968 if ( isset( $this->manager->selective_refresh ) ) { 969 $script = wp_scripts()->registered['customize-preview-nav-menus']; 970 $script->deps[] = 'customize-selective-refresh'; 971 } 972 973 wp_enqueue_script( 'customize-preview-nav-menus' ); // Note that we have overridden this. 939 974 wp_enqueue_style( 'customize-preview' ); 940 941 add_action( 'wp_print_footer_scripts', array( $this, 'export_preview_data' ) ); 942 } 943 944 /** 945 * Export data from PHP to JS. 946 * 947 * @since 4.3.0 975 } 976 977 /** 978 * Exports data from PHP to JS. 979 * 980 * @since 4.3.0 981 * @deprecated 4.5.0 Obsolete 948 982 * @access public 949 983 */ 950 984 public function export_preview_data() { 951 952 // Why not wp_localize_script? Because we're not localizing, and it forces values into strings. 953 $exports = array( 954 'renderQueryVar' => self::RENDER_QUERY_VAR, 955 'renderNonceValue' => wp_create_nonce( self::RENDER_AJAX_ACTION ), 956 'renderNoncePostKey' => self::RENDER_NONCE_POST_KEY, 957 'navMenuInstanceArgs' => $this->preview_nav_menu_instance_args, 958 'l10n' => array( 959 'editNavMenuItemTooltip' => __( 'Shift-click to edit this menu item.' ), 960 ), 961 ); 962 963 printf( '<script>var _wpCustomizePreviewNavMenusExports = %s;</script>', wp_json_encode( $exports ) ); 985 _deprecated_function( __METHOD__, '4.5.0' ); 964 986 } 965 987 … … 971 993 * 972 994 * @see wp_nav_menu() 973 */ 974 public function render_menu() { 975 if ( empty( $_POST[ self::RENDER_QUERY_VAR ] ) ) { 976 return; 977 } 978 979 $this->manager->remove_preview_signature(); 980 981 if ( empty( $_POST[ self::RENDER_NONCE_POST_KEY ] ) ) { 982 wp_send_json_error( 'missing_nonce_param' ); 983 } 984 985 if ( ! is_customize_preview() ) { 986 wp_send_json_error( 'expected_customize_preview' ); 987 } 988 989 if ( ! check_ajax_referer( self::RENDER_AJAX_ACTION, self::RENDER_NONCE_POST_KEY, false ) ) { 990 wp_send_json_error( 'nonce_check_fail' ); 991 } 992 993 if ( ! current_user_can( 'edit_theme_options' ) ) { 994 wp_send_json_error( 'unauthorized' ); 995 } 996 997 if ( ! isset( $_POST['wp_nav_menu_args'] ) ) { 998 wp_send_json_error( 'missing_param' ); 999 } 1000 1001 if ( ! isset( $_POST['wp_nav_menu_args_hash'] ) ) { 1002 wp_send_json_error( 'missing_param' ); 1003 } 1004 1005 $wp_nav_menu_args = json_decode( wp_unslash( $_POST['wp_nav_menu_args'] ), true ); 1006 if ( ! is_array( $wp_nav_menu_args ) ) { 1007 wp_send_json_error( 'wp_nav_menu_args_not_array' ); 1008 } 1009 1010 $wp_nav_menu_args_hash = sanitize_text_field( wp_unslash( $_POST['wp_nav_menu_args_hash'] ) ); 1011 if ( ! hash_equals( $this->hash_nav_menu_args( $wp_nav_menu_args ), $wp_nav_menu_args_hash ) ) { 1012 wp_send_json_error( 'wp_nav_menu_args_hash_mismatch' ); 1013 } 1014 1015 $wp_nav_menu_args['echo'] = false; 1016 wp_send_json_success( wp_nav_menu( $wp_nav_menu_args ) ); 995 * 996 * @param WP_Customize_Partial $partial Partial. 997 * @param array $nav_menu_args Nav menu args supplied as container context. 998 * @return string|false 999 */ 1000 public function render_nav_menu_partial( $partial, $nav_menu_args ) { 1001 unset( $partial ); 1002 1003 if ( ! isset( $nav_menu_args['args_hmac'] ) ) { 1004 // Error: missing_args_hmac. 1005 return false; 1006 } 1007 1008 $nav_menu_args_hmac = $nav_menu_args['args_hmac']; 1009 unset( $nav_menu_args['args_hmac'] ); 1010 1011 ksort( $nav_menu_args ); 1012 if ( ! hash_equals( $this->hash_nav_menu_args( $nav_menu_args ), $nav_menu_args_hmac ) ) { 1013 // Error: args_hmac_mismatch. 1014 return false; 1015 } 1016 1017 ob_start(); 1018 wp_nav_menu( $nav_menu_args ); 1019 $content = ob_get_clean(); 1020 1021 return $content; 1017 1022 } 1018 1023 } -
trunk/src/wp-includes/class-wp-customize-widgets.php
r36414 r36586 101 101 add_filter( 'is_active_sidebar', array( $this, 'tally_sidebars_via_is_active_sidebar_calls' ), 10, 2 ); 102 102 add_filter( 'dynamic_sidebar_has_widgets', array( $this, 'tally_sidebars_via_dynamic_sidebar_calls' ), 10, 2 ); 103 104 // Selective Refresh. 105 add_filter( 'customize_dynamic_partial_args', array( $this, 'customize_dynamic_partial_args' ), 10, 2 ); 106 add_action( 'customize_preview_init', array( $this, 'selective_refresh_init' ) ); 103 107 } 104 108 … … 683 687 'moveWidgetArea' => $move_widget_area_tpl, 684 688 ), 689 'selectiveRefresh' => isset( $this->manager->selective_refresh ), 685 690 ); 686 691 … … 763 768 'type' => 'option', 764 769 'capability' => 'edit_theme_options', 765 'transport' => 'refresh',770 'transport' => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh', 766 771 'default' => array(), 767 772 ); … … 885 890 'is_disabled' => $is_disabled, 886 891 'id_base' => $id_base, 887 'transport' => 'refresh',892 'transport' => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh', 888 893 'width' => $wp_registered_widget_controls[$widget['id']]['width'], 889 894 'height' => $wp_registered_widget_controls[$widget['id']]['height'], … … 1062 1067 'registeredWidgets' => $wp_registered_widgets, 1063 1068 'l10n' => array( 1064 'widgetTooltip' => __( 'Shift-click to edit this widget.' ),1069 'widgetTooltip' => __( 'Shift-click to edit this widget.' ), 1065 1070 ), 1071 'selectiveRefresh' => isset( $this->manager->selective_refresh ), 1066 1072 ); 1067 1073 foreach ( $settings['registeredWidgets'] as &$registered_widget ) { … … 1460 1466 } 1461 1467 1462 /*************************************************************************** 1463 * Option Update Capturing 1464 ***************************************************************************/ 1468 /* 1469 * Selective Refresh Methods 1470 */ 1471 1472 /** 1473 * Filter args for dynamic widget partials. 1474 * 1475 * @since 4.5.0 1476 * 1477 * @param array|false $partial_args Partial args. 1478 * @param string $partial_id Partial ID. 1479 * @return array Partial args 1480 */ 1481 public function customize_dynamic_partial_args( $partial_args, $partial_id ) { 1482 1483 if ( preg_match( '/^widget\[.+\]$/', $partial_id ) ) { 1484 if ( false === $partial_args ) { 1485 $partial_args = array(); 1486 } 1487 $partial_args = array_merge( 1488 $partial_args, 1489 array( 1490 'type' => 'widget', 1491 'render_callback' => array( $this, 'render_widget_partial' ), 1492 'container_inclusive' => true, 1493 ) 1494 ); 1495 } 1496 1497 return $partial_args; 1498 } 1499 1500 /** 1501 * Add hooks for selective refresh. 1502 * 1503 * @since 4.5.0 1504 * @access public 1505 */ 1506 public function selective_refresh_init() { 1507 if ( ! isset( $this->manager->selective_refresh ) ) { 1508 return; 1509 } 1510 1511 add_action( 'wp_enqueue_scripts', array( $this, 'customize_preview_enqueue_deps' ) ); 1512 add_filter( 'dynamic_sidebar_params', array( $this, 'filter_dynamic_sidebar_params' ) ); 1513 add_filter( 'wp_kses_allowed_html', array( $this, 'filter_wp_kses_allowed_data_attributes' ) ); 1514 add_action( 'dynamic_sidebar_before', array( $this, 'start_dynamic_sidebar' ) ); 1515 add_action( 'dynamic_sidebar_after', array( $this, 'end_dynamic_sidebar' ) ); 1516 } 1517 1518 /** 1519 * Enqueue scripts for the Customizer preview. 1520 * 1521 * @since 4.5.0 1522 * @access public 1523 */ 1524 public function customize_preview_enqueue_deps() { 1525 if ( isset( $this->manager->selective_refresh ) ) { 1526 $script = wp_scripts()->registered['customize-preview-widgets']; 1527 $script->deps[] = 'customize-selective-refresh'; 1528 } 1529 1530 wp_enqueue_script( 'customize-preview-widgets' ); 1531 wp_enqueue_style( 'customize-preview' ); 1532 } 1533 1534 /** 1535 * Inject selective refresh data attributes into widget container elements. 1536 * 1537 * @param array $params { 1538 * Dynamic sidebar params. 1539 * 1540 * @type array $args Sidebar args. 1541 * @type array $widget_args Widget args. 1542 * } 1543 * @see WP_Customize_Nav_Menus_Partial_Refresh::filter_wp_nav_menu_args() 1544 * 1545 * @return array Params. 1546 */ 1547 public function filter_dynamic_sidebar_params( $params ) { 1548 $sidebar_args = array_merge( 1549 array( 1550 'before_widget' => '', 1551 'after_widget' => '', 1552 ), 1553 $params[0] 1554 ); 1555 1556 // Skip widgets not in a registered sidebar or ones which lack a proper wrapper element to attach the data-* attributes to. 1557 $matches = array(); 1558 $is_valid = ( 1559 isset( $sidebar_args['id'] ) 1560 && 1561 is_registered_sidebar( $sidebar_args['id'] ) 1562 && 1563 ( isset( $this->current_dynamic_sidebar_id_stack[0] ) && $this->current_dynamic_sidebar_id_stack[0] === $sidebar_args['id'] ) 1564 && 1565 preg_match( '#^<(?P<tag_name>\w+)#', $sidebar_args['before_widget'], $matches ) 1566 ); 1567 if ( ! $is_valid ) { 1568 return $params; 1569 } 1570 $this->before_widget_tags_seen[ $matches['tag_name'] ] = true; 1571 1572 $context = array( 1573 'sidebar_id' => $sidebar_args['id'], 1574 ); 1575 if ( isset( $this->context_sidebar_instance_number ) ) { 1576 $context['sidebar_instance_number'] = $this->context_sidebar_instance_number; 1577 } else if ( isset( $sidebar_args['id'] ) && isset( $this->sidebar_instance_count[ $sidebar_args['id'] ] ) ) { 1578 $context['sidebar_instance_number'] = $this->sidebar_instance_count[ $sidebar_args['id'] ]; 1579 } 1580 1581 $attributes = sprintf( ' data-customize-partial-id="%s"', esc_attr( 'widget[' . $sidebar_args['widget_id'] . ']' ) ); 1582 $attributes .= ' data-customize-partial-type="widget"'; 1583 $attributes .= sprintf( ' data-customize-partial-placement-context="%s"', esc_attr( wp_json_encode( $context ) ) ); 1584 $attributes .= sprintf( ' data-customize-widget-id="%s"', esc_attr( $sidebar_args['widget_id'] ) ); 1585 $sidebar_args['before_widget'] = preg_replace( '#^(<\w+)#', '$1 ' . $attributes, $sidebar_args['before_widget'] ); 1586 1587 $params[0] = $sidebar_args; 1588 return $params; 1589 } 1590 1591 /** 1592 * List of the tag names seen for before_widget strings. 1593 * 1594 * This is used in the filter_wp_kses_allowed_html filter to ensure that the 1595 * data-* attributes can be whitelisted. 1596 * 1597 * @since 4.5.0 1598 * @access private 1599 * @var array 1600 */ 1601 protected $before_widget_tags_seen = array(); 1602 1603 /** 1604 * Ensure that the HTML data-* attributes for selective refresh are allowed by kses. 1605 * 1606 * This is needed in case the $before_widget is run through wp_kses() when printed. 1607 * 1608 * @since 4.5.0 1609 * @access public 1610 * 1611 * @param array $allowed_html Allowed HTML. 1612 * @return array Allowed HTML. 1613 */ 1614 public function filter_wp_kses_allowed_data_attributes( $allowed_html ) { 1615 foreach ( array_keys( $this->before_widget_tags_seen ) as $tag_name ) { 1616 if ( ! isset( $allowed_html[ $tag_name ] ) ) { 1617 $allowed_html[ $tag_name ] = array(); 1618 } 1619 $allowed_html[ $tag_name ] = array_merge( 1620 $allowed_html[ $tag_name ], 1621 array_fill_keys( array( 1622 'data-customize-partial-id', 1623 'data-customize-partial-type', 1624 'data-customize-partial-placement-context', 1625 'data-customize-partial-widget-id', 1626 'data-customize-partial-options', 1627 ), true ) 1628 ); 1629 } 1630 return $allowed_html; 1631 } 1632 1633 /** 1634 * Keep track of the number of times that dynamic_sidebar() was called for a given sidebar index. 1635 * 1636 * This helps facilitate the uncommon scenario where a single sidebar is rendered multiple times on a template. 1637 * 1638 * @since 4.5.0 1639 * @access private 1640 * @var array 1641 */ 1642 protected $sidebar_instance_count = array(); 1643 1644 /** 1645 * The current request's sidebar_instance_number context. 1646 * 1647 * @since 4.5.0 1648 * @access private 1649 * @var int 1650 */ 1651 protected $context_sidebar_instance_number; 1652 1653 /** 1654 * Current sidebar ID being rendered. 1655 * 1656 * @since 4.5.0 1657 * @access private 1658 * @var array 1659 */ 1660 protected $current_dynamic_sidebar_id_stack = array(); 1661 1662 /** 1663 * Start keeping track of the current sidebar being rendered. 1664 * 1665 * Insert marker before widgets are rendered in a dynamic sidebar. 1666 * 1667 * @since 4.5.0 1668 * 1669 * @param int|string $index Index, name, or ID of the dynamic sidebar. 1670 */ 1671 public function start_dynamic_sidebar( $index ) { 1672 array_unshift( $this->current_dynamic_sidebar_id_stack, $index ); 1673 if ( ! isset( $this->sidebar_instance_count[ $index ] ) ) { 1674 $this->sidebar_instance_count[ $index ] = 0; 1675 } 1676 $this->sidebar_instance_count[ $index ] += 1; 1677 if ( ! $this->manager->selective_refresh->is_render_partials_request() ) { 1678 printf( "\n<!--dynamic_sidebar_before:%s:%d-->\n", esc_html( $index ), intval( $this->sidebar_instance_count[ $index ] ) ); 1679 } 1680 } 1681 1682 /** 1683 * Finish keeping track of the current sidebar being rendered. 1684 * 1685 * Insert marker after widgets are rendered in a dynamic sidebar. 1686 * 1687 * @since 4.5.0 1688 * 1689 * @param int|string $index Index, name, or ID of the dynamic sidebar. 1690 */ 1691 public function end_dynamic_sidebar( $index ) { 1692 if ( ! $this->manager->selective_refresh->is_render_partials_request() ) { 1693 printf( "\n<!--dynamic_sidebar_after:%s:%d-->\n", esc_html( $index ), intval( $this->sidebar_instance_count[ $index ] ) ); 1694 } 1695 } 1696 1697 /** 1698 * Current sidebar being rendered. 1699 * 1700 * @since 4.5.0 1701 * @access private 1702 * @var string 1703 */ 1704 protected $rendering_widget_id; 1705 1706 /** 1707 * Current widget being rendered. 1708 * 1709 * @since 4.5.0 1710 * @access private 1711 * @var string 1712 */ 1713 protected $rendering_sidebar_id; 1714 1715 /** 1716 * Filter sidebars_widgets to ensure the currently-rendered widget is the only widget in the current sidebar. 1717 * 1718 * @since 4.5.0 1719 * @access private 1720 * 1721 * @param array $sidebars_widgets Sidebars widgets. 1722 * @return array Sidebars widgets. 1723 */ 1724 public function filter_sidebars_widgets_for_rendering_widget( $sidebars_widgets ) { 1725 $sidebars_widgets[ $this->rendering_sidebar_id ] = array( $this->rendering_widget_id ); 1726 return $sidebars_widgets; 1727 } 1728 1729 /** 1730 * Render a specific widget using the supplied sidebar arguments. 1731 * 1732 * @since 4.5.0 1733 * @access public 1734 * 1735 * @see dynamic_sidebar() 1736 * 1737 * @param WP_Customize_Partial $partial Partial. 1738 * @param array $context { 1739 * Sidebar args supplied as container context. 1740 * 1741 * @type string $sidebar_id ID for sidebar for widget to render into. 1742 * @type int [$sidebar_instance_number] Disambiguating instance number. 1743 * } 1744 * @return string|false 1745 */ 1746 public function render_widget_partial( $partial, $context ) { 1747 $id_data = $partial->id_data(); 1748 $widget_id = array_shift( $id_data['keys'] ); 1749 1750 if ( ! is_array( $context ) 1751 || empty( $context['sidebar_id'] ) 1752 || ! is_registered_sidebar( $context['sidebar_id'] ) 1753 ) { 1754 return false; 1755 } 1756 1757 $this->rendering_sidebar_id = $context['sidebar_id']; 1758 1759 if ( isset( $context['sidebar_instance_number'] ) ) { 1760 $this->context_sidebar_instance_number = intval( $context['sidebar_instance_number'] ); 1761 } 1762 1763 // Filter sidebars_widgets so that only the queried widget is in the sidebar. 1764 $this->rendering_widget_id = $widget_id; 1765 1766 $filter_callback = array( $this, 'filter_sidebars_widgets_for_rendering_widget' ); 1767 add_filter( 'sidebars_widgets', $filter_callback, 1000 ); 1768 1769 // Render the widget. 1770 ob_start(); 1771 dynamic_sidebar( $this->rendering_sidebar_id = $context['sidebar_id'] ); 1772 $container = ob_get_clean(); 1773 1774 // Reset variables for next partial render. 1775 remove_filter( 'sidebars_widgets', $filter_callback, 1000 ); 1776 1777 $this->context_sidebar_instance_number = null; 1778 $this->rendering_sidebar_id = null; 1779 $this->rendering_widget_id = null; 1780 1781 return $container; 1782 } 1783 1784 // 1785 // Option Update Capturing 1786 // 1465 1787 1466 1788 /** … … 1612 1934 } 1613 1935 1614 remove_filter( 'pre_update_option', array( $this, 'capture_filter_pre_update_option' ), 10 , 3);1936 remove_filter( 'pre_update_option', array( $this, 'capture_filter_pre_update_option' ), 10 ); 1615 1937 1616 1938 foreach ( array_keys( $this->_captured_options ) as $option_name ) { -
trunk/src/wp-includes/css/customize-preview.css
r33859 r36586 5 5 cursor: progress; 6 6 } 7 8 /* Override highlight when refreshing */ 9 .customize-partial-refreshing.widget-customizer-highlighted-widget { 10 -webkit-box-shadow: none; 11 box-shadow: none; 12 } 13 14 .customize-render-content-error { 15 outline: solid 1px red; 16 } 17 .customize-render-content-error-message { 18 display: block; 19 padding: 1em; 20 background-color: #FFCCCC; 21 } -
trunk/src/wp-includes/customize/class-wp-customize-nav-menu-item-setting.php
r35724 r36586 68 68 * 69 69 * @since 4.3.0 70 * @since 4.5.0 Default changed to 'refresh' 70 71 * @access public 71 72 * @var string 72 73 */ 73 public $transport = ' postMessage';74 public $transport = 'refresh'; 74 75 75 76 /** -
trunk/src/wp-includes/js/customize-preview-nav-menus.js
r36523 r36586 1 /* global JSON, _wpCustomizePreviewNavMenusExports */ 2 3 ( function( $, _, wp ) { 1 wp.customize.navMenusPreview = wp.customize.MenusCustomizerPreview = ( function( $, _, wp, api ) { 4 2 'use strict'; 5 3 6 if ( ! wp || ! wp.customize ) { return; } 7 8 var api = wp.customize, 9 currentRefreshDebounced = {}, 10 refreshDebounceDelay = 200, 11 settings = {}, 12 defaultSettings = { 13 renderQueryVar: null, 14 renderNonceValue: null, 15 renderNoncePostKey: null, 16 requestUri: '/', 17 navMenuInstanceArgs: {}, 18 l10n: {} 4 var self = {}; 5 6 /** 7 * Initialize nav menus preview. 8 */ 9 self.init = function() { 10 var self = this; 11 12 if ( api.selectiveRefresh ) { 13 self.watchNavMenuLocationChanges(); 14 } 15 16 api.preview.bind( 'active', function() { 17 self.highlightControls(); 18 } ); 19 }; 20 21 if ( api.selectiveRefresh ) { 22 23 /** 24 * Partial representing an invocation of wp_nav_menu(). 25 * 26 * @class 27 * @augments wp.customize.selectiveRefresh.Partial 28 * @since 4.5.0 29 */ 30 self.NavMenuInstancePartial = api.selectiveRefresh.Partial.extend({ 31 32 /** 33 * Constructor. 34 * 35 * @since 4.5.0 36 * @param {string} id - Partial ID. 37 * @param {Object} options 38 * @param {Object} options.params 39 * @param {Object} options.params.navMenuArgs 40 * @param {string} options.params.navMenuArgs.args_hmac 41 * @param {string} [options.params.navMenuArgs.theme_location] 42 * @param {number} [options.params.navMenuArgs.menu] 43 * @param {object} [options.constructingContainerContext] 44 */ 45 initialize: function( id, options ) { 46 var partial = this, matches, argsHmac; 47 matches = id.match( /^nav_menu_instance\[([0-9a-f]{32})]$/ ); 48 if ( ! matches ) { 49 throw new Error( 'Illegal id for nav_menu_instance partial. The key corresponds with the args HMAC.' ); 50 } 51 argsHmac = matches[1]; 52 53 options = options || {}; 54 options.params = _.extend( 55 { 56 selector: '[data-customize-partial-id="' + id + '"]', 57 navMenuArgs: options.constructingContainerContext || {}, 58 containerInclusive: true 59 }, 60 options.params || {} 61 ); 62 api.selectiveRefresh.Partial.prototype.initialize.call( partial, id, options ); 63 64 if ( ! _.isObject( partial.params.navMenuArgs ) ) { 65 throw new Error( 'Missing navMenuArgs' ); 66 } 67 if ( partial.params.navMenuArgs.args_hmac !== argsHmac ) { 68 throw new Error( 'args_hmac mismatch with id' ); 69 } 70 }, 71 72 /** 73 * Return whether the setting is related to this partial. 74 * 75 * @since 4.5.0 76 * @param {wp.customize.Value|string} setting - Object or ID. 77 * @param {number|object|false|null} newValue - New value, or null if the setting was just removed. 78 * @param {number|object|false|null} oldValue - Old value, or null if the setting was just added. 79 * @returns {boolean} 80 */ 81 isRelatedSetting: function( setting, newValue, oldValue ) { 82 var partial = this, navMenuLocationSetting, navMenuId, isNavMenuItemSetting; 83 if ( _.isString( setting ) ) { 84 setting = api( setting ); 85 } 86 87 /* 88 * Prevent nav_menu_item changes only containing type_label differences triggering a refresh. 89 * These settings in the preview do not include type_label property, and so if one of these 90 * nav_menu_item settings is dirty, after a refresh the nav menu instance would do a selective 91 * refresh immediately because the setting from the pane would have the type_label whereas 92 * the setting in the preview would not, thus triggering a change event. The following 93 * condition short-circuits this unnecessary selective refresh and also prevents an infinite 94 * loop in the case where a nav_menu_instance partial had done a fallback refresh. 95 * @todo Nav menu item settings should not include a type_label property to begin with. 96 */ 97 isNavMenuItemSetting = /^nav_menu_item\[/.test( setting.id ); 98 if ( isNavMenuItemSetting && _.isObject( newValue ) && _.isObject( oldValue ) ) { 99 delete newValue.type_label; 100 delete oldValue.type_label; 101 if ( _.isEqual( oldValue, newValue ) ) { 102 return false; 103 } 104 } 105 106 if ( partial.params.navMenuArgs.theme_location ) { 107 if ( 'nav_menu_locations[' + partial.params.navMenuArgs.theme_location + ']' === setting.id ) { 108 return true; 109 } 110 navMenuLocationSetting = api( 'nav_menu_locations[' + partial.params.navMenuArgs.theme_location + ']' ); 111 } 112 113 navMenuId = partial.params.navMenuArgs.menu; 114 if ( ! navMenuId && navMenuLocationSetting ) { 115 navMenuId = navMenuLocationSetting(); 116 } 117 118 if ( ! navMenuId ) { 119 return false; 120 } 121 return ( 122 ( 'nav_menu[' + navMenuId + ']' === setting.id ) || 123 ( isNavMenuItemSetting && ( 124 ( newValue && newValue.nav_menu_term_id === navMenuId ) || 125 ( oldValue && oldValue.nav_menu_term_id === navMenuId ) 126 ) ) 127 ); 128 }, 129 130 /** 131 * Render content. 132 * 133 * @inheritdoc 134 * @param {wp.customize.selectiveRefresh.Placement} placement 135 */ 136 renderContent: function( placement ) { 137 var partial = this, previousContainer = placement.container; 138 if ( api.selectiveRefresh.Partial.prototype.renderContent.call( partial, placement ) ) { 139 140 // Trigger deprecated event. 141 $( document ).trigger( 'customize-preview-menu-refreshed', [ { 142 instanceNumber: null, // @deprecated 143 wpNavArgs: placement.context, // @deprecated 144 wpNavMenuArgs: placement.context, 145 oldContainer: previousContainer, 146 newContainer: placement.container 147 } ] ); 148 } 149 } 150 }); 151 152 api.selectiveRefresh.partialConstructor.nav_menu_instance = self.NavMenuInstancePartial; 153 154 /** 155 * Watch for changes to nav_menu_locations[] settings. 156 * 157 * Refresh partials associated with the given nav_menu_locations[] setting, 158 * or request an entire preview refresh if there are no containers in the 159 * document for a partial associated with the theme location. 160 * 161 * @since 4.5.0 162 */ 163 self.watchNavMenuLocationChanges = function() { 164 api.bind( 'change', function( setting ) { 165 var themeLocation, themeLocationPartialFound = false, matches = setting.id.match( /^nav_menu_locations\[(.+)]$/ ); 166 if ( ! matches ) { 167 return; 168 } 169 themeLocation = matches[1]; 170 api.selectiveRefresh.partial.each( function( partial ) { 171 if ( partial.extended( self.NavMenuInstancePartial ) && partial.params.navMenuArgs.theme_location === themeLocation ) { 172 partial.refresh(); 173 themeLocationPartialFound = true; 174 } 175 } ); 176 177 if ( ! themeLocationPartialFound ) { 178 api.selectiveRefresh.requestFullRefresh(); 179 } 180 } ); 19 181 }; 20 21 api.MenusCustomizerPreview = { 22 /** 23 * Bootstrap functionality. 24 */ 25 init : function() { 26 var self = this, initializedSettings = {}; 27 28 settings = _.extend( {}, defaultSettings ); 29 if ( 'undefined' !== typeof _wpCustomizePreviewNavMenusExports ) { 30 _.extend( settings, _wpCustomizePreviewNavMenusExports ); 31 } 32 33 api.each( function( setting, id ) { 34 setting.id = id; 35 initializedSettings[ setting.id ] = true; 36 self.bindListener( setting ); 37 } ); 38 39 api.preview.bind( 'setting', function( args ) { 40 var id, value, setting; 41 args = args.slice(); 42 id = args.shift(); 43 value = args.shift(); 44 45 setting = api( id ); 46 if ( ! setting ) { 47 // Currently customize-preview.js is not creating settings for dynamically-created settings in the pane, so we have to do it. 48 setting = api.create( id, value ); // @todo This should be in core 49 } 50 if ( ! setting.id ) { 51 // Currently customize-preview.js doesn't set the id property for each setting, like customize-controls.js does. 52 setting.id = id; 53 } 54 55 if ( ! initializedSettings[ setting.id ] ) { 56 initializedSettings[ setting.id ] = true; 57 if ( self.bindListener( setting ) ) { 58 setting.callbacks.fireWith( setting, [ setting(), null ] ); 59 } 60 } 61 } ); 62 63 self.highlightControls(); 64 }, 65 66 /** 67 * 68 * @param {wp.customize.Value} setting 69 * @returns {boolean} Whether the setting was bound. 70 */ 71 bindListener : function( setting ) { 72 var matches, themeLocation; 73 74 matches = setting.id.match( /^nav_menu\[(-?\d+)]$/ ); 75 if ( matches ) { 76 setting.navMenuId = parseInt( matches[1], 10 ); 77 setting.bind( this.onChangeNavMenuSetting ); 78 return true; 79 } 80 81 matches = setting.id.match( /^nav_menu_item\[(-?\d+)]$/ ); 82 if ( matches ) { 83 setting.navMenuItemId = parseInt( matches[1], 10 ); 84 setting.bind( this.onChangeNavMenuItemSetting ); 85 return true; 86 } 87 88 matches = setting.id.match( /^nav_menu_locations\[(.+?)]/ ); 89 if ( matches ) { 90 themeLocation = matches[1]; 91 setting.bind( _.bind( function() { 92 this.refreshMenuLocation( themeLocation ); 93 }, this ) ); 94 return true; 95 } 96 97 return false; 98 }, 99 100 /** 101 * Handle changing of a nav_menu setting. 102 * 103 * @this {wp.customize.Setting} 104 */ 105 onChangeNavMenuSetting : function() { 106 var setting = this; 107 if ( ! setting.navMenuId ) { 108 throw new Error( 'Expected navMenuId property to be set.' ); 109 } 110 api.MenusCustomizerPreview.refreshMenu( setting.navMenuId ); 111 }, 112 113 /** 114 * Handle changing of a nav_menu_item setting. 115 * 116 * @this {wp.customize.Setting} 117 * @param {object} to 118 * @param {object} from 119 */ 120 onChangeNavMenuItemSetting : function( to, from ) { 121 if ( from && from.nav_menu_term_id && ( ! to || from.nav_menu_term_id !== to.nav_menu_term_id ) ) { 122 api.MenusCustomizerPreview.refreshMenu( from.nav_menu_term_id ); 123 } 124 if ( to && to.nav_menu_term_id ) { 125 api.MenusCustomizerPreview.refreshMenu( to.nav_menu_term_id ); 126 } 127 }, 128 129 /** 130 * Update a given menu rendered in the preview. 131 * 132 * @param {int} menuId 133 */ 134 refreshMenu : function( menuId ) { 135 var assignedLocations = []; 136 137 api.each(function( setting, id ) { 138 var matches = id.match( /^nav_menu_locations\[(.+?)]/ ); 139 if ( matches && menuId === setting() ) { 140 assignedLocations.push( matches[1] ); 141 } 142 }); 143 144 _.each( settings.navMenuInstanceArgs, function( navMenuArgs, instanceNumber ) { 145 if ( menuId === navMenuArgs.menu || -1 !== _.indexOf( assignedLocations, navMenuArgs.theme_location ) ) { 146 this.refreshMenuInstanceDebounced( instanceNumber ); 147 } 148 }, this ); 149 }, 150 151 /** 152 * Refresh the menu(s) associated with a given nav menu location. 153 * 154 * @param {string} location 155 */ 156 refreshMenuLocation : function( location ) { 157 var foundInstance = false; 158 _.each( settings.navMenuInstanceArgs, function( navMenuArgs, instanceNumber ) { 159 if ( location === navMenuArgs.theme_location ) { 160 this.refreshMenuInstanceDebounced( instanceNumber ); 161 foundInstance = true; 162 } 163 }, this ); 164 if ( ! foundInstance ) { 165 api.preview.send( 'refresh' ); 166 } 167 }, 168 169 /** 170 * Update a specific instance of a given menu on the page. 171 * 172 * @param {int} instanceNumber 173 */ 174 refreshMenuInstance : function( instanceNumber ) { 175 var data, menuId, customized, container, request, wpNavMenuArgs, instance, containerInstanceClassName; 176 177 if ( ! settings.navMenuInstanceArgs[ instanceNumber ] ) { 178 throw new Error( 'unknown_instance_number' ); 179 } 180 instance = settings.navMenuInstanceArgs[ instanceNumber ]; 181 182 containerInstanceClassName = 'partial-refreshable-nav-menu-' + String( instanceNumber ); 183 container = $( '.' + containerInstanceClassName ); 184 185 if ( _.isNumber( instance.menu ) ) { 186 menuId = instance.menu; 187 } else if ( instance.theme_location && api.has( 'nav_menu_locations[' + instance.theme_location + ']' ) ) { 188 menuId = api( 'nav_menu_locations[' + instance.theme_location + ']' ).get(); 189 } 190 191 if ( ! menuId || ! instance.can_partial_refresh || 0 === container.length ) { 192 api.preview.send( 'refresh' ); 182 } 183 184 /** 185 * Connect nav menu items with their corresponding controls in the pane. 186 * 187 * Setup shift-click on nav menu items which are more granular than the nav menu partial itself. 188 * Also this applies even if a nav menu is not partial-refreshable. 189 * 190 * @since 4.5.0 191 */ 192 self.highlightControls = function() { 193 var selector = '.menu-item'; 194 195 // Focus on the menu item control when shift+clicking the menu item. 196 $( document ).on( 'click', selector, function( e ) { 197 var navMenuItemParts; 198 if ( ! e.shiftKey ) { 193 199 return; 194 200 } 195 menuId = parseInt( menuId, 10 ); 196 197 data = { 198 nonce: wp.customize.settings.nonce.preview, 199 wp_customize: 'on' 200 }; 201 if ( ! wp.customize.settings.theme.active ) { 202 data.theme = wp.customize.settings.theme.stylesheet; 201 202 navMenuItemParts = $( this ).attr( 'class' ).match( /(?:^|\s)menu-item-(\d+)(?:\s|$)/ ); 203 if ( navMenuItemParts ) { 204 e.preventDefault(); 205 e.stopPropagation(); // Make sure a sub-nav menu item will get focused instead of parent items. 206 api.preview.send( 'focus-nav-menu-item-control', parseInt( navMenuItemParts[1], 10 ) ); 203 207 } 204 data[ settings.renderQueryVar ] = '1'; 205 206 // Gather settings to send in partial refresh request. 207 customized = {}; 208 api.each( function( setting, id ) { 209 var value = setting.get(), shouldSend = false; 210 // @todo Core should propagate the dirty state into the Preview as well so we can use that here. 211 212 // Send setting if it is a nav_menu_locations[] setting. 213 shouldSend = shouldSend || /^nav_menu_locations\[/.test( id ); 214 215 // Send setting if it is the setting for this menu. 216 shouldSend = shouldSend || id === 'nav_menu[' + String( menuId ) + ']'; 217 218 // Send setting if it is one that is associated with this menu, or it is deleted. 219 shouldSend = shouldSend || ( /^nav_menu_item\[/.test( id ) && ( false === value || menuId === value.nav_menu_term_id ) ); 220 221 if ( shouldSend ) { 222 customized[ id ] = value; 223 } 224 } ); 225 data.customized = JSON.stringify( customized ); 226 data[ settings.renderNoncePostKey ] = settings.renderNonceValue; 227 228 wpNavMenuArgs = $.extend( {}, instance ); 229 data.wp_nav_menu_args_hash = wpNavMenuArgs.args_hash; 230 delete wpNavMenuArgs.args_hash; 231 data.wp_nav_menu_args = JSON.stringify( wpNavMenuArgs ); 232 233 container.addClass( 'customize-partial-refreshing' ); 234 235 request = wp.ajax.send( null, { 236 data: data, 237 url: api.settings.url.self 238 } ); 239 request.done( function( data ) { 240 // If the menu is now not visible, refresh since the page layout may have changed. 241 if ( false === data ) { 242 api.preview.send( 'refresh' ); 243 return; 244 } 245 246 var eventParam, previousContainer = container; 247 container = $( data ); 248 container.addClass( containerInstanceClassName ); 249 container.addClass( 'partial-refreshable-nav-menu customize-partial-refreshing' ); 250 previousContainer.replaceWith( container ); 251 eventParam = { 252 instanceNumber: instanceNumber, 253 wpNavArgs: wpNavMenuArgs, // @deprecated 254 wpNavMenuArgs: wpNavMenuArgs, 255 oldContainer: previousContainer, 256 newContainer: container 257 }; 258 container.removeClass( 'customize-partial-refreshing' ); 259 $( document ).trigger( 'customize-preview-menu-refreshed', [ eventParam ] ); 260 } ); 261 request.fail( function() { 262 api.preview.send( 'refresh' ); 263 } ); 264 }, 265 266 refreshMenuInstanceDebounced : function( instanceNumber ) { 267 if ( currentRefreshDebounced[ instanceNumber ] ) { 268 clearTimeout( currentRefreshDebounced[ instanceNumber ] ); 269 } 270 currentRefreshDebounced[ instanceNumber ] = setTimeout( 271 _.bind( function() { 272 this.refreshMenuInstance( instanceNumber ); 273 }, this ), 274 refreshDebounceDelay 275 ); 276 }, 277 278 /** 279 * Connect nav menu items with their corresponding controls in the pane. 280 */ 281 highlightControls: function() { 282 var selector = '.menu-item', 283 addTooltips; 284 285 // Open expand the menu item control when shift+clicking the menu item 286 $( document ).on( 'click', selector, function( e ) { 287 var navMenuItemParts; 288 if ( ! e.shiftKey ) { 289 return; 290 } 291 292 navMenuItemParts = $( this ).attr( 'class' ).match( /(?:^|\s)menu-item-(\d+)(?:\s|$)/ ); 293 if ( navMenuItemParts ) { 294 e.preventDefault(); 295 e.stopPropagation(); // Make sure a sub-nav menu item will get focused instead of parent items. 296 api.preview.send( 'focus-nav-menu-item-control', parseInt( navMenuItemParts[1], 10 ) ); 297 } 298 }); 299 300 addTooltips = function( e, params ) { 301 params.newContainer.find( selector ).attr( 'title', settings.l10n.editNavMenuItemTooltip ); 302 }; 303 304 addTooltips( null, { newContainer: $( document.body ) } ); 305 $( document ).on( 'customize-preview-menu-refreshed', addTooltips ); 306 } 208 }); 307 209 }; 308 210 309 211 api.bind( 'preview-ready', function() { 310 api.preview.bind( 'active', function() { 311 api.MenusCustomizerPreview.init(); 312 } ); 212 self.init(); 313 213 } ); 314 214 315 }( jQuery, _, wp ) ); 215 return self; 216 217 }( jQuery, _, wp, wp.customize ) ); -
trunk/src/wp-includes/js/customize-preview-widgets.js
r35783 r36586 1 (function( wp, $ ){ 2 3 if ( ! wp || ! wp.customize ) { return; } 4 5 var api = wp.customize; 6 7 /** 8 * wp.customize.WidgetCustomizerPreview 9 * 10 */ 11 api.WidgetCustomizerPreview = { 12 renderedSidebars: {}, // @todo Make rendered a property of the Backbone model 13 renderedWidgets: {}, // @todo Make rendered a property of the Backbone model 14 registeredSidebars: [], // @todo Make a Backbone collection 15 registeredWidgets: {}, // @todo Make array, Backbone collection 1 /* global _wpWidgetCustomizerPreviewSettings */ 2 wp.customize.widgetsPreview = wp.customize.WidgetCustomizerPreview = (function( $, _, wp, api ) { 3 4 var self; 5 6 self = { 7 renderedSidebars: {}, 8 renderedWidgets: {}, 9 registeredSidebars: [], 10 registeredWidgets: {}, 16 11 widgetSelectors: [], 17 12 preview: null, 18 l10n: {}, 19 20 init: function () { 21 var self = this; 22 23 this.preview = api.preview; 24 this.buildWidgetSelectors(); 25 this.highlightControls(); 26 27 this.preview.bind( 'highlight-widget', self.highlightWidget ); 28 }, 13 l10n: { 14 widgetTooltip: '' 15 } 16 }; 17 18 /** 19 * Init widgets preview. 20 * 21 * @since 4.5.0 22 */ 23 self.init = function() { 24 var self = this; 25 26 self.preview = api.preview; 27 if ( api.selectiveRefresh ) { 28 self.addPartials(); 29 } 30 31 self.buildWidgetSelectors(); 32 self.highlightControls(); 33 34 self.preview.bind( 'highlight-widget', self.highlightWidget ); 35 36 api.preview.bind( 'active', function() { 37 self.highlightControls(); 38 } ); 39 }; 40 41 if ( api.selectiveRefresh ) { 29 42 30 43 /** 31 * Calculate the selector for the sidebar's widgets based on the registered sidebar's info 44 * Partial representing a widget instance. 45 * 46 * @class 47 * @augments wp.customize.selectiveRefresh.Partial 48 * @since 4.5.0 32 49 */ 33 buildWidgetSelectors: function () { 34 var self = this; 35 36 $.each( this.registeredSidebars, function ( i, sidebar ) { 37 var widgetTpl = [ 38 sidebar.before_widget.replace('%1$s', '').replace('%2$s', ''), 39 sidebar.before_title, 40 sidebar.after_title, 41 sidebar.after_widget 42 ].join(''), 43 emptyWidget, 44 widgetSelector, 45 widgetClasses; 46 47 emptyWidget = $(widgetTpl); 48 widgetSelector = emptyWidget.prop('tagName'); 49 widgetClasses = emptyWidget.prop('className'); 50 51 // Prevent a rare case when before_widget, before_title, after_title and after_widget is empty. 52 if ( ! widgetClasses ) { 50 self.WidgetPartial = api.selectiveRefresh.Partial.extend({ 51 52 /** 53 * Constructor. 54 * 55 * @since 4.5.0 56 * @param {string} id - Partial ID. 57 * @param {Object} options 58 * @param {Object} options.params 59 */ 60 initialize: function( id, options ) { 61 var partial = this, matches; 62 matches = id.match( /^widget\[(.+)]$/ ); 63 if ( ! matches ) { 64 throw new Error( 'Illegal id for widget partial.' ); 65 } 66 67 partial.widgetId = matches[1]; 68 options = options || {}; 69 options.params = _.extend( 70 { 71 /* Note that a selector of ('#' + partial.widgetId) is faster, but jQuery will only return the one result. */ 72 selector: '[id="' + partial.widgetId + '"]', // Alternatively, '[data-customize-widget-id="' + partial.widgetId + '"]' 73 settings: [ self.getWidgetSettingId( partial.widgetId ) ], 74 containerInclusive: true 75 }, 76 options.params || {} 77 ); 78 79 api.selectiveRefresh.Partial.prototype.initialize.call( partial, id, options ); 80 }, 81 82 /** 83 * Send widget-updated message to parent so spinner will get removed from widget control. 84 * 85 * @inheritdoc 86 * @param {wp.customize.selectiveRefresh.Placement} placement 87 */ 88 renderContent: function( placement ) { 89 var partial = this; 90 if ( api.selectiveRefresh.Partial.prototype.renderContent.call( partial, placement ) ) { 91 api.preview.send( 'widget-updated', partial.widgetId ); 92 api.selectiveRefresh.trigger( 'widget-updated', partial ); 93 } 94 } 95 }); 96 97 /** 98 * Partial representing a widget area. 99 * 100 * @class 101 * @augments wp.customize.selectiveRefresh.Partial 102 * @since 4.5.0 103 */ 104 self.SidebarPartial = api.selectiveRefresh.Partial.extend({ 105 106 /** 107 * Constructor. 108 * 109 * @since 4.5.0 110 * @param {string} id - Partial ID. 111 * @param {Object} options 112 * @param {Object} options.params 113 */ 114 initialize: function( id, options ) { 115 var partial = this, matches; 116 matches = id.match( /^sidebar\[(.+)]$/ ); 117 if ( ! matches ) { 118 throw new Error( 'Illegal id for sidebar partial.' ); 119 } 120 partial.sidebarId = matches[1]; 121 122 options = options || {}; 123 options.params = _.extend( 124 { 125 settings: [ 'sidebars_widgets[' + partial.sidebarId + ']' ] 126 }, 127 options.params || {} 128 ); 129 130 api.selectiveRefresh.Partial.prototype.initialize.call( partial, id, options ); 131 132 if ( ! partial.params.sidebarArgs ) { 133 throw new Error( 'The sidebarArgs param was not provided.' ); 134 } 135 if ( partial.params.settings.length > 1 ) { 136 throw new Error( 'Expected SidebarPartial to only have one associated setting' ); 137 } 138 }, 139 140 /** 141 * Set up the partial. 142 * 143 * @since 4.5.0 144 */ 145 ready: function() { 146 var sidebarPartial = this; 147 148 // Watch for changes to the sidebar_widgets setting. 149 _.each( sidebarPartial.settings(), function( settingId ) { 150 api( settingId ).bind( _.bind( sidebarPartial.handleSettingChange, sidebarPartial ) ); 151 } ); 152 153 // Trigger an event for this sidebar being updated whenever a widget inside is rendered. 154 api.selectiveRefresh.bind( 'partial-content-rendered', function( placement ) { 155 var isAssignedWidgetPartial = ( 156 placement.partial.extended( self.WidgetPartial ) && 157 ( -1 !== _.indexOf( sidebarPartial.getWidgetIds(), placement.partial.widgetId ) ) 158 ); 159 if ( isAssignedWidgetPartial ) { 160 api.selectiveRefresh.trigger( 'sidebar-updated', sidebarPartial ); 161 } 162 } ); 163 164 // Make sure that a widget partial has a container in the DOM prior to a refresh. 165 api.bind( 'change', function( widgetSetting ) { 166 var widgetId, parsedId; 167 parsedId = self.parseWidgetSettingId( widgetSetting.id ); 168 if ( ! parsedId ) { 169 return; 170 } 171 widgetId = parsedId.idBase; 172 if ( parsedId.number ) { 173 widgetId += '-' + String( parsedId.number ); 174 } 175 if ( -1 !== _.indexOf( sidebarPartial.getWidgetIds(), widgetId ) ) { 176 sidebarPartial.ensureWidgetPlacementContainers( widgetId ); 177 } 178 } ); 179 }, 180 181 /** 182 * Get the before/after boundary nodes for all instances of this sidebar (usually one). 183 * 184 * Note that TreeWalker is not implemented in IE8. 185 * 186 * @since 4.5.0 187 * @returns {Array.<{before: Comment, after: Comment, instanceNumber: number}>} 188 */ 189 findDynamicSidebarBoundaryNodes: function() { 190 var partial = this, regExp, boundaryNodes = {}, recursiveCommentTraversal; 191 regExp = /^(dynamic_sidebar_before|dynamic_sidebar_after):(.+):(\d+)$/; 192 recursiveCommentTraversal = function( childNodes ) { 193 _.each( childNodes, function( node ) { 194 var matches; 195 if ( 8 === node.nodeType ) { 196 matches = node.nodeValue.match( regExp ); 197 if ( ! matches || matches[2] !== partial.sidebarId ) { 198 return; 199 } 200 if ( _.isUndefined( boundaryNodes[ matches[3] ] ) ) { 201 boundaryNodes[ matches[3] ] = { 202 before: null, 203 after: null, 204 instanceNumber: parseInt( matches[3], 10 ) 205 }; 206 } 207 if ( 'dynamic_sidebar_before' === matches[1] ) { 208 boundaryNodes[ matches[3] ].before = node; 209 } else { 210 boundaryNodes[ matches[3] ].after = node; 211 } 212 } else if ( 1 === node.nodeType ) { 213 recursiveCommentTraversal( node.childNodes ); 214 } 215 } ); 216 }; 217 218 recursiveCommentTraversal( document.body.childNodes ); 219 return _.values( boundaryNodes ); 220 }, 221 222 /** 223 * Get the placements for this partial. 224 * 225 * @since 4.5.0 226 * @returns {Array} 227 */ 228 placements: function() { 229 var partial = this; 230 return _.map( partial.findDynamicSidebarBoundaryNodes(), function( boundaryNodes ) { 231 return new api.selectiveRefresh.Placement( { 232 partial: partial, 233 container: null, 234 startNode: boundaryNodes.before, 235 endNode: boundaryNodes.after, 236 context: { 237 instanceNumber: boundaryNodes.instanceNumber 238 } 239 } ); 240 } ); 241 }, 242 243 /** 244 * Get the list of widget IDs associated with this widget area. 245 * 246 * @since 4.5.0 247 * 248 * @returns {Array} 249 */ 250 getWidgetIds: function() { 251 var sidebarPartial = this, settingId, widgetIds; 252 settingId = sidebarPartial.settings()[0]; 253 if ( ! settingId ) { 254 throw new Error( 'Missing associated setting.' ); 255 } 256 if ( ! api.has( settingId ) ) { 257 throw new Error( 'Setting does not exist.' ); 258 } 259 widgetIds = api( settingId ).get(); 260 if ( ! _.isArray( widgetIds ) ) { 261 throw new Error( 'Expected setting to be array of widget IDs' ); 262 } 263 return widgetIds.slice( 0 ); 264 }, 265 266 /** 267 * Reflow widgets in the sidebar, ensuring they have the proper position in the DOM. 268 * 269 * @since 4.5.0 270 * 271 * @return {Array.<wp.customize.selectiveRefresh.Placement>} List of placements that were reflowed. 272 */ 273 reflowWidgets: function() { 274 var sidebarPartial = this, sidebarPlacements, widgetIds, widgetPartials, sortedSidebarContainers = []; 275 widgetIds = sidebarPartial.getWidgetIds(); 276 sidebarPlacements = sidebarPartial.placements(); 277 278 widgetPartials = {}; 279 _.each( widgetIds, function( widgetId ) { 280 var widgetPartial = api.selectiveRefresh.partial( 'widget[' + widgetId + ']' ); 281 if ( widgetPartial ) { 282 widgetPartials[ widgetId ] = widgetPartial; 283 } 284 } ); 285 286 _.each( sidebarPlacements, function( sidebarPlacement ) { 287 var sidebarWidgets = [], needsSort = false, thisPosition, lastPosition = -1; 288 289 // Gather list of widget partial containers in this sidebar, and determine if a sort is needed. 290 _.each( widgetPartials, function( widgetPartial ) { 291 _.each( widgetPartial.placements(), function( widgetPlacement ) { 292 293 if ( sidebarPlacement.context.instanceNumber === widgetPlacement.context.sidebar_instance_number ) { 294 thisPosition = widgetPlacement.container.index(); 295 sidebarWidgets.push( { 296 partial: widgetPartial, 297 placement: widgetPlacement, 298 position: thisPosition 299 } ); 300 if ( thisPosition < lastPosition ) { 301 needsSort = true; 302 } 303 lastPosition = thisPosition; 304 } 305 } ); 306 } ); 307 308 if ( needsSort ) { 309 _.each( sidebarWidgets, function( sidebarWidget ) { 310 sidebarPlacement.endNode.parentNode.insertBefore( 311 sidebarWidget.placement.container[0], 312 sidebarPlacement.endNode 313 ); 314 315 // @todo Rename partial-placement-moved? 316 api.selectiveRefresh.trigger( 'partial-content-moved', sidebarWidget.placement ); 317 } ); 318 319 sortedSidebarContainers.push( sidebarPlacement ); 320 } 321 } ); 322 323 if ( sortedSidebarContainers.length > 0 ) { 324 api.selectiveRefresh.trigger( 'sidebar-updated', sidebarPartial ); 325 } 326 327 return sortedSidebarContainers; 328 }, 329 330 /** 331 * Make sure there is a widget instance container in this sidebar for the given widget ID. 332 * 333 * @since 4.5.0 334 * 335 * @param {string} widgetId 336 * @returns {wp.customize.selectiveRefresh.Partial} Widget instance partial. 337 */ 338 ensureWidgetPlacementContainers: function( widgetId ) { 339 var sidebarPartial = this, widgetPartial, wasInserted = false, partialId = 'widget[' + widgetId + ']'; 340 widgetPartial = api.selectiveRefresh.partial( partialId ); 341 if ( ! widgetPartial ) { 342 widgetPartial = new self.WidgetPartial( partialId, { 343 params: {} 344 } ); 345 api.selectiveRefresh.partial.add( widgetPartial.id, widgetPartial ); 346 } 347 348 // Make sure that there is a container element for the widget in the sidebar, if at least a placeholder. 349 _.each( sidebarPartial.placements(), function( sidebarPlacement ) { 350 var foundWidgetPlacement, widgetContainerElement; 351 352 foundWidgetPlacement = _.find( widgetPartial.placements(), function( widgetPlacement ) { 353 return ( widgetPlacement.context.sidebar_instance_number === sidebarPlacement.context.instanceNumber ); 354 } ); 355 if ( foundWidgetPlacement ) { 356 return; 357 } 358 359 widgetContainerElement = $( 360 sidebarPartial.params.sidebarArgs.before_widget.replace( '%1$s', widgetId ).replace( '%2$s', 'widget' ) + 361 sidebarPartial.params.sidebarArgs.after_widget 362 ); 363 364 widgetContainerElement.attr( 'data-customize-partial-id', widgetPartial.id ); 365 widgetContainerElement.attr( 'data-customize-partial-type', 'widget' ); 366 widgetContainerElement.attr( 'data-customize-widget-id', widgetId ); 367 368 /* 369 * Make sure the widget container element has the customize-container context data. 370 * The sidebar_instance_number is used to disambiguate multiple instances of the 371 * same sidebar are rendered onto the template, and so the same widget is embedded 372 * multiple times. 373 */ 374 widgetContainerElement.data( 'customize-partial-placement-context', { 375 'sidebar_id': sidebarPartial.sidebarId, 376 'sidebar_instance_number': sidebarPlacement.context.instanceNumber 377 } ); 378 379 sidebarPlacement.endNode.parentNode.insertBefore( widgetContainerElement[0], sidebarPlacement.endNode ); 380 wasInserted = true; 381 } ); 382 383 if ( wasInserted ) { 384 sidebarPartial.reflowWidgets(); 385 } 386 387 return widgetPartial; 388 }, 389 390 /** 391 * Handle change to the sidebars_widgets[] setting. 392 * 393 * @since 4.5.0 394 * 395 * @param {Array} newWidgetIds New widget ids. 396 * @param {Array} oldWidgetIds Old widget ids. 397 */ 398 handleSettingChange: function( newWidgetIds, oldWidgetIds ) { 399 var sidebarPartial = this, needsRefresh, widgetsRemoved, widgetsAdded, addedWidgetPartials = []; 400 401 needsRefresh = ( 402 ( oldWidgetIds.length > 0 && 0 === newWidgetIds.length ) || 403 ( newWidgetIds.length > 0 && 0 === oldWidgetIds.length ) 404 ); 405 if ( needsRefresh ) { 406 sidebarPartial.fallback(); 53 407 return; 54 408 } 55 409 56 widgetClasses = widgetClasses.replace(/^\s+|\s+$/g, ''); 57 58 if ( widgetClasses ) { 59 widgetSelector += '.' + widgetClasses.split(/\s+/).join('.'); 60 } 61 self.widgetSelectors.push(widgetSelector); 62 }); 63 }, 410 // Handle removal of widgets. 411 widgetsRemoved = _.difference( oldWidgetIds, newWidgetIds ); 412 _.each( widgetsRemoved, function( removedWidgetId ) { 413 var widgetPartial = api.selectiveRefresh.partial( 'widget[' + removedWidgetId + ']' ); 414 if ( widgetPartial ) { 415 _.each( widgetPartial.placements(), function( placement ) { 416 var isRemoved = ( 417 placement.context.sidebar_id === sidebarPartial.sidebarId || 418 ( placement.context.sidebar_args && placement.context.sidebar_args.id === sidebarPartial.sidebarId ) 419 ); 420 if ( isRemoved ) { 421 placement.container.remove(); 422 } 423 } ); 424 } 425 } ); 426 427 // Handle insertion of widgets. 428 widgetsAdded = _.difference( newWidgetIds, oldWidgetIds ); 429 _.each( widgetsAdded, function( addedWidgetId ) { 430 var widgetPartial = sidebarPartial.ensureWidgetPlacementContainers( addedWidgetId ); 431 addedWidgetPartials.push( widgetPartial ); 432 } ); 433 434 _.each( addedWidgetPartials, function( widgetPartial ) { 435 widgetPartial.refresh(); 436 } ); 437 438 api.selectiveRefresh.trigger( 'sidebar-updated', sidebarPartial ); 439 }, 440 441 /** 442 * Note that the meat is handled in handleSettingChange because it has the context of which widgets were removed. 443 * 444 * @since 4.5.0 445 */ 446 refresh: function() { 447 var partial = this, deferred = $.Deferred(); 448 449 deferred.fail( function() { 450 partial.fallback(); 451 } ); 452 453 if ( 0 === partial.placements().length ) { 454 deferred.reject(); 455 } else { 456 _.each( partial.reflowWidgets(), function( sidebarPlacement ) { 457 api.selectiveRefresh.trigger( 'partial-content-rendered', sidebarPlacement ); 458 } ); 459 deferred.resolve(); 460 } 461 462 return deferred.promise(); 463 } 464 }); 465 466 api.selectiveRefresh.partialConstructor.sidebar = self.SidebarPartial; 467 api.selectiveRefresh.partialConstructor.widget = self.WidgetPartial; 64 468 65 469 /** 66 * Highlight the widget on widget updates or widget control mouse overs.470 * Add partials for the registered widget areas (sidebars). 67 471 * 68 * @ param {string} widgetId ID of the widget.472 * @since 4.5.0 69 473 */ 70 highlightWidget: function( widgetId ) { 71 var $body = $( document.body ), 72 $widget = $( '#' + widgetId ); 73 74 $body.find( '.widget-customizer-highlighted-widget' ).removeClass( 'widget-customizer-highlighted-widget' ); 75 76 $widget.addClass( 'widget-customizer-highlighted-widget' ); 77 setTimeout( function () { 78 $widget.removeClass( 'widget-customizer-highlighted-widget' ); 79 }, 500 ); 80 }, 81 82 /** 83 * Show a title and highlight widgets on hover. On shift+clicking 84 * focus the widget control. 85 */ 86 highlightControls: function() { 87 var self = this, 88 selector = this.widgetSelectors.join(','); 89 90 $(selector).attr( 'title', this.l10n.widgetTooltip ); 91 92 $(document).on( 'mouseenter', selector, function () { 93 self.preview.send( 'highlight-widget-control', $( this ).prop( 'id' ) ); 94 }); 95 96 // Open expand the widget control when shift+clicking the widget element 97 $(document).on( 'click', selector, function ( e ) { 98 if ( ! e.shiftKey ) { 99 return; 100 } 101 e.preventDefault(); 102 103 self.preview.send( 'focus-widget-control', $( this ).prop( 'id' ) ); 104 }); 474 self.addPartials = function() { 475 _.each( self.registeredSidebars, function( registeredSidebar ) { 476 var partial, partialId = 'sidebar[' + registeredSidebar.id + ']'; 477 partial = api.selectiveRefresh.partial( partialId ); 478 if ( ! partial ) { 479 partial = new self.SidebarPartial( partialId, { 480 params: { 481 sidebarArgs: registeredSidebar 482 } 483 } ); 484 api.selectiveRefresh.partial.add( partial.id, partial ); 485 } 486 } ); 487 }; 488 489 } 490 491 /** 492 * Calculate the selector for the sidebar's widgets based on the registered sidebar's info. 493 * 494 * @since 3.9.0 495 */ 496 self.buildWidgetSelectors = function() { 497 var self = this; 498 499 $.each( self.registeredSidebars, function( i, sidebar ) { 500 var widgetTpl = [ 501 sidebar.before_widget.replace( '%1$s', '' ).replace( '%2$s', '' ), 502 sidebar.before_title, 503 sidebar.after_title, 504 sidebar.after_widget 505 ].join( '' ), 506 emptyWidget, 507 widgetSelector, 508 widgetClasses; 509 510 emptyWidget = $( widgetTpl ); 511 widgetSelector = emptyWidget.prop( 'tagName' ); 512 widgetClasses = emptyWidget.prop( 'className' ); 513 514 // Prevent a rare case when before_widget, before_title, after_title and after_widget is empty. 515 if ( ! widgetClasses ) { 516 return; 517 } 518 519 widgetClasses = widgetClasses.replace( /^\s+|\s+$/g, '' ); 520 521 if ( widgetClasses ) { 522 widgetSelector += '.' + widgetClasses.split( /\s+/ ).join( '.' ); 523 } 524 self.widgetSelectors.push( widgetSelector ); 525 }); 526 }; 527 528 /** 529 * Highlight the widget on widget updates or widget control mouse overs. 530 * 531 * @since 3.9.0 532 * @param {string} widgetId ID of the widget. 533 */ 534 self.highlightWidget = function( widgetId ) { 535 var $body = $( document.body ), 536 $widget = $( '#' + widgetId ); 537 538 $body.find( '.widget-customizer-highlighted-widget' ).removeClass( 'widget-customizer-highlighted-widget' ); 539 540 $widget.addClass( 'widget-customizer-highlighted-widget' ); 541 setTimeout( function() { 542 $widget.removeClass( 'widget-customizer-highlighted-widget' ); 543 }, 500 ); 544 }; 545 546 /** 547 * Show a title and highlight widgets on hover. On shift+clicking 548 * focus the widget control. 549 * 550 * @since 3.9.0 551 */ 552 self.highlightControls = function() { 553 var self = this, 554 selector = this.widgetSelectors.join( ',' ); 555 556 $( selector ).attr( 'title', this.l10n.widgetTooltip ); 557 558 $( document ).on( 'mouseenter', selector, function() { 559 self.preview.send( 'highlight-widget-control', $( this ).prop( 'id' ) ); 560 }); 561 562 // Open expand the widget control when shift+clicking the widget element 563 $( document ).on( 'click', selector, function( e ) { 564 if ( ! e.shiftKey ) { 565 return; 566 } 567 e.preventDefault(); 568 569 self.preview.send( 'focus-widget-control', $( this ).prop( 'id' ) ); 570 }); 571 }; 572 573 /** 574 * Parse a widget ID. 575 * 576 * @since 4.5.0 577 * 578 * @param {string} widgetId Widget ID. 579 * @returns {{idBase: string, number: number|null}} 580 */ 581 self.parseWidgetId = function( widgetId ) { 582 var matches, parsed = { 583 idBase: '', 584 number: null 585 }; 586 587 matches = widgetId.match( /^(.+)-(\d+)$/ ); 588 if ( matches ) { 589 parsed.idBase = matches[1]; 590 parsed.number = parseInt( matches[2], 10 ); 591 } else { 592 parsed.idBase = widgetId; // Likely an old single widget. 105 593 } 106 }; 107 108 $(function () { 109 var settings = window._wpWidgetCustomizerPreviewSettings; 110 if ( ! settings ) { 111 return; 594 595 return parsed; 596 }; 597 598 /** 599 * Parse a widget setting ID. 600 * 601 * @since 4.5.0 602 * 603 * @param {string} settingId Widget setting ID. 604 * @returns {{idBase: string, number: number|null}|null} 605 */ 606 self.parseWidgetSettingId = function( settingId ) { 607 var matches, parsed = { 608 idBase: '', 609 number: null 610 }; 611 612 matches = settingId.match( /^widget_([^\[]+?)(?:\[(\d+)])?$/ ); 613 if ( ! matches ) { 614 return null; 112 615 } 113 114 $.extend( api.WidgetCustomizerPreview, settings ); 115 116 api.WidgetCustomizerPreview.init(); 616 parsed.idBase = matches[1]; 617 if ( matches[2] ) { 618 parsed.number = parseInt( matches[2], 10 ); 619 } 620 return parsed; 621 }; 622 623 /** 624 * Convert a widget ID into a Customizer setting ID. 625 * 626 * @since 4.5.0 627 * 628 * @param {string} widgetId Widget ID. 629 * @returns {string} settingId Setting ID. 630 */ 631 self.getWidgetSettingId = function( widgetId ) { 632 var parsed = this.parseWidgetId( widgetId ), settingId; 633 634 settingId = 'widget_' + parsed.idBase; 635 if ( parsed.number ) { 636 settingId += '[' + String( parsed.number ) + ']'; 637 } 638 639 return settingId; 640 }; 641 642 api.bind( 'preview-ready', function() { 643 $.extend( self, _wpWidgetCustomizerPreviewSettings ); 644 self.init(); 117 645 }); 118 646 119 })( window.wp, jQuery ); 647 return self; 648 })( jQuery, _, wp, wp.customize ); -
trunk/src/wp-includes/script-loader.php
r36551 r36586 448 448 'allowedFiles' => __( 'Allowed Files' ), 449 449 ) ); 450 $scripts->add( 'customize-selective-refresh', "/wp-includes/js/customize-selective-refresh$suffix.js", array( 'jquery', 'wp-util', 'customize-preview' ), false, 1 ); 450 451 451 452 $scripts->add( 'customize-widgets', "/wp-admin/js/customize-widgets$suffix.js", array( 'jquery', 'jquery-ui-sortable', 'jquery-ui-droppable', 'wp-backbone', 'customize-controls' ), false, 1 ); -
trunk/tests/phpunit/tests/customize/manager.php
r36532 r36586 426 426 $this->assertNotEmpty( $data ); 427 427 428 $this->assertEqualSets( array( 'theme', 'url', 'browser', 'panels', 'sections', 'nonce', 'autofocus', 'documentTitleTmpl', 'previewableDevices' ), array_keys( $data ) );428 $this->assertEqualSets( array( 'theme', 'url', 'browser', 'panels', 'sections', 'nonce', 'autofocus', 'documentTitleTmpl', 'previewableDevices', 'selectiveRefreshEnabled' ), array_keys( $data ) ); 429 429 $this->assertEquals( $autofocus, $data['autofocus'] ); 430 430 $this->assertArrayHasKey( 'save', $data['nonce'] ); -
trunk/tests/phpunit/tests/customize/nav-menu-item-setting.php
r35583 r36586 70 70 $setting = new WP_Customize_Nav_Menu_Item_Setting( $this->wp_customize, 'nav_menu_item[123]' ); 71 71 $this->assertEquals( 'nav_menu_item', $setting->type ); 72 $this->assertEquals( 'postMessage', $setting->transport );73 72 $this->assertEquals( 123, $setting->post_id ); 74 73 $this->assertNull( $setting->previous_post_id ); -
trunk/tests/phpunit/tests/customize/nav-menus.php
r36414 r36586 354 354 $expected = array( 'type' => 'nav_menu_item' ); 355 355 $results = $menus->filter_dynamic_setting_args( $this->wp_customize, 'nav_menu_item[123]' ); 356 $this->assertEquals( $expected , $results);356 $this->assertEquals( $expected['type'], $results['type'] ); 357 357 358 358 $expected = array( 'type' => 'nav_menu' ); 359 359 $results = $menus->filter_dynamic_setting_args( $this->wp_customize, 'nav_menu[123]' ); 360 $this->assertEquals( $expected , $results);360 $this->assertEquals( $expected['type'], $results['type'] ); 361 361 } 362 362 … … 524 524 525 525 /** 526 * Test WP_Customize_Nav_Menus::customize_dynamic_partial_args(). 527 * 528 * @see WP_Customize_Nav_Menus::customize_dynamic_partial_args() 529 */ 530 function test_customize_dynamic_partial_args() { 531 do_action( 'customize_register', $this->wp_customize ); 532 533 $args = apply_filters( 'customize_dynamic_partial_args', false, 'nav_menu_instance[68b329da9893e34099c7d8ad5cb9c940]' ); 534 $this->assertInternalType( 'array', $args ); 535 $this->assertEquals( 'nav_menu_instance', $args['type'] ); 536 $this->assertEquals( array( $this->wp_customize->nav_menus, 'render_nav_menu_partial' ), $args['render_callback'] ); 537 $this->assertTrue( $args['container_inclusive'] ); 538 539 $args = apply_filters( 'customize_dynamic_partial_args', array( 'fallback_refresh' => false ), 'nav_menu_instance[4099c7d8ad5cb9c94068b329da9893e3]' ); 540 $this->assertInternalType( 'array', $args ); 541 $this->assertEquals( 'nav_menu_instance', $args['type'] ); 542 $this->assertEquals( array( $this->wp_customize->nav_menus, 'render_nav_menu_partial' ), $args['render_callback'] ); 543 $this->assertTrue( $args['container_inclusive'] ); 544 $this->assertFalse( $args['fallback_refresh'] ); 545 } 546 547 /** 526 548 * Test the customize_preview_init method. 527 549 * … … 533 555 534 556 $menus->customize_preview_init(); 535 $this->assertEquals( 10, has_action( 'template_redirect', array( $menus, 'render_menu' ) ) );536 557 $this->assertEquals( 10, has_action( 'wp_enqueue_scripts', array( $menus, 'customize_preview_enqueue_deps' ) ) ); 537 538 if ( ! isset( $_REQUEST[ WP_Customize_Nav_Menus::RENDER_QUERY_VAR ] ) ) { 539 $this->assertEquals( 1000, has_filter( 'wp_nav_menu_args', array( $menus, 'filter_wp_nav_menu_args' ) ) ); 540 $this->assertEquals( 10, has_filter( 'wp_nav_menu', array( $menus, 'filter_wp_nav_menu' ) ) ); 541 } 558 $this->assertEquals( 1000, has_filter( 'wp_nav_menu_args', array( $menus, 'filter_wp_nav_menu_args' ) ) ); 559 $this->assertEquals( 10, has_filter( 'wp_nav_menu', array( $menus, 'filter_wp_nav_menu' ) ) ); 542 560 } 543 561 … … 549 567 function test_filter_wp_nav_menu_args() { 550 568 do_action( 'customize_register', $this->wp_customize ); 551 $menus = new WP_Customize_Nav_Menus( $this->wp_customize );569 $menus = $this->wp_customize->nav_menus; 552 570 553 571 $results = $menus->filter_wp_nav_menu_args( array( … … 556 574 'walker' => '', 557 575 'menu' => wp_create_nav_menu( 'Foo' ), 576 'items_wrap' => '<ul id="%1$s" class="%2$s">%3$s</ul>', 558 577 ) ); 559 $this->assertEquals( 1, $results['can_partial_refresh'] ); 560 561 $expected = array( 562 'echo', 563 'can_partial_refresh', 564 'fallback_cb', 565 'instance_number', 566 'walker', 567 ); 578 $this->assertArrayHasKey( 'customize_preview_nav_menus_args', $results ); 579 568 580 $results = $menus->filter_wp_nav_menu_args( array( 569 581 'echo' => false, 570 582 'fallback_cb' => 'wp_page_menu', 571 583 'walker' => new Walker_Nav_Menu(), 584 'items_wrap' => '<ul id="%1$s" class="%2$s">%3$s</ul>', 572 585 ) ); 573 $this->assert EqualSets( $expected, array_keys( $results ));586 $this->assertArrayNotHasKey( 'customize_preview_nav_menus_args', $results ); 574 587 $this->assertEquals( 'wp_page_menu', $results['fallback_cb'] ); 575 $this->assertEquals( 0, $results['can_partial_refresh'] ); 576 577 $this->assertNotEmpty( $menus->preview_nav_menu_instance_args[ $results['instance_number'] ] ); 578 $preview_nav_menu_instance_args = $menus->preview_nav_menu_instance_args[ $results['instance_number'] ]; 579 $this->assertEquals( '', $preview_nav_menu_instance_args['fallback_cb'] ); 580 $this->assertEquals( '', $preview_nav_menu_instance_args['walker'] ); 581 $this->assertNotEmpty( $preview_nav_menu_instance_args['args_hash'] ); 588 589 $nav_menu_term = get_term( wp_create_nav_menu( 'Bar' ) ); 590 $results = $menus->filter_wp_nav_menu_args( array( 591 'echo' => true, 592 'fallback_cb' => 'wp_page_menu', 593 'walker' => '', 594 'menu' => $nav_menu_term, 595 'items_wrap' => '<ul id="%1$s" class="%2$s">%3$s</ul>', 596 ) ); 597 $this->assertArrayHasKey( 'customize_preview_nav_menus_args', $results ); 598 $this->assertEquals( $nav_menu_term->term_id, $results['customize_preview_nav_menus_args']['menu'] ); 582 599 } 583 600 … … 596 613 'fallback_cb' => 'wp_page_menu', 597 614 'walker' => '', 615 'items_wrap' => '<ul id="%1$s" class="%2$s">%3$s</ul>', 598 616 ) ); 599 617 … … 602 620 $nav_menu_content = ob_get_clean(); 603 621 604 $object_args = json_decode( json_encode( $args ), false ); 605 $result = $menus->filter_wp_nav_menu( $nav_menu_content, $object_args ); 606 $expected = sprintf( 607 '<div class="partial-refreshable-nav-menu partial-refreshable-nav-menu-%1$d menu">', 608 $args['instance_number'] 609 ); 610 $this->assertStringStartsWith( $expected, $result ); 622 $result = $menus->filter_wp_nav_menu( $nav_menu_content, (object) $args ); 623 624 $this->assertContains( sprintf( ' data-customize-partial-id="nav_menu_instance[%s]"', $args['customize_preview_nav_menus_args']['args_hmac'] ), $result ); 625 $this->assertContains( ' data-customize-partial-type="nav_menu_instance"', $result ); 626 $this->assertContains( ' data-customize-partial-placement-context="', $result ); 611 627 } 612 628 … … 623 639 624 640 $this->assertTrue( wp_script_is( 'customize-preview-nav-menus' ) ); 625 $this->assertEquals( 10, has_action( 'wp_print_footer_scripts', array( $menus, 'export_preview_data' ) ) ); 626 } 627 628 /** 629 * Test the export_preview_data method. 641 } 642 643 /** 644 * Test WP_Customize_Nav_Menus::export_preview_data() method. 630 645 * 631 646 * @see WP_Customize_Nav_Menus::export_preview_data() 632 647 */ 633 648 function test_export_preview_data() { 634 do_action( 'customize_register', $this->wp_customize ); 635 $menus = new WP_Customize_Nav_Menus( $this->wp_customize ); 636 637 $request_uri = $_SERVER['REQUEST_URI']; 638 639 ob_start(); 640 $_SERVER['REQUEST_URI'] = '/wp-admin'; 641 $menus->export_preview_data(); 642 $data = ob_get_clean(); 643 644 $_SERVER['REQUEST_URI'] = $request_uri; 645 646 $this->assertContains( '_wpCustomizePreviewNavMenusExports', $data ); 647 $this->assertContains( 'renderQueryVar', $data ); 648 $this->assertContains( 'renderNonceValue', $data ); 649 $this->assertContains( 'renderNoncePostKey', $data ); 650 $this->assertContains( 'navMenuInstanceArgs', $data ); 649 $this->setExpectedDeprecated( 'WP_Customize_Nav_Menus::export_preview_data' ); 650 $this->wp_customize->nav_menus->export_preview_data(); 651 } 652 653 /** 654 * Test WP_Customize_Nav_Menus::render_nav_menu_partial() method. 655 * 656 * @see WP_Customize_Nav_Menus::render_nav_menu_partial() 657 */ 658 function test_render_nav_menu_partial() { 659 $this->wp_customize->nav_menus->customize_preview_init(); 660 661 $menu = wp_create_nav_menu( 'Foo' ); 662 wp_update_nav_menu_item( $menu, 0, array( 663 'menu-item-type' => 'custom', 664 'menu-item-title' => 'WordPress.org', 665 'menu-item-url' => 'https://wordpress.org', 666 'menu-item-status' => 'publish', 667 ) ); 668 669 $nav_menu_args = $this->wp_customize->nav_menus->filter_wp_nav_menu_args( array( 670 'echo' => true, 671 'menu' => $menu, 672 'fallback_cb' => 'wp_page_menu', 673 'walker' => '', 674 'items_wrap' => '<ul id="%1$s" class="%2$s">%3$s</ul>', 675 ) ); 676 677 $partial_id = sprintf( 'nav_menu_instance[%s]', $nav_menu_args['customize_preview_nav_menus_args']['args_hmac'] ); 678 $partials = $this->wp_customize->selective_refresh->add_dynamic_partials( array( $partial_id ) ); 679 $this->assertNotEmpty( $partials ); 680 $partial = array_shift( $partials ); 681 $this->assertEquals( $partial_id, $partial->id ); 682 683 $missing_args_hmac_args = array_merge( 684 $nav_menu_args['customize_preview_nav_menus_args'], 685 array( 'args_hmac' => null ) 686 ); 687 $this->assertFalse( $partial->render( $missing_args_hmac_args ) ); 688 689 $args_hmac_mismatch_args = array_merge( 690 $nav_menu_args['customize_preview_nav_menus_args'], 691 array( 'args_hmac' => strrev( $nav_menu_args['customize_preview_nav_menus_args']['args_hmac'] ) ) 692 ); 693 $this->assertFalse( $partial->render( $args_hmac_mismatch_args ) ); 694 695 $rendered = $partial->render( $nav_menu_args['customize_preview_nav_menus_args'] ); 696 $this->assertContains( 'data-customize-partial-type="nav_menu_instance"', $rendered ); 697 $this->assertContains( 'WordPress.org', $rendered ); 651 698 } 652 699 } -
trunk/tests/phpunit/tests/customize/widgets.php
r35754 r36586 40 40 remove_action( 'after_setup_theme', 'twentyfifteen_setup' ); 41 41 remove_action( 'after_setup_theme', 'twentysixteen_setup' ); 42 remove_action( 'customize_register', 'twentysixteen_customize_register', 11 ); 42 43 43 44 $user_id = self::factory()->user->create( array( 'role' => 'administrator' ) ); … … 122 123 'type' => 'option', 123 124 'capability' => 'edit_theme_options', 124 'transport' => ' refresh',125 'transport' => 'postMessage', 125 126 'default' => array(), 126 127 'sanitize_callback' => array( $this->manager->widgets, 'sanitize_widget_instance' ), … … 151 152 'type' => 'option', 152 153 'capability' => 'edit_theme_options', 153 'transport' => ' refresh',154 'transport' => 'postMessage', 154 155 'default' => array(), 155 156 'sanitize_callback' => array( $this->manager->widgets, 'sanitize_sidebar_widgets' ), … … 347 348 $this->assertEquals( $post_value, $this->manager->widgets->sanitize_widget_js_instance( $instance ) ); 348 349 } 350 351 /** 352 * Test WP_Customize_Widgets::customize_dynamic_partial_args(). 353 * 354 * @see WP_Customize_Widgets::customize_dynamic_partial_args() 355 */ 356 function test_customize_dynamic_partial_args() { 357 do_action( 'customize_register', $this->manager ); 358 359 $args = apply_filters( 'customize_dynamic_partial_args', false, 'widget[search-2]' ); 360 $this->assertInternalType( 'array', $args ); 361 $this->assertEquals( 'widget', $args['type'] ); 362 $this->assertEquals( array( $this->manager->widgets, 'render_widget_partial' ), $args['render_callback'] ); 363 $this->assertTrue( $args['container_inclusive'] ); 364 365 $args = apply_filters( 'customize_dynamic_partial_args', array( 'fallback_refresh' => false ), 'widget[search-2]' ); 366 $this->assertInternalType( 'array', $args ); 367 $this->assertEquals( 'widget', $args['type'] ); 368 $this->assertEquals( array( $this->manager->widgets, 'render_widget_partial' ), $args['render_callback'] ); 369 $this->assertTrue( $args['container_inclusive'] ); 370 $this->assertFalse( $args['fallback_refresh'] ); 371 } 372 373 /** 374 * Test WP_Customize_Widgets::selective_refresh_init(). 375 * 376 * @see WP_Customize_Widgets::selective_refresh_init() 377 */ 378 function test_selective_refresh_init() { 379 $this->manager->widgets->selective_refresh_init(); 380 $this->assertEquals( 10, has_action( 'wp_enqueue_scripts', array( $this->manager->widgets, 'customize_preview_enqueue_deps' ) ) ); 381 $this->assertEquals( 10, has_action( 'dynamic_sidebar_before', array( $this->manager->widgets, 'start_dynamic_sidebar' ) ) ); 382 $this->assertEquals( 10, has_action( 'dynamic_sidebar_after', array( $this->manager->widgets, 'end_dynamic_sidebar' ) ) ); 383 $this->assertEquals( 10, has_filter( 'dynamic_sidebar_params', array( $this->manager->widgets, 'filter_dynamic_sidebar_params' ) ) ); 384 $this->assertEquals( 10, has_filter( 'wp_kses_allowed_html', array( $this->manager->widgets, 'filter_wp_kses_allowed_data_attributes' ) ) ); 385 } 386 387 /** 388 * Test WP_Customize_Widgets::customize_preview_enqueue_deps(). 389 * 390 * @see WP_Customize_Widgets::customize_preview_enqueue_deps() 391 */ 392 function test_customize_preview_enqueue_deps() { 393 $this->manager->widgets->customize_preview_enqueue_deps(); 394 $this->assertTrue( wp_script_is( 'customize-preview-widgets', 'enqueued' ) ); 395 $this->assertTrue( wp_style_is( 'customize-preview', 'enqueued' ) ); 396 $script = wp_scripts()->registered['customize-preview-widgets']; 397 $this->assertContains( 'customize-selective-refresh', $script->deps ); 398 } 399 400 /** 401 * Test extensions to dynamic_sidebar(). 402 * 403 * @see WP_Customize_Widgets::filter_dynamic_sidebar_params() 404 * @see WP_Customize_Widgets::start_dynamic_sidebar() 405 * @see WP_Customize_Widgets::end_dynamic_sidebar() 406 */ 407 function test_filter_dynamic_sidebar_params() { 408 global $wp_registered_sidebars; 409 register_sidebar( array( 410 'id' => 'foo', 411 ) ); 412 413 $this->manager->widgets->selective_refresh_init(); 414 415 $params = array( 416 array_merge( 417 $wp_registered_sidebars['foo'], 418 array( 419 'widget_id' => 'search-2', 420 ) 421 ), 422 array(), 423 ); 424 $this->assertEquals( $params, $this->manager->widgets->filter_dynamic_sidebar_params( $params ), 'Expected short-circuit if not called after dynamic_sidebar_before.' ); 425 426 ob_start(); 427 do_action( 'dynamic_sidebar_before', 'foo' ); 428 $output = ob_get_clean(); 429 $this->assertEquals( '<!--dynamic_sidebar_before:foo:1-->', trim( $output ) ); 430 431 $bad_params = $params; 432 unset( $bad_params[0]['id'] ); 433 $this->assertEquals( $bad_params, $this->manager->widgets->filter_dynamic_sidebar_params( $bad_params ) ); 434 435 $bad_params = $params; 436 $bad_params[0]['id'] = 'non-existing'; 437 $this->assertEquals( $bad_params, $this->manager->widgets->filter_dynamic_sidebar_params( $bad_params ) ); 438 439 $bad_params = $params; 440 $bad_params[0]['before_widget'] = ' <oops>'; 441 $this->assertEquals( $bad_params, $this->manager->widgets->filter_dynamic_sidebar_params( $bad_params ) ); 442 443 $filtered_params = $this->manager->widgets->filter_dynamic_sidebar_params( $params ); 444 $this->assertNotEquals( $params, $filtered_params ); 445 ob_start(); 446 do_action( 'dynamic_sidebar_after', 'foo' ); 447 $output = ob_get_clean(); 448 $this->assertEquals( '<!--dynamic_sidebar_after:foo:1-->', trim( $output ) ); 449 450 $output = wp_kses_post( $filtered_params[0]['before_widget'] ); 451 $this->assertContains( 'data-customize-partial-id="widget[search-2]"', $output ); 452 $this->assertContains( 'data-customize-partial-type="widget"', $output ); 453 } 454 455 /** 456 * Test WP_Customize_Widgets::render_widget_partial() method. 457 * 458 * @see WP_Customize_Widgets::render_widget_partial() 459 */ 460 function test_render_widget_partial() { 461 $this->manager->widgets->selective_refresh_init(); 462 463 $partial_id = 'widget[search-2]'; 464 $partials = $this->manager->selective_refresh->add_dynamic_partials( array( $partial_id ) ); 465 $this->assertNotEmpty( $partials ); 466 $partial = array_shift( $partials ); 467 $this->assertEquals( $partial_id, $partial->id ); 468 469 $this->assertFalse( $this->manager->widgets->render_widget_partial( $partial, array() ) ); 470 $this->assertFalse( $this->manager->widgets->render_widget_partial( $partial, array( 'sidebar_id' => 'non-existing' ) ) ); 471 472 $output = $this->manager->widgets->render_widget_partial( $partial, array( 'sidebar_id' => 'sidebar-1' ) ); 473 474 $this->assertEquals( 1, substr_count( $output, 'data-customize-partial-id' ) ); 475 $this->assertEquals( 1, substr_count( $output, 'data-customize-partial-type="widget"' ) ); 476 $this->assertContains( ' id="search-2"', $output ); 477 } 478 479 /** 480 * Test deprecated methods. 481 */ 482 public function test_deprecated_methods() { 483 $this->setExpectedDeprecated( 'WP_Customize_Widgets::setup_widget_addition_previews' ); 484 $this->setExpectedDeprecated( 'WP_Customize_Widgets::prepreview_added_sidebars_widgets' ); 485 $this->setExpectedDeprecated( 'WP_Customize_Widgets::prepreview_added_widget_instance' ); 486 $this->setExpectedDeprecated( 'WP_Customize_Widgets::remove_prepreview_filters' ); 487 $this->manager->widgets->setup_widget_addition_previews(); 488 $this->manager->widgets->prepreview_added_sidebars_widgets(); 489 $this->manager->widgets->prepreview_added_widget_instance(); 490 $this->manager->widgets->remove_prepreview_filters(); 491 } 349 492 } -
trunk/tests/qunit/fixtures/customize-menus.js
r36574 r36586 3 3 'nonce': 'yo', 4 4 'phpIntMax': '2147483647', 5 ' menuItemTransport': 'postMessage',5 'settingTransport': 'postMessage', 6 6 'allMenus': [{ 7 7 'term_id': '2',
Note: See TracChangeset
for help on using the changeset viewer.