Ticket #27355: 27355.2.diff
File 27355.2.diff, 168.4 KB (added by , 9 years ago) |
---|
-
src/wp-admin/js/customize-controls.js
diff --git src/wp-admin/js/customize-controls.js src/wp-admin/js/customize-controls.js index 0a61a86..d7c2e75 100644
3786 3786 }); 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 3789 3804 api.trigger( 'ready' ); 3790 3805 3791 3806 // Make sure left column gets focus -
src/wp-admin/js/customize-nav-menus.js
diff --git src/wp-admin/js/customize-nav-menus.js src/wp-admin/js/customize-nav-menus.js index 6dea2b4..8e345e9 100644
19 19 api.Menus.data = { 20 20 itemTypes: [], 21 21 l10n: {}, 22 menuItemTransport: 'postMessage',22 settingTransport: 'refresh', 23 23 phpIntMax: 0, 24 24 defaultSettingValues: { 25 25 nav_menu: {}, … … 2307 2307 customizeId = 'nav_menu_item[' + String( placeholderId ) + ']'; 2308 2308 settingArgs = { 2309 2309 type: 'nav_menu_item', 2310 transport: 'postMessage',2310 transport: api.Menus.data.settingTransport, 2311 2311 previewer: api.previewer 2312 2312 }; 2313 2313 setting = api.create( customizeId, customizeId, {}, settingArgs ); … … 2396 2396 // Register the menu control setting. 2397 2397 api.create( customizeId, customizeId, {}, { 2398 2398 type: 'nav_menu', 2399 transport: 'postMessage',2399 transport: api.Menus.data.settingTransport, 2400 2400 previewer: api.previewer 2401 2401 } ); 2402 2402 api( customizeId ).set( $.extend( … … 2532 2532 newCustomizeId = 'nav_menu[' + String( update.term_id ) + ']'; 2533 2533 newSetting = api.create( newCustomizeId, newCustomizeId, settingValue, { 2534 2534 type: 'nav_menu', 2535 transport: 'postMessage',2535 transport: api.Menus.data.settingTransport, 2536 2536 previewer: api.previewer 2537 2537 } ); 2538 2538 … … 2680 2680 newCustomizeId = 'nav_menu_item[' + String( update.post_id ) + ']'; 2681 2681 newSetting = api.create( newCustomizeId, newCustomizeId, settingValue, { 2682 2682 type: 'nav_menu_item', 2683 transport: 'postMessage',2683 transport: api.Menus.data.settingTransport, 2684 2684 previewer: api.previewer 2685 2685 } ); 2686 2686 -
src/wp-admin/js/customize-widgets.js
diff --git src/wp-admin/js/customize-widgets.js src/wp-admin/js/customize-widgets.js index 360c183..91a6516 100644
34 34 multi_number: null, 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, 40 40 height: null, … … 1982 1982 isExistingWidget = api.has( settingId ); 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 }; 1988 1988 setting = api.create( settingId, settingId, '', settingArgs ); -
src/wp-content/themes/twentythirteen/js/theme-customizer.js
diff --git src/wp-content/themes/twentythirteen/js/theme-customizer.js src/wp-content/themes/twentythirteen/js/theme-customizer.js index 6072104..8519752 100644
38 38 } 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 ); -
src/wp-includes/class-wp-customize-manager.php
diff --git src/wp-includes/class-wp-customize-manager.php src/wp-includes/class-wp-customize-manager.php index 942d907..937689b 100644
final class WP_Customize_Manager { 67 67 public $nav_menus; 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 * 72 81 * @since 3.4.0 … … final class WP_Customize_Manager { 100 109 * @access protected 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 /** 106 115 * Registered instances of WP_Customize_Section. … … final class WP_Customize_Manager { 249 258 */ 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 ); 259 269 } 260 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 ); 274 } 275 261 276 add_filter( 'wp_die_handler', array( $this, 'wp_die_handler' ) ); 262 277 263 278 add_action( 'setup_theme', array( $this, 'setup_theme' ) ); … … final class WP_Customize_Manager { 1711 1726 'autofocus' => array(), 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 1716 1732 // Prepare Customize Section objects to pass to JavaScript. -
src/wp-includes/class-wp-customize-nav-menus.php
diff --git src/wp-includes/class-wp-customize-nav-menus.php src/wp-includes/class-wp-customize-nav-menus.php index 5453c17..4acb07b 100644
final class WP_Customize_Nav_Menus { 61 61 add_action( 'customize_controls_print_footer_scripts', array( $this, 'print_templates' ) ); 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 66 69 /** … … final class WP_Customize_Nav_Menus { 375 378 'reorderLabelOn' => esc_attr__( 'Reorder menu items' ), 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( 381 384 'nav_menu' => $temp_nav_menu_setting->default, … … final class WP_Customize_Nav_Menus { 425 428 public function filter_dynamic_setting_args( $setting_args, $setting_id ) { 426 429 if ( preg_match( WP_Customize_Nav_Menu_Setting::ID_PATTERN, $setting_id ) ) { 427 430 $setting_args = array( 428 'type' => WP_Customize_Nav_Menu_Setting::TYPE, 431 'type' => WP_Customize_Nav_Menu_Setting::TYPE, 432 'transport' => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh', 429 433 ); 430 434 } elseif ( preg_match( WP_Customize_Nav_Menu_Item_Setting::ID_PATTERN, $setting_id ) ) { 431 435 $setting_args = array( 432 'type' => WP_Customize_Nav_Menu_Item_Setting::TYPE, 436 'type' => WP_Customize_Nav_Menu_Item_Setting::TYPE, 437 'transport' => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh', 433 438 ); 434 439 } 435 440 return $setting_args; … … final class WP_Customize_Nav_Menus { 514 519 515 520 $setting = $this->manager->get_setting( $setting_id ); 516 521 if ( $setting ) { 517 $setting->transport = 'postMessage';522 $setting->transport = isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh'; 518 523 remove_filter( "customize_sanitize_{$setting_id}", 'absint' ); 519 524 add_filter( "customize_sanitize_{$setting_id}", array( $this, 'intval_base10' ) ); 520 525 } else { … … final class WP_Customize_Nav_Menus { 522 527 'sanitize_callback' => array( $this, 'intval_base10' ), 523 528 'theme_supports' => 'menus', 524 529 'type' => 'theme_mod', 525 'transport' => 'postMessage',530 'transport' => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh', 526 531 'default' => 0, 527 532 ) ); 528 533 } … … final class WP_Customize_Nav_Menus { 548 553 ) ) ); 549 554 550 555 $nav_menu_setting_id = 'nav_menu[' . $menu_id . ']'; 551 $this->manager->add_setting( new WP_Customize_Nav_Menu_Setting( $this->manager, $nav_menu_setting_id ) ); 556 $this->manager->add_setting( new WP_Customize_Nav_Menu_Setting( $this->manager, $nav_menu_setting_id, array( 557 'transport' => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh', 558 ) ) ); 552 559 553 560 // Add the menu contents. 554 561 $menu_items = (array) wp_get_nav_menu_items( $menu_id ); … … final class WP_Customize_Nav_Menus { 561 568 $value = (array) $item; 562 569 $value['nav_menu_term_id'] = $menu_id; 563 570 $this->manager->add_setting( new WP_Customize_Nav_Menu_Item_Setting( $this->manager, $menu_item_setting_id, array( 564 'value' => $value, 571 'value' => $value, 572 'transport' => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh', 565 573 ) ) ); 566 574 567 575 // Create a control for each menu item. … … final class WP_Customize_Nav_Menus { 585 593 $this->manager->add_setting( 'new_menu_name', array( 586 594 'type' => 'new_menu', 587 595 'default' => '', 588 'transport' => 'postMessage',596 'transport' => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh', 589 597 ) ); 590 598 591 599 $this->manager->add_control( 'new_menu_name', array( … … final class WP_Customize_Nav_Menus { 801 809 <?php 802 810 } 803 811 812 // 804 813 // Start functionality specific to partial-refresh of menu changes in Customizer preview. 805 const RENDER_AJAX_ACTION = 'customize_render_menu_partial'; 806 const RENDER_NONCE_POST_KEY = 'render-menu-nonce'; 807 const RENDER_QUERY_VAR = 'wp_customize_menu_render'; 814 // 808 815 809 816 /** 810 * The number of wp_nav_menu() calls which have happened in the preview.817 * Filters arguments for dynamic nav_menu selective refresh partials. 811 818 * 812 * @since 4. 3.0819 * @since 4.5.0 813 820 * @access public 814 * @var int815 */816 public $preview_nav_menu_instance_number = 0;817 818 /**819 * Nav menu args used for each instance.820 821 * 821 * @ since 4.3.0822 * @ access public823 * @ var array822 * @param array|false $partial_args Partial args. 823 * @param string $partial_id Partial ID. 824 * @return array Partial args 824 825 */ 825 public $preview_nav_menu_instance_args = array(); 826 public function customize_dynamic_partial_args( $partial_args, $partial_id ) { 827 828 if ( preg_match( '/^nav_menu_instance\[[0-9a-f]{32}\]$/', $partial_id ) ) { 829 if ( false === $partial_args ) { 830 $partial_args = array(); 831 } 832 $partial_args = array_merge( 833 $partial_args, 834 array( 835 'type' => 'nav_menu_instance', 836 'render_callback' => array( $this, 'render_nav_menu_partial' ), 837 'container_inclusive' => true, 838 ) 839 ); 840 } 841 842 return $partial_args; 843 } 826 844 827 845 /** 828 846 * Add hooks for the Customizer preview. … … final class WP_Customize_Nav_Menus { 831 849 * @access public 832 850 */ 833 851 public function customize_preview_init() { 834 add_action( 'template_redirect', array( $this, 'render_menu' ) );835 852 add_action( 'wp_enqueue_scripts', array( $this, 'customize_preview_enqueue_deps' ) ); 836 837 if ( ! isset( $_REQUEST[ self::RENDER_QUERY_VAR ] ) ) { 838 add_filter( 'wp_nav_menu_args', array( $this, 'filter_wp_nav_menu_args' ), 1000 ); 839 add_filter( 'wp_nav_menu', array( $this, 'filter_wp_nav_menu' ), 10, 2 ); 840 } 853 add_filter( 'wp_nav_menu_args', array( $this, 'filter_wp_nav_menu_args' ), 1000 ); 854 add_filter( 'wp_nav_menu', array( $this, 'filter_wp_nav_menu' ), 10, 2 ); 841 855 } 842 856 843 857 /** … … final class WP_Customize_Nav_Menus { 845 859 * 846 860 * @since 4.3.0 847 861 * @access public 848 *849 862 * @see wp_nav_menu() 863 * @see WP_Customize_Widgets_Partial_Refresh::filter_dynamic_sidebar_params() 850 864 * 851 865 * @param array $args An array containing wp_nav_menu() arguments. 852 866 * @return array Arguments. 853 867 */ 854 868 public function filter_wp_nav_menu_args( $args ) { 855 $this->preview_nav_menu_instance_number += 1;856 $args['instance_number'] = $this->preview_nav_menu_instance_number;857 858 869 $can_partial_refresh = ( 859 870 ! empty( $args['echo'] ) 860 871 && … … final class WP_Customize_Nav_Menus { 867 878 || 868 879 ( ! empty( $args['menu'] ) && ( is_numeric( $args['menu'] ) || is_object( $args['menu'] ) ) ) 869 880 ) 881 && 882 ( 883 ! empty( $args['container'] ) 884 || 885 ( isset( $args['items_wrap'] ) && '<' === substr( $args['items_wrap'], 0, 1 ) ) 886 ) 870 887 ); 871 $args['can_partial_refresh'] = $can_partial_refresh;872 873 $hashed_args = $args;874 888 875 889 if ( ! $can_partial_refresh ) { 876 $hashed_args['fallback_cb'] = ''; 877 $hashed_args['walker'] = ''; 890 return $args; 878 891 } 879 892 880 // Replace object menu arg with a term_id menu arg, as this exports better to JS and is easier to compare hashes. 881 if ( ! empty( $hashed_args['menu'] ) && is_object( $hashed_args['menu'] ) ) { 882 $hashed_args['menu'] = $hashed_args['menu']->term_id; 893 $exported_args = $args; 894 895 /* 896 * Replace object menu arg with a term_id menu arg, as this exports better 897 * to JS and is easier to compare hashes. 898 */ 899 if ( ! empty( $exported_args['menu'] ) && is_object( $exported_args['menu'] ) ) { 900 $exported_args['menu'] = $exported_args['menu']->term_id; 883 901 } 884 902 885 ksort( $hashed_args ); 886 $hashed_args['args_hash'] = $this->hash_nav_menu_args( $hashed_args ); 903 ksort( $exported_args ); 904 $exported_args['args_hmac'] = $this->hash_nav_menu_args( $exported_args ); 905 906 $args['customize_preview_nav_menus_args'] = $exported_args; 887 907 888 $this->preview_nav_menu_instance_args[ $this->preview_nav_menu_instance_number ] = $hashed_args;889 908 return $args; 890 909 } 891 910 892 911 /** 893 * Prepare wp_nav_menu() calls for partial refresh. Wraps output in container for refreshing. 912 * Prepares wp_nav_menu() calls for partial refresh. 913 * 914 * Injects attributes into container element. 894 915 * 895 916 * @since 4.3.0 896 917 * @access public … … final class WP_Customize_Nav_Menus { 902 923 * @return null 903 924 */ 904 925 public function filter_wp_nav_menu( $nav_menu_content, $args ) { 905 if ( ! empty( $args->can_partial_refresh ) && ! empty( $args->instance_number ) ) { 906 $nav_menu_content = preg_replace( 907 '/(?<=class=")/', 908 sprintf( 'partial-refreshable-nav-menu partial-refreshable-nav-menu-%1$d ', $args->instance_number ), 909 $nav_menu_content, 910 1 // Only update the class on the first element found, the menu container. 911 ); 926 if ( ! empty( $args->customize_preview_nav_menus_args ) ) { 927 $attributes = sprintf( ' data-customize-partial-id="%s"', esc_attr( 'nav_menu_instance[' . $args->customize_preview_nav_menus_args['args_hmac'] . ']' ) ); 928 $attributes .= ' data-customize-partial-type="nav_menu_instance"'; 929 $attributes .= sprintf( ' data-customize-partial-placement-context="%s"', esc_attr( wp_json_encode( $args->customize_preview_nav_menus_args ) ) ); 930 $nav_menu_content = preg_replace( '#^(<\w+)#', '$1 ' . $attributes, $nav_menu_content, 1 ); 912 931 } 913 932 return $nav_menu_content; 914 933 } 915 934 916 935 /** 917 * Hash (hmac) the arguments with the nonce and secret auth key to ensure they 918 * are not tampered with when submitted in the Ajax request. 936 * Hashes (hmac) the nav menu arguments to ensure they are not tampered with when 937 * submitted in the Ajax request. 938 * 939 * Note that the array is expected to be pre-sorted. 919 940 * 920 941 * @since 4.3.0 921 942 * @access public 922 943 * 923 944 * @param array $args The arguments to hash. 924 * @return string 945 * @return string Hashed nav menu arguments. 925 946 */ 926 947 public function hash_nav_menu_args( $args ) { 927 return wp_hash( wp_create_nonce( self::RENDER_AJAX_ACTION ) .serialize( $args ) );948 return wp_hash( serialize( $args ) ); 928 949 } 929 950 930 951 /** … … final class WP_Customize_Nav_Menus { 934 955 * @access public 935 956 */ 936 957 public function customize_preview_enqueue_deps() { 937 wp_enqueue_script( 'customize-preview-nav-menus' ); 938 wp_enqueue_style( 'customize-preview' ); 958 if ( isset( $this->manager->selective_refresh ) ) { 959 $script = wp_scripts()->registered['customize-preview-nav-menus']; 960 $script->deps[] = 'customize-selective-refresh'; 961 } 939 962 940 add_action( 'wp_print_footer_scripts', array( $this, 'export_preview_data' ) ); 963 wp_enqueue_script( 'customize-preview-nav-menus' ); // Note that we have overridden this. 964 wp_enqueue_style( 'customize-preview' ); 941 965 } 942 966 943 967 /** 944 * Export data from PHP to JS.968 * Exports data from PHP to JS. 945 969 * 946 970 * @since 4.3.0 971 * @deprecated 4.5.0 Obsolete 947 972 * @access public 948 973 */ 949 974 public function export_preview_data() { 950 951 // Why not wp_localize_script? Because we're not localizing, and it forces values into strings. 952 $exports = array( 953 'renderQueryVar' => self::RENDER_QUERY_VAR, 954 'renderNonceValue' => wp_create_nonce( self::RENDER_AJAX_ACTION ), 955 'renderNoncePostKey' => self::RENDER_NONCE_POST_KEY, 956 'navMenuInstanceArgs' => $this->preview_nav_menu_instance_args, 957 'l10n' => array( 958 'editNavMenuItemTooltip' => __( 'Shift-click to edit this menu item.' ), 959 ), 960 ); 961 962 printf( '<script>var _wpCustomizePreviewNavMenusExports = %s;</script>', wp_json_encode( $exports ) ); 975 _deprecated_function( __METHOD__, '4.5.0' ); 963 976 } 964 977 965 978 /** … … final class WP_Customize_Nav_Menus { 969 982 * @access public 970 983 * 971 984 * @see wp_nav_menu() 985 * 986 * @param WP_Customize_Partial $partial Partial. 987 * @param array $nav_menu_args Nav menu args supplied as container context. 988 * @return string|false 972 989 */ 973 public function render_menu() { 974 if ( empty( $_POST[ self::RENDER_QUERY_VAR ] ) ) { 975 return; 976 } 977 978 $this->manager->remove_preview_signature(); 990 public function render_nav_menu_partial( $partial, $nav_menu_args ) { 991 unset( $partial ); 979 992 980 if ( empty( $_POST[ self::RENDER_NONCE_POST_KEY ] ) ) { 981 wp_send_json_error( 'missing_nonce_param' ); 993 if ( ! isset( $nav_menu_args['args_hmac'] ) ) { 994 // Error: missing_args_hmac. 995 return false; 982 996 } 983 997 984 if ( ! is_customize_preview() ) { 985 wp_send_json_error( 'expected_customize_preview' ); 986 } 998 $nav_menu_args_hmac = $nav_menu_args['args_hmac']; 999 unset( $nav_menu_args['args_hmac'] ); 987 1000 988 if ( ! check_ajax_referer( self::RENDER_AJAX_ACTION, self::RENDER_NONCE_POST_KEY, false ) ) { 989 wp_send_json_error( 'nonce_check_fail' ); 1001 ksort( $nav_menu_args ); 1002 if ( ! hash_equals( $this->hash_nav_menu_args( $nav_menu_args ), $nav_menu_args_hmac ) ) { 1003 // Error: args_hmac_mismatch. 1004 return false; 990 1005 } 991 1006 992 if ( ! current_user_can( 'edit_theme_options' ) ) { 993 wp_send_json_error( 'unauthorized' ); 994 } 995 996 if ( ! isset( $_POST['wp_nav_menu_args'] ) ) { 997 wp_send_json_error( 'missing_param' ); 998 } 999 1000 if ( ! isset( $_POST['wp_nav_menu_args_hash'] ) ) { 1001 wp_send_json_error( 'missing_param' ); 1002 } 1003 1004 $wp_nav_menu_args = json_decode( wp_unslash( $_POST['wp_nav_menu_args'] ), true ); 1005 if ( ! is_array( $wp_nav_menu_args ) ) { 1006 wp_send_json_error( 'wp_nav_menu_args_not_array' ); 1007 } 1008 1009 $wp_nav_menu_args_hash = sanitize_text_field( wp_unslash( $_POST['wp_nav_menu_args_hash'] ) ); 1010 if ( ! hash_equals( $this->hash_nav_menu_args( $wp_nav_menu_args ), $wp_nav_menu_args_hash ) ) { 1011 wp_send_json_error( 'wp_nav_menu_args_hash_mismatch' ); 1012 } 1007 ob_start(); 1008 wp_nav_menu( $nav_menu_args ); 1009 $content = ob_get_clean(); 1013 1010 1014 $wp_nav_menu_args['echo'] = false; 1015 wp_send_json_success( wp_nav_menu( $wp_nav_menu_args ) ); 1011 return $content; 1016 1012 } 1017 1013 } -
src/wp-includes/class-wp-customize-widgets.php
diff --git src/wp-includes/class-wp-customize-widgets.php src/wp-includes/class-wp-customize-widgets.php index 5a0e62b..d655d03 100644
final class WP_Customize_Widgets { 100 100 add_action( 'dynamic_sidebar', array( $this, 'tally_rendered_widgets' ) ); 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_filter( 'widget_customizer_setting_args', array( $this, 'filter_widget_customizer_setting_args' ), 10, 2 ); 107 add_action( 'customize_preview_init', array( $this, 'selective_refresh_init' ) ); 103 108 } 104 109 105 110 /** … … final class WP_Customize_Widgets { 682 687 'widgetReorderNav' => $widget_reorder_nav_tpl, 683 688 'moveWidgetArea' => $move_widget_area_tpl, 684 689 ), 690 'selectiveRefresh' => isset( $this->manager->selective_refresh ), 685 691 ); 686 692 687 693 foreach ( $settings['registeredWidgets'] as &$registered_widget ) { … … final class WP_Customize_Widgets { 762 768 $args = array( 763 769 'type' => 'option', 764 770 'capability' => 'edit_theme_options', 765 'transport' => 'refresh',771 'transport' => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh', 766 772 'default' => array(), 767 773 ); 768 774 … … final class WP_Customize_Widgets { 884 890 'multi_number' => ( $args['_add'] === 'multi' ) ? $args['_multi_num'] : false, 885 891 'is_disabled' => $is_disabled, 886 892 'id_base' => $id_base, 887 'transport' => 'refresh',893 'transport' => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh', 888 894 'width' => $wp_registered_widget_controls[$widget['id']]['width'], 889 895 'height' => $wp_registered_widget_controls[$widget['id']]['height'], 890 896 'is_wide' => $this->is_wide_widget( $widget['id'] ), … … final class WP_Customize_Widgets { 1061 1067 'registeredSidebars' => array_values( $wp_registered_sidebars ), 1062 1068 'registeredWidgets' => $wp_registered_widgets, 1063 1069 'l10n' => array( 1064 'widgetTooltip' => __( 'Shift-click to edit this widget.' ),1070 'widgetTooltip' => __( 'Shift-click to edit this widget.' ), 1065 1071 ), 1072 'selectiveRefresh' => isset( $this->manager->selective_refresh ), 1066 1073 ); 1067 1074 foreach ( $settings['registeredWidgets'] as &$registered_widget ) { 1068 1075 unset( $registered_widget['callback'] ); // may not be JSON-serializeable … … final class WP_Customize_Widgets { 1459 1466 wp_send_json_success( compact( 'form', 'instance' ) ); 1460 1467 } 1461 1468 1462 /*************************************************************************** 1463 * Option Update Capturing 1464 ***************************************************************************/ 1469 /* 1470 * Selective Refresh Methods 1471 */ 1472 1473 /** 1474 * Let sidebars_widgets and widget instance settings all have postMessage transport. 1475 * 1476 * The preview will determine whether or not the setting change requires a full refresh. 1477 * 1478 * @param array $args Setting args. 1479 * @return array 1480 */ 1481 public function filter_widget_customizer_setting_args( $args ) { 1482 $args['transport'] = isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh'; 1483 return $args; 1484 } 1485 1486 /** 1487 * Filter args for dynamic widget partials. 1488 * 1489 * @since 4.5.0 1490 * 1491 * @param array|false $partial_args Partial args. 1492 * @param string $partial_id Partial ID. 1493 * @return array Partial args 1494 */ 1495 public function customize_dynamic_partial_args( $partial_args, $partial_id ) { 1496 1497 if ( preg_match( '/^widget\[.+\]$/', $partial_id ) ) { 1498 if ( false === $partial_args ) { 1499 $partial_args = array(); 1500 } 1501 $partial_args = array_merge( 1502 $partial_args, 1503 array( 1504 'type' => 'widget', 1505 'render_callback' => array( $this, 'render_widget_partial' ), 1506 ) 1507 ); 1508 } 1509 1510 return $partial_args; 1511 } 1512 1513 /** 1514 * Add hooks for selective refresh. 1515 * 1516 * @since 4.5.0 1517 * @access public 1518 */ 1519 public function selective_refresh_init() { 1520 if ( ! isset( $this->manager->selective_refresh ) ) { 1521 return; 1522 } 1523 1524 add_action( 'wp_enqueue_scripts', array( $this, 'customize_preview_enqueue_deps' ) ); 1525 add_filter( 'dynamic_sidebar_params', array( $this, 'filter_dynamic_sidebar_params' ) ); 1526 add_filter( 'wp_kses_allowed_html', array( $this, 'filter_wp_kses_allowed_data_attributes' ) ); 1527 add_action( 'dynamic_sidebar_before', array( $this, 'start_dynamic_sidebar' ) ); 1528 add_action( 'dynamic_sidebar_after', array( $this, 'end_dynamic_sidebar' ) ); 1529 } 1530 1531 /** 1532 * Enqueue scripts for the Customizer preview. 1533 * 1534 * @since 4.5.0 1535 * @access public 1536 */ 1537 public function customize_preview_enqueue_deps() { 1538 if ( isset( $this->manager->selective_refresh ) ) { 1539 $script = wp_scripts()->registered['customize-preview-widgets']; 1540 $script->deps[] = 'customize-selective-refresh'; 1541 } 1542 1543 wp_enqueue_script( 'customize-preview-widgets' ); 1544 wp_enqueue_style( 'customize-preview' ); 1545 wp_enqueue_style( 'customize-partial-refresh-widgets-preview' ); 1546 } 1547 1548 /** 1549 * Keep track of the arguments that are being passed to the_widget(). 1550 * 1551 * @param array $params { 1552 * Dynamic sidebar params. 1553 * 1554 * @type array $args Sidebar args. 1555 * @type array $widget_args Widget args. 1556 * } 1557 * @see WP_Customize_Nav_Menus_Partial_Refresh::filter_wp_nav_menu_args() 1558 * 1559 * @return array Params. 1560 */ 1561 public function filter_dynamic_sidebar_params( $params ) { 1562 $sidebar_args = array_merge( 1563 array( 1564 'before_widget' => '', 1565 'after_widget' => '', 1566 ), 1567 $params[0] 1568 ); 1569 1570 // Skip widgets not in a registered sidebar or ones which lack a proper wrapper element to attach the data-* attributes to. 1571 $matches = array(); 1572 $is_valid = ( 1573 isset( $sidebar_args['id'] ) 1574 && 1575 is_registered_sidebar( $sidebar_args['id'] ) 1576 && 1577 ( isset( $this->current_dynamic_sidebar_id_stack[0] ) && $this->current_dynamic_sidebar_id_stack[0] === $sidebar_args['id'] ) 1578 && 1579 preg_match( '#^<(?P<tag_name>\w+)#', $sidebar_args['before_widget'], $matches ) 1580 ); 1581 if ( ! $is_valid ) { 1582 return $params; 1583 } 1584 $this->before_widget_tags_seen[ $matches['tag_name'] ] = true; 1585 1586 $context = array( 1587 'sidebar_id' => $sidebar_args['id'], 1588 ); 1589 if ( isset( $this->context_sidebar_instance_number ) ) { 1590 $context['sidebar_instance_number'] = $this->context_sidebar_instance_number; 1591 } else if ( isset( $sidebar_args['id'] ) && isset( $this->sidebar_instance_count[ $sidebar_args['id'] ] ) ) { 1592 $context['sidebar_instance_number'] = $this->sidebar_instance_count[ $sidebar_args['id'] ]; 1593 } 1594 1595 $attributes = sprintf( ' data-customize-partial-id="%s"', esc_attr( 'widget[' . $sidebar_args['widget_id'] . ']' ) ); 1596 $attributes .= ' data-customize-partial-type="widget"'; 1597 $attributes .= sprintf( ' data-customize-partial-placement-context="%s"', esc_attr( wp_json_encode( $context ) ) ); 1598 $attributes .= sprintf( ' data-customize-widget-id="%s"', esc_attr( $sidebar_args['widget_id'] ) ); 1599 $sidebar_args['before_widget'] = preg_replace( '#^(<\w+)#', '$1 ' . $attributes, $sidebar_args['before_widget'] ); 1600 1601 $params[0] = $sidebar_args; 1602 return $params; 1603 } 1604 1605 /** 1606 * List of the tag names seen for before_widget strings. 1607 * 1608 * This is used in the filter_wp_kses_allowed_html filter to ensure that the 1609 * data-* attributes can be whitelisted. 1610 * 1611 * @since 4.5.0 1612 * @access private 1613 * @var array 1614 */ 1615 protected $before_widget_tags_seen = array(); 1616 1617 /** 1618 * Ensure that the HTML data-* attributes for selective refresh are allowed by kses. 1619 * 1620 * This is needed in case the $before_widget is run through wp_kses() when printed. 1621 * 1622 * @since 4.5.0 1623 * @access private 1624 * 1625 * @param array $allowed_html Allowed HTML. 1626 * @return array Allowed HTML. 1627 */ 1628 function filter_wp_kses_allowed_data_attributes( $allowed_html ) { 1629 foreach ( array_keys( $this->before_widget_tags_seen ) as $tag_name ) { 1630 if ( ! isset( $allowed_html[ $tag_name ] ) ) { 1631 $allowed_html[ $tag_name ] = array(); 1632 } 1633 $allowed_html[ $tag_name ] = array_merge( 1634 $allowed_html[ $tag_name ], 1635 array_fill_keys( array( 1636 'data-customize-partial-id', 1637 'data-customize-partial-type', 1638 'data-customize-partial-placement-context', 1639 'data-customize-partial-widget-id', 1640 'data-customize-partial-options', 1641 ), true ) 1642 ); 1643 } 1644 return $allowed_html; 1645 } 1646 1647 /** 1648 * Keep track of the number of times that dynamic_sidebar() was called for a given sidebar index. 1649 * 1650 * This helps facilitate the uncommon scenario where a single sidebar is rendered multiple times on a template. 1651 * 1652 * @since 4.5.0 1653 * @access private 1654 * @var array 1655 */ 1656 protected $sidebar_instance_count = array(); 1657 1658 /** 1659 * The current request's sidebar_instance_number context. 1660 * 1661 * @since 4.5.0 1662 * @access private 1663 * @var int 1664 */ 1665 protected $context_sidebar_instance_number; 1666 1667 /** 1668 * Current sidebar ID being rendered. 1669 * 1670 * @since 4.5.0 1671 * @access private 1672 * @var array 1673 */ 1674 protected $current_dynamic_sidebar_id_stack = array(); 1675 1676 /** 1677 * Start keeping track of the current sidebar being rendered. 1678 * 1679 * Insert marker before widgets are rendered in a dynamic sidebar. 1680 * 1681 * @since 4.5.0 1682 * 1683 * @param int|string $index Index, name, or ID of the dynamic sidebar. 1684 */ 1685 public function start_dynamic_sidebar( $index ) { 1686 array_unshift( $this->current_dynamic_sidebar_id_stack, $index ); 1687 if ( ! isset( $this->sidebar_instance_count[ $index ] ) ) { 1688 $this->sidebar_instance_count[ $index ] = 0; 1689 } 1690 $this->sidebar_instance_count[ $index ] += 1; 1691 if ( ! $this->manager->selective_refresh->is_render_partials_request() ) { 1692 printf( "\n<!--dynamic_sidebar_before:%s:%d-->\n", esc_html( $index ), intval( $this->sidebar_instance_count[ $index ] ) ); 1693 } 1694 } 1695 1696 /** 1697 * Finish keeping track of the current sidebar being rendered. 1698 * 1699 * Insert marker after widgets are rendered in a dynamic sidebar. 1700 * 1701 * @since 4.5.0 1702 * 1703 * @param int|string $index Index, name, or ID of the dynamic sidebar. 1704 */ 1705 public function end_dynamic_sidebar( $index ) { 1706 assert( array_shift( $this->current_dynamic_sidebar_id_stack ) === $index ); 1707 if ( ! $this->manager->selective_refresh->is_render_partials_request() ) { 1708 printf( "\n<!--dynamic_sidebar_after:%s:%d-->\n", esc_html( $index ), intval( $this->sidebar_instance_count[ $index ] ) ); 1709 } 1710 } 1711 1712 /** 1713 * Current sidebar being rendered. 1714 * 1715 * @since 4.5.0 1716 * @access private 1717 * @var string 1718 */ 1719 protected $rendering_widget_id; 1720 1721 /** 1722 * Current widget being rendered. 1723 * 1724 * @since 4.5.0 1725 * @access private 1726 * @var string 1727 */ 1728 protected $rendering_sidebar_id; 1729 1730 /** 1731 * Filter sidebars_widgets to ensure the currently-rendered widget is the only widget in the current sidebar. 1732 * 1733 * @since 4.5.0 1734 * @access private 1735 * 1736 * @param array $sidebars_widgets Sidebars widgets. 1737 * @return array Sidebars widgets. 1738 */ 1739 public function filter_sidebars_widgets_for_rendering_widget( $sidebars_widgets ) { 1740 $sidebars_widgets[ $this->rendering_sidebar_id ] = array( $this->rendering_widget_id ); 1741 return $sidebars_widgets; 1742 } 1743 1744 /** 1745 * Render a specific widget using the supplied sidebar arguments. 1746 * 1747 * @since 4.5.0 1748 * @access public 1749 * 1750 * @see dynamic_sidebar() 1751 * 1752 * @param WP_Customize_Partial $partial Partial. 1753 * @param array $context { 1754 * Sidebar args supplied as container context. 1755 * 1756 * @type string $sidebar_id ID for sidebar for widget to render into. 1757 * @type int [$sidebar_instance_number] Disambiguating instance number. 1758 * } 1759 * @return string|false 1760 */ 1761 public function render_widget_partial( $partial, $context ) { 1762 $id_data = $partial->id_data(); 1763 $widget_id = array_shift( $id_data['keys'] ); 1764 1765 if ( ! is_array( $context ) 1766 || empty( $context['sidebar_id'] ) 1767 || ! is_registered_sidebar( $context['sidebar_id'] ) 1768 ) { 1769 return false; 1770 } 1771 1772 $this->rendering_sidebar_id = $context['sidebar_id']; 1773 1774 if ( isset( $context['sidebar_instance_number'] ) ) { 1775 $this->context_sidebar_instance_number = intval( $context['sidebar_instance_number'] ); 1776 } 1777 1778 // Filter sidebars_widgets so that only the queried widget is in the sidebar. 1779 $this->rendering_widget_id = $widget_id; 1780 1781 $filter_callback = array( $this, 'filter_sidebars_widgets_for_rendering_widget' ); 1782 add_filter( 'sidebars_widgets', $filter_callback, 1000 ); 1783 1784 // Render the widget. 1785 ob_start(); 1786 dynamic_sidebar( $this->rendering_sidebar_id = $context['sidebar_id'] ); 1787 $container = ob_get_clean(); 1788 1789 // Reset variables for next partial render. 1790 remove_filter( 'sidebars_widgets', $filter_callback, 1000 ); 1791 1792 $this->context_sidebar_instance_number = null; 1793 $this->rendering_sidebar_id = null; 1794 $this->rendering_widget_id = null; 1795 1796 return $container; 1797 } 1798 1799 // 1800 // Option Update Capturing 1801 // 1465 1802 1466 1803 /** 1467 1804 * List of captured widget option updates. … … final class WP_Customize_Widgets { 1611 1948 return; 1612 1949 } 1613 1950 1614 remove_filter( 'pre_update_option', array( $this, 'capture_filter_pre_update_option' ), 10 , 3);1951 remove_filter( 'pre_update_option', array( $this, 'capture_filter_pre_update_option' ), 10 ); 1615 1952 1616 1953 foreach ( array_keys( $this->_captured_options ) as $option_name ) { 1617 1954 remove_filter( "pre_option_{$option_name}", array( $this, 'capture_filter_pre_get_option' ) ); -
src/wp-includes/css/customize-preview.css
diff --git src/wp-includes/css/customize-preview.css src/wp-includes/css/customize-preview.css index bc4a6fe..75251ea 100644
4 4 transition: opacity 0.25s; 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 } -
src/wp-includes/customize/class-wp-customize-nav-menu-item-setting.php
diff --git src/wp-includes/customize/class-wp-customize-nav-menu-item-setting.php src/wp-includes/customize/class-wp-customize-nav-menu-item-setting.php index 073423e..b89b56c 100644
class WP_Customize_Nav_Menu_Item_Setting extends WP_Customize_Setting { 67 67 * Default transport. 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 /** 76 77 * The post ID represented by this setting instance. This is the db_id. -
new file src/wp-includes/customize/class-wp-customize-partial.php
diff --git src/wp-includes/customize/class-wp-customize-partial.php src/wp-includes/customize/class-wp-customize-partial.php new file mode 100644 index 0000000..f6e5e44
- + 1 <?php 2 /** 3 * WordPress Customize Partial class 4 * 5 * @package WordPress 6 * @subpackage Customize 7 * @since 4.5.0 8 */ 9 10 /** 11 * Customize Partial class. 12 * 13 * Representation of a rendered region in the previewed page that gets 14 * selectively refreshed when an associated setting is changed. 15 * This class is analogous of WP_Customize_Control. 16 * 17 * @since 4.5.0 18 */ 19 class WP_Customize_Partial { 20 21 /** 22 * Component. 23 * 24 * @since 4.5.0 25 * @access public 26 * @var WP_Customize_Selective_Refresh 27 */ 28 public $component; 29 30 /** 31 * Unique identifier for the partial. 32 * 33 * If the partial is used to display a single setting, this would generally 34 * be the same as the associated setting's ID. 35 * 36 * @since 4.5.0 37 * @access public 38 * @var string 39 */ 40 public $id; 41 42 /** 43 * Parsed ID. 44 * 45 * @since 4.5.0 46 * @access private 47 * @var array { 48 * @type string $base ID base. 49 * @type array $keys Keys for multidimensional. 50 * } 51 */ 52 protected $id_data = array(); 53 54 /** 55 * Type of this partial. 56 * 57 * @since 4.5.0 58 * @access public 59 * @var string 60 */ 61 public $type = 'default'; 62 63 /** 64 * The jQuery selector to find the container element for the partial. 65 * 66 * @since 4.5.0 67 * @access public 68 * @var string 69 */ 70 public $selector; 71 72 /** 73 * All settings tied to the partial. 74 * 75 * @access public 76 * @since 4.5.0 77 * @var WP_Customize_Setting[] 78 */ 79 public $settings; 80 81 /** 82 * The ID for the setting that this partial is primarily responsible for rendering. 83 * 84 * If not supplied, it will default to the ID of the first setting. 85 * 86 * @since 4.5.0 87 * @access public 88 * @var string 89 */ 90 public $primary_setting; 91 92 /** 93 * Render callback. 94 * 95 * @since 4.5.0 96 * @access public 97 * @see WP_Customize_Partial::render() 98 * @var callable Callback is called with one argument, the instance of 99 * WP_Customize_Partial. The callback can either echo the 100 * partial or return the partial as a string, or return false if error. 101 */ 102 public $render_callback; 103 104 /** 105 * Whether the container element is included in the partial, or if only the contents are rendered. 106 * 107 * @since 4.5.0 108 * @access public 109 * @var bool 110 */ 111 public $container_inclusive = false; 112 113 /** 114 * Whether to refresh the entire preview in case a partial cannot be refreshed. 115 * 116 * A partial render is considered a failure if the render_callback returns false. 117 * 118 * @since 4.5.0 119 * @access public 120 * @var bool 121 */ 122 public $fallback_refresh = true; 123 124 /** 125 * Constructor. 126 * 127 * Supplied `$args` override class property defaults. 128 * 129 * If `$args['settings']` is not defined, use the $id as the setting ID. 130 * 131 * @since 4.5.0 132 * @access public 133 * 134 * @param WP_Customize_Selective_Refresh $component Customize Partial Refresh plugin instance. 135 * @param string $id Control ID. 136 * @param array $args { 137 * Optional. Arguments to override class property defaults. 138 * 139 * @type array|string $settings All settings IDs tied to the partial. If undefined, `$id` will be used. 140 * } 141 */ 142 public function __construct( WP_Customize_Selective_Refresh $component, $id, $args = array() ) { 143 $keys = array_keys( get_object_vars( $this ) ); 144 foreach ( $keys as $key ) { 145 if ( isset( $args[ $key ] ) ) { 146 $this->$key = $args[ $key ]; 147 } 148 } 149 150 $this->component = $component; 151 $this->id = $id; 152 $this->id_data['keys'] = preg_split( '/\[/', str_replace( ']', '', $this->id ) ); 153 $this->id_data['base'] = array_shift( $this->id_data['keys'] ); 154 155 if ( empty( $this->render_callback ) ) { 156 $this->render_callback = array( $this, 'render_callback' ); 157 } 158 159 // Process settings. 160 if ( empty( $this->settings ) ) { 161 $this->settings = array( $id ); 162 } else if ( is_string( $this->settings ) ) { 163 $this->settings = array( $this->settings ); 164 } 165 166 if ( empty( $this->primary_setting ) ) { 167 $this->primary_setting = current( $this->settings ); 168 } 169 } 170 171 /** 172 * Retrieves parsed ID data for multidimensional setting. 173 * 174 * @since 4.5.0 175 * @access public 176 * 177 * @return array { 178 * ID data for multidimensional partial. 179 * 180 * @type string $base ID base. 181 * @type array $keys Keys for multidimensional array. 182 * } 183 */ 184 final public function id_data() { 185 return $this->id_data; 186 } 187 188 /** 189 * Renders the template partial involving the associated settings. 190 * 191 * @since 4.5.0 192 * @access public 193 * 194 * @param array $container_context Optional. Array of context data associated with the target container (placement). 195 * Default empty array. 196 * @return string|array|false The rendered partial as a string, raw data array (for client-side JS template), 197 * or false if no render applied. 198 */ 199 final public function render( $container_context = array() ) { 200 $partial = $this; 201 $rendered = false; 202 203 if ( ! empty( $this->render_callback ) ) { 204 ob_start(); 205 $return_render = call_user_func( $this->render_callback, $this, $container_context ); 206 $ob_render = ob_get_clean(); 207 208 if ( null !== $return_render && '' !== $ob_render ) { 209 _doing_it_wrong( __FUNCTION__, __( 'Partial render must echo the content or return the content string (or array), but not both.' ), '4.5.0' ); 210 } 211 212 /* 213 * Note that the string return takes precedence because the $ob_render may just\ 214 * include PHP warnings or notices. 215 */ 216 $rendered = null !== $return_render ? $return_render : $ob_render; 217 } 218 219 /** 220 * Filters partial rendering. 221 * 222 * @since 4.5.0 223 * 224 * @param string|array|false $rendered The partial value. Default false. 225 * @param WP_Customize_Partial $partial WP_Customize_Setting instance. 226 * @param array $container_context Optional array of context data associated with 227 * the target container. 228 */ 229 $rendered = apply_filters( 'customize_partial_render', $rendered, $partial, $container_context ); 230 231 /** 232 * Filters partial rendering for a specific partial. 233 * 234 * The dynamic portion of the hook name, `$partial->ID` refers to the partial ID. 235 * 236 * @since 4.5.0 237 * 238 * @param string|array|false $rendered The partial value. Default false. 239 * @param WP_Customize_Partial $partial WP_Customize_Setting instance. 240 * @param array $container_context Optional array of context data associated with 241 * the target container. 242 */ 243 $rendered = apply_filters( "customize_partial_render_{$partial->id}", $rendered, $partial, $container_context ); 244 245 return $rendered; 246 } 247 248 /** 249 * Default callback used when invoking WP_Customize_Control::render(). 250 * 251 * Note that this method may echo the partial *or* return the partial as 252 * a string or array, but not both. Output buffering is performed when this 253 * is called. Subclasses can override this with their specific logic, or they 254 * may provide an 'render_callback' argument to the constructor. 255 * 256 * This method may return an HTML string for straight DOM injection, or it 257 * may return an array for supporting Partial JS subclasses to render by 258 * applying to client-side templating. 259 * 260 * @since 4.5.0 261 * @access public 262 * 263 * @return string|array|false 264 */ 265 public function render_callback() { 266 return false; 267 } 268 269 /** 270 * Retrieves the data to export to the client via JSON. 271 * 272 * @since 4.5.0 273 * @access public 274 * 275 * @return array Array of parameters passed to the JavaScript. 276 */ 277 public function json() { 278 $exports = array( 279 'settings' => $this->settings, 280 'primarySetting' => $this->primary_setting, 281 'selector' => $this->selector, 282 'type' => $this->type, 283 'fallbackRefresh' => $this->fallback_refresh, 284 'containerInclusive' => $this->container_inclusive, 285 ); 286 return $exports; 287 } 288 } -
new file src/wp-includes/customize/class-wp-customize-selective-refresh.php
diff --git src/wp-includes/customize/class-wp-customize-selective-refresh.php src/wp-includes/customize/class-wp-customize-selective-refresh.php new file mode 100644 index 0000000..f6f72c1
- + 1 <?php 2 /** 3 * WordPress Customize Selective Refresh class 4 * 5 * @package WordPress 6 * @subpackage Customize 7 * @since 4.5.0 8 */ 9 10 /** 11 * WordPress Customize Selective Refresh class. 12 * 13 * @since 4.5.0 14 */ 15 class WP_Customize_Selective_Refresh { 16 17 /** 18 * Query var used in requests to render partials. 19 * 20 * @since 4.5.0 21 */ 22 const RENDER_QUERY_VAR = 'wp_customize_render_partials'; 23 24 /** 25 * Customize manager. 26 * 27 * @var WP_Customize_Manager 28 */ 29 public $manager; 30 31 /** 32 * Registered instances of WP_Customize_Partial. 33 * 34 * @since 4.5.0 35 * @access protected 36 * @var WP_Customize_Partial[] 37 */ 38 protected $partials = array(); 39 40 /** 41 * Log of errors triggered when partials are rendered. 42 * 43 * @since 4.5.0 44 * @access private 45 * @var array 46 */ 47 protected $triggered_errors = array(); 48 49 /** 50 * Keep track of the current partial being rendered. 51 * 52 * @since 4.5.0 53 * @access private 54 * @var string 55 */ 56 protected $current_partial_id; 57 58 /** 59 * Plugin bootstrap for Partial Refresh functionality. 60 * 61 * @since 4.5.0 62 * @access public 63 * 64 * @param WP_Customize_Manager $manager Manager instance. 65 */ 66 public function __construct( WP_Customize_Manager $manager ) { 67 $this->manager = $manager; 68 require_once( ABSPATH . WPINC . '/customize/class-wp-customize-partial.php' ); 69 70 add_action( 'customize_preview_init', array( $this, 'init_preview' ) ); 71 } 72 73 /** 74 * Retrieves the registered partials. 75 * 76 * @since 4.5.0 77 * @access public 78 * 79 * @return array Partials. 80 */ 81 public function partials() { 82 return $this->partials; 83 } 84 85 /** 86 * Adds a partial. 87 * 88 * @since 4.5.0 89 * @access public 90 * 91 * @param WP_Customize_Partial|string $id Customize Partial object, or Panel ID. 92 * @param array $args Optional. Partial arguments. Default empty array. 93 * @return WP_Customize_Partial The instance of the panel that was added. 94 */ 95 public function add_partial( $id, $args = array() ) { 96 if ( $id instanceof WP_Customize_Partial ) { 97 $partial = $id; 98 } else { 99 $class = 'WP_Customize_Partial'; 100 101 /** This filter (will be) documented in wp-includes/class-wp-customize-manager.php */ 102 $args = apply_filters( 'customize_dynamic_partial_args', $args, $id ); 103 104 /** This filter (will be) documented in wp-includes/class-wp-customize-manager.php */ 105 $class = apply_filters( 'customize_dynamic_partial_class', $class, $id, $args ); 106 107 $partial = new $class( $this, $id, $args ); 108 } 109 110 $this->partials[ $partial->id ] = $partial; 111 return $partial; 112 } 113 114 /** 115 * Retrieves a partial. 116 * 117 * @since 4.5.0 118 * @access public 119 * 120 * @param string $id Customize Partial ID. 121 * @return WP_Customize_Partial|null The partial, if set. Otherwise null. 122 */ 123 public function get_partial( $id ) { 124 if ( isset( $this->partials[ $id ] ) ) { 125 return $this->partials[ $id ]; 126 } else { 127 return null; 128 } 129 } 130 131 /** 132 * Removes a partial. 133 * 134 * @since 4.5.0 135 * @access public 136 * 137 * @param string $id Customize Partial ID. 138 */ 139 public function remove_partial( $id ) { 140 unset( $this->partials[ $id ] ); 141 } 142 143 /** 144 * Initializes the Customizer preview. 145 * 146 * @since 4.5.0 147 * @access public 148 */ 149 public function init_preview() { 150 add_action( 'template_redirect', array( $this, 'handle_render_partials_request' ) ); 151 add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_preview_scripts' ) ); 152 } 153 154 /** 155 * Enqueues preview scripts. 156 * 157 * @since 4.5.0 158 * @access public 159 */ 160 public function enqueue_preview_scripts() { 161 wp_enqueue_script( 'customize-selective-refresh' ); 162 add_action( 'wp_footer', array( $this, 'export_preview_data' ), 1000 ); 163 } 164 165 /** 166 * Exports data in preview after it has finished rendering so that partials can be added at runtime. 167 * 168 * @since 4.5.0 169 * @access public 170 */ 171 public function export_preview_data() { 172 $partials = array(); 173 174 foreach ( $this->partials() as $partial ) { 175 $partials[ $partial->id ] = $partial->json(); 176 } 177 178 $exports = array( 179 'partials' => $partials, 180 'renderQueryVar' => self::RENDER_QUERY_VAR, 181 'l10n' => array( 182 'shiftClickToEdit' => __( 'Shift-click to edit this element.' ), 183 ), 184 ); 185 186 // Export data to JS. 187 echo sprintf( '<script>var _customizePartialRefreshExports = %s;</script>', wp_json_encode( $exports ) ); 188 } 189 190 /** 191 * Registers dynamically-created partials. 192 * 193 * @since 4.5.0 194 * @access public 195 * 196 * @see WP_Customize_Manager::add_dynamic_settings() 197 * 198 * @param array $partial_ids The partial ID to add. 199 * @return array Added WP_Customize_Partial instances. 200 */ 201 public function add_dynamic_partials( $partial_ids ) { 202 $new_partials = array(); 203 204 foreach ( $partial_ids as $partial_id ) { 205 206 // Skip partials already created. 207 $partial = $this->get_partial( $partial_id ); 208 if ( $partial ) { 209 continue; 210 } 211 212 $partial_args = false; 213 $partial_class = 'WP_Customize_Partial'; 214 215 /** 216 * Filters a dynamic partial's constructor arguments. 217 * 218 * For a dynamic partial to be registered, this filter must be employed 219 * to override the default false value with an array of args to pass to 220 * the WP_Customize_Partial constructor. 221 * 222 * @since 4.5.0 223 * 224 * @param false|array $partial_args The arguments to the WP_Customize_Partial constructor. 225 * @param string $partial_id ID for dynamic partial. 226 */ 227 $partial_args = apply_filters( 'customize_dynamic_partial_args', $partial_args, $partial_id ); 228 if ( false === $partial_args ) { 229 continue; 230 } 231 232 /** 233 * Filters the class used to construct partials. 234 * 235 * Allow non-statically created partials to be constructed with custom WP_Customize_Partial subclass. 236 * 237 * @since 4.5.0 238 * 239 * @param string $partial_class WP_Customize_Partial or a subclass. 240 * @param string $partial_id ID for dynamic partial. 241 * @param array $partial_args The arguments to the WP_Customize_Partial constructor. 242 */ 243 $partial_class = apply_filters( 'customize_dynamic_partial_class', $partial_class, $partial_id, $partial_args ); 244 245 $partial = new $partial_class( $this, $partial_id, $partial_args ); 246 247 $this->add_partial( $partial ); 248 $new_partials[] = $partial; 249 } 250 return $new_partials; 251 } 252 253 /** 254 * Checks whether the request is for rendering partials. 255 * 256 * Note that this will not consider whether the request is authorized or valid, 257 * just that essentially the route is a match. 258 * 259 * @since 4.5.0 260 * @access public 261 * 262 * @return bool Whether the request is for rendering partials. 263 */ 264 public function is_render_partials_request() { 265 return ! empty( $_POST[ self::RENDER_QUERY_VAR ] ); 266 } 267 268 /** 269 * Handles PHP errors triggered during rendering the partials. 270 * 271 * These errors will be relayed back to the client in the Ajax response. 272 * 273 * @since 4.5.0 274 * @access private 275 * 276 * @param int $errno Error number. 277 * @param string $errstr Error string. 278 * @param string $errfile Error file. 279 * @param string $errline Error line. 280 * @return true Always true. 281 */ 282 public function handle_error( $errno, $errstr, $errfile = null, $errline = null ) { 283 $this->triggered_errors[] = array( 284 'partial' => $this->current_partial_id, 285 'error_number' => $errno, 286 'error_string' => $errstr, 287 'error_file' => $errfile, 288 'error_line' => $errline, 289 ); 290 return true; 291 } 292 293 /** 294 * Handles the Ajax request to return the rendered partials for the requested placements. 295 * 296 * @since 4.5.0 297 * @access public 298 */ 299 public function handle_render_partials_request() { 300 if ( ! $this->is_render_partials_request() ) { 301 return; 302 } 303 304 $this->manager->remove_preview_signature(); 305 306 /* 307 * Note that is_customize_preview() returning true will entail that the 308 * user passed the 'customize' capability check and the nonce check, since 309 * WP_Customize_Manager::setup_theme() is where the previewing flag is set. 310 */ 311 if ( ! is_customize_preview() ) { 312 status_header( 403 ); 313 wp_send_json_error( 'expected_customize_preview' ); 314 } else if ( ! isset( $_POST['partials'] ) ) { 315 status_header( 400 ); 316 wp_send_json_error( 'missing_partials' ); 317 } 318 319 $partials = json_decode( wp_unslash( $_POST['partials'] ), true ); 320 321 if ( ! is_array( $partials ) ) { 322 wp_send_json_error( 'malformed_partials' ); 323 } 324 325 $this->add_dynamic_partials( array_keys( $partials ) ); 326 327 /** 328 * Fires immediately before partials are rendered. 329 * 330 * Plugins may do things like call wp_enqueue_scripts() and gather a list of the scripts 331 * and styles which may get enqueued in the response. 332 * 333 * @since 4.5.0 334 * 335 * @param WP_Customize_Selective_Refresh $this Selective refresh component. 336 * @param array $partials Placements' context data for the partials rendered in the request. 337 * The array is keyed by partial ID, with each item being an array of 338 * the placements' context data. 339 */ 340 do_action( 'customize_render_partials_before', $this, $partials ); 341 342 set_error_handler( array( $this, 'handle_error' ), error_reporting() ); 343 344 $contents = array(); 345 346 foreach ( $partials as $partial_id => $container_contexts ) { 347 $this->current_partial_id = $partial_id; 348 349 if ( ! is_array( $container_contexts ) ) { 350 wp_send_json_error( 'malformed_container_contexts' ); 351 } 352 353 $partial = $this->get_partial( $partial_id ); 354 355 if ( ! $partial ) { 356 $contents[ $partial_id ] = null; 357 continue; 358 } 359 360 $contents[ $partial_id ] = array(); 361 362 // @todo The array should include not only the contents, but also whether the container is included? 363 if ( empty( $container_contexts ) ) { 364 // Since there are no container contexts, render just once. 365 $contents[ $partial_id ][] = $partial->render( null ); 366 } else { 367 foreach ( $container_contexts as $container_context ) { 368 $contents[ $partial_id ][] = $partial->render( $container_context ); 369 } 370 } 371 } 372 $this->current_partial_id = null; 373 374 restore_error_handler(); 375 376 /** 377 * Fires immediately after partials are rendered. 378 * 379 * Plugins may do things like call wp_footer() to scrape scripts output and return them 380 * via the {@see 'customize_render_partials_response'} filter. 381 * 382 * @since 4.5.0 383 * 384 * @param WP_Customize_Selective_Refresh $this Selective refresh component. 385 * @param array $partials Placements' context data for the partials rendered in the request. 386 * The array is keyed by partial ID, with each item being an array of 387 * the placements' context data. 388 */ 389 do_action( 'customize_render_partials_after', $this, $partials ); 390 391 $response = array( 392 'contents' => $contents, 393 ); 394 395 if ( defined( 'WP_DEBUG_DISPLAY' ) && WP_DEBUG_DISPLAY ) { 396 $response['errors'] = $this->triggered_errors; 397 } 398 399 /** 400 * Filters the response from rendering the partials. 401 * 402 * Plugins may use this filter to inject `$scripts` and `$styles`, which are dependencies 403 * for the partials being rendered. The response data will be available to the client via 404 * the `render-partials-response` JS event, so the client can then inject the scripts and 405 * styles into the DOM if they have not already been enqueued there. 406 * 407 * If plugins do this, they'll need to take care for any scripts that do `document.write()` 408 * and make sure that these are not injected, or else to override the function to no-op, 409 * or else the page will be destroyed. 410 * 411 * Plugins should be aware that `$scripts` and `$styles` may eventually be included by 412 * default in the response. 413 * 414 * @since 4.5.0 415 * 416 * @param array $response { 417 * Response. 418 * 419 * @type array $contents Associative array mapping a partial ID its corresponding array of contents 420 * for the containers requested. 421 * @type array $errors List of errors triggered during rendering of partials, if `WP_DEBUG_DISPLAY` 422 * is enabled. 423 * } 424 * @param WP_Customize_Selective_Refresh $this Selective refresh component. 425 * @param array $partials Placements' context data for the partials rendered in the request. 426 * The array is keyed by partial ID, with each item being an array of 427 * the placements' context data. 428 */ 429 $response = apply_filters( 'customize_render_partials_response', $response, $this, $partials ); 430 431 wp_send_json_success( $response ); 432 } 433 } -
src/wp-includes/js/customize-preview-nav-menus.js
diff --git src/wp-includes/js/customize-preview-nav-menus.js src/wp-includes/js/customize-preview-nav-menus.js index 9e84494..c61e620 100644
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: {} 19 }; 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(); 4 var self = {}; 44 5 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 } 6 /** 7 * Initialize nav menus preview. 8 */ 9 self.init = function() { 10 var self = this; 54 11 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 } ); 12 if ( api.selectiveRefresh ) { 13 self.watchNavMenuLocationChanges(); 14 } 62 15 16 api.preview.bind( 'active', function() { 63 17 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 }, 18 } ); 19 }; 112 20 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 }, 21 if ( api.selectiveRefresh ) { 128 22 129 23 /** 130 * Update a given menu rendered in the preview.24 * Partial representing an invocation of wp_nav_menu(). 131 25 * 132 * @param {int} menuId 26 * @class 27 * @augments wp.customize.selectiveRefresh.Partial 28 * @since 4.5.0 133 29 */ 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] ); 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.' ); 141 50 } 142 }); 143 144 _.each( settings.navMenuInstanceArgs, function( navMenuArgs, instanceNumber ) { 145 if ( menuId === navMenuArgs.menu || -1 !== _.indexOf( assignedLocations, navMenuArgs.theme_location ) ) { 146 this.refreshMenuInstanceDebounced( instanceNumber ); 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' ); 147 66 } 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; 67 if ( partial.params.navMenuArgs.args_hmac !== argsHmac ) { 68 throw new Error( 'args_hmac mismatch with id' ); 162 69 } 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' ); 193 return; 194 } 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; 203 } 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; 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; 83 if ( _.isString( setting ) ) { 84 setting = api( setting ); 223 85 } 224 } );225 data.customized = JSON.stringify( customized );226 data[ settings.renderNoncePostKey ] = settings.renderNonceValue;227 86 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; 87 if ( partial.params.navMenuArgs.theme_location ) { 88 if ( 'nav_menu_locations[' + partial.params.navMenuArgs.theme_location + ']' === setting.id ) { 89 return true; 90 } 91 navMenuLocationSetting = api( 'nav_menu_locations[' + partial.params.navMenuArgs.theme_location + ']' ); 244 92 } 245 93 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 }, 94 navMenuId = partial.params.navMenuArgs.menu; 95 if ( ! navMenuId && navMenuLocationSetting ) { 96 navMenuId = navMenuLocationSetting(); 97 } 265 98 266 refreshMenuInstanceDebounced : function( instanceNumber ) { 267 if ( currentRefreshDebounced[ instanceNumber ] ) { 268 clearTimeout( currentRefreshDebounced[ instanceNumber ] ); 99 if ( ! navMenuId ) { 100 return false; 101 } 102 return ( 103 ( 'nav_menu[' + navMenuId + ']' === setting.id ) || 104 ( /^nav_menu_item\[/.test( setting.id ) && 105 ( ( newValue && newValue.nav_menu_term_id === navMenuId ) || ( oldValue && oldValue.nav_menu_term_id === navMenuId ) ) 106 ) 107 ); 108 }, 109 110 /** 111 * Render content. 112 * 113 * @inheritdoc 114 * @param {wp.customize.selectiveRefresh.Placement} placement 115 */ 116 renderContent: function( placement ) { 117 var partial = this, previousContainer = placement.container; 118 if ( api.selectiveRefresh.Partial.prototype.renderContent.call( partial, placement ) ) { 119 120 // Trigger deprecated event. 121 $( document ).trigger( 'customize-preview-menu-refreshed', [ { 122 instanceNumber: null, // @deprecated 123 wpNavArgs: placement.context, // @deprecated 124 wpNavMenuArgs: placement.context, 125 oldContainer: previousContainer, 126 newContainer: placement.container 127 } ] ); 128 } 269 129 } 270 currentRefreshDebounced[ instanceNumber ] = setTimeout( 271 _.bind( function() { 272 this.refreshMenuInstance( instanceNumber ); 273 }, this ), 274 refreshDebounceDelay 275 ); 276 }, 130 }); 131 132 api.selectiveRefresh.partialConstructor.nav_menu_instance = self.NavMenuInstancePartial; 277 133 278 134 /** 279 * Connect nav menu items with their corresponding controls in the pane. 135 * Watch for changes to nav_menu_locations[] settings. 136 * 137 * Refresh partials associated with the given nav_menu_locations[] setting, 138 * or request an entire preview refresh if there are no containers in the 139 * document for a partial associated with the theme location. 140 * 141 * @since 4.5.0 280 142 */ 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 ) { 143 self.watchNavMenuLocationChanges = function() { 144 api.bind( 'change', function( setting ) { 145 var themeLocation, themeLocationPartialFound = false, matches = setting.id.match( /^nav_menu_locations\[(.+)]$/ ); 146 if ( ! matches ) { 289 147 return; 290 148 } 149 themeLocation = matches[1]; 150 api.selectiveRefresh.partial.each( function( partial ) { 151 if ( partial.extended( self.NavMenuInstancePartial ) && partial.params.navMenuArgs.theme_location === themeLocation ) { 152 partial.refresh(); 153 themeLocationPartialFound = true; 154 } 155 } ); 291 156 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 ) ); 157 if ( ! themeLocationPartialFound ) { 158 api.selectiveRefresh.requestFullRefresh(); 297 159 } 298 }); 299 300 addTooltips = function( e, params ) { 301 params.newContainer.find( selector ).attr( 'title', settings.l10n.editNavMenuItemTooltip ); 302 }; 160 } ); 161 }; 162 } 163 164 /** 165 * Connect nav menu items with their corresponding controls in the pane. 166 * 167 * Setup shift-click on nav menu items which are more granular than the nav menu partial itself. 168 * Also this applies even if a nav menu is not partial-refreshable. 169 * 170 * @since 4.5.0 171 */ 172 self.highlightControls = function() { 173 var selector = '.menu-item'; 174 175 // Focus on the menu item control when shift+clicking the menu item. 176 $( document ).on( 'click', selector, function( e ) { 177 var navMenuItemParts; 178 if ( ! e.shiftKey ) { 179 return; 180 } 303 181 304 addTooltips( null, { newContainer: $( document.body ) } ); 305 $( document ).on( 'customize-preview-menu-refreshed', addTooltips ); 306 } 182 navMenuItemParts = $( this ).attr( 'class' ).match( /(?:^|\s)menu-item-(\d+)(?:\s|$)/ ); 183 if ( navMenuItemParts ) { 184 e.preventDefault(); 185 e.stopPropagation(); // Make sure a sub-nav menu item will get focused instead of parent items. 186 api.preview.send( 'focus-nav-menu-item-control', parseInt( navMenuItemParts[1], 10 ) ); 187 } 188 }); 307 189 }; 308 190 309 191 api.bind( 'preview-ready', function() { 310 api.preview.bind( 'active', function() { 311 api.MenusCustomizerPreview.init(); 312 } ); 192 self.init(); 313 193 } ); 314 194 315 }( jQuery, _, wp ) ); 195 return self; 196 197 }( jQuery, _, wp, wp.customize ) ); -
src/wp-includes/js/customize-preview-widgets.js
diff --git src/wp-includes/js/customize-preview-widgets.js src/wp-includes/js/customize-preview-widgets.js index f982829..92e7732 100644
1 (function( wp, $ ){ 1 /* global _wpWidgetCustomizerPreviewSettings */ 2 wp.customize.widgetsPreview = wp.customize.WidgetCustomizerPreview = (function( $, _, wp, api ) { 2 3 3 if ( ! wp || ! wp.customize ) { return; }4 var self; 4 5 5 var api = wp.customize; 6 self = { 7 renderedSidebars: {}, 8 renderedWidgets: {}, 9 registeredSidebars: [], 10 registeredWidgets: {}, 11 widgetSelectors: [], 12 preview: null, 13 l10n: { 14 widgetTooltip: '' 15 } 16 }; 6 17 7 18 /** 8 * wp.customize.WidgetCustomizerPreview19 * Init widgets preview. 9 20 * 21 * @since 4.5.0 10 22 */ 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 16 widgetSelectors: [], 17 preview: null, 18 l10n: {}, 23 self.init = function() { 24 var self = this; 19 25 20 init: function () { 21 var self = this; 26 self.preview = api.preview; 27 if ( api.selectiveRefresh ) { 28 self.addPartials(); 29 } 22 30 23 this.preview = api.preview; 24 this.buildWidgetSelectors(); 25 this.highlightControls(); 31 self.buildWidgetSelectors(); 32 self.highlightControls(); 26 33 27 this.preview.bind( 'highlight-widget', self.highlightWidget ); 28 }, 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 ) { 53 return; 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.' ); 54 65 } 55 66 56 widgetClasses = widgetClasses.replace(/^\s+|\s+$/g, ''); 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 }, 57 81 58 if ( widgetClasses ) { 59 widgetSelector += '.' + widgetClasses.split(/\s+/).join('.'); 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 ); 60 93 } 61 self.widgetSelectors.push(widgetSelector); 62 }); 63 }, 94 } 95 }); 64 96 65 97 /** 66 * Highlight the widget on widget updates or widget control mouse overs.98 * Partial representing a widget area. 67 99 * 68 * @param {string} widgetId ID of the widget. 100 * @class 101 * @augments wp.customize.selectiveRefresh.Partial 102 * @since 4.5.0 69 103 */ 70 highlightWidget: function( widgetId ) { 71 var $body = $( document.body ), 72 $widget = $( '#' + widgetId ); 104 self.SidebarPartial = api.selectiveRefresh.Partial.extend({ 73 105 74 $body.find( '.widget-customizer-highlighted-widget' ).removeClass( 'widget-customizer-highlighted-widget' ); 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]; 75 121 76 $widget.addClass( 'widget-customizer-highlighted-widget' ); 77 setTimeout( function () { 78 $widget.removeClass( 'widget-customizer-highlighted-widget' ); 79 }, 500 ); 80 }, 122 options = options || {}; 123 options.params = _.extend( 124 { 125 settings: [ 'sidebars_widgets[' + partial.sidebarId + ']' ] 126 }, 127 options.params || {} 128 ); 81 129 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(','); 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 } 89 347 90 $(selector).attr( 'title', this.l10n.widgetTooltip ); 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; 91 351 92 $(document).on( 'mouseenter', selector, function () { 93 self.preview.send( 'highlight-widget-control', $( this ).prop( 'id' ) ); 94 }); 352 foundWidgetPlacement = _.find( widgetPartial.placements(), function( widgetPlacement ) { 353 return ( widgetPlacement.context.sidebar_instance_number === sidebarPlacement.context.instanceNumber ); 354 } ); 355 if ( foundWidgetPlacement ) { 356 return; 357 } 95 358 96 // Open expand the widget control when shift+clicking the widget element 97 $(document).on( 'click', selector, function ( e ) { 98 if ( ! e.shiftKey ) { 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(); 99 407 return; 100 408 } 101 e.preventDefault();102 409 103 self.preview.send( 'focus-widget-control', $( this ).prop( 'id' ) ); 104 }); 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; 468 469 /** 470 * Add partials for the registered widget areas (sidebars). 471 * 472 * @since 4.5.0 473 */ 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. 593 } 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; 105 615 } 616 parsed.idBase = matches[1]; 617 if ( matches[2] ) { 618 parsed.number = parseInt( matches[2], 10 ); 619 } 620 return parsed; 106 621 }; 107 622 108 $(function () { 109 var settings = window._wpWidgetCustomizerPreviewSettings; 110 if ( ! settings ) { 111 return; 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 ) + ']'; 112 637 } 113 638 114 $.extend( api.WidgetCustomizerPreview, settings ); 639 return settingId; 640 }; 115 641 116 api.WidgetCustomizerPreview.init(); 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 ); -
new file src/wp-includes/js/customize-selective-refresh.js
diff --git src/wp-includes/js/customize-selective-refresh.js src/wp-includes/js/customize-selective-refresh.js new file mode 100644 index 0000000..3da6144
- + 1 /* global jQuery, JSON, _customizePartialRefreshExports, console */ 2 3 wp.customize.selectiveRefresh = ( function( $, api ) { 4 'use strict'; 5 var self, Partial, Placement; 6 7 self = { 8 ready: $.Deferred(), 9 data: { 10 partials: {}, 11 renderQueryVar: '', 12 l10n: { 13 shiftClickToEdit: '' 14 }, 15 refreshBuffer: 250 16 }, 17 currentRequest: null 18 }; 19 20 _.extend( self, api.Events ); 21 22 /** 23 * A Customizer Partial. 24 * 25 * A partial provides a rendering of one or more settings according to a template. 26 * 27 * @see PHP class WP_Customize_Partial. 28 * 29 * @class 30 * @augments wp.customize.Class 31 * @since 4.5.0 32 * 33 * @param {string} id Unique identifier for the control instance. 34 * @param {object} options Options hash for the control instance. 35 * @param {object} options.params 36 * @param {string} options.params.type Type of partial (e.g. nav_menu, widget, etc) 37 * @param {string} options.params.selector jQuery selector to find the container element in the page. 38 * @param {array} options.params.settings The IDs for the settings the partial relates to. 39 * @param {string} options.params.primarySetting The ID for the primary setting the partial renders. 40 * @param {bool} options.params.fallbackRefresh Whether to refresh the entire preview in case of a partial refresh failure. 41 */ 42 Partial = self.Partial = api.Class.extend({ 43 44 id: null, 45 46 /** 47 * Constructor. 48 * 49 * @since 4.5.0 50 * 51 * @param {string} id - Partial ID. 52 * @param {Object} options 53 * @param {Object} options.params 54 */ 55 initialize: function( id, options ) { 56 var partial = this; 57 options = options || {}; 58 partial.id = id; 59 60 partial.params = _.extend( 61 { 62 selector: null, 63 settings: [], 64 primarySetting: null, 65 containerInclusive: false, 66 fallbackRefresh: true // Note this needs to be false in a frontend editing context. 67 }, 68 options.params || {} 69 ); 70 71 partial.deferred = {}; 72 partial.deferred.ready = $.Deferred(); 73 74 partial.deferred.ready.done( function() { 75 partial.ready(); 76 } ); 77 }, 78 79 /** 80 * Set up the partial. 81 * 82 * @since 4.5.0 83 */ 84 ready: function() { 85 var partial = this; 86 _.each( _.pluck( partial.placements(), 'container' ), function( container ) { 87 $( container ).attr( 'title', self.data.l10n.shiftClickToEdit ); 88 } ); 89 $( document ).on( 'click', partial.params.selector, function( e ) { 90 if ( ! e.shiftKey ) { 91 return; 92 } 93 e.preventDefault(); 94 _.each( partial.placements(), function( placement ) { 95 if ( $( placement.container ).is( e.currentTarget ) ) { 96 partial.showControl(); 97 } 98 } ); 99 } ); 100 }, 101 102 /** 103 * Find all placements for this partial int he document. 104 * 105 * @since 4.5.0 106 * 107 * @return {Array.<Placement>} 108 */ 109 placements: function() { 110 var partial = this, selector; 111 112 selector = partial.params.selector; 113 if ( selector ) { 114 selector += ', '; 115 } 116 selector += '[data-customize-partial-id="' + partial.id + '"]'; // @todo Consider injecting customize-partial-id-${id} classnames instead. 117 118 return $( selector ).map( function() { 119 var container = $( this ), context; 120 121 context = container.data( 'customize-partial-placement-context' ); 122 if ( _.isString( context ) && '{' === context.substr( 0, 1 ) ) { 123 throw new Error( 'context JSON parse error' ); 124 } 125 126 return new Placement( { 127 partial: partial, 128 container: container, 129 context: context 130 } ); 131 } ).get(); 132 }, 133 134 /** 135 * Get list of setting IDs related to this partial. 136 * 137 * @since 4.5.0 138 * 139 * @return {String[]} 140 */ 141 settings: function() { 142 var partial = this; 143 if ( partial.params.settings && 0 !== partial.params.settings.length ) { 144 return partial.params.settings; 145 } else if ( partial.params.primarySetting ) { 146 return [ partial.params.primarySetting ]; 147 } else { 148 return [ partial.id ]; 149 } 150 }, 151 152 /** 153 * Return whether the setting is related to the partial. 154 * 155 * @since 4.5.0 156 * 157 * @param {wp.customize.Value|string} setting ID or object for setting. 158 * @return {boolean} Whether the setting is related to the partial. 159 */ 160 isRelatedSetting: function( setting /*... newValue, oldValue */ ) { 161 var partial = this; 162 if ( _.isString( setting ) ) { 163 setting = api( setting ); 164 } 165 if ( ! setting ) { 166 return false; 167 } 168 return -1 !== _.indexOf( partial.settings(), setting.id ); 169 }, 170 171 /** 172 * Show the control to modify this partial's setting(s). 173 * 174 * This may be overridden for inline editing. 175 * 176 * @since 4.5.0 177 */ 178 showControl: function() { 179 var partial = this, settingId = partial.params.primarySetting; 180 if ( ! settingId ) { 181 settingId = _.first( partial.settings() ); 182 } 183 api.preview.send( 'focus-control-for-setting', settingId ); 184 }, 185 186 /** 187 * Prepare container for selective refresh. 188 * 189 * @since 4.5.0 190 * 191 * @param {Placement} placement 192 */ 193 preparePlacement: function( placement ) { 194 $( placement.container ).addClass( 'customize-partial-refreshing' ); 195 }, 196 197 /** 198 * Reference to the pending promise returned from self.requestPartial(). 199 * 200 * @since 4.5.0 201 * @private 202 */ 203 _pendingRefreshPromise: null, 204 205 /** 206 * Request the new partial and render it into the placements. 207 * 208 * @since 4.5.0 209 * 210 * @this {wp.customize.selectiveRefresh.Partial} 211 * @return {jQuery.Promise} 212 */ 213 refresh: function() { 214 var partial = this, refreshPromise; 215 216 refreshPromise = self.requestPartial( partial ); 217 218 if ( ! partial._pendingRefreshPromise ) { 219 _.each( partial.placements(), function( placement ) { 220 partial.preparePlacement( placement ); 221 } ); 222 223 refreshPromise.done( function( placements ) { 224 _.each( placements, function( placement ) { 225 partial.renderContent( placement ); 226 } ); 227 } ); 228 229 refreshPromise.fail( function( data, placements ) { 230 partial.fallback( data, placements ); 231 } ); 232 233 // Allow new request when this one finishes. 234 partial._pendingRefreshPromise = refreshPromise; 235 refreshPromise.always( function() { 236 partial._pendingRefreshPromise = null; 237 } ); 238 } 239 240 return refreshPromise; 241 }, 242 243 /** 244 * Apply the addedContent in the placement to the document. 245 * 246 * Note the placement object will have its container and removedNodes 247 * properties updated. 248 * 249 * @since 4.5.0 250 * 251 * @param {Placement} placement 252 * @param {Element|jQuery} [placement.container] - This param will be empty if there was no element matching the selector. 253 * @param {string|object|boolean} placement.addedContent - Rendered HTML content, a data object for JS templates to render, or false if no render. 254 * @param {object} [placement.context] - Optional context information about the container. 255 * @returns {boolean} Whether the rendering was successful and the fallback was not invoked. 256 */ 257 renderContent: function( placement ) { 258 var partial = this, content, newContainerElement; 259 if ( ! placement.container ) { 260 partial.fallback( new Error( 'no_container' ), [ placement ] ); 261 return false; 262 } 263 placement.container = $( placement.container ); 264 if ( false === placement.addedContent ) { 265 partial.fallback( new Error( 'missing_render' ), [ placement ] ); 266 return false; 267 } 268 269 // Currently a subclass needs to override renderContent to handle partials returning data object. 270 if ( ! _.isString( placement.addedContent ) ) { 271 partial.fallback( new Error( 'non_string_content' ), [ placement ] ); 272 return false; 273 } 274 275 content = placement.addedContent; 276 if ( wp.emoji && wp.emoji.parse && ! $.contains( document.head, placement.container[0] ) ) { 277 content = wp.emoji.parse( content ); 278 } 279 280 // @todo Should containerInclusive be context information as opposed to a param? 281 if ( partial.params.containerInclusive ) { 282 283 // Note that content may be an empty string, and in this case jQuery will just remove the oldContainer 284 newContainerElement = $( content ); 285 286 // Merge the new context on top of the old context. 287 placement.context = _.extend( 288 placement.context, 289 newContainerElement.data( 'customize-partial-placement-context' ) || {} 290 ); 291 newContainerElement.data( 'customize-partial-placement-context', placement.context ); 292 293 placement.removedNodes = placement.container; 294 placement.container = newContainerElement; 295 placement.removedNodes.replaceWith( placement.container ); 296 placement.container.attr( 'title', self.data.l10n.shiftClickToEdit ); 297 } else { 298 placement.removedNodes = document.createDocumentFragment(); 299 while ( placement.container[0].firstChild ) { 300 placement.removedNodes.appendChild( placement.container[0].firstChild ); 301 } 302 303 placement.container.html( content ); 304 } 305 306 placement.container.removeClass( 'customize-partial-refreshing' ); 307 308 // Prevent placement container from being being re-triggered as being rendered among nested partials. 309 placement.container.data( 'customize-partial-content-rendered', true ); 310 311 /** 312 * Announce when a partial's placement has been rendered so that dynamic elements can be re-built. 313 */ 314 self.trigger( 'partial-content-rendered', placement ); 315 return true; 316 }, 317 318 /** 319 * Handle fail to render partial. 320 * 321 * The first argument is either the failing jqXHR or an Error object, and the second argument is the array of containers. 322 * 323 * @since 4.5.0 324 */ 325 fallback: function() { 326 var partial = this; 327 if ( partial.params.fallbackRefresh ) { 328 self.requestFullRefresh(); 329 } 330 } 331 } ); 332 333 /** 334 * A Placement for a Partial. 335 * 336 * A partial placement is the actual physical representation of a partial for a given context. 337 * It also may have information in relation to how a placement may have just changed. 338 * The placement is conceptually similar to a DOM Range or MutationRecord. 339 * 340 * @class 341 * @augments wp.customize.Class 342 * @since 4.5.0 343 */ 344 self.Placement = Placement = api.Class.extend({ 345 346 /** 347 * The partial with which the container is associated. 348 * 349 * @param {wp.customize.selectiveRefresh.Partial} 350 */ 351 partial: null, 352 353 /** 354 * DOM element which contains the placement's contents. 355 * 356 * This will be null if the startNode and endNode do not point to the same 357 * DOM element, such as in the case of a sidebar partial. 358 * This container element itself will be replaced for partials that 359 * have containerInclusive param defined as true. 360 */ 361 container: null, 362 363 /** 364 * DOM node for the initial boundary of the placement. 365 * 366 * This will normally be the same as endNode since most placements appear as elements. 367 * This is primarily useful for widget sidebars which do not have intrinsic containers, but 368 * for which an HTML comment is output before to mark the starting position. 369 */ 370 startNode: null, 371 372 /** 373 * DOM node for the terminal boundary of the placement. 374 * 375 * This will normally be the same as startNode since most placements appear as elements. 376 * This is primarily useful for widget sidebars which do not have intrinsic containers, but 377 * for which an HTML comment is output before to mark the ending position. 378 */ 379 endNode: null, 380 381 /** 382 * Context data. 383 * 384 * This provides information about the placement which is included in the request 385 * in order to render the partial properly. 386 * 387 * @param {object} 388 */ 389 context: null, 390 391 /** 392 * The content for the partial when refreshed. 393 * 394 * @param {string} 395 */ 396 addedContent: null, 397 398 /** 399 * DOM node(s) removed when the partial is refreshed. 400 * 401 * If the partial is containerInclusive, then the removedNodes will be 402 * the single Element that was the partial's former placement. If the 403 * partial is not containerInclusive, then the removedNodes will be a 404 * documentFragment containing the nodes removed. 405 * 406 * @param {Element|DocumentFragment} 407 */ 408 removedNodes: null, 409 410 /** 411 * Constructor. 412 * 413 * @since 4.5.0 414 * 415 * @param {object} args 416 * @param {Partial} args.partial 417 * @param {jQuery|Element} [args.container] 418 * @param {Node} [args.startNode] 419 * @param {Node} [args.endNode] 420 * @param {object} [args.context] 421 * @param {string} [args.addedContent] 422 * @param {jQuery|DocumentFragment} [args.removedNodes] 423 */ 424 initialize: function( args ) { 425 var placement = this; 426 427 args = _.extend( {}, args || {} ); 428 if ( ! args.partial || ! args.partial.extended( Partial ) ) { 429 throw new Error( 'Missing partial' ); 430 } 431 args.context = args.context || {}; 432 if ( args.container ) { 433 args.container = $( args.container ); 434 } 435 436 _.extend( placement, args ); 437 } 438 439 }); 440 441 /** 442 * Mapping of type names to Partial constructor subclasses. 443 * 444 * @since 4.5.0 445 * 446 * @type {Object.<string, wp.customize.selectiveRefresh.Partial>} 447 */ 448 self.partialConstructor = {}; 449 450 self.partial = new api.Values({ defaultConstructor: Partial }); 451 452 /** 453 * Get the POST vars for a Customizer preview request. 454 * 455 * @since 4.5.0 456 * @see wp.customize.previewer.query() 457 * 458 * @return {object} 459 */ 460 self.getCustomizeQuery = function() { 461 var dirtyCustomized = {}; 462 api.each( function( value, key ) { 463 if ( value._dirty ) { 464 dirtyCustomized[ key ] = value(); 465 } 466 } ); 467 468 return { 469 wp_customize: 'on', 470 nonce: api.settings.nonce.preview, 471 theme: api.settings.theme.stylesheet, 472 customized: JSON.stringify( dirtyCustomized ) 473 }; 474 }; 475 476 /** 477 * Currently-requested partials and their associated deferreds. 478 * 479 * @since 4.5.0 480 * @type {Object<string, { deferred: jQuery.Promise, partial: wp.customize.selectiveRefresh.Partial }>} 481 */ 482 self._pendingPartialRequests = {}; 483 484 /** 485 * Timeout ID for the current requesr, or null if no request is current. 486 * 487 * @since 4.5.0 488 * @type {number|null} 489 * @private 490 */ 491 self._debouncedTimeoutId = null; 492 493 /** 494 * Current jqXHR for the request to the partials. 495 * 496 * @since 4.5.0 497 * @type {jQuery.jqXHR|null} 498 * @private 499 */ 500 self._currentRequest = null; 501 502 /** 503 * Request full page refresh. 504 * 505 * When selective refresh is embedded in the context of frontend editing, this request 506 * must fail or else changes will be lost, unless transactions are implemented. 507 * 508 * @since 4.5.0 509 */ 510 self.requestFullRefresh = function() { 511 api.preview.send( 'refresh' ); 512 }; 513 514 /** 515 * Request a re-rendering of a partial. 516 * 517 * @since 4.5.0 518 * 519 * @param {wp.customize.selectiveRefresh.Partial} partial 520 * @return {jQuery.Promise} 521 */ 522 self.requestPartial = function( partial ) { 523 var partialRequest; 524 525 if ( self._debouncedTimeoutId ) { 526 clearTimeout( self._debouncedTimeoutId ); 527 self._debouncedTimeoutId = null; 528 } 529 if ( self._currentRequest ) { 530 self._currentRequest.abort(); 531 self._currentRequest = null; 532 } 533 534 partialRequest = self._pendingPartialRequests[ partial.id ]; 535 if ( ! partialRequest || 'pending' !== partialRequest.deferred.state() ) { 536 partialRequest = { 537 deferred: $.Deferred(), 538 partial: partial 539 }; 540 self._pendingPartialRequests[ partial.id ] = partialRequest; 541 } 542 543 // Prevent leaking partial into debounced timeout callback. 544 partial = null; 545 546 self._debouncedTimeoutId = setTimeout( 547 function() { 548 var data, partialPlacementContexts, partialsPlacements, request; 549 550 self._debouncedTimeoutId = null; 551 data = self.getCustomizeQuery(); 552 553 /* 554 * It is key that the containers be fetched exactly at the point of the request being 555 * made, because the containers need to be mapped to responses by array indices. 556 */ 557 partialsPlacements = {}; 558 559 partialPlacementContexts = {}; 560 561 _.each( self._pendingPartialRequests, function( pending, partialId ) { 562 partialsPlacements[ partialId ] = pending.partial.placements(); 563 if ( ! self.partial.has( partialId ) ) { 564 pending.deferred.rejectWith( pending.partial, [ new Error( 'partial_removed' ), partialsPlacements[ partialId ] ] ); 565 } else { 566 /* 567 * Note that this may in fact be an empty array. In that case, it is the responsibility 568 * of the Partial subclass instance to know where to inject the response, or else to 569 * just issue a refresh (default behavior). The data being returned with each container 570 * is the context information that may be needed to render certain partials, such as 571 * the contained sidebar for rendering widgets or what the nav menu args are for a menu. 572 */ 573 partialPlacementContexts[ partialId ] = _.map( partialsPlacements[ partialId ], function( placement ) { 574 return placement.context || {}; 575 } ); 576 } 577 } ); 578 579 data.partials = JSON.stringify( partialPlacementContexts ); 580 data[ self.data.renderQueryVar ] = '1'; 581 582 request = self._currentRequest = wp.ajax.send( null, { 583 data: data, 584 url: api.settings.url.self 585 } ); 586 587 request.done( function( data ) { 588 589 /** 590 * Announce the data returned from a request to render partials. 591 * 592 * The data is filtered on the server via customize_render_partials_response 593 * so plugins can inject data from the server to be utilized 594 * on the client via this event. Plugins may use this filter 595 * to communicate script and style dependencies that need to get 596 * injected into the page to support the rendered partials. 597 * This is similar to the 'saved' event. 598 */ 599 self.trigger( 'render-partials-response', data ); 600 601 // Relay errors (warnings) captured during rendering and relay to console. 602 if ( data.errors && 'undefined' !== typeof console && console.warn ) { 603 _.each( data.errors, function( error ) { 604 console.warn( error ); 605 } ); 606 } 607 608 /* 609 * Note that data is an array of items that correspond to the array of 610 * containers that were submitted in the request. So we zip up the 611 * array of containers with the array of contents for those containers, 612 * and send them into . 613 */ 614 _.each( self._pendingPartialRequests, function( pending, partialId ) { 615 var placementsContents; 616 if ( ! _.isArray( data.contents[ partialId ] ) ) { 617 pending.deferred.rejectWith( pending.partial, [ new Error( 'unrecognized_partial' ), partialsPlacements[ partialId ] ] ); 618 } else { 619 placementsContents = _.map( data.contents[ partialId ], function( content, i ) { 620 var partialPlacement = partialsPlacements[ partialId ][ i ]; 621 if ( partialPlacement ) { 622 partialPlacement.addedContent = content; 623 } else { 624 partialPlacement = new Placement( { 625 partial: pending.partial, 626 addedContent: content 627 } ); 628 } 629 return partialPlacement; 630 } ); 631 pending.deferred.resolveWith( pending.partial, [ placementsContents ] ); 632 } 633 } ); 634 self._pendingPartialRequests = {}; 635 } ); 636 637 request.fail( function( data, statusText ) { 638 639 /* 640 * Ignore failures caused by partial.currentRequest.abort() 641 * The pending deferreds will remain in self._pendingPartialRequests 642 * for re-use with the next request. 643 */ 644 if ( 'abort' === statusText ) { 645 return; 646 } 647 648 _.each( self._pendingPartialRequests, function( pending, partialId ) { 649 pending.deferred.rejectWith( pending.partial, [ data, partialsPlacements[ partialId ] ] ); 650 } ); 651 self._pendingPartialRequests = {}; 652 } ); 653 }, 654 self.data.refreshBuffer 655 ); 656 657 return partialRequest.deferred.promise(); 658 }; 659 660 /** 661 * Add partials for any nav menu container elements in the document. 662 * 663 * This method may be called multiple times. Containers that already have been 664 * seen will be skipped. 665 * 666 * @since 4.5.0 667 * 668 * @param {jQuery|HTMLElement} [rootElement] 669 * @param {object} [options] 670 * @param {boolean=true} [options.triggerRendered] 671 */ 672 self.addPartials = function( rootElement, options ) { 673 var containerElements; 674 if ( ! rootElement ) { 675 rootElement = document.documentElement; 676 } 677 rootElement = $( rootElement ); 678 options = _.extend( 679 { 680 triggerRendered: true 681 }, 682 options || {} 683 ); 684 685 containerElements = rootElement.find( '[data-customize-partial-id]' ); 686 if ( rootElement.is( '[data-customize-partial-id]' ) ) { 687 containerElements = containerElements.add( rootElement ); 688 } 689 containerElements.each( function() { 690 var containerElement = $( this ), partial, id, Constructor, partialOptions, containerContext; 691 id = containerElement.data( 'customize-partial-id' ); 692 if ( ! id ) { 693 return; 694 } 695 containerContext = containerElement.data( 'customize-partial-placement-context' ) || {}; 696 697 partial = self.partial( id ); 698 if ( ! partial ) { 699 partialOptions = containerElement.data( 'customize-partial-options' ) || {}; 700 partialOptions.constructingContainerContext = containerElement.data( 'customize-partial-placement-context' ) || {}; 701 Constructor = self.partialConstructor[ containerElement.data( 'customize-partial-type' ) ] || self.Partial; 702 partial = new Constructor( id, partialOptions ); 703 self.partial.add( partial.id, partial ); 704 } 705 706 /* 707 * Only trigger renders on (nested) partials that have been not been 708 * handled yet. An example where this would apply is a nav menu 709 * embedded inside of a custom menu widget. When the widget's title 710 * is updated, the entire widget will re-render and then the event 711 * will be triggered for the nested nav menu to do any initialization. 712 */ 713 if ( options.triggerRendered && ! containerElement.data( 'customize-partial-content-rendered' ) ) { 714 715 /** 716 * Announce when a partial's nested placement has been re-rendered. 717 */ 718 self.trigger( 'partial-content-rendered', new Placement( { 719 partial: partial, 720 context: containerContext, 721 container: containerElement 722 } ) ); 723 } 724 containerElement.data( 'customize-partial-content-rendered', true ); 725 } ); 726 }; 727 728 api.bind( 'preview-ready', function() { 729 var handleSettingChange, watchSettingChange, unwatchSettingChange; 730 731 // Polyfill for IE8 to support the document.head attribute. 732 if ( ! document.head ) { 733 document.head = $( 'head:first' )[0]; 734 } 735 736 _.extend( self.data, _customizePartialRefreshExports ); 737 738 // Create the partial JS models. 739 _.each( self.data.partials, function( data, id ) { 740 var Constructor, partial = self.partial( id ); 741 if ( ! partial ) { 742 Constructor = self.partialConstructor[ data.type ] || self.Partial; 743 partial = new Constructor( id, { params: data } ); 744 self.partial.add( id, partial ); 745 } else { 746 _.extend( partial.params, data ); 747 } 748 } ); 749 750 /** 751 * Handle change to a setting. 752 * 753 * Note this is largely needed because adding a 'change' event handler to wp.customize 754 * will only include the changed setting object as an argument, not including the 755 * new value or the old value. 756 * 757 * @since 4.5.0 758 * @this {wp.customize.Setting} 759 * 760 * @param {*|null} newValue New value, or null if the setting was just removed. 761 * @param {*|null} oldValue Old value, or null if the setting was just added. 762 */ 763 handleSettingChange = function( newValue, oldValue ) { 764 var setting = this; 765 self.partial.each( function( partial ) { 766 if ( partial.isRelatedSetting( setting, newValue, oldValue ) ) { 767 partial.refresh(); 768 } 769 } ); 770 }; 771 772 /** 773 * Trigger the initial change for the added setting, and watch for changes. 774 * 775 * @since 4.5.0 776 * @this {wp.customize.Values} 777 * 778 * @param {wp.customize.Setting} setting 779 */ 780 watchSettingChange = function( setting ) { 781 handleSettingChange.call( setting, setting(), null ); 782 setting.bind( handleSettingChange ); 783 }; 784 785 /** 786 * Trigger the final change for the removed setting, and unwatch for changes. 787 * 788 * @since 4.5.0 789 * @this {wp.customize.Values} 790 * 791 * @param {wp.customize.Setting} setting 792 */ 793 unwatchSettingChange = function( setting ) { 794 handleSettingChange.call( setting, null, setting() ); 795 setting.unbind( handleSettingChange ); 796 }; 797 798 api.bind( 'add', watchSettingChange ); 799 api.bind( 'remove', unwatchSettingChange ); 800 api.each( function( setting ) { 801 setting.bind( handleSettingChange ); 802 } ); 803 804 // Add (dynamic) initial partials that are declared via data-* attributes. 805 self.addPartials( document.documentElement, { 806 triggerRendered: false 807 } ); 808 809 // Add new dynamic partials when the document changes. 810 if ( 'undefined' !== typeof MutationObserver ) { 811 self.mutationObserver = new MutationObserver( function( mutations ) { 812 _.each( mutations, function( mutation ) { 813 self.addPartials( $( mutation.target ) ); 814 } ); 815 } ); 816 self.mutationObserver.observe( document.documentElement, { 817 childList: true, 818 subtree: true 819 } ); 820 } 821 822 /** 823 * Handle rendering of partials. 824 * 825 * @param {api.selectiveRefresh.Placement} placement 826 */ 827 api.selectiveRefresh.bind( 'partial-content-rendered', function( placement ) { 828 if ( placement.container ) { 829 self.addPartials( placement.container ); 830 } 831 } ); 832 833 api.preview.bind( 'active', function() { 834 835 // Make all partials ready. 836 self.partial.each( function( partial ) { 837 partial.deferred.ready.resolve(); 838 } ); 839 840 // Make all partials added henceforth as ready upon add. 841 self.partial.bind( 'add', function( partial ) { 842 partial.deferred.ready.resolve(); 843 } ); 844 } ); 845 846 } ); 847 848 return self; 849 }( jQuery, wp.customize ) ); -
src/wp-includes/script-loader.php
diff --git src/wp-includes/script-loader.php src/wp-includes/script-loader.php index 1a9d259..01be0f5 100644
function wp_default_scripts( &$scripts ) { 447 447 // Used for overriding the file types allowed in plupload. 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 ); 452 453 $scripts->add( 'customize-preview-widgets', "/wp-includes/js/customize-preview-widgets$suffix.js", array( 'jquery', 'wp-util', 'customize-preview' ), false, 1 ); -
tests/phpunit/tests/customize/manager.php
diff --git tests/phpunit/tests/customize/manager.php tests/phpunit/tests/customize/manager.php index 0b86b4c..6f5789d 100644
class Tests_WP_Customize_Manager extends WP_UnitTestCase { 425 425 $data = json_decode( $json, true ); 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'] ); 431 431 $this->assertArrayHasKey( 'preview', $data['nonce'] ); -
tests/phpunit/tests/customize/nav-menu-item-setting.php
diff --git tests/phpunit/tests/customize/nav-menu-item-setting.php tests/phpunit/tests/customize/nav-menu-item-setting.php index 39ed42e..3431ef8 100644
class Test_WP_Customize_Nav_Menu_Item_Setting extends WP_UnitTestCase { 69 69 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 ); 75 74 $this->assertNull( $setting->update_status ); -
tests/phpunit/tests/customize/nav-menus.php
diff --git tests/phpunit/tests/customize/nav-menus.php tests/phpunit/tests/customize/nav-menus.php index 2969a2d..a65b428 100644
class Test_WP_Customize_Nav_Menus extends WP_UnitTestCase { 353 353 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 363 363 /** … … class Test_WP_Customize_Nav_Menus extends WP_UnitTestCase { 532 532 $menus = new WP_Customize_Nav_Menus( $this->wp_customize ); 533 533 534 534 $menus->customize_preview_init(); 535 $this->assertEquals( 10, has_action( 'template_redirect', array( $menus, 'render_menu' ) ) );536 535 $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 } 536 $this->assertEquals( 1000, has_filter( 'wp_nav_menu_args', array( $menus, 'filter_wp_nav_menu_args' ) ) ); 537 $this->assertEquals( 10, has_filter( 'wp_nav_menu', array( $menus, 'filter_wp_nav_menu' ) ) ); 542 538 } 543 539 544 540 /** … … class Test_WP_Customize_Nav_Menus extends WP_UnitTestCase { 548 544 */ 549 545 function test_filter_wp_nav_menu_args() { 550 546 do_action( 'customize_register', $this->wp_customize ); 551 $menus = new WP_Customize_Nav_Menus( $this->wp_customize );547 $menus = $this->wp_customize->nav_menus; 552 548 553 549 $results = $menus->filter_wp_nav_menu_args( array( 554 550 'echo' => true, 555 551 'fallback_cb' => 'wp_page_menu', 556 552 'walker' => '', 557 553 'menu' => wp_create_nav_menu( 'Foo' ), 554 'items_wrap' => '<ul id="%1$s" class="%2$s">%3$s</ul>', 558 555 ) ); 559 $this->assert Equals( 1, $results['can_partial_refresh']);556 $this->assertArrayHasKey( 'customize_preview_nav_menus_args', $results ); 560 557 561 $expected = array(562 'echo',563 'can_partial_refresh',564 'fallback_cb',565 'instance_number',566 'walker',567 );568 558 $results = $menus->filter_wp_nav_menu_args( array( 569 559 'echo' => false, 570 560 'fallback_cb' => 'wp_page_menu', 571 561 'walker' => new Walker_Nav_Menu(), 562 'items_wrap' => '<ul id="%1$s" class="%2$s">%3$s</ul>', 572 563 ) ); 573 $this->assert EqualSets( $expected, array_keys( $results ));564 $this->assertArrayNotHasKey( 'customize_preview_nav_menus_args', $results ); 574 565 $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'] );582 566 } 583 567 584 568 /** … … class Test_WP_Customize_Nav_Menus extends WP_UnitTestCase { 595 579 'menu' => wp_create_nav_menu( 'Foo' ), 596 580 'fallback_cb' => 'wp_page_menu', 597 581 'walker' => '', 582 'items_wrap' => '<ul id="%1$s" class="%2$s">%3$s</ul>', 598 583 ) ); 599 584 600 585 ob_start(); 601 586 wp_nav_menu( $args ); 602 587 $nav_menu_content = ob_get_clean(); 603 588 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 ); 589 $result = $menus->filter_wp_nav_menu( $nav_menu_content, (object) $args ); 590 591 $this->assertContains( sprintf( ' data-customize-partial-id="nav_menu_instance[%s]"', $args['customize_preview_nav_menus_args']['args_hmac'] ), $result ); 592 $this->assertContains( ' data-customize-partial-type="nav_menu_instance"', $result ); 593 $this->assertContains( ' data-customize-partial-placement-context="', $result ); 611 594 } 612 595 613 596 /** … … class Test_WP_Customize_Nav_Menus extends WP_UnitTestCase { 622 605 $menus->customize_preview_enqueue_deps(); 623 606 624 607 $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.630 *631 * @see WP_Customize_Nav_Menus::export_preview_data()632 */633 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 );651 608 } 652 609 } -
new file tests/phpunit/tests/customize/partial.php
diff --git tests/phpunit/tests/customize/partial.php tests/phpunit/tests/customize/partial.php new file mode 100644 index 0000000..6120355
- + 1 <?php 2 /** 3 * Test_WP_Customize_Partial tests. 4 * 5 * @package WordPress 6 */ 7 8 /** 9 * Tests for the Test_WP_Customize_Partial class. 10 * 11 * @group customize 12 */ 13 class Test_WP_Customize_Partial extends WP_UnitTestCase { 14 15 /** 16 * Manager. 17 * 18 * @var WP_Customize_Manager 19 */ 20 public $wp_customize; 21 22 /** 23 * Component. 24 * 25 * @var WP_Customize_Selective_Refresh 26 */ 27 public $selective_refresh; 28 29 /** 30 * Set up. 31 */ 32 function setUp() { 33 parent::setUp(); 34 require_once( ABSPATH . WPINC . '/class-wp-customize-manager.php' ); 35 // @codingStandardsIgnoreStart 36 $GLOBALS['wp_customize'] = new WP_Customize_Manager(); 37 // @codingStandardsIgnoreEnd 38 $this->wp_customize = $GLOBALS['wp_customize']; 39 if ( isset( $this->wp_customize->selective_refresh ) ) { 40 $this->selective_refresh = $this->wp_customize->selective_refresh; 41 } 42 } 43 44 /** 45 * Test WP_Customize_Partial::__construct(). 46 * 47 * @see WP_Customize_Partial::__construct() 48 */ 49 function test_construct_default_args() { 50 $partial_id = 'blogname'; 51 $partial = new WP_Customize_Partial( $this->selective_refresh, $partial_id ); 52 $this->assertEquals( $partial_id, $partial->id ); 53 $this->assertEquals( $this->selective_refresh, $partial->component ); 54 $this->assertEquals( 'default', $partial->type ); 55 $this->assertEmpty( $partial->selector ); 56 $this->assertEquals( array( $partial_id ), $partial->settings ); 57 $this->assertEquals( $partial_id, $partial->primary_setting ); 58 $this->assertEquals( array( $partial, 'render_callback' ), $partial->render_callback ); 59 $this->assertEquals( false, $partial->container_inclusive ); 60 $this->assertEquals( true, $partial->fallback_refresh ); 61 } 62 63 /** 64 * Render post content partial. 65 * 66 * @param WP_Customize_Partial $partial Partial. 67 * @return string|false Content or false if error. 68 */ 69 function render_post_content_partial( $partial ) { 70 $id_data = $partial->id_data(); 71 $post_id = intval( $id_data['keys'][0] ); 72 if ( empty( $post_id ) ) { 73 return false; 74 } 75 $post = get_post( $post_id ); 76 if ( ! $post ) { 77 return false; 78 } 79 return apply_filters( 'the_content', $post->post_content ); 80 } 81 82 /** 83 * Test WP_Customize_Partial::__construct(). 84 * 85 * @see WP_Customize_Partial::__construct() 86 */ 87 function test_construct_non_default_args() { 88 89 $post_id = self::factory()->post->create( array( 90 'post_title' => 'Hello World', 91 'post_content' => 'Lorem Ipsum', 92 ) ); 93 94 $partial_id = sprintf( 'post_content[%d]', $post_id ); 95 $args = array( 96 'type' => 'post', 97 'selector' => "article.post-$post_id .entry-content", 98 'settings' => array( 'user[1]', "post[$post_id]" ), 99 'primary_setting' => "post[$post_id]", 100 'render_callback' => array( $this, 'render_post_content_partial' ), 101 'container_inclusive' => false, 102 'fallback_refresh' => false, 103 ); 104 $partial = new WP_Customize_Partial( $this->selective_refresh, $partial_id, $args ); 105 $this->assertEquals( $partial_id, $partial->id ); 106 $this->assertEquals( $this->selective_refresh, $partial->component ); 107 $this->assertEquals( $args['type'], $partial->type ); 108 $this->assertEquals( $args['selector'], $partial->selector ); 109 $this->assertEqualSets( $args['settings'], $partial->settings ); 110 $this->assertEquals( $args['primary_setting'], $partial->primary_setting ); 111 $this->assertEquals( $args['render_callback'], $partial->render_callback ); 112 $this->assertEquals( false, $partial->container_inclusive ); 113 $this->assertEquals( false, $partial->fallback_refresh ); 114 $this->assertContains( 'Lorem Ipsum', $partial->render() ); 115 116 $partial = new WP_Customize_Partial( $this->selective_refresh, $partial_id, array( 117 'settings' => 'blogdescription', 118 ) ); 119 $this->assertEquals( array( 'blogdescription' ), $partial->settings ); 120 $this->assertEquals( 'blogdescription', $partial->primary_setting ); 121 } 122 123 /** 124 * Test WP_Customize_Partial::id_data(). 125 * 126 * @see WP_Customize_Partial::id_data() 127 */ 128 function test_id_data() { 129 $partial = new WP_Customize_Partial( $this->selective_refresh, 'foo' ); 130 $id_data = $partial->id_data(); 131 $this->assertEquals( 'foo', $id_data['base'] ); 132 $this->assertEquals( array(), $id_data['keys'] ); 133 134 $partial = new WP_Customize_Partial( $this->selective_refresh, 'bar[baz][quux]' ); 135 $id_data = $partial->id_data(); 136 $this->assertEquals( 'bar', $id_data['base'] ); 137 $this->assertEquals( array( 'baz', 'quux' ), $id_data['keys'] ); 138 } 139 140 /** 141 * Keep track of filter calls to customize_partial_render. 142 * 143 * @var int 144 */ 145 protected $count_filter_customize_partial_render = 0; 146 147 /** 148 * Keep track of filter calls to customize_partial_render_{$partial->id}. 149 * 150 * @var int 151 */ 152 protected $count_filter_customize_partial_render_with_id = 0; 153 154 /** 155 * Filter customize_partial_render. 156 * 157 * @param string|false $rendered Content. 158 * @param WP_Customize_Partial $partial Partial. 159 * @param array $container_context Data. 160 * @return string|false Content. 161 */ 162 function filter_customize_partial_render( $rendered, $partial, $container_context ) { 163 $this->assertTrue( false === $rendered || is_string( $rendered ) ); 164 $this->assertInstanceOf( 'WP_Customize_Partial', $partial ); 165 $this->assertInternalType( 'array', $container_context ); 166 $this->count_filter_customize_partial_render += 1; 167 return $rendered; 168 } 169 170 /** 171 * Filter customize_partial_render_{$partial->id}. 172 * 173 * @param string|false $rendered Content. 174 * @param WP_Customize_Partial $partial Partial. 175 * @param array $container_context Data. 176 * @return string|false Content. 177 */ 178 function filter_customize_partial_render_with_id( $rendered, $partial, $container_context ) { 179 $this->assertEquals( sprintf( 'customize_partial_render_%s', $partial->id ), current_filter() ); 180 $this->assertTrue( false === $rendered || is_string( $rendered ) ); 181 $this->assertInstanceOf( 'WP_Customize_Partial', $partial ); 182 $this->assertInternalType( 'array', $container_context ); 183 $this->count_filter_customize_partial_render_with_id += 1; 184 return $rendered; 185 } 186 187 /** 188 * Bad render_callback(). 189 * 190 * @return string Content. 191 */ 192 function render_echo_and_return() { 193 echo 'foo'; 194 return 'bar'; 195 } 196 197 /** 198 * Echo render_callback(). 199 */ 200 function render_echo() { 201 echo 'foo'; 202 } 203 204 /** 205 * Return render_callback(). 206 * 207 * @return string Content. 208 */ 209 function render_return() { 210 return 'bar'; 211 } 212 213 /** 214 * Test WP_Customize_Partial::render() with a bad return_callback. 215 * 216 * @see WP_Customize_Partial::render() 217 */ 218 function test_render_bad_callback() { 219 $partial = new WP_Customize_Partial( $this->selective_refresh, 'foo', array( 220 'render_callback' => array( $this, 'render_echo_and_return' ), 221 ) ); 222 $this->setExpectedIncorrectUsage( 'render' ); 223 $partial->render(); 224 } 225 226 /** 227 * Test WP_Customize_Partial::render() with a return_callback that echos. 228 * 229 * @see WP_Customize_Partial::render() 230 */ 231 function test_render_echo_callback() { 232 $partial = new WP_Customize_Partial( $this->selective_refresh, 'foo', array( 233 'render_callback' => array( $this, 'render_echo' ), 234 ) ); 235 $count_filter_customize_partial_render = $this->count_filter_customize_partial_render; 236 $count_filter_customize_partial_render_with_id = $this->count_filter_customize_partial_render_with_id; 237 add_filter( 'customize_partial_render', array( $this, 'filter_customize_partial_render' ), 10, 3 ); 238 add_filter( "customize_partial_render_{$partial->id}", array( $this, 'filter_customize_partial_render_with_id' ), 10, 3 ); 239 $rendered = $partial->render(); 240 $this->assertEquals( 'foo', $rendered ); 241 $this->assertEquals( $count_filter_customize_partial_render + 1, $this->count_filter_customize_partial_render ); 242 $this->assertEquals( $count_filter_customize_partial_render_with_id + 1, $this->count_filter_customize_partial_render_with_id ); 243 } 244 245 /** 246 * Test WP_Customize_Partial::render() with a return_callback that echos. 247 * 248 * @see WP_Customize_Partial::render() 249 */ 250 function test_render_return_callback() { 251 $partial = new WP_Customize_Partial( $this->selective_refresh, 'foo', array( 252 'render_callback' => array( $this, 'render_return' ), 253 ) ); 254 $count_filter_customize_partial_render = $this->count_filter_customize_partial_render; 255 $count_filter_customize_partial_render_with_id = $this->count_filter_customize_partial_render_with_id; 256 add_filter( 'customize_partial_render', array( $this, 'filter_customize_partial_render' ), 10, 3 ); 257 add_filter( "customize_partial_render_{$partial->id}", array( $this, 'filter_customize_partial_render_with_id' ), 10, 3 ); 258 $rendered = $partial->render(); 259 $this->assertEquals( 'bar', $rendered ); 260 $this->assertEquals( $count_filter_customize_partial_render + 1, $this->count_filter_customize_partial_render ); 261 $this->assertEquals( $count_filter_customize_partial_render_with_id + 1, $this->count_filter_customize_partial_render_with_id ); 262 } 263 264 /** 265 * Test WP_Customize_Partial::render_callback() default. 266 * 267 * @see WP_Customize_Partial::render_callback() 268 */ 269 function test_render_callback_default() { 270 $partial = new WP_Customize_Partial( $this->selective_refresh, 'foo' ); 271 $this->assertFalse( $partial->render_callback() ); 272 $this->assertFalse( call_user_func( $partial->render_callback ) ); 273 } 274 275 /** 276 * Test WP_Customize_Partial::json() default. 277 * 278 * @see WP_Customize_Partial::json() 279 */ 280 function test_json() { 281 $post_id = 123; 282 $partial_id = sprintf( 'post_content[%d]', $post_id ); 283 $args = array( 284 'type' => 'post', 285 'selector' => "article.post-$post_id .entry-content", 286 'settings' => array( 'user[1]', "post[$post_id]" ), 287 'primary_setting' => "post[$post_id]", 288 'render_callback' => array( $this, 'render_post_content_partial' ), 289 'container_inclusive' => false, 290 'fallback_refresh' => false, 291 ); 292 $partial = new WP_Customize_Partial( $this->selective_refresh, $partial_id, $args ); 293 294 $exported = $partial->json(); 295 $this->assertArrayHasKey( 'settings', $exported ); 296 $this->assertArrayHasKey( 'primarySetting', $exported ); 297 $this->assertArrayHasKey( 'selector', $exported ); 298 $this->assertArrayHasKey( 'type', $exported ); 299 $this->assertArrayHasKey( 'fallbackRefresh', $exported ); 300 $this->assertArrayHasKey( 'containerInclusive', $exported ); 301 } 302 303 /** 304 * Tear down. 305 */ 306 function tearDown() { 307 $this->wp_customize = null; 308 unset( $GLOBALS['wp_customize'] ); 309 parent::tearDown(); 310 } 311 } -
new file tests/phpunit/tests/customize/selective-refresh-ajax.php
diff --git tests/phpunit/tests/customize/selective-refresh-ajax.php tests/phpunit/tests/customize/selective-refresh-ajax.php new file mode 100644 index 0000000..09406de
- + 1 <?php 2 /** 3 * WP_Customize_Selective_Refresh Ajax tests. 4 * 5 * @package WordPress 6 * @subpackage UnitTests 7 * @since 4.5.0 8 * @group ajax 9 * @group customize 10 */ 11 12 /** 13 * Tests for the WP_Customize_Selective_Refresh class Ajax. 14 * 15 * Note that this is intentionally not extending WP_Ajax_UnitTestCase because it 16 * is not admin ajax. 17 */ 18 class Test_WP_Customize_Selective_Refresh_Ajax extends WP_UnitTestCase { 19 20 /** 21 * Manager. 22 * 23 * @var WP_Customize_Manager 24 */ 25 public $wp_customize; 26 27 /** 28 * Component. 29 * 30 * @var WP_Customize_Selective_Refresh 31 */ 32 public $selective_refresh; 33 34 /** 35 * Set up the test fixture. 36 */ 37 function setUp() { 38 parent::setUp(); 39 40 // Define DOING_AJAX so that wp_die() will be used instead of die(). 41 if ( ! defined( 'DOING_AJAX' ) ) { 42 define( 'DOING_AJAX', true ); 43 } 44 add_filter( 'wp_die_ajax_handler', array( $this, 'get_wp_die_handler' ), 1, 1 ); 45 46 require_once( ABSPATH . WPINC . '/class-wp-customize-manager.php' ); 47 // @codingStandardsIgnoreStart 48 $GLOBALS['wp_customize'] = new WP_Customize_Manager(); 49 // @codingStandardsIgnoreEnd 50 $this->wp_customize = $GLOBALS['wp_customize']; 51 if ( isset( $this->wp_customize->selective_refresh ) ) { 52 $this->selective_refresh = $this->wp_customize->selective_refresh; 53 } 54 55 } 56 57 /** 58 * Do Customizer boot actions. 59 */ 60 function do_customize_boot_actions() { 61 // Remove actions that call add_theme_support( 'title-tag' ). 62 remove_action( 'after_setup_theme', 'twentyfifteen_setup' ); 63 remove_action( 'after_setup_theme', 'twentysixteen_setup' ); 64 65 $_SERVER['REQUEST_METHOD'] = 'POST'; 66 do_action( 'setup_theme' ); 67 do_action( 'after_setup_theme' ); 68 do_action( 'init' ); 69 do_action( 'customize_register', $this->wp_customize ); 70 $this->wp_customize->customize_preview_init(); 71 do_action( 'wp', $GLOBALS['wp'] ); 72 } 73 74 /** 75 * Test WP_Customize_Selective_Refresh::handle_render_partials_request(). 76 * 77 * @see WP_Customize_Selective_Refresh::handle_render_partials_request() 78 */ 79 function test_handle_render_partials_request_for_unauthenticated_user() { 80 $_POST[ WP_Customize_Selective_Refresh::RENDER_QUERY_VAR ] = '1'; 81 82 // Check current_user_cannot_customize. 83 ob_start(); 84 try { 85 $this->selective_refresh->handle_render_partials_request(); 86 } catch ( WPDieException $e ) { 87 unset( $e ); 88 } 89 $output = json_decode( ob_get_clean(), true ); 90 $this->assertFalse( $output['success'] ); 91 $this->assertEquals( 'expected_customize_preview', $output['data'] ); 92 93 // Check expected_customize_preview. 94 wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) ); 95 $_REQUEST['nonce'] = wp_create_nonce( 'preview-customize_' . $this->wp_customize->theme()->get_stylesheet() ); 96 ob_start(); 97 try { 98 $this->selective_refresh->handle_render_partials_request(); 99 } catch ( WPDieException $e ) { 100 unset( $e ); 101 } 102 $output = json_decode( ob_get_clean(), true ); 103 $this->assertFalse( $output['success'] ); 104 $this->assertEquals( 'expected_customize_preview', $output['data'] ); 105 106 // Check missing_partials. 107 $this->do_customize_boot_actions(); 108 ob_start(); 109 try { 110 $this->selective_refresh->handle_render_partials_request(); 111 } catch ( WPDieException $e ) { 112 unset( $e ); 113 } 114 $output = json_decode( ob_get_clean(), true ); 115 $this->assertFalse( $output['success'] ); 116 $this->assertEquals( 'missing_partials', $output['data'] ); 117 118 // Check missing_partials. 119 $_POST['partials'] = 'bad'; 120 $this->do_customize_boot_actions(); 121 ob_start(); 122 try { 123 $this->selective_refresh->handle_render_partials_request(); 124 } catch ( WPDieException $e ) { 125 $this->assertEquals( '', $e->getMessage() ); 126 } 127 $output = json_decode( ob_get_clean(), true ); 128 $this->assertFalse( $output['success'] ); 129 $this->assertEquals( 'malformed_partials', $output['data'] ); 130 } 131 132 /** 133 * Set the current user to be an admin, add the preview nonce, and set the query var. 134 */ 135 function setup_valid_render_partials_request_environment() { 136 wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) ); 137 $_REQUEST['nonce'] = wp_create_nonce( 'preview-customize_' . $this->wp_customize->theme()->get_stylesheet() ); 138 $_POST[ WP_Customize_Selective_Refresh::RENDER_QUERY_VAR ] = '1'; 139 $this->do_customize_boot_actions(); 140 } 141 142 /** 143 * Make sure that the Customizer "signature" is not included in partial render responses. 144 * 145 * @see WP_Customize_Selective_Refresh::handle_render_partials_request() 146 */ 147 function test_handle_render_partials_request_removes_customize_signature() { 148 $this->setup_valid_render_partials_request_environment(); 149 $this->assertTrue( is_customize_preview() ); 150 $this->assertEquals( 1000, has_action( 'shutdown', array( $this->wp_customize, 'customize_preview_signature' ) ) ); 151 ob_start(); 152 try { 153 $this->selective_refresh->handle_render_partials_request(); 154 } catch ( WPDieException $e ) { 155 unset( $e ); 156 } 157 ob_end_clean(); 158 $this->assertFalse( has_action( 'shutdown', array( $this->wp_customize, 'customize_preview_signature' ) ) ); 159 } 160 161 /** 162 * Test WP_Customize_Selective_Refresh::handle_render_partials_request() for an unrecognized partial. 163 * 164 * @see WP_Customize_Selective_Refresh::handle_render_partials_request() 165 */ 166 function test_handle_render_partials_request_for_unrecognized_partial() { 167 $this->setup_valid_render_partials_request_environment(); 168 $context_data = array(); 169 $placements = array( $context_data ); 170 171 $_POST['partials'] = wp_slash( wp_json_encode( array( 172 'foo' => $placements, 173 ) ) ); 174 175 ob_start(); 176 try { 177 $this->expected_partial_ids = array( 'foo' ); 178 add_filter( 'customize_render_partials_response', array( $this, 'filter_customize_render_partials_response' ), 10, 3 ); 179 add_action( 'customize_render_partials_before', array( $this, 'handle_action_customize_render_partials_before' ), 10, 2 ); 180 add_action( 'customize_render_partials_after', array( $this, 'handle_action_customize_render_partials_after' ), 10, 2 ); 181 $this->selective_refresh->handle_render_partials_request(); 182 } catch ( WPDieException $e ) { 183 $this->assertEquals( '', $e->getMessage() ); 184 } 185 $output = json_decode( ob_get_clean(), true ); 186 $this->assertTrue( $output['success'] ); 187 $this->assertInternalType( 'array', $output['data'] ); 188 $this->assertArrayHasKey( 'contents', $output['data'] ); 189 $this->assertArrayHasKey( 'errors', $output['data'] ); 190 $this->assertArrayHasKey( 'foo', $output['data']['contents'] ); 191 $this->assertEquals( null, $output['data']['contents']['foo'] ); 192 } 193 194 /** 195 * Test WP_Customize_Selective_Refresh::handle_render_partials_request() for a partial that does not render. 196 * 197 * @see WP_Customize_Selective_Refresh::handle_render_partials_request() 198 */ 199 function test_handle_render_partials_request_for_non_rendering_partial() { 200 $this->setup_valid_render_partials_request_environment(); 201 $this->wp_customize->selective_refresh->add_partial( 'foo', array( 'settings' => array( 'home' ) ) ); 202 $context_data = array(); 203 $placements = array( $context_data ); 204 205 $_POST['partials'] = wp_slash( wp_json_encode( array( 206 'foo' => $placements, 207 ) ) ); 208 209 $count_customize_render_partials_before = has_action( 'customize_render_partials_before' ); 210 $count_customize_render_partials_after = has_action( 'customize_render_partials_after' ); 211 ob_start(); 212 try { 213 $this->expected_partial_ids = array( 'foo' ); 214 add_filter( 'customize_render_partials_response', array( $this, 'filter_customize_render_partials_response' ), 10, 3 ); 215 add_action( 'customize_render_partials_before', array( $this, 'handle_action_customize_render_partials_before' ), 10, 2 ); 216 add_action( 'customize_render_partials_after', array( $this, 'handle_action_customize_render_partials_after' ), 10, 2 ); 217 $this->selective_refresh->handle_render_partials_request(); 218 } catch ( WPDieException $e ) { 219 $this->assertEquals( '', $e->getMessage() ); 220 } 221 $this->assertEquals( $count_customize_render_partials_before + 1, has_action( 'customize_render_partials_before' ) ); 222 $this->assertEquals( $count_customize_render_partials_after + 1, has_action( 'customize_render_partials_after' ) ); 223 $output = json_decode( ob_get_clean(), true ); 224 $this->assertEquals( array( false ), $output['data']['contents']['foo'] ); 225 } 226 227 /** 228 * Get the rendered blogname. 229 * 230 * @param WP_Customize_Partial $partial Partial. 231 * @param array $context Context data. 232 * @return string 233 */ 234 function render_callback_blogname( $partial, $context ) { 235 $this->assertInternalType( 'array', $context ); 236 $this->assertInstanceOf( 'WP_Customize_Partial', $partial ); 237 return get_bloginfo( 'name', 'display' ); 238 } 239 240 /** 241 * Get the rendered blogdescription. 242 * 243 * @param WP_Customize_Partial $partial Partial. 244 * @param array $context Context data. 245 * @return string 246 */ 247 function render_callback_blogdescription( $partial, $context ) { 248 $this->assertInternalType( 'array', $context ); 249 $this->assertInstanceOf( 'WP_Customize_Partial', $partial ); 250 $x = get_bloginfo( 'description', 'display' ); 251 return $x; 252 } 253 254 /** 255 * Test WP_Customize_Selective_Refresh::handle_render_partials_request() for a partial that does render. 256 * 257 * @see WP_Customize_Selective_Refresh::handle_render_partials_request() 258 */ 259 function test_handle_render_partials_request_with_single_valid_placement() { 260 $this->setup_valid_render_partials_request_environment(); 261 262 $this->wp_customize->selective_refresh->add_partial( 'test_blogname', array( 263 'settings' => array( 'blogname' ), 264 'render_callback' => array( $this, 'render_callback_blogname' ), 265 ) ); 266 267 $context_data = array(); 268 $placements = array( $context_data ); 269 270 $_POST['partials'] = wp_slash( wp_json_encode( array( 271 'test_blogname' => $placements, 272 ) ) ); 273 274 $count_customize_render_partials_before = has_action( 'customize_render_partials_before' ); 275 $count_customize_render_partials_after = has_action( 'customize_render_partials_after' ); 276 ob_start(); 277 try { 278 $this->expected_partial_ids = array( 'test_blogname' ); 279 add_filter( 'customize_render_partials_response', array( $this, 'filter_customize_render_partials_response' ), 10, 3 ); 280 add_action( 'customize_render_partials_before', array( $this, 'handle_action_customize_render_partials_before' ), 10, 2 ); 281 add_action( 'customize_render_partials_after', array( $this, 'handle_action_customize_render_partials_after' ), 10, 2 ); 282 $this->selective_refresh->handle_render_partials_request(); 283 } catch ( WPDieException $e ) { 284 $this->assertEquals( '', $e->getMessage() ); 285 } 286 $this->assertEquals( $count_customize_render_partials_before + 1, has_action( 'customize_render_partials_before' ) ); 287 &n