3 * WordPress Customize Setting classes
6 * @subpackage Customize
11 * Customize Setting class.
13 * Handles saving and sanitizing of settings.
17 * @see WP_Customize_Manager
19 class WP_Customize_Setting {
22 * @var WP_Customize_Manager
36 public $type = 'theme_mod';
39 * Capability required to edit this setting.
43 public $capability = 'edit_theme_options';
46 * Feature a theme is required to support to enable this setting.
51 public $theme_supports = '';
53 public $transport = 'refresh';
56 * Server-side sanitization callback for the setting's value.
60 public $sanitize_callback = '';
61 public $sanitize_js_callback = '';
64 * Whether or not the setting is initially dirty when created.
66 * This is used to ensure that a setting will be sent from the pane to the
67 * preview when loading the Customizer. Normally a setting only is synced to
68 * the preview if it has been changed. This allows the setting to be sent
75 public $dirty = false;
77 protected $id_data = array();
82 * Any supplied $args override class property defaults.
86 * @param WP_Customize_Manager $manager
87 * @param string $id An specific ID of the setting. Can be a
88 * theme mod or option name.
89 * @param array $args Setting arguments.
91 public function __construct( $manager, $id, $args = array() ) {
92 $keys = array_keys( get_object_vars( $this ) );
93 foreach ( $keys as $key ) {
94 if ( isset( $args[ $key ] ) )
95 $this->$key = $args[ $key ];
98 $this->manager = $manager;
101 // Parse the ID for array keys.
102 $this->id_data[ 'keys' ] = preg_split( '/\[/', str_replace( ']', '', $this->id ) );
103 $this->id_data[ 'base' ] = array_shift( $this->id_data[ 'keys' ] );
106 $this->id = $this->id_data[ 'base' ];
107 if ( ! empty( $this->id_data[ 'keys' ] ) )
108 $this->id .= '[' . implode( '][', $this->id_data[ 'keys' ] ) . ']';
110 if ( $this->sanitize_callback )
111 add_filter( "customize_sanitize_{$this->id}", $this->sanitize_callback, 10, 2 );
113 if ( $this->sanitize_js_callback )
114 add_filter( "customize_sanitize_js_{$this->id}", $this->sanitize_js_callback, 10, 2 );
118 * The ID for the current blog when the preview() method was called.
124 protected $_previewed_blog_id;
127 * Return true if the current blog is not the same as the previewed blog.
132 * @return bool If preview() has been called.
134 public function is_current_blog_previewed() {
135 if ( ! isset( $this->_previewed_blog_id ) ) {
138 return ( get_current_blog_id() === $this->_previewed_blog_id );
142 * Original non-previewed value stored by the preview method.
144 * @see WP_Customize_Setting::preview()
148 protected $_original_value;
151 * Handle previewing the setting.
155 public function preview() {
156 if ( ! isset( $this->_original_value ) ) {
157 $this->_original_value = $this->value();
159 if ( ! isset( $this->_previewed_blog_id ) ) {
160 $this->_previewed_blog_id = get_current_blog_id();
163 switch( $this->type ) {
165 add_filter( 'theme_mod_' . $this->id_data[ 'base' ], array( $this, '_preview_filter' ) );
168 if ( empty( $this->id_data[ 'keys' ] ) )
169 add_filter( 'pre_option_' . $this->id_data[ 'base' ], array( $this, '_preview_filter' ) );
171 add_filter( 'option_' . $this->id_data[ 'base' ], array( $this, '_preview_filter' ) );
172 add_filter( 'default_option_' . $this->id_data[ 'base' ], array( $this, '_preview_filter' ) );
178 * Fires when the {@see WP_Customize_Setting::preview()} method is called for settings
179 * not handled as theme_mods or options.
181 * The dynamic portion of the hook name, `$this->id`, refers to the setting ID.
185 * @param WP_Customize_Setting $this {@see WP_Customize_Setting} instance.
187 do_action( "customize_preview_{$this->id}", $this );
190 * Fires when the {@see WP_Customize_Setting::preview()} method is called for settings
191 * not handled as theme_mods or options.
193 * The dynamic portion of the hook name, `$this->type`, refers to the setting type.
197 * @param WP_Customize_Setting $this {@see WP_Customize_Setting} instance.
199 do_action( "customize_preview_{$this->type}", $this );
204 * Callback function to filter the theme mods and options.
206 * If switch_to_blog() was called after the preview() method, and the current
207 * blog is now not the same blog, then this method does a no-op and returns
208 * the original value.
211 * @uses WP_Customize_Setting::multidimensional_replace()
213 * @param mixed $original Old value.
214 * @return mixed New or old value.
216 public function _preview_filter( $original ) {
217 if ( ! $this->is_current_blog_previewed() ) {
221 $undefined = new stdClass(); // symbol hack
222 $post_value = $this->post_value( $undefined );
223 if ( $undefined === $post_value ) {
224 $value = $this->_original_value;
226 $value = $post_value;
229 return $this->multidimensional_replace( $original, $this->id_data['keys'], $value );
233 * Check user capabilities and theme supports, and then save
234 * the value of the setting.
238 * @return false|void False if cap check fails or value isn't set.
240 final public function save() {
241 $value = $this->post_value();
243 if ( ! $this->check_capabilities() || ! isset( $value ) )
247 * Fires when the WP_Customize_Setting::save() method is called.
249 * The dynamic portion of the hook name, `$this->id_data['base']` refers to
250 * the base slug of the setting name.
254 * @param WP_Customize_Setting $this {@see WP_Customize_Setting} instance.
256 do_action( 'customize_save_' . $this->id_data[ 'base' ], $this );
258 $this->update( $value );
262 * Fetch and sanitize the $_POST value for the setting.
266 * @param mixed $default A default value which is used as a fallback. Default is null.
267 * @return mixed The default value on failure, otherwise the sanitized value.
269 final public function post_value( $default = null ) {
270 return $this->manager->post_value( $this, $default );
278 * @param string|array $value The value to sanitize.
279 * @return string|array|null Null if an input isn't valid, otherwise the sanitized value.
281 public function sanitize( $value ) {
282 $value = wp_unslash( $value );
285 * Filter a Customize setting value in un-slashed form.
289 * @param mixed $value Value of the setting.
290 * @param WP_Customize_Setting $this WP_Customize_Setting instance.
292 return apply_filters( "customize_sanitize_{$this->id}", $value, $this );
296 * Save the value of the setting, using the related API.
300 * @param mixed $value The value to update.
301 * @return mixed The result of saving the value.
303 protected function update( $value ) {
304 switch( $this->type ) {
306 return $this->_update_theme_mod( $value );
309 return $this->_update_option( $value );
314 * Fires when the {@see WP_Customize_Setting::update()} method is called for settings
315 * not handled as theme_mods or options.
317 * The dynamic portion of the hook name, `$this->type`, refers to the type of setting.
321 * @param mixed $value Value of the setting.
322 * @param WP_Customize_Setting $this WP_Customize_Setting instance.
324 return do_action( 'customize_update_' . $this->type, $value, $this );
329 * Update the theme mod from the value of the parameter.
333 * @param mixed $value The value to update.
335 protected function _update_theme_mod( $value ) {
336 // Handle non-array theme mod.
337 if ( empty( $this->id_data[ 'keys' ] ) ) {
338 set_theme_mod( $this->id_data[ 'base' ], $value );
341 // Handle array-based theme mod.
342 $mods = get_theme_mod( $this->id_data[ 'base' ] );
343 $mods = $this->multidimensional_replace( $mods, $this->id_data[ 'keys' ], $value );
344 if ( isset( $mods ) ) {
345 set_theme_mod( $this->id_data[ 'base' ], $mods );
350 * Update the option from the value of the setting.
354 * @param mixed $value The value to update.
355 * @return bool The result of saving the value.
357 protected function _update_option( $value ) {
358 // Handle non-array option.
359 if ( empty( $this->id_data[ 'keys' ] ) )
360 return update_option( $this->id_data[ 'base' ], $value );
362 // Handle array-based options.
363 $options = get_option( $this->id_data[ 'base' ] );
364 $options = $this->multidimensional_replace( $options, $this->id_data[ 'keys' ], $value );
365 if ( isset( $options ) )
366 return update_option( $this->id_data[ 'base' ], $options );
370 * Fetch the value of the setting.
374 * @return mixed The value.
376 public function value() {
377 // Get the callback that corresponds to the setting type.
378 switch( $this->type ) {
380 $function = 'get_theme_mod';
383 $function = 'get_option';
388 * Filter a Customize setting value not handled as a theme_mod or option.
390 * The dynamic portion of the hook name, `$this->id_date['base']`, refers to
391 * the base slug of the setting name.
393 * For settings handled as theme_mods or options, see those corresponding
394 * functions for available hooks.
398 * @param mixed $default The setting default value. Default empty.
400 return apply_filters( 'customize_value_' . $this->id_data[ 'base' ], $this->default );
403 // Handle non-array value
404 if ( empty( $this->id_data[ 'keys' ] ) )
405 return $function( $this->id_data[ 'base' ], $this->default );
407 // Handle array-based value
408 $values = $function( $this->id_data[ 'base' ] );
409 return $this->multidimensional_get( $values, $this->id_data[ 'keys' ], $this->default );
413 * Sanitize the setting's value for use in JavaScript.
417 * @return mixed The requested escaped value.
419 public function js_value() {
422 * Filter a Customize setting value for use in JavaScript.
424 * The dynamic portion of the hook name, `$this->id`, refers to the setting ID.
428 * @param mixed $value The setting value.
429 * @param WP_Customize_Setting $this {@see WP_Customize_Setting} instance.
431 $value = apply_filters( "customize_sanitize_js_{$this->id}", $this->value(), $this );
433 if ( is_string( $value ) )
434 return html_entity_decode( $value, ENT_QUOTES, 'UTF-8');
440 * Validate user capabilities whether the theme supports the setting.
444 * @return bool False if theme doesn't support the setting or user can't change setting, otherwise true.
446 final public function check_capabilities() {
447 if ( $this->capability && ! call_user_func_array( 'current_user_can', (array) $this->capability ) )
450 if ( $this->theme_supports && ! call_user_func_array( 'current_theme_supports', (array) $this->theme_supports ) )
457 * Multidimensional helper function.
463 * @param bool $create Default is false.
464 * @return array|void Keys are 'root', 'node', and 'key'.
466 final protected function multidimensional( &$root, $keys, $create = false ) {
467 if ( $create && empty( $root ) )
470 if ( ! isset( $root ) || empty( $keys ) )
473 $last = array_pop( $keys );
476 foreach ( $keys as $key ) {
477 if ( $create && ! isset( $node[ $key ] ) )
478 $node[ $key ] = array();
480 if ( ! is_array( $node ) || ! isset( $node[ $key ] ) )
483 $node = &$node[ $key ];
487 if ( ! is_array( $node ) ) {
488 // account for an array overriding a string or object value
491 if ( ! isset( $node[ $last ] ) ) {
492 $node[ $last ] = array();
496 if ( ! isset( $node[ $last ] ) )
507 * Will attempt to replace a specific value in a multidimensional array.
513 * @param mixed $value The value to update.
516 final protected function multidimensional_replace( $root, $keys, $value ) {
517 if ( ! isset( $value ) )
519 elseif ( empty( $keys ) ) // If there are no keys, we're replacing the root.
522 $result = $this->multidimensional( $root, $keys, true );
524 if ( isset( $result ) )
525 $result['node'][ $result['key'] ] = $value;
531 * Will attempt to fetch a specific value from a multidimensional array.
537 * @param mixed $default A default value which is used as a fallback. Default is null.
538 * @return mixed The requested value or the default value.
540 final protected function multidimensional_get( $root, $keys, $default = null ) {
541 if ( empty( $keys ) ) // If there are no keys, test the root.
542 return isset( $root ) ? $root : $default;
544 $result = $this->multidimensional( $root, $keys );
545 return isset( $result ) ? $result['node'][ $result['key'] ] : $default;
549 * Will attempt to check if a specific value in a multidimensional array is set.
555 * @return bool True if value is set, false if not.
557 final protected function multidimensional_isset( $root, $keys ) {
558 $result = $this->multidimensional_get( $root, $keys );
559 return isset( $result );
564 * A setting that is used to filter a value, but will not save the results.
566 * Results should be properly handled using another setting or callback.
570 * @see WP_Customize_Setting
572 class WP_Customize_Filter_Setting extends WP_Customize_Setting {
577 public function update( $value ) {}
581 * A setting that is used to filter a value, but will not save the results.
583 * Results should be properly handled using another setting or callback.
587 * @see WP_Customize_Setting
589 final class WP_Customize_Header_Image_Setting extends WP_Customize_Setting {
590 public $id = 'header_image_data';
595 * @global Custom_Image_Header $custom_image_header
599 public function update( $value ) {
600 global $custom_image_header;
602 // If the value doesn't exist (removed or random),
603 // use the header_image value.
605 $value = $this->manager->get_setting('header_image')->post_value();
607 if ( is_array( $value ) && isset( $value['choice'] ) )
608 $custom_image_header->set_header_image( $value['choice'] );
610 $custom_image_header->set_header_image( $value );
615 * Customizer Background Image Setting class.
619 * @see WP_Customize_Setting
621 final class WP_Customize_Background_Image_Setting extends WP_Customize_Setting {
622 public $id = 'background_image_thumb';
629 public function update( $value ) {
630 remove_theme_mod( 'background_image_thumb' );
635 * Customize Setting to represent a nav_menu.
637 * Subclass of WP_Customize_Setting to represent a nav_menu taxonomy term, and
638 * the IDs for the nav_menu_items associated with the nav menu.
642 * @see WP_Customize_Setting
644 class WP_Customize_Nav_Menu_Item_Setting extends WP_Customize_Setting {
646 const ID_PATTERN = '/^nav_menu_item\[(?P<id>-?\d+)\]$/';
648 const POST_TYPE = 'nav_menu_item';
650 const TYPE = 'nav_menu_item';
659 public $type = self::TYPE;
662 * Default setting value.
668 * @see wp_setup_nav_menu_item()
670 public $default = array(
671 // The $menu_item_data for wp_update_nav_menu_item().
673 'object' => '', // Taxonomy name.
674 'menu_item_parent' => 0, // A.K.A. menu-item-parent-id; note that post_parent is different, and not included.
675 'position' => 0, // A.K.A. menu_order.
676 'type' => 'custom', // Note that type_label is not included here.
684 'status' => 'publish',
685 'original_title' => '',
686 'nav_menu_term_id' => 0, // This will be supplied as the $menu_id arg for wp_update_nav_menu_item().
687 // @todo also expose invalid?
697 public $transport = 'postMessage';
700 * The post ID represented by this setting instance. This is the db_id.
702 * A negative value represents a placeholder ID for a new menu not yet saved.
711 * Storage of pre-setup menu item to prevent wasted calls to wp_setup_nav_menu_item().
720 * Previous (placeholder) post ID used before creating a new menu item.
722 * This value will be exported to JS via the customize_save_response filter
723 * so that JavaScript can update the settings to refer to the newly-assigned
724 * post ID. This value is always negative to indicate it does not refer to
731 * @see WP_Customize_Nav_Menu_Item_Setting::update()
732 * @see WP_Customize_Nav_Menu_Item_Setting::amend_customize_save_response()
734 public $previous_post_id;
737 * When previewing or updating a menu item, this stores the previous nav_menu_term_id
738 * which ensures that we can apply the proper filters.
744 public $original_nav_menu_term_id;
747 * Whether or not preview() was called.
753 protected $is_previewed = false;
756 * Whether or not update() was called.
762 protected $is_updated = false;
765 * Status for calling the update method, used in customize_save_response filter.
767 * When status is inserted, the placeholder post ID is stored in $previous_post_id.
768 * When status is error, the error is stored in $update_error.
772 * @var string updated|inserted|deleted|error
774 * @see WP_Customize_Nav_Menu_Item_Setting::update()
775 * @see WP_Customize_Nav_Menu_Item_Setting::amend_customize_save_response()
777 public $update_status;
780 * Any error object returned by wp_update_nav_menu_item() when setting is updated.
786 * @see WP_Customize_Nav_Menu_Item_Setting::update()
787 * @see WP_Customize_Nav_Menu_Item_Setting::amend_customize_save_response()
789 public $update_error;
794 * Any supplied $args override class property defaults.
799 * @param WP_Customize_Manager $manager Bootstrap Customizer instance.
800 * @param string $id An specific ID of the setting. Can be a
801 * theme mod or option name.
802 * @param array $args Optional. Setting arguments.
804 * @throws Exception If $id is not valid for this setting type.
806 public function __construct( WP_Customize_Manager $manager, $id, array $args = array() ) {
807 if ( empty( $manager->nav_menus ) ) {
808 throw new Exception( 'Expected WP_Customize_Manager::$nav_menus to be set.' );
811 if ( ! preg_match( self::ID_PATTERN, $id, $matches ) ) {
812 throw new Exception( "Illegal widget setting ID: $id" );
815 $this->post_id = intval( $matches['id'] );
816 add_action( 'wp_update_nav_menu_item', array( $this, 'flush_cached_value' ), 10, 2 );
818 parent::__construct( $manager, $id, $args );
820 // Ensure that an initially-supplied value is valid.
821 if ( isset( $this->value ) ) {
822 $this->populate_value();
823 foreach ( array_diff( array_keys( $this->default ), array_keys( $this->value ) ) as $missing ) {
824 throw new Exception( "Supplied nav_menu_item value missing property: $missing" );
831 * Clear the cached value when this nav menu item is updated.
836 * @param int $menu_id The term ID for the menu.
837 * @param int $menu_item_id The post ID for the menu item.
839 public function flush_cached_value( $menu_id, $menu_item_id ) {
841 if ( $menu_item_id === $this->post_id ) {
847 * Get the instance data for a given widget setting.
852 * @see wp_setup_nav_menu_item()
854 * @return array|false Instance data array, or false if the item is marked for deletion.
856 public function value() {
857 if ( $this->is_previewed && $this->_previewed_blog_id === get_current_blog_id() ) {
858 $undefined = new stdClass(); // Symbol.
859 $post_value = $this->post_value( $undefined );
861 if ( $undefined === $post_value ) {
862 $value = $this->_original_value;
864 $value = $post_value;
866 } else if ( isset( $this->value ) ) {
867 $value = $this->value;
871 // Note that a ID of less than one indicates a nav_menu not yet inserted.
872 if ( $this->post_id > 0 ) {
873 $post = get_post( $this->post_id );
874 if ( $post && self::POST_TYPE === $post->post_type ) {
875 $value = (array) wp_setup_nav_menu_item( $post );
879 if ( ! is_array( $value ) ) {
880 $value = $this->default;
883 // Cache the value for future calls to avoid having to re-call wp_setup_nav_menu_item().
884 $this->value = $value;
885 $this->populate_value();
886 $value = $this->value;
893 * Ensure that the value is fully populated with the necessary properties.
895 * Translates some properties added by wp_setup_nav_menu_item() and removes others.
900 * @see WP_Customize_Nav_Menu_Item_Setting::value()
902 protected function populate_value() {
903 if ( ! is_array( $this->value ) ) {
907 if ( isset( $this->value['menu_order'] ) ) {
908 $this->value['position'] = $this->value['menu_order'];
909 unset( $this->value['menu_order'] );
911 if ( isset( $this->value['post_status'] ) ) {
912 $this->value['status'] = $this->value['post_status'];
913 unset( $this->value['post_status'] );
916 if ( ! isset( $this->value['original_title'] ) ) {
917 $original_title = '';
918 if ( 'post_type' === $this->value['type'] ) {
919 $original_title = get_the_title( $this->value['object_id'] );
920 } elseif ( 'taxonomy' === $this->value['type'] ) {
921 $original_title = get_term_field( 'name', $this->value['object_id'], $this->value['object'], 'raw' );
922 if ( is_wp_error( $original_title ) ) {
923 $original_title = '';
926 $this->value['original_title'] = html_entity_decode( $original_title, ENT_QUOTES, get_bloginfo( 'charset' ) );
929 if ( ! isset( $this->value['nav_menu_term_id'] ) && $this->post_id > 0 ) {
930 $menus = wp_get_post_terms( $this->post_id, WP_Customize_Nav_Menu_Setting::TAXONOMY, array(
933 if ( ! empty( $menus ) ) {
934 $this->value['nav_menu_term_id'] = array_shift( $menus );
936 $this->value['nav_menu_term_id'] = 0;
940 foreach ( array( 'object_id', 'menu_item_parent', 'nav_menu_term_id' ) as $key ) {
941 if ( ! is_int( $this->value[ $key ] ) ) {
942 $this->value[ $key ] = intval( $this->value[ $key ] );
946 // Remove remaining properties available on a setup nav_menu_item post object which aren't relevant to the setting value.
947 $irrelevant_properties = array(
958 'post_content_filtered',
972 foreach ( $irrelevant_properties as $property ) {
973 unset( $this->value[ $property ] );
978 * Handle previewing the setting.
983 * @see WP_Customize_Manager::post_value()
985 public function preview() {
986 if ( $this->is_previewed ) {
990 $this->is_previewed = true;
991 $this->_original_value = $this->value();
992 $this->original_nav_menu_term_id = $this->_original_value['nav_menu_term_id'];
993 $this->_previewed_blog_id = get_current_blog_id();
995 add_filter( 'wp_get_nav_menu_items', array( $this, 'filter_wp_get_nav_menu_items' ), 10, 3 );
997 $sort_callback = array( __CLASS__, 'sort_wp_get_nav_menu_items' );
998 if ( ! has_filter( 'wp_get_nav_menu_items', $sort_callback ) ) {
999 add_filter( 'wp_get_nav_menu_items', array( __CLASS__, 'sort_wp_get_nav_menu_items' ), 1000, 3 );
1002 // @todo Add get_post_metadata filters for plugins to add their data.
1006 * Filter the wp_get_nav_menu_items() result to supply the previewed menu items.
1011 * @see wp_get_nav_menu_items()
1013 * @param array $items An array of menu item post objects.
1014 * @param object $menu The menu object.
1015 * @param array $args An array of arguments used to retrieve menu item objects.
1016 * @return array Array of menu items,
1018 public function filter_wp_get_nav_menu_items( $items, $menu, $args ) {
1019 $this_item = $this->value();
1020 $current_nav_menu_term_id = $this_item['nav_menu_term_id'];
1021 unset( $this_item['nav_menu_term_id'] );
1024 $menu->term_id === $this->original_nav_menu_term_id
1026 $menu->term_id === $current_nav_menu_term_id
1028 if ( ! $should_filter ) {
1032 // Handle deleted menu item, or menu item moved to another menu.
1034 false === $this_item
1037 $this->original_nav_menu_term_id === $menu->term_id
1039 $current_nav_menu_term_id !== $this->original_nav_menu_term_id
1042 if ( $should_remove ) {
1043 $filtered_items = array();
1044 foreach ( $items as $item ) {
1045 if ( $item->db_id !== $this->post_id ) {
1046 $filtered_items[] = $item;
1049 return $filtered_items;
1054 is_array( $this_item )
1056 $current_nav_menu_term_id === $menu->term_id
1058 if ( $should_update ) {
1059 foreach ( $items as $item ) {
1060 if ( $item->db_id === $this->post_id ) {
1061 foreach ( get_object_vars( $this->value_as_wp_post_nav_menu_item() ) as $key => $value ) {
1062 $item->$key = $value;
1068 // Not found so we have to append it..
1070 $items[] = $this->value_as_wp_post_nav_menu_item();
1078 * Re-apply the tail logic also applied on $items by wp_get_nav_menu_items().
1084 * @see wp_get_nav_menu_items()
1086 * @param array $items An array of menu item post objects.
1087 * @param object $menu The menu object.
1088 * @param array $args An array of arguments used to retrieve menu item objects.
1089 * @return array Array of menu items,
1091 public static function sort_wp_get_nav_menu_items( $items, $menu, $args ) {
1092 // @todo We should probably re-apply some constraints imposed by $args.
1093 unset( $args['include'] );
1095 // Remove invalid items only in frontend.
1096 if ( ! is_admin() ) {
1097 $items = array_filter( $items, '_is_valid_nav_menu_item' );
1100 if ( ARRAY_A === $args['output'] ) {
1101 $GLOBALS['_menu_item_sort_prop'] = $args['output_key'];
1102 usort( $items, '_sort_nav_menu_items' );
1105 foreach ( $items as $k => $item ) {
1106 $items[ $k ]->{$args['output_key']} = $i++;
1114 * Get the value emulated into a WP_Post and set up as a nav_menu_item.
1119 * @return WP_Post With wp_setup_nav_menu_item() applied.
1121 public function value_as_wp_post_nav_menu_item() {
1122 $item = (object) $this->value();
1123 unset( $item->nav_menu_term_id );
1125 $item->post_status = $item->status;
1126 unset( $item->status );
1128 $item->post_type = 'nav_menu_item';
1129 $item->menu_order = $item->position;
1130 unset( $item->position );
1132 if ( $item->title ) {
1133 $item->post_title = $item->title;
1136 $item->ID = $this->post_id;
1137 $item->db_id = $this->post_id;
1138 $post = new WP_Post( (object) $item );
1140 if ( empty( $post->post_author ) ) {
1141 $post->post_author = get_current_user_id();
1144 if ( ! isset( $post->type_label ) ) {
1145 if ( 'post_type' === $post->type ) {
1146 $object = get_post_type_object( $post->object );
1148 $post->type_label = $object->labels->singular_name;
1150 $post->type_label = $post->object;
1152 } elseif ( 'taxonomy' == $post->type ) {
1153 $object = get_taxonomy( $post->object );
1155 $post->type_label = $object->labels->singular_name;
1157 $post->type_label = $post->object;
1160 $post->type_label = __( 'Custom Link' );
1168 * Sanitize an input.
1170 * Note that parent::sanitize() erroneously does wp_unslash() on $value, but
1171 * we remove that in this override.
1176 * @param array $menu_item_value The value to sanitize.
1177 * @return array|false|null Null if an input isn't valid. False if it is marked for deletion.
1178 * Otherwise the sanitized value.
1180 public function sanitize( $menu_item_value ) {
1181 // Menu is marked for deletion.
1182 if ( false === $menu_item_value ) {
1183 return $menu_item_value;
1187 if ( ! is_array( $menu_item_value ) ) {
1194 'menu_item_parent' => 0,
1201 'description' => '',
1204 'status' => 'publish',
1205 'original_title' => '',
1206 'nav_menu_term_id' => 0,
1208 $menu_item_value = array_merge( $default, $menu_item_value );
1209 $menu_item_value = wp_array_slice_assoc( $menu_item_value, array_keys( $default ) );
1210 $menu_item_value['position'] = max( 0, intval( $menu_item_value['position'] ) );
1212 foreach ( array( 'object_id', 'menu_item_parent', 'nav_menu_term_id' ) as $key ) {
1213 // Note we need to allow negative-integer IDs for previewed objects not inserted yet.
1214 $menu_item_value[ $key ] = intval( $menu_item_value[ $key ] );
1217 foreach ( array( 'type', 'object', 'target' ) as $key ) {
1218 $menu_item_value[ $key ] = sanitize_key( $menu_item_value[ $key ] );
1221 foreach ( array( 'xfn', 'classes' ) as $key ) {
1222 $value = $menu_item_value[ $key ];
1223 if ( ! is_array( $value ) ) {
1224 $value = explode( ' ', $value );
1226 $menu_item_value[ $key ] = implode( ' ', array_map( 'sanitize_html_class', $value ) );
1229 foreach ( array( 'title', 'attr_title', 'description', 'original_title' ) as $key ) {
1230 // @todo Should esc_attr() the attr_title as well?
1231 $menu_item_value[ $key ] = sanitize_text_field( $menu_item_value[ $key ] );
1234 $menu_item_value['url'] = esc_url_raw( $menu_item_value['url'] );
1235 if ( ! get_post_status_object( $menu_item_value['status'] ) ) {
1236 $menu_item_value['status'] = 'publish';
1239 /** This filter is documented in wp-includes/class-wp-customize-setting.php */
1240 return apply_filters( "customize_sanitize_{$this->id}", $menu_item_value, $this );
1244 * Create/update the nav_menu_item post for this setting.
1246 * Any created menu items will have their assigned post IDs exported to the client
1247 * via the customize_save_response filter. Likewise, any errors will be exported
1248 * to the client via the customize_save_response() filter.
1250 * To delete a menu, the client can send false as the value.
1255 * @see wp_update_nav_menu_item()
1257 * @param array|false $value The menu item array to update. If false, then the menu item will be deleted
1258 * entirely. See WP_Customize_Nav_Menu_Item_Setting::$default for what the value
1259 * should consist of.
1262 protected function update( $value ) {
1263 if ( $this->is_updated ) {
1267 $this->is_updated = true;
1268 $is_placeholder = ( $this->post_id < 0 );
1269 $is_delete = ( false === $value );
1271 // Update the cached value.
1272 $this->value = $value;
1274 add_filter( 'customize_save_response', array( $this, 'amend_customize_save_response' ) );
1277 // If the current setting post is a placeholder, a delete request is a no-op.
1278 if ( $is_placeholder ) {
1279 $this->update_status = 'deleted';
1281 $r = wp_delete_post( $this->post_id, true );
1283 if ( false === $r ) {
1284 $this->update_error = new WP_Error( 'delete_failure' );
1285 $this->update_status = 'error';
1287 $this->update_status = 'deleted';
1289 // @todo send back the IDs for all associated nav menu items deleted, so these settings (and controls) can be removed from Customizer?
1293 // Handle saving menu items for menus that are being newly-created.
1294 if ( $value['nav_menu_term_id'] < 0 ) {
1295 $nav_menu_setting_id = sprintf( 'nav_menu[%s]', $value['nav_menu_term_id'] );
1296 $nav_menu_setting = $this->manager->get_setting( $nav_menu_setting_id );
1298 if ( ! $nav_menu_setting || ! ( $nav_menu_setting instanceof WP_Customize_Nav_Menu_Setting ) ) {
1299 $this->update_status = 'error';
1300 $this->update_error = new WP_Error( 'unexpected_nav_menu_setting' );
1304 if ( false === $nav_menu_setting->save() ) {
1305 $this->update_status = 'error';
1306 $this->update_error = new WP_Error( 'nav_menu_setting_failure' );
1310 if ( $nav_menu_setting->previous_term_id !== intval( $value['nav_menu_term_id'] ) ) {
1311 $this->update_status = 'error';
1312 $this->update_error = new WP_Error( 'unexpected_previous_term_id' );
1316 $value['nav_menu_term_id'] = $nav_menu_setting->term_id;
1319 // Handle saving a nav menu item that is a child of a nav menu item being newly-created.
1320 if ( $value['menu_item_parent'] < 0 ) {
1321 $parent_nav_menu_item_setting_id = sprintf( 'nav_menu_item[%s]', $value['menu_item_parent'] );
1322 $parent_nav_menu_item_setting = $this->manager->get_setting( $parent_nav_menu_item_setting_id );
1324 if ( ! $parent_nav_menu_item_setting || ! ( $parent_nav_menu_item_setting instanceof WP_Customize_Nav_Menu_Item_Setting ) ) {
1325 $this->update_status = 'error';
1326 $this->update_error = new WP_Error( 'unexpected_nav_menu_item_setting' );
1330 if ( false === $parent_nav_menu_item_setting->save() ) {
1331 $this->update_status = 'error';
1332 $this->update_error = new WP_Error( 'nav_menu_item_setting_failure' );
1336 if ( $parent_nav_menu_item_setting->previous_post_id !== intval( $value['menu_item_parent'] ) ) {
1337 $this->update_status = 'error';
1338 $this->update_error = new WP_Error( 'unexpected_previous_post_id' );
1342 $value['menu_item_parent'] = $parent_nav_menu_item_setting->post_id;
1345 // Insert or update menu.
1346 $menu_item_data = array(
1347 'menu-item-object-id' => $value['object_id'],
1348 'menu-item-object' => $value['object'],
1349 'menu-item-parent-id' => $value['menu_item_parent'],
1350 'menu-item-position' => $value['position'],
1351 'menu-item-type' => $value['type'],
1352 'menu-item-title' => $value['title'],
1353 'menu-item-url' => $value['url'],
1354 'menu-item-description' => $value['description'],
1355 'menu-item-attr-title' => $value['attr_title'],
1356 'menu-item-target' => $value['target'],
1357 'menu-item-classes' => $value['classes'],
1358 'menu-item-xfn' => $value['xfn'],
1359 'menu-item-status' => $value['status'],
1362 $r = wp_update_nav_menu_item(
1363 $value['nav_menu_term_id'],
1364 $is_placeholder ? 0 : $this->post_id,
1368 if ( is_wp_error( $r ) ) {
1369 $this->update_status = 'error';
1370 $this->update_error = $r;
1372 if ( $is_placeholder ) {
1373 $this->previous_post_id = $this->post_id;
1374 $this->post_id = $r;
1375 $this->update_status = 'inserted';
1377 $this->update_status = 'updated';
1385 * Export data for the JS client.
1390 * @see WP_Customize_Nav_Menu_Item_Setting::update()
1392 * @param array $data Additional information passed back to the 'saved' event on `wp.customize`.
1393 * @return array Save response data.
1395 public function amend_customize_save_response( $data ) {
1396 if ( ! isset( $data['nav_menu_item_updates'] ) ) {
1397 $data['nav_menu_item_updates'] = array();
1400 $data['nav_menu_item_updates'][] = array(
1401 'post_id' => $this->post_id,
1402 'previous_post_id' => $this->previous_post_id,
1403 'error' => $this->update_error ? $this->update_error->get_error_code() : null,
1404 'status' => $this->update_status,
1411 * Customize Setting to represent a nav_menu.
1413 * Subclass of WP_Customize_Setting to represent a nav_menu taxonomy term, and
1414 * the IDs for the nav_menu_items associated with the nav menu.
1418 * @see wp_get_nav_menu_object()
1419 * @see WP_Customize_Setting
1421 class WP_Customize_Nav_Menu_Setting extends WP_Customize_Setting {
1423 const ID_PATTERN = '/^nav_menu\[(?P<id>-?\d+)\]$/';
1425 const TAXONOMY = 'nav_menu';
1427 const TYPE = 'nav_menu';
1436 public $type = self::TYPE;
1439 * Default setting value.
1445 * @see wp_get_nav_menu_object()
1447 public $default = array(
1449 'description' => '',
1451 'auto_add' => false,
1455 * Default transport.
1461 public $transport = 'postMessage';
1464 * The term ID represented by this setting instance.
1466 * A negative value represents a placeholder ID for a new menu not yet saved.
1475 * Previous (placeholder) term ID used before creating a new menu.
1477 * This value will be exported to JS via the customize_save_response filter
1478 * so that JavaScript can update the settings to refer to the newly-assigned
1479 * term ID. This value is always negative to indicate it does not refer to
1486 * @see WP_Customize_Nav_Menu_Setting::update()
1487 * @see WP_Customize_Nav_Menu_Setting::amend_customize_save_response()
1489 public $previous_term_id;
1492 * Whether or not preview() was called.
1498 protected $is_previewed = false;
1501 * Whether or not update() was called.
1507 protected $is_updated = false;
1510 * Status for calling the update method, used in customize_save_response filter.
1512 * When status is inserted, the placeholder term ID is stored in $previous_term_id.
1513 * When status is error, the error is stored in $update_error.
1517 * @var string updated|inserted|deleted|error
1519 * @see WP_Customize_Nav_Menu_Setting::update()
1520 * @see WP_Customize_Nav_Menu_Setting::amend_customize_save_response()
1522 public $update_status;
1525 * Any error object returned by wp_update_nav_menu_object() when setting is updated.
1531 * @see WP_Customize_Nav_Menu_Setting::update()
1532 * @see WP_Customize_Nav_Menu_Setting::amend_customize_save_response()
1534 public $update_error;
1539 * Any supplied $args override class property defaults.
1544 * @param WP_Customize_Manager $manager Bootstrap Customizer instance.
1545 * @param string $id An specific ID of the setting. Can be a
1546 * theme mod or option name.
1547 * @param array $args Optional. Setting arguments.
1549 * @throws Exception If $id is not valid for this setting type.
1551 public function __construct( WP_Customize_Manager $manager, $id, array $args = array() ) {
1552 if ( empty( $manager->nav_menus ) ) {
1553 throw new Exception( 'Expected WP_Customize_Manager::$nav_menus to be set.' );
1556 if ( ! preg_match( self::ID_PATTERN, $id, $matches ) ) {
1557 throw new Exception( "Illegal widget setting ID: $id" );
1560 $this->term_id = intval( $matches['id'] );
1562 parent::__construct( $manager, $id, $args );
1566 * Get the instance data for a given widget setting.
1571 * @see wp_get_nav_menu_object()
1573 * @return array Instance data.
1575 public function value() {
1576 if ( $this->is_previewed && $this->_previewed_blog_id === get_current_blog_id() ) {
1577 $undefined = new stdClass(); // Symbol.
1578 $post_value = $this->post_value( $undefined );
1580 if ( $undefined === $post_value ) {
1581 $value = $this->_original_value;
1583 $value = $post_value;
1588 // Note that a term_id of less than one indicates a nav_menu not yet inserted.
1589 if ( $this->term_id > 0 ) {
1590 $term = wp_get_nav_menu_object( $this->term_id );
1593 $value = wp_array_slice_assoc( (array) $term, array_keys( $this->default ) );
1595 $nav_menu_options = (array) get_option( 'nav_menu_options', array() );
1596 $value['auto_add'] = false;
1598 if ( isset( $nav_menu_options['auto_add'] ) && is_array( $nav_menu_options['auto_add'] ) ) {
1599 $value['auto_add'] = in_array( $term->term_id, $nav_menu_options['auto_add'] );
1604 if ( ! is_array( $value ) ) {
1605 $value = $this->default;
1612 * Handle previewing the setting.
1617 * @see WP_Customize_Manager::post_value()
1619 public function preview() {
1620 if ( $this->is_previewed ) {
1624 $this->is_previewed = true;
1625 $this->_original_value = $this->value();
1626 $this->_previewed_blog_id = get_current_blog_id();
1628 add_filter( 'wp_get_nav_menus', array( $this, 'filter_wp_get_nav_menus' ), 10, 2 );
1629 add_filter( 'wp_get_nav_menu_object', array( $this, 'filter_wp_get_nav_menu_object' ), 10, 2 );
1630 add_filter( 'default_option_nav_menu_options', array( $this, 'filter_nav_menu_options' ) );
1631 add_filter( 'option_nav_menu_options', array( $this, 'filter_nav_menu_options' ) );
1635 * Filter the wp_get_nav_menus() result to ensure the inserted menu object is included, and the deleted one is removed.
1640 * @see wp_get_nav_menus()
1642 * @param array $menus An array of menu objects.
1643 * @param array $args An array of arguments used to retrieve menu objects.
1646 public function filter_wp_get_nav_menus( $menus, $args ) {
1647 if ( get_current_blog_id() !== $this->_previewed_blog_id ) {
1651 $setting_value = $this->value();
1652 $is_delete = ( false === $setting_value );
1655 // Find the existing menu item's position in the list.
1656 foreach ( $menus as $i => $menu ) {
1657 if ( (int) $this->term_id === (int) $menu->term_id || (int) $this->previous_term_id === (int) $menu->term_id ) {
1664 // Handle deleted menu by removing it from the list.
1665 if ( -1 !== $index ) {
1666 array_splice( $menus, $index, 1 );
1669 // Handle menus being updated or inserted.
1670 $menu_obj = (object) array_merge( array(
1671 'term_id' => $this->term_id,
1672 'term_taxonomy_id' => $this->term_id,
1673 'slug' => sanitize_title( $setting_value['name'] ),
1676 'taxonomy' => self::TAXONOMY,
1678 ), $setting_value );
1680 array_splice( $menus, $index, ( -1 === $index ? 0 : 1 ), array( $menu_obj ) );
1683 // Make sure the menu objects get re-sorted after an update/insert.
1684 if ( ! $is_delete && ! empty( $args['orderby'] ) ) {
1685 $this->_current_menus_sort_orderby = $args['orderby'];
1686 usort( $menus, array( $this, '_sort_menus_by_orderby' ) );
1688 // @todo add support for $args['hide_empty'] === true
1694 * Temporary non-closure passing of orderby value to function.
1700 * @see WP_Customize_Nav_Menu_Setting::filter_wp_get_nav_menus()
1701 * @see WP_Customize_Nav_Menu_Setting::_sort_menus_by_orderby()
1703 protected $_current_menus_sort_orderby;
1706 * Sort menu objects by the class-supplied orderby property.
1708 * This is a workaround for a lack of closures.
1712 * @param object $menu1
1713 * @param object $menu2
1716 * @see WP_Customize_Nav_Menu_Setting::filter_wp_get_nav_menus()
1718 protected function _sort_menus_by_orderby( $menu1, $menu2 ) {
1719 $key = $this->_current_menus_sort_orderby;
1720 return strcmp( $menu1->$key, $menu2->$key );
1724 * Filter the wp_get_nav_menu_object() result to supply the previewed menu object.
1726 * Requesting a nav_menu object by anything but ID is not supported.
1731 * @see wp_get_nav_menu_object()
1733 * @param object|null $menu_obj Object returned by wp_get_nav_menu_object().
1734 * @param string $menu_id ID of the nav_menu term. Requests by slug or name will be ignored.
1735 * @return object|null
1737 public function filter_wp_get_nav_menu_object( $menu_obj, $menu_id ) {
1739 get_current_blog_id() === $this->_previewed_blog_id
1743 $menu_id === $this->term_id
1749 $setting_value = $this->value();
1751 // Handle deleted menus.
1752 if ( false === $setting_value ) {
1756 // Handle sanitization failure by preventing short-circuiting.
1757 if ( null === $setting_value ) {
1761 $menu_obj = (object) array_merge( array(
1762 'term_id' => $this->term_id,
1763 'term_taxonomy_id' => $this->term_id,
1764 'slug' => sanitize_title( $setting_value['name'] ),
1767 'taxonomy' => self::TAXONOMY,
1769 ), $setting_value );
1775 * Filter the nav_menu_options option to include this menu's auto_add preference.
1780 * @param array $nav_menu_options Nav menu options including auto_add.
1781 * @return array (Kaybe) modified nav menu options.
1783 public function filter_nav_menu_options( $nav_menu_options ) {
1784 if ( $this->_previewed_blog_id !== get_current_blog_id() ) {
1785 return $nav_menu_options;
1788 $menu = $this->value();
1789 $nav_menu_options = $this->filter_nav_menu_options_value(
1792 false === $menu ? false : $menu['auto_add']
1795 return $nav_menu_options;
1799 * Sanitize an input.
1801 * Note that parent::sanitize() erroneously does wp_unslash() on $value, but
1802 * we remove that in this override.
1807 * @param array $value The value to sanitize.
1808 * @return array|false|null Null if an input isn't valid. False if it is marked for deletion.
1809 * Otherwise the sanitized value.
1811 public function sanitize( $value ) {
1812 // Menu is marked for deletion.
1813 if ( false === $value ) {
1818 if ( ! is_array( $value ) ) {
1824 'description' => '',
1826 'auto_add' => false,
1828 $value = array_merge( $default, $value );
1829 $value = wp_array_slice_assoc( $value, array_keys( $default ) );
1831 $value['name'] = trim( esc_html( $value['name'] ) ); // This sanitization code is used in wp-admin/nav-menus.php.
1832 $value['description'] = sanitize_text_field( $value['description'] );
1833 $value['parent'] = max( 0, intval( $value['parent'] ) );
1834 $value['auto_add'] = ! empty( $value['auto_add'] );
1836 if ( '' === $value['name'] ) {
1837 $value['name'] = _x( '(unnamed)', 'Missing menu name.' );
1840 /** This filter is documented in wp-includes/class-wp-customize-setting.php */
1841 return apply_filters( "customize_sanitize_{$this->id}", $value, $this );
1845 * Storage for data to be sent back to client in customize_save_response filter.
1851 * @see WP_Customize_Nav_Menu_Setting::amend_customize_save_response()
1853 protected $_widget_nav_menu_updates = array();
1856 * Create/update the nav_menu term for this setting.
1858 * Any created menus will have their assigned term IDs exported to the client
1859 * via the customize_save_response filter. Likewise, any errors will be exported
1860 * to the client via the customize_save_response() filter.
1862 * To delete a menu, the client can send false as the value.
1867 * @see wp_update_nav_menu_object()
1869 * @param array|false $value {
1870 * The value to update. Note that slug cannot be updated via wp_update_nav_menu_object().
1871 * If false, then the menu will be deleted entirely.
1873 * @type string $name The name of the menu to save.
1874 * @type string $description The term description. Default empty string.
1875 * @type int $parent The id of the parent term. Default 0.
1876 * @type bool $auto_add Whether pages will auto_add to this menu. Default false.
1880 protected function update( $value ) {
1881 if ( $this->is_updated ) {
1885 $this->is_updated = true;
1886 $is_placeholder = ( $this->term_id < 0 );
1887 $is_delete = ( false === $value );
1889 add_filter( 'customize_save_response', array( $this, 'amend_customize_save_response' ) );
1893 // If the current setting term is a placeholder, a delete request is a no-op.
1894 if ( $is_placeholder ) {
1895 $this->update_status = 'deleted';
1897 $r = wp_delete_nav_menu( $this->term_id );
1899 if ( is_wp_error( $r ) ) {
1900 $this->update_status = 'error';
1901 $this->update_error = $r;
1903 $this->update_status = 'deleted';
1908 // Insert or update menu.
1909 $menu_data = wp_array_slice_assoc( $value, array( 'description', 'parent' ) );
1910 $menu_data['menu-name'] = $value['name'];
1912 $menu_id = $is_placeholder ? 0 : $this->term_id;
1913 $r = wp_update_nav_menu_object( $menu_id, $menu_data );
1914 $original_name = $menu_data['menu-name'];
1915 $name_conflict_suffix = 1;
1916 while ( is_wp_error( $r ) && 'menu_exists' === $r->get_error_code() ) {
1917 $name_conflict_suffix += 1;
1918 /* translators: 1: original menu name, 2: duplicate count */
1919 $menu_data['menu-name'] = sprintf( __( '%1$s (%2$d)' ), $original_name, $name_conflict_suffix );
1920 $r = wp_update_nav_menu_object( $menu_id, $menu_data );
1923 if ( is_wp_error( $r ) ) {
1924 $this->update_status = 'error';
1925 $this->update_error = $r;
1927 if ( $is_placeholder ) {
1928 $this->previous_term_id = $this->term_id;
1929 $this->term_id = $r;
1930 $this->update_status = 'inserted';
1932 $this->update_status = 'updated';
1935 $auto_add = $value['auto_add'];
1939 if ( null !== $auto_add ) {
1940 $nav_menu_options = $this->filter_nav_menu_options_value(
1941 (array) get_option( 'nav_menu_options', array() ),
1945 update_option( 'nav_menu_options', $nav_menu_options );
1948 if ( 'inserted' === $this->update_status ) {
1949 // Make sure that new menus assigned to nav menu locations use their new IDs.
1950 foreach ( $this->manager->settings() as $setting ) {
1951 if ( ! preg_match( '/^nav_menu_locations\[/', $setting->id ) ) {
1955 $post_value = $setting->post_value( null );
1956 if ( ! is_null( $post_value ) && $this->previous_term_id === intval( $post_value ) ) {
1957 $this->manager->set_post_value( $setting->id, $this->term_id );
1962 // Make sure that any nav_menu widgets referencing the placeholder nav menu get updated and sent back to client.
1963 foreach ( array_keys( $this->manager->unsanitized_post_values() ) as $setting_id ) {
1964 $nav_menu_widget_setting = $this->manager->get_setting( $setting_id );
1965 if ( ! $nav_menu_widget_setting || ! preg_match( '/^widget_nav_menu\[/', $nav_menu_widget_setting->id ) ) {
1969 $widget_instance = $nav_menu_widget_setting->post_value(); // Note that this calls WP_Customize_Widgets::sanitize_widget_instance().
1970 if ( empty( $widget_instance['nav_menu'] ) || intval( $widget_instance['nav_menu'] ) !== $this->previous_term_id ) {
1974 $widget_instance['nav_menu'] = $this->term_id;
1975 $updated_widget_instance = $this->manager->widgets->sanitize_widget_js_instance( $widget_instance );
1976 $this->manager->set_post_value( $nav_menu_widget_setting->id, $updated_widget_instance );
1977 $nav_menu_widget_setting->save();
1979 $this->_widget_nav_menu_updates[ $nav_menu_widget_setting->id ] = $updated_widget_instance;
1985 * Updates a nav_menu_options array.
1990 * @see WP_Customize_Nav_Menu_Setting::filter_nav_menu_options()
1991 * @see WP_Customize_Nav_Menu_Setting::update()
1993 * @param array $nav_menu_options Array as returned by get_option( 'nav_menu_options' ).
1994 * @param int $menu_id The term ID for the given menu.
1995 * @param bool $auto_add Whether to auto-add or not.
1996 * @return array (Maybe) modified nav_menu_otions array.
1998 protected function filter_nav_menu_options_value( $nav_menu_options, $menu_id, $auto_add ) {
1999 $nav_menu_options = (array) $nav_menu_options;
2000 if ( ! isset( $nav_menu_options['auto_add'] ) ) {
2001 $nav_menu_options['auto_add'] = array();
2004 $i = array_search( $menu_id, $nav_menu_options['auto_add'] );
2005 if ( $auto_add && false === $i ) {
2006 array_push( $nav_menu_options['auto_add'], $this->term_id );
2007 } elseif ( ! $auto_add && false !== $i ) {
2008 array_splice( $nav_menu_options['auto_add'], $i, 1 );
2011 return $nav_menu_options;
2015 * Export data for the JS client.
2020 * @see WP_Customize_Nav_Menu_Setting::update()
2022 * @param array $data Additional information passed back to the 'saved' event on `wp.customize`.
2023 * @return array Export data.
2025 public function amend_customize_save_response( $data ) {
2026 if ( ! isset( $data['nav_menu_updates'] ) ) {
2027 $data['nav_menu_updates'] = array();
2029 if ( ! isset( $data['widget_nav_menu_updates'] ) ) {
2030 $data['widget_nav_menu_updates'] = array();
2033 $data['nav_menu_updates'][] = array(
2034 'term_id' => $this->term_id,
2035 'previous_term_id' => $this->previous_term_id,
2036 'error' => $this->update_error ? $this->update_error->get_error_code() : null,
2037 'status' => $this->update_status,
2038 'saved_value' => 'deleted' === $this->update_status ? null : $this->value(),
2041 $data['widget_nav_menu_updates'] = array_merge(
2042 $data['widget_nav_menu_updates'],
2043 $this->_widget_nav_menu_updates
2045 $this->_widget_nav_menu_updates = array();