Ticket #27355: 27355.diff
File 27355.diff, 128.7 KB (added by , 9 years ago) |
---|
-
src/wp-admin/js/customize-controls.js
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
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
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
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
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 … … 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. … … 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' ) ); … … 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
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 /** … … 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, … … 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; … … 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 { … … 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 } … … 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 ); … … 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. … … 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( … … 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 ) { 826 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 } 844 827 845 /** 828 846 * Add hooks for the Customizer preview. 829 847 * … … 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 /** … … 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 && … … 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 888 873 $hashed_args = $args;874 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 ); 887 905 888 $this->preview_nav_menu_instance_args[ $this->preview_nav_menu_instance_number ] = $hashed_args; 906 $args['customize_preview_nav_menus_args'] = $exported_args; 907 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. 894 913 * 914 * Injects attributes into container element. 915 * 895 916 * @since 4.3.0 896 917 * @access public 897 918 * … … 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 they918 * are not tampered with whensubmitted 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. 919 938 * 939 * Note that the array is expected to be pre-sorted. 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 /** … … 934 955 * @access public 935 956 */ 936 957 public function customize_preview_enqueue_deps() { 937 wp_enqueue_script( 'customize-preview-nav-menus' ); 958 if ( isset( $this->manager->selective_refresh ) ) { 959 $script = wp_scripts()->registered['customize-preview-nav-menus']; 960 $script->deps[] = 'customize-selective-refresh'; 961 } 962 963 wp_enqueue_script( 'customize-preview-nav-menus' ); // Note that we have overridden this. 938 964 wp_enqueue_style( 'customize-preview' ); 939 940 add_action( 'wp_print_footer_scripts', array( $this, 'export_preview_data' ) );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 /** … … 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 } 990 public function render_nav_menu_partial( $partial, $nav_menu_args ) { 991 unset( $partial ); 977 992 978 $this->manager->remove_preview_signature(); 979 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 }1007 ob_start(); 1008 wp_nav_menu( $nav_menu_args ); 1009 $content = ob_get_clean(); 995 1010 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 } 1013 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
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 /** … … 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 ) { … … 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 … … 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'] ), … … 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 … … 1459 1466 wp_send_json_success( compact( 'form', 'instance' ) ); 1460 1467 } 1461 1468 1462 /* **************************************************************************1463 * Option Update Capturing1464 * **************************************************************************/1469 /* 1470 * Selective Refresh Methods 1471 */ 1465 1472 1466 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 // 1802 1803 /** 1467 1804 * List of captured widget option updates. 1468 1805 * 1469 1806 * @since 3.9.0 … … 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
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
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. -
src/wp-includes/customize/class-wp-customize-partial.php
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_array( $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. 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 } -
src/wp-includes/customize/class-wp-customize-selective-refresh.php
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_controls_print_footer_scripts', array( $this, 'enqueue_pane_scripts' ) ); 71 add_action( 'customize_preview_init', array( $this, 'init_preview' ) ); 72 } 73 74 /** 75 * Retrieves the registered partials. 76 * 77 * @since 4.5.0 78 * @access public 79 * 80 * @return array Partials. 81 */ 82 public function partials() { 83 return $this->partials; 84 } 85 86 /** 87 * Adds a partial. 88 * 89 * @since 4.5.0 90 * @access public 91 * 92 * @param WP_Customize_Partial|string $id Customize Partial object, or Panel ID. 93 * @param array $args Optional. Partial arguments. Default empty array. 94 * @return WP_Customize_Partial The instance of the panel that was added. 95 */ 96 public function add_partial( $id, $args = array() ) { 97 if ( $id instanceof WP_Customize_Partial ) { 98 $partial = $id; 99 } else { 100 $class = 'WP_Customize_Partial'; 101 102 /** This filter (will be) documented in wp-includes/class-wp-customize-manager.php */ 103 $args = apply_filters( 'customize_dynamic_partial_args', $args, $id ); 104 105 /** This filter (will be) documented in wp-includes/class-wp-customize-manager.php */ 106 $class = apply_filters( 'customize_dynamic_partial_class', $class, $id, $args ); 107 108 $partial = new $class( $this, $id, $args ); 109 } 110 111 $this->partials[ $partial->id ] = $partial; 112 return $partial; 113 } 114 115 /** 116 * Retrieves a partial. 117 * 118 * @since 4.5.0 119 * @access public 120 * 121 * @param string $id Customize Partial ID. 122 * @return WP_Customize_Partial|null The partial, if set. Otherwise null. 123 */ 124 public function get_partial( $id ) { 125 if ( isset( $this->partials[ $id ] ) ) { 126 return $this->partials[ $id ]; 127 } else { 128 return null; 129 } 130 } 131 132 /** 133 * Removes a partial. 134 * 135 * @since 4.5.0 136 * @access public 137 * 138 * @param string $id Customize Partial ID. 139 */ 140 public function remove_partial( $id ) { 141 unset( $this->partials[ $id ] ); 142 } 143 144 /** 145 * Initializes the Customizer preview. 146 * 147 * @since 4.5.0 148 * @access public 149 */ 150 public function init_preview() { 151 add_action( 'template_redirect', array( $this, 'handle_render_partials_request' ) ); 152 add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_preview_scripts' ) ); 153 } 154 155 /** 156 * Enqueues pane scripts. 157 * 158 * @since 4.5.0 159 * @access public 160 */ 161 public function enqueue_pane_scripts() { 162 wp_enqueue_script( 'customize-controls-hacks' ); 163 } 164 165 /** 166 * Enqueues preview scripts. 167 * 168 * @since 4.5.0 169 * @access public 170 */ 171 public function enqueue_preview_scripts() { 172 wp_enqueue_script( 'customize-selective-refresh' ); 173 add_action( 'wp_footer', array( $this, 'export_preview_data' ), 1000 ); 174 } 175 176 /** 177 * Exports data in preview after it has finished rendering so that partials can be added at runtime. 178 * 179 * @since 4.5.0 180 * @access public 181 */ 182 public function export_preview_data() { 183 $partials = array(); 184 185 foreach ( $this->partials() as $partial ) { 186 $partials[ $partial->id ] = $partial->json(); 187 } 188 189 $exports = array( 190 'partials' => $partials, 191 'renderQueryVar' => self::RENDER_QUERY_VAR, 192 'l10n' => array( 193 'shiftClickToEdit' => __( 'Shift-click to edit this element.' ), 194 ), 195 ); 196 197 // Export data to JS. 198 echo sprintf( '<script>var _customizePartialRefreshExports = %s;</script>', wp_json_encode( $exports ) ); 199 } 200 201 /** 202 * Registers dynamically-created partials. 203 * 204 * @since 4.5.0 205 * @access public 206 * 207 * @see WP_Customize_Manager::add_dynamic_settings() 208 * 209 * @param array $partial_ids The partial ID to add. 210 * @return array Added WP_Customize_Partial instances. 211 */ 212 public function add_dynamic_partials( $partial_ids ) { 213 $new_partials = array(); 214 215 foreach ( $partial_ids as $partial_id ) { 216 217 // Skip partials already created. 218 $partial = $this->get_partial( $partial_id ); 219 if ( $partial ) { 220 continue; 221 } 222 223 $partial_args = false; 224 $partial_class = 'WP_Customize_Partial'; 225 226 /** 227 * Filters a dynamic partial's constructor arguments. 228 * 229 * For a dynamic partial to be registered, this filter must be employed 230 * to override the default false value with an array of args to pass to 231 * the WP_Customize_Partial constructor. 232 * 233 * @since 4.5.0 234 * 235 * @param false|array $partial_args The arguments to the WP_Customize_Partial constructor. 236 * @param string $partial_id ID for dynamic partial. 237 */ 238 $partial_args = apply_filters( 'customize_dynamic_partial_args', $partial_args, $partial_id ); 239 if ( false === $partial_args ) { 240 continue; 241 } 242 243 /** 244 * Filters the class used to construct partials. 245 * 246 * Allow non-statically created partials to be constructed with custom WP_Customize_Partial subclass. 247 * 248 * @since 4.5.0 249 * 250 * @param string $partial_class WP_Customize_Partial or a subclass. 251 * @param string $partial_id ID for dynamic partial. 252 * @param array $partial_args The arguments to the WP_Customize_Partial constructor. 253 */ 254 $partial_class = apply_filters( 'customize_dynamic_partial_class', $partial_class, $partial_id, $partial_args ); 255 256 $partial = new $partial_class( $this, $partial_id, $partial_args ); 257 258 $this->add_partial( $partial ); 259 $new_partials[] = $partial; 260 } 261 return $new_partials; 262 } 263 264 /** 265 * Checks whether the request is for rendering partials. 266 * 267 * Note that this will not consider whether the request is authorized or valid, 268 * just that essentially the route is a match. 269 * 270 * @since 4.5.0 271 * @access public 272 * 273 * @return bool Whether the request is for rendering partials. 274 */ 275 public function is_render_partials_request() { 276 return ! empty( $_POST[ self::RENDER_QUERY_VAR ] ); 277 } 278 279 /** 280 * Handles PHP errors triggered during rendering the partials. 281 * 282 * These errors will be relayed back to the client in the Ajax response. 283 * 284 * @since 4.5.0 285 * @access private 286 * 287 * @param int $errno Error number. 288 * @param string $errstr Error string. 289 * @param string $errfile Error file. 290 * @param string $errline Error line. 291 * @return true Always true. 292 */ 293 public function handle_error( $errno, $errstr, $errfile = null, $errline = null ) { 294 $this->triggered_errors[] = array( 295 'partial' => $this->current_partial_id, 296 'error_number' => $errno, 297 'error_string' => $errstr, 298 'error_file' => $errfile, 299 'error_line' => $errline, 300 ); 301 return true; 302 } 303 304 /** 305 * Handles the Ajax request to return the rendered partials for the requested placements. 306 * 307 * @since 4.5.0 308 * @access public 309 */ 310 public function handle_render_partials_request() { 311 if ( ! $this->is_render_partials_request() ) { 312 return; 313 } 314 315 $this->manager->remove_preview_signature(); 316 317 /* 318 * Note that is_customize_preview() returning true will entail that the 319 * user passed the 'customize' capability check and the nonce check, since 320 * WP_Customize_Manager::setup_theme() is where the previewing flag is set. 321 */ 322 if ( ! is_customize_preview() ) { 323 status_header( 403 ); 324 wp_send_json_error( 'expected_customize_preview' ); 325 } else if ( ! isset( $_POST['partials'] ) ) { 326 status_header( 400 ); 327 wp_send_json_error( 'missing_partials' ); 328 } 329 330 $partials = json_decode( wp_unslash( $_POST['partials'] ), true ); 331 332 if ( ! is_array( $partials ) ) { 333 wp_send_json_error( 'malformed_partials' ); 334 } 335 336 $this->add_dynamic_partials( array_keys( $partials ) ); 337 338 /** 339 * Fires immediately before partials are rendered. 340 * 341 * Plugins may do things like call wp_enqueue_scripts() and gather a list of the scripts 342 * and styles which may get enqueued in the response. 343 * 344 * @since 4.5.0 345 * 346 * @param WP_Customize_Selective_Refresh $this Selective refresh component. 347 * @param array $partials IDs for the partials to render in the request. 348 */ 349 do_action( 'customize_render_partials_before', $this, $partials ); 350 351 set_error_handler( array( $this, 'handle_error' ), error_reporting() ); 352 353 $contents = array(); 354 355 foreach ( $partials as $partial_id => $container_contexts ) { 356 $this->current_partial_id = $partial_id; 357 358 if ( ! is_array( $container_contexts ) ) { 359 wp_send_json_error( 'malformed_container_contexts' ); 360 } 361 362 $partial = $this->get_partial( $partial_id ); 363 364 if ( ! $partial ) { 365 $contents[ $partial_id ] = null; 366 continue; 367 } 368 369 $contents[ $partial_id ] = array(); 370 371 // @todo The array should include not only the contents, but also whether the container is included? 372 if ( empty( $container_contexts ) ) { 373 // Since there are no container contexts, render just once. 374 $contents[ $partial_id ][] = $partial->render( null ); 375 } else { 376 foreach ( $container_contexts as $container_context ) { 377 $contents[ $partial_id ][] = $partial->render( $container_context ); 378 } 379 } 380 } 381 $this->current_partial_id = null; 382 383 restore_error_handler(); 384 385 /** 386 * Fires immediately after partials are rendered. 387 * 388 * Plugins may do things like call wp_footer() to scrape scripts output and return them 389 * via the {@see 'customize_render_partials_response'} filter. 390 * 391 * @since 4.5.0 392 * 393 * @param WP_Customize_Selective_Refresh $this Selective refresh component. 394 * @param array $partials IDs for the partials to rendered in the request. 395 */ 396 do_action( 'customize_render_partials_after', $this, $partials ); 397 398 $response = array( 399 'contents' => $contents, 400 ); 401 402 if ( defined( 'WP_DEBUG_DISPLAY' ) && WP_DEBUG_DISPLAY ) { 403 $response['errors'] = $this->triggered_errors; 404 } 405 406 /** 407 * Filters the response from rendering the partials. 408 * 409 * Plugins may use this filter to inject `$scripts` and `$styles`, which are dependencies 410 * for the partials being rendered. The response data will be available to the client via 411 * the `render-partials-response` JS event, so the client can then inject the scripts and 412 * styles into the DOM if they have not already been enqueued there. 413 * 414 * If plugins do this, they'll need to take care for any scripts that do `document.write()` 415 * and make sure that these are not injected, or else to override the function to no-op, 416 * or else the page will be destroyed. 417 * 418 * Plugins should be aware that `$scripts` and `$styles` may eventually be included by 419 * default in the response. 420 * 421 * @since 4.5.0 422 * 423 * @param array $response { 424 * Response. 425 * 426 * @type array $contents Associative array mapping a partial ID its corresponding array of contents 427 * for the containers requested. 428 * @type array $errors List of errors triggered during rendering of partials, if `WP_DEBUG_DISPLAY` 429 * is enabled. 430 * } 431 * @param WP_Customize_Selective_Refresh $this Selective refresh component. 432 * @param array $partials IDs for the partials to rendered in the request. 433 */ 434 $response = apply_filters( 'customize_render_partials_response', $response, $this, $partials ); 435 436 wp_send_json_success( $response ); 437 } 438 } -
src/wp-includes/js/customize-preview-nav-menus.js
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; }4 var self = {}; 7 5 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 }; 6 /** 7 * Initialize nav menus preview. 8 */ 9 self.init = function() { 10 var self = this; 20 11 21 api.MenusCustomizerPreview = { 22 /** 23 * Bootstrap functionality. 24 */ 25 init : function() { 26 var self = this, initializedSettings = {}; 12 if ( api.selectiveRefresh ) { 13 self.watchNavMenuLocationChanges(); 14 } 27 15 28 settings = _.extend( {}, defaultSettings ); 29 if ( 'undefined' !== typeof _wpCustomizePreviewNavMenusExports ) { 30 _.extend( settings, _wpCustomizePreviewNavMenusExports ); 31 } 32 33 api.each( function( setting, id ) { 34 setting.id = id; 35 initializedSettings[ setting.id ] = true; 36 self.bindListener( setting ); 37 } ); 38 39 api.preview.bind( 'setting', function( args ) { 40 var id, value, setting; 41 args = args.slice(); 42 id = args.shift(); 43 value = args.shift(); 44 45 setting = api( id ); 46 if ( ! setting ) { 47 // Currently customize-preview.js is not creating settings for dynamically-created settings in the pane, so we have to do it. 48 setting = api.create( id, value ); // @todo This should be in core 49 } 50 if ( ! setting.id ) { 51 // Currently customize-preview.js doesn't set the id property for each setting, like customize-controls.js does. 52 setting.id = id; 53 } 54 55 if ( ! initializedSettings[ setting.id ] ) { 56 initializedSettings[ setting.id ] = true; 57 if ( self.bindListener( setting ) ) { 58 setting.callbacks.fireWith( setting, [ setting(), null ] ); 59 } 60 } 61 } ); 62 16 api.preview.bind( 'active', function() { 63 17 self.highlightControls(); 64 }, 18 } ); 19 }; 65 20 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; 21 if ( api.selectiveRefresh ) { 73 22 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 23 /** 101 * Handle changing of a nav_menu setting.24 * Partial representing an invocation of wp_nav_menu(). 102 25 * 103 * @this {wp.customize.Setting} 26 * @class 27 * @augments wp.customize.selectiveRefresh.Partial 28 * @since 4.5.0 104 29 */ 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 }, 30 self.NavMenuInstancePartial = api.selectiveRefresh.Partial.extend({ 112 31 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 }, 32 /** 33 * Constructor. 34 * 35 * @since 4.5.0 36 * @param {string} id - Partial ID. 37 * @param {Object} options 38 * @param {Object} options.params 39 * @param {Object} options.params.navMenuArgs 40 * @param {string} options.params.navMenuArgs.args_hmac 41 * @param {string} [options.params.navMenuArgs.theme_location] 42 * @param {number} [options.params.navMenuArgs.menu] 43 * @param {object} [options.constructingContainerContext] 44 */ 45 initialize: function( id, options ) { 46 var partial = this, matches, argsHmac; 47 matches = id.match( /^nav_menu_instance\[([0-9a-f]{32})]$/ ); 48 if ( ! matches ) { 49 throw new Error( 'Illegal id for nav_menu_instance partial. The key corresponds with the args HMAC.' ); 50 } 51 argsHmac = matches[1]; 128 52 129 /** 130 * Update a given menu rendered in the preview. 131 * 132 * @param {int} menuId 133 */ 134 refreshMenu : function( menuId ) { 135 var assignedLocations = []; 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 ); 136 63 137 api.each(function( setting, id ) { 138 var matches = id.match( /^nav_menu_locations\[(.+?)]/ ); 139 if ( matches && menuId === setting() ) { 140 assignedLocations.push( matches[1] ); 64 if ( ! _.isObject( partial.params.navMenuArgs ) ) { 65 throw new Error( 'Missing navMenuArgs' ); 141 66 } 142 }); 67 if ( partial.params.navMenuArgs.args_hmac !== argsHmac ) { 68 throw new Error( 'args_hmac mismatch with id' ); 69 } 70 }, 143 71 144 _.each( settings.navMenuInstanceArgs, function( navMenuArgs, instanceNumber ) { 145 if ( menuId === navMenuArgs.menu || -1 !== _.indexOf( assignedLocations, navMenuArgs.theme_location ) ) { 146 this.refreshMenuInstanceDebounced( instanceNumber ); 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 ); 147 85 } 148 }, this );149 },150 86 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; 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 + ']' ); 162 92 } 163 }, this );164 if ( ! foundInstance ) {165 api.preview.send( 'refresh' );166 }167 },168 93 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; 94 navMenuId = partial.params.navMenuArgs.menu; 95 if ( ! navMenuId && navMenuLocationSetting ) { 96 navMenuId = navMenuLocationSetting(); 97 } 176 98 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; 99 if ( ! navMenuId ) { 100 return false; 223 101 } 224 } ); 225 data.customized = JSON.stringify( customized ); 226 data[ settings.renderNoncePostKey ] = settings.renderNonceValue; 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 }, 227 109 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 ); 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 ) ) { 232 119 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; 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 } ] ); 244 128 } 129 } 130 }); 245 131 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 }, 132 api.selectiveRefresh.partialConstructor.nav_menu_instance = self.NavMenuInstancePartial; 265 133 266 refreshMenuInstanceDebounced : function( instanceNumber ) {267 if ( currentRefreshDebounced[ instanceNumber ] ) {268 clearTimeout( currentRefreshDebounced[ instanceNumber ] );269 }270 currentRefreshDebounced[ instanceNumber ] = setTimeout(271 _.bind( function() {272 this.refreshMenuInstance( instanceNumber );273 }, this ),274 refreshDebounceDelay275 );276 },277 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 }); 160 } ); 161 }; 162 } 299 163 300 addTooltips = function( e, params ) { 301 params.newContainer.find( selector ).attr( 'title', settings.l10n.editNavMenuItemTooltip ); 302 }; 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'; 303 174 304 addTooltips( null, { newContainer: $( document.body ) } ); 305 $( document ).on( 'customize-preview-menu-refreshed', addTooltips ); 306 } 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 } 181 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
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 ); 29 35 36 api.preview.bind( 'active', function() { 37 self.highlightControls(); 38 } ); 39 }; 40 41 if ( api.selectiveRefresh ) { 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; 50 self.WidgetPartial = api.selectiveRefresh.Partial.extend({ 35 51 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; 52 /** 53 * Constructor. 54 * 55 * @since 4.5.0 56 * @param {string} id - Partial ID. 57 * @param {Object} options 58 * @param {Object} options.params 59 */ 60 initialize: function( id, options ) { 61 var partial = this, matches; 62 matches = id.match( /^widget\[(.+)]$/ ); 63 if ( ! matches ) { 64 throw new Error( 'Illegal id for widget partial.' ); 65 } 46 66 47 emptyWidget = $(widgetTpl); 48 widgetSelector = emptyWidget.prop('tagName'); 49 widgetClasses = emptyWidget.prop('className'); 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 ); 50 78 51 // Prevent a rare case when before_widget, before_title, after_title and after_widget is empty. 52 if ( ! widgetClasses ) { 79 api.selectiveRefresh.Partial.prototype.initialize.call( partial, id, options ); 80 }, 81 82 /** 83 * Send widget-updated message to parent so spinner will get removed from widget control. 84 * 85 * @inheritdoc 86 * @param {wp.customize.selectiveRefresh.Placement} placement 87 */ 88 renderContent: function( placement ) { 89 var partial = this; 90 if ( api.selectiveRefresh.Partial.prototype.renderContent.call( partial, placement ) ) { 91 api.preview.send( 'widget-updated', partial.widgetId ); 92 api.selectiveRefresh.trigger( 'widget-updated', partial ); 93 } 94 } 95 }); 96 97 /** 98 * Partial representing a widget area. 99 * 100 * @class 101 * @augments wp.customize.selectiveRefresh.Partial 102 * @since 4.5.0 103 */ 104 self.SidebarPartial = api.selectiveRefresh.Partial.extend({ 105 106 /** 107 * Constructor. 108 * 109 * @since 4.5.0 110 * @param {string} id - Partial ID. 111 * @param {Object} options 112 * @param {Object} options.params 113 */ 114 initialize: function( id, options ) { 115 var partial = this, matches; 116 matches = id.match( /^sidebar\[(.+)]$/ ); 117 if ( ! matches ) { 118 throw new Error( 'Illegal id for sidebar partial.' ); 119 } 120 partial.sidebarId = matches[1]; 121 122 options = options || {}; 123 options.params = _.extend( 124 { 125 settings: [ 'sidebars_widgets[' + partial.sidebarId + ']' ] 126 }, 127 options.params || {} 128 ); 129 130 api.selectiveRefresh.Partial.prototype.initialize.call( partial, id, options ); 131 132 if ( ! partial.params.sidebarArgs ) { 133 throw new Error( 'The sidebarArgs param was not provided.' ); 134 } 135 if ( partial.params.settings.length > 1 ) { 136 throw new Error( 'Expected SidebarPartial to only have one associated setting' ); 137 } 138 }, 139 140 /** 141 * Set up the partial. 142 * 143 * @since 4.5.0 144 */ 145 ready: function() { 146 var sidebarPartial = this; 147 148 // Watch for changes to the sidebar_widgets setting. 149 _.each( sidebarPartial.settings(), function( settingId ) { 150 api( settingId ).bind( _.bind( sidebarPartial.handleSettingChange, sidebarPartial ) ); 151 } ); 152 153 // Trigger an event for this sidebar being updated whenever a widget inside is rendered. 154 api.selectiveRefresh.bind( 'partial-content-rendered', function( placement ) { 155 var isAssignedWidgetPartial = ( 156 placement.partial.extended( self.WidgetPartial ) && 157 ( -1 !== _.indexOf( sidebarPartial.getWidgetIds(), placement.partial.widgetId ) ) 158 ); 159 if ( isAssignedWidgetPartial ) { 160 api.selectiveRefresh.trigger( 'sidebar-updated', sidebarPartial ); 161 } 162 } ); 163 164 // Make sure that a widget partial has a container in the DOM prior to a refresh. 165 api.bind( 'change', function( widgetSetting ) { 166 var widgetId, parsedId; 167 parsedId = self.parseWidgetSettingId( widgetSetting.id ); 168 if ( ! parsedId ) { 169 return; 170 } 171 widgetId = parsedId.idBase; 172 if ( parsedId.number ) { 173 widgetId += '-' + String( parsedId.number ); 174 } 175 if ( -1 !== _.indexOf( sidebarPartial.getWidgetIds(), widgetId ) ) { 176 sidebarPartial.ensureWidgetPlacementContainers( widgetId ); 177 } 178 } ); 179 }, 180 181 /** 182 * Get the before/after boundary nodes for all instances of this sidebar (usually one). 183 * 184 * Note that TreeWalker is not implemented in IE8. 185 * 186 * @since 4.5.0 187 * @returns {Array.<{before: Comment, after: Comment, instanceNumber: number}>} 188 */ 189 findDynamicSidebarBoundaryNodes: function() { 190 var partial = this, regExp, boundaryNodes = {}, recursiveCommentTraversal; 191 regExp = /^(dynamic_sidebar_before|dynamic_sidebar_after):(.+):(\d+)$/; 192 recursiveCommentTraversal = function( childNodes ) { 193 _.each( childNodes, function( node ) { 194 var matches; 195 if ( 8 === node.nodeType ) { 196 matches = node.nodeValue.match( regExp ); 197 if ( ! matches || matches[2] !== partial.sidebarId ) { 198 return; 199 } 200 if ( _.isUndefined( boundaryNodes[ matches[3] ] ) ) { 201 boundaryNodes[ matches[3] ] = { 202 before: null, 203 after: null, 204 instanceNumber: parseInt( matches[3], 10 ) 205 }; 206 } 207 if ( 'dynamic_sidebar_before' === matches[1] ) { 208 boundaryNodes[ matches[3] ].before = node; 209 } else { 210 boundaryNodes[ matches[3] ].after = node; 211 } 212 } else if ( 1 === node.nodeType ) { 213 recursiveCommentTraversal( node.childNodes ); 214 } 215 } ); 216 }; 217 218 recursiveCommentTraversal( document.body.childNodes ); 219 return _.values( boundaryNodes ); 220 }, 221 222 /** 223 * Get the placements for this partial. 224 * 225 * @since 4.5.0 226 * @returns {Array} 227 */ 228 placements: function() { 229 var partial = this; 230 return _.map( partial.findDynamicSidebarBoundaryNodes(), function( boundaryNodes ) { 231 return new api.selectiveRefresh.Placement( { 232 partial: partial, 233 container: null, 234 startNode: boundaryNodes.before, 235 endNode: boundaryNodes.after, 236 context: { 237 instanceNumber: boundaryNodes.instanceNumber 238 } 239 } ); 240 } ); 241 }, 242 243 /** 244 * Get the list of widget IDs associated with this widget area. 245 * 246 * @since 4.5.0 247 * 248 * @returns {Array} 249 */ 250 getWidgetIds: function() { 251 var sidebarPartial = this, settingId, widgetIds; 252 settingId = sidebarPartial.settings()[0]; 253 if ( ! settingId ) { 254 throw new Error( 'Missing associated setting.' ); 255 } 256 if ( ! api.has( settingId ) ) { 257 throw new Error( 'Setting does not exist.' ); 258 } 259 widgetIds = api( settingId ).get(); 260 if ( ! _.isArray( widgetIds ) ) { 261 throw new Error( 'Expected setting to be array of widget IDs' ); 262 } 263 return widgetIds.slice( 0 ); 264 }, 265 266 /** 267 * Reflow widgets in the sidebar, ensuring they have the proper position in the DOM. 268 * 269 * @since 4.5.0 270 * 271 * @return {Array.<wp.customize.selectiveRefresh.Placement>} List of placements that were reflowed. 272 */ 273 reflowWidgets: function() { 274 var sidebarPartial = this, sidebarPlacements, widgetIds, widgetPartials, sortedSidebarContainers = []; 275 widgetIds = sidebarPartial.getWidgetIds(); 276 sidebarPlacements = sidebarPartial.placements(); 277 278 widgetPartials = {}; 279 _.each( widgetIds, function( widgetId ) { 280 var widgetPartial = api.selectiveRefresh.partial( 'widget[' + widgetId + ']' ); 281 if ( widgetPartial ) { 282 widgetPartials[ widgetId ] = widgetPartial; 283 } 284 } ); 285 286 _.each( sidebarPlacements, function( sidebarPlacement ) { 287 var sidebarWidgets = [], needsSort = false, thisPosition, lastPosition = -1; 288 289 // Gather list of widget partial containers in this sidebar, and determine if a sort is needed. 290 _.each( widgetPartials, function( widgetPartial ) { 291 _.each( widgetPartial.placements(), function( widgetPlacement ) { 292 293 if ( sidebarPlacement.context.instanceNumber === widgetPlacement.context.sidebar_instance_number ) { 294 thisPosition = widgetPlacement.container.index(); 295 sidebarWidgets.push( { 296 partial: widgetPartial, 297 placement: widgetPlacement, 298 position: thisPosition 299 } ); 300 if ( thisPosition < lastPosition ) { 301 needsSort = true; 302 } 303 lastPosition = thisPosition; 304 } 305 } ); 306 } ); 307 308 if ( needsSort ) { 309 _.each( sidebarWidgets, function( sidebarWidget ) { 310 sidebarPlacement.endNode.parentNode.insertBefore( 311 sidebarWidget.placement.container[0], 312 sidebarPlacement.endNode 313 ); 314 315 // @todo Rename partial-placement-moved? 316 api.selectiveRefresh.trigger( 'partial-content-moved', sidebarWidget.placement ); 317 } ); 318 319 sortedSidebarContainers.push( sidebarPlacement ); 320 } 321 } ); 322 323 if ( sortedSidebarContainers.length > 0 ) { 324 api.selectiveRefresh.trigger( 'sidebar-updated', sidebarPartial ); 325 } 326 327 return sortedSidebarContainers; 328 }, 329 330 /** 331 * Make sure there is a widget instance container in this sidebar for the given widget ID. 332 * 333 * @since 4.5.0 334 * 335 * @param {string} widgetId 336 * @returns {wp.customize.selectiveRefresh.Partial} Widget instance partial. 337 */ 338 ensureWidgetPlacementContainers: function( widgetId ) { 339 var sidebarPartial = this, widgetPartial, wasInserted = false, partialId = 'widget[' + widgetId + ']'; 340 widgetPartial = api.selectiveRefresh.partial( partialId ); 341 if ( ! widgetPartial ) { 342 widgetPartial = new self.WidgetPartial( partialId, { 343 params: {} 344 } ); 345 api.selectiveRefresh.partial.add( widgetPartial.id, widgetPartial ); 346 } 347 348 // Make sure that there is a container element for the widget in the sidebar, if at least a placeholder. 349 _.each( sidebarPartial.placements(), function( sidebarPlacement ) { 350 var foundWidgetPlacement, widgetContainerElement; 351 352 foundWidgetPlacement = _.find( widgetPartial.placements(), function( widgetPlacement ) { 353 return ( widgetPlacement.context.sidebar_instance_number === sidebarPlacement.context.instanceNumber ); 354 } ); 355 if ( foundWidgetPlacement ) { 356 return; 357 } 358 359 widgetContainerElement = $( 360 sidebarPartial.params.sidebarArgs.before_widget.replace( '%1$s', widgetId ).replace( '%2$s', 'widget' ) + 361 sidebarPartial.params.sidebarArgs.after_widget 362 ); 363 364 widgetContainerElement.attr( 'data-customize-partial-id', widgetPartial.id ); 365 widgetContainerElement.attr( 'data-customize-partial-type', 'widget' ); 366 widgetContainerElement.attr( 'data-customize-widget-id', widgetId ); 367 368 /* 369 * Make sure the widget container element has the customize-container context data. 370 * The sidebar_instance_number is used to disambiguate multiple instances of the 371 * same sidebar are rendered onto the template, and so the same widget is embedded 372 * multiple times. 373 */ 374 widgetContainerElement.data( 'customize-partial-placement-context', { 375 'sidebar_id': sidebarPartial.sidebarId, 376 'sidebar_instance_number': sidebarPlacement.context.instanceNumber 377 } ); 378 379 sidebarPlacement.endNode.parentNode.insertBefore( widgetContainerElement[0], sidebarPlacement.endNode ); 380 wasInserted = true; 381 } ); 382 383 if ( wasInserted ) { 384 sidebarPartial.reflowWidgets(); 385 } 386 387 return widgetPartial; 388 }, 389 390 /** 391 * Handle change to the sidebars_widgets[] setting. 392 * 393 * @since 4.5.0 394 * 395 * @param {Array} newWidgetIds New widget ids. 396 * @param {Array} oldWidgetIds Old widget ids. 397 */ 398 handleSettingChange: function( newWidgetIds, oldWidgetIds ) { 399 var sidebarPartial = this, needsRefresh, widgetsRemoved, widgetsAdded, addedWidgetPartials = []; 400 401 needsRefresh = ( 402 ( oldWidgetIds.length > 0 && 0 === newWidgetIds.length ) || 403 ( newWidgetIds.length > 0 && 0 === oldWidgetIds.length ) 404 ); 405 if ( needsRefresh ) { 406 sidebarPartial.fallback(); 53 407 return; 54 408 } 55 409 56 widgetClasses = widgetClasses.replace(/^\s+|\s+$/g, ''); 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 } ); 57 426 58 if ( widgetClasses ) { 59 widgetSelector += '.' + widgetClasses.split(/\s+/).join('.'); 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(); 60 460 } 61 self.widgetSelectors.push(widgetSelector);62 });63 },64 461 462 return deferred.promise(); 463 } 464 }); 465 466 api.selectiveRefresh.partialConstructor.sidebar = self.SidebarPartial; 467 api.selectiveRefresh.partialConstructor.widget = self.WidgetPartial; 468 65 469 /** 66 * Highlight the widget on widget updates or widget control mouse overs.470 * Add partials for the registered widget areas (sidebars). 67 471 * 68 * @ param {string} widgetId ID of the widget.472 * @since 4.5.0 69 473 */ 70 highlightWidget: function( widgetId ) { 71 var $body = $( document.body ), 72 $widget = $( '#' + widgetId ); 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 }; 73 488 74 $body.find( '.widget-customizer-highlighted-widget' ).removeClass( 'widget-customizer-highlighted-widget' );489 } 75 490 76 $widget.addClass( 'widget-customizer-highlighted-widget' ); 77 setTimeout( function () { 78 $widget.removeClass( 'widget-customizer-highlighted-widget' ); 79 }, 500 ); 80 }, 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; 81 498 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(','); 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; 89 509 90 $(selector).attr( 'title', this.l10n.widgetTooltip ); 510 emptyWidget = $( widgetTpl ); 511 widgetSelector = emptyWidget.prop( 'tagName' ); 512 widgetClasses = emptyWidget.prop( 'className' ); 91 513 92 $(document).on( 'mouseenter', selector, function () { 93 self.preview.send( 'highlight-widget-control', $( this ).prop( 'id' ) ); 94 }); 514 // Prevent a rare case when before_widget, before_title, after_title and after_widget is empty. 515 if ( ! widgetClasses ) { 516 return; 517 } 95 518 96 // Open expand the widget control when shift+clicking the widget element 97 $(document).on( 'click', selector, function ( e ) { 98 if ( ! e.shiftKey ) { 99 return; 100 } 101 e.preventDefault(); 519 widgetClasses = widgetClasses.replace( /^\s+|\s+$/g, '' ); 102 520 103 self.preview.send( 'focus-widget-control', $( this ).prop( 'id' ) ); 104 }); 521 if ( widgetClasses ) { 522 widgetSelector += '.' + widgetClasses.split( /\s+/ ).join( '.' ); 523 } 524 self.widgetSelectors.push( widgetSelector ); 525 }); 526 }; 527 528 /** 529 * Highlight the widget on widget updates or widget control mouse overs. 530 * 531 * @since 3.9.0 532 * @param {string} widgetId ID of the widget. 533 */ 534 self.highlightWidget = function( widgetId ) { 535 var $body = $( document.body ), 536 $widget = $( '#' + widgetId ); 537 538 $body.find( '.widget-customizer-highlighted-widget' ).removeClass( 'widget-customizer-highlighted-widget' ); 539 540 $widget.addClass( 'widget-customizer-highlighted-widget' ); 541 setTimeout( function() { 542 $widget.removeClass( 'widget-customizer-highlighted-widget' ); 543 }, 500 ); 544 }; 545 546 /** 547 * Show a title and highlight widgets on hover. On shift+clicking 548 * focus the widget control. 549 * 550 * @since 3.9.0 551 */ 552 self.highlightControls = function() { 553 var self = this, 554 selector = this.widgetSelectors.join( ',' ); 555 556 $( selector ).attr( 'title', this.l10n.widgetTooltip ); 557 558 $( document ).on( 'mouseenter', selector, function() { 559 self.preview.send( 'highlight-widget-control', $( this ).prop( 'id' ) ); 560 }); 561 562 // Open expand the widget control when shift+clicking the widget element 563 $( document ).on( 'click', selector, function( e ) { 564 if ( ! e.shiftKey ) { 565 return; 566 } 567 e.preventDefault(); 568 569 self.preview.send( 'focus-widget-control', $( this ).prop( 'id' ) ); 570 }); 571 }; 572 573 /** 574 * Parse a widget ID. 575 * 576 * @since 4.5.0 577 * 578 * @param {string} widgetId Widget ID. 579 * @returns {{idBase: string, number: number|null}} 580 */ 581 self.parseWidgetId = function( widgetId ) { 582 var matches, parsed = { 583 idBase: '', 584 number: null 585 }; 586 587 matches = widgetId.match( /^(.+)-(\d+)$/ ); 588 if ( matches ) { 589 parsed.idBase = matches[1]; 590 parsed.number = parseInt( matches[2], 10 ); 591 } else { 592 parsed.idBase = widgetId; // Likely an old single widget. 105 593 } 594 595 return parsed; 106 596 }; 107 597 108 $(function () { 109 var settings = window._wpWidgetCustomizerPreviewSettings; 110 if ( ! settings ) { 111 return; 598 /** 599 * Parse a widget setting ID. 600 * 601 * @since 4.5.0 602 * 603 * @param {string} settingId Widget setting ID. 604 * @returns {{idBase: string, number: number|null}|null} 605 */ 606 self.parseWidgetSettingId = function( settingId ) { 607 var matches, parsed = { 608 idBase: '', 609 number: null 610 }; 611 612 matches = settingId.match( /^widget_([^\[]+?)(?:\[(\d+)])?$/ ); 613 if ( ! matches ) { 614 return null; 112 615 } 616 parsed.idBase = matches[1]; 617 if ( matches[2] ) { 618 parsed.number = parseInt( matches[2], 10 ); 619 } 620 return parsed; 621 }; 113 622 114 $.extend( api.WidgetCustomizerPreview, settings ); 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; 115 633 116 api.WidgetCustomizerPreview.init(); 634 settingId = 'widget_' + parsed.idBase; 635 if ( parsed.number ) { 636 settingId += '[' + String( parsed.number ) + ']'; 637 } 638 639 return settingId; 640 }; 641 642 api.bind( 'preview-ready', function() { 643 $.extend( self, _wpWidgetCustomizerPreviewSettings ); 644 self.init(); 117 645 }); 118 646 119 })( window.wp, jQuery ); 647 return self; 648 })( jQuery, _, wp, wp.customize ); -
src/wp-includes/js/customize-selective-refresh.js
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
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
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
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
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 /** … … 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 /** … … 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 /** … … 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 /** … … 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 608 } 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 }652 609 } -
tests/phpunit/tests/customize/widgets.php
121 121 $default_args = array( 122 122 'type' => 'option', 123 123 'capability' => 'edit_theme_options', 124 'transport' => ' refresh',124 'transport' => 'postMessage', 125 125 'default' => array(), 126 126 'sanitize_callback' => array( $this->manager->widgets, 'sanitize_widget_instance' ), 127 127 'sanitize_js_callback' => array( $this->manager->widgets, 'sanitize_widget_js_instance' ), … … 150 150 $default_args = array( 151 151 'type' => 'option', 152 152 'capability' => 'edit_theme_options', 153 'transport' => ' refresh',153 'transport' => 'postMessage', 154 154 'default' => array(), 155 155 'sanitize_callback' => array( $this->manager->widgets, 'sanitize_sidebar_widgets' ), 156 156 'sanitize_js_callback' => array( $this->manager->widgets, 'sanitize_sidebar_widgets_js_instance' ), -
tests/qunit/fixtures/customize-menus.js
2 2 window._wpCustomizeNavMenusSettings = { 3 3 'nonce': 'yo', 4 4 'phpIntMax': '2147483647', 5 ' menuItemTransport': 'postMessage',5 'settingTransport': 'postMessage', 6 6 'allMenus': [{ 7 7 'term_id': '2', 8 8 'name': 'Social Menu',