Make WordPress Core

Ticket #34923: 34923.3.diff

File 34923.3.diff, 21.1 KB (added by celloexpressions, 10 years ago)

Complete first pass at functionality, no plugin dependency (and customize posts conflicts so it needs to be deactivated). Create auto-draft posts and publish them when the user saves & publishes. Add new content to the available menu items panel, and improve the tab order for accessibility. Prevent race conditions by only allowing one new post to be submitted at a time.

  • src/wp-admin/css/customize-nav-menus.css

     
    479479        color: #23282d;
    480480}
    481481
    482 #available-menu-items .accordion-section-content {
     482#available-menu-items .available-menu-items-list {
    483483        overflow-y: auto;
    484484        max-height: 200px; /* This gets set in JS to fit the screen size, and based on # of sections. */
    485485        background: transparent;
     
    516516}
    517517
    518518#available-menu-items .accordion-section-content {
    519         padding: 1px 15px 15px 15px;
     519        max-height: 290px;
    520520        margin: 0;
    521         max-height: 290px;
     521        padding: 0;
     522        position: relative;
     523        background: transparent;
    522524}
    523525
     526#available-menu-items .accordion-section-content .available-menu-items-list {
     527        margin: 0 0 45px 0;
     528        padding: 1px 15px 15px 15px;
     529}
     530
     531#available-menu-items .accordion-section-content .available-menu-items-list:only-child { /* Types that do not support new items for the current user */
     532        margin-bottom: 0;
     533}
     534
     535#new-custom-menu-item .accordion-section-content {
     536        padding: 0 15px 15px 15px;
     537}
     538
     539#available-menu-items .accordion-section-content .new-content-item {
     540        width: calc(100% - 30px);
     541        padding: 8px 15px;
     542        position: absolute;
     543        bottom: 0;
     544        z-index: 10;
     545        background: #eee;
     546}
     547
     548#available-menu-items .new-content-item .add-content {
     549        float: right;
     550        padding-left: 6px;
     551}
     552
     553#available-menu-items .new-content-item .add-content:before {
     554        content: "\f543";
     555        font: 20px/16px dashicons;
     556        position: relative;
     557        display: inline-block;
     558        top: 5px;
     559        left: -2px;
     560}
     561
    524562#available-menu-items .menu-item-tpl {
    525563        margin: 0;
    526564}
  • src/wp-admin/js/customize-nav-menus.js

     
    100100                        'click .menu-item-tpl': '_submit',
    101101                        'click #custom-menu-item-submit': '_submitLink',
    102102                        'keypress #custom-menu-item-name': '_submitLink',
     103                        'click .new-content-item .add-content': '_submitNew',
     104                        'keypress .create-item-input': '_submitNew',
    103105                        'keydown': 'keyboardAccessible'
    104106                },
    105107
     
    115117                pages: {},
    116118                sectionContent: '',
    117119                loading: false,
     120                addingNew: false,
    118121
    119122                initialize: function() {
    120123                        var self = this;
     
    124127                        }
    125128
    126129                        this.$search = $( '#menu-items-search' );
    127                         this.sectionContent = this.$el.find( '.accordion-section-content' );
     130                        this.sectionContent = this.$el.find( '.available-menu-items-list' );
    128131
    129132                        this.debounceSearch = _.debounce( self.search, 500 );
    130133
     
    168171
    169172                        // Load more items.
    170173                        this.sectionContent.scroll( function() {
    171                                 var totalHeight = self.$el.find( '.accordion-section.open .accordion-section-content' ).prop( 'scrollHeight' ),
     174                                var totalHeight = self.$el.find( '.accordion-section.open .available-menu-items-list' ).prop( 'scrollHeight' ),
    172175                                        visibleHeight = self.$el.find( '.accordion-section.open' ).height();
    173176
    174177                                if ( ! self.loading && $( this ).scrollTop() > 3 / 4 * totalHeight - visibleHeight ) {
     
    349352                                }
    350353                                items = new api.Menus.AvailableItemCollection( items ); // @todo Why is this collection created and then thrown away?
    351354                                self.collection.add( items.models );
    352                                 typeInner = availableMenuItemContainer.find( '.accordion-section-content' );
     355                                typeInner = availableMenuItemContainer.find( '.available-menu-items-list' );
    353356                                items.each(function( menuItem ) {
    354357                                        typeInner.append( itemTemplate( menuItem.attributes ) );
    355358                                });
     
    368371
    369372                // Adjust the height of each section of items to fit the screen.
    370373                itemSectionHeight: function() {
    371                         var sections, totalHeight, accordionHeight, diff;
     374                        var sections, lists, totalHeight, accordionHeight, diff, totalWidth, button, buttonWidth;
    372375                        totalHeight = window.innerHeight;
    373376                        sections = this.$el.find( '.accordion-section:not( #available-menu-items-search ) .accordion-section-content' );
    374                         accordionHeight =  46 * ( 2 + sections.length ) - 13; // Magic numbers.
     377                        lists = this.$el.find( '.accordion-section:not( #available-menu-items-search ) .available-menu-items-list:not(":only-child")' );
     378                        accordionHeight =  46 * ( 1 + sections.length ) + 14; // Magic numbers.
    375379                        diff = totalHeight - accordionHeight;
    376380                        if ( 120 < diff && 290 > diff ) {
    377381                                sections.css( 'max-height', diff );
     382                                lists.css( 'max-height', ( diff - 60 ) );
    378383                        }
     384                        // Fit the new-content input and button in the available space.
     385                        totalWidth = this.$el.width();
     386                        // Clone button to get width of invisible element.
     387                        button = this.$el.find( '.accordion-section .new-content-item .add-content' ).first().clone().appendTo( 'body' ).css({ 'display': 'block', 'visibility': 'hidden' });
     388                        buttonWidth = button.outerWidth();
     389                        button.remove();
     390                        this.$el.find( '.accordion-section .new-content-item .create-item-input' ).width( ( totalWidth - buttonWidth - 70 ) ); // 70 = additional margins and padding.
    379391                },
    380392
    381393                // Highlights a menu item.
     
    468480                        itemName.val( '' );
    469481                },
    470482
     483                // Submit handler for keypress (enter) on field and click on button.
     484                _submitNew: function( event ) {
     485                        // Only proceed with keypress if it is Enter.
     486                        if ( 'keypress' === event.type && 13 !== event.which ) {
     487                                return;
     488                        }
     489
     490                        if ( this.addingNew ) {
     491                                return;
     492                        }
     493
     494                        var container = $( event.target ).closest( '.accordion-section' );
     495                       
     496                        this.submitNew( container );
     497                },
     498
     499                // Creates a new object and adds an associated menu item to the menu.
     500                submitNew: function( container ) {
     501                        var panel = this,
     502                                itemName = container.find( '.create-item-input' ),
     503                                title = itemName.val(),
     504                                dataContainer = container.find( '.available-menu-items-list' ),
     505                                itemType = dataContainer.data( 'type' ),
     506                                itemObject = dataContainer.data( 'object' ),
     507                                itemTypeLabel = dataContainer.data( 'type_label' ),
     508                                promise;
     509
     510                        if ( ! this.currentMenuControl ) {
     511                                return;
     512                        }
     513
     514                        if ( '' === itemName.val() ) {
     515                                itemName.addClass( 'invalid' );
     516                                return;
     517                        } else {
     518                                container.find( '.accordion-section-title' ).addClass( 'loading' );
     519                        }
     520
     521                        // Only posts are supported currently.
     522                        if ( 'post_type' !== itemType ) {
     523                                return;
     524                        }
     525
     526                        panel.addingNew = true;
     527                        itemName.attr( 'disabled', 'disabled' );
     528                        promise = wp.customize.Posts.insertAutoDraftPost( {
     529                                post_title: title,
     530                                post_type: itemObject,
     531                                post_status: 'publish'
     532                        } );
     533                        promise.done( function( data ) {
     534                                var menuItem = {
     535                                        'title': itemName.val(),
     536                                        'type': itemType,
     537                                        'type_label': itemTypeLabel,
     538                                        'object': itemObject,
     539                                        'object_id': data.postId,
     540                                        'url': data.url
     541                                }, availableItems, $content, itemTemplate;
     542
     543                                // Add new item to menu.
     544                                panel.currentMenuControl.addItemToMenu( menuItem );
     545
     546                                // Add the new item to the list of available items.
     547                                menuItem['id'] = 'post-' + data.postId; // `id` is used for available menu item Backbone models.
     548                                availableItems = new api.Menus.AvailableItemCollection( [ menuItem ] );
     549                                api.Menus.availableMenuItemsPanel.collection.add( availableItems.models );
     550                                $content = container.find( '.available-menu-items-list' ),
     551                                itemTemplate = wp.template( 'available-menu-item' );
     552                                $content.prepend( itemTemplate( menuItem ) );
     553                                $content.scrollTop();
     554
     555                                // Reset the create content form.
     556                                itemName.val( '' )
     557                                        .removeAttr( 'disabled' )
     558                                        .focus();
     559                                panel.addingNew = false;
     560                                container.find( '.accordion-section-title' ).removeClass( 'loading' );
     561                        } );
     562                },
     563
    471564                // Opens the panel.
    472565                open: function( menuControl ) {
    473566                        this.currentMenuControl = menuControl;
  • src/wp-admin/js/customize-posts.js

     
     1/* global jQuery, wp, _, console */
     2
     3(function( api, $ ) {
     4        'use strict';
     5
     6        var component;
     7
     8        if ( ! api.Posts ) {
     9                api.Posts = {};
     10        }
     11
     12        component = api.Posts;
     13
     14        component.autoDrafts = [];
     15
     16        /**
     17         * Insert a new `auto-draft` post.
     18         *
     19         * @param {object} params - Parameters for the draft post to create.
     20         * @param {string} params.post_type - Post type to add.
     21         * @param {number} params.title - Post title to use.
     22         * @return {jQuery.promise} Promise resolved with the added post.
     23         */
     24        component.insertAutoDraftPost = function( params ) {
     25                var request, deferred = $.Deferred();
     26
     27                request = wp.ajax.post( 'customize-posts-insert-auto-draft', {
     28                        'customize-menus-nonce': api.settings.nonce['customize-menus'],
     29                        'wp_customize': 'on',
     30                        'params': params
     31                } );
     32
     33                request.done( function( response ) {
     34                        if ( response.postId ) {
     35                                deferred.resolve( response );
     36                                component.autoDrafts.push( response.postId );
     37                                api( 'nav_menus_created_posts' ).set( component.autoDrafts );
     38                        }
     39                } );
     40
     41                request.fail( function( response ) {
     42                        var error = response || '';
     43
     44                        if ( 'undefined' !== typeof response.message ) {
     45                                error = response.message;
     46                        }
     47
     48                        console.error( error );
     49                        deferred.rejectWith( error );
     50                } );
     51
     52                return deferred.promise();
     53        };
     54
     55        api.bind( 'ready', function() {
     56
     57                api.bind( 'saved', function( data ) {
     58                        // @todo: show users links to edit newly-published posts.
     59
     60                        // Reset auto-drafts.
     61                        component.autoDrafts = []; // Reset the array the next time an item is created. Don't update the setting yet as that would trigger the customizer's dirty state.
     62                } );
     63
     64        } );
     65
     66})( wp.customize, jQuery );
  • src/wp-includes/class-wp-customize-nav-menus.php

     
    4545         * @param object $manager An instance of the WP_Customize_Manager class.
    4646         */
    4747        public function __construct( $manager ) {
    48                 $this->previewed_menus = array();
    49                 $this->manager         = $manager;
     48                $this->previewed_menus            = array();
     49                $this->manager                    = $manager;
    5050
    5151                // Skip useless hooks when the user can't manage nav menus anyway.
    5252                if ( ! current_user_can( 'edit_theme_options' ) ) {
     
    5656                add_filter( 'customize_refresh_nonces', array( $this, 'filter_nonces' ) );
    5757                add_action( 'wp_ajax_load-available-menu-items-customizer', array( $this, 'ajax_load_available_items' ) );
    5858                add_action( 'wp_ajax_search-available-menu-items-customizer', array( $this, 'ajax_search_available_items' ) );
     59                add_action( 'wp_ajax_customize-posts-insert-auto-draft', array( $this, 'ajax_add_new_auto_draft_post' ) );
    5960                add_action( 'customize_controls_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
    60 
    61                 // Needs to run after core Navigation section is set up.
    6261                add_action( 'customize_register', array( $this, 'customize_register' ), 11 );
    63 
    6462                add_filter( 'customize_dynamic_setting_args', array( $this, 'filter_dynamic_setting_args' ), 10, 2 );
    6563                add_filter( 'customize_dynamic_setting_class', array( $this, 'filter_dynamic_setting_class' ), 10, 3 );
    6664                add_action( 'customize_controls_print_footer_scripts', array( $this, 'print_templates' ) );
    6765                add_action( 'customize_controls_print_footer_scripts', array( $this, 'available_items_template' ) );
    6866                add_action( 'customize_preview_init', array( $this, 'customize_preview_init' ) );
     67                add_action( 'customize_preview_init', array( $this, 'make_auto_draft_status_previewable' ) );
     68                add_action( 'customize_save_nav_menus_created_posts', array( $this, 'publish_auto_draft_posts' ) );
    6969
    7070                // Selective Refresh partials.
    7171                add_filter( 'customize_dynamic_partial_args', array( $this, 'customize_dynamic_partial_args' ), 10, 2 );
     
    356356        public function enqueue_scripts() {
    357357                wp_enqueue_style( 'customize-nav-menus' );
    358358                wp_enqueue_script( 'customize-nav-menus' );
     359                wp_enqueue_script( 'customize-posts' );
    359360
    360361                $temp_nav_menu_setting      = new WP_Customize_Nav_Menu_Setting( $this->manager, 'nav_menu[-1]' );
    361362                $temp_nav_menu_item_setting = new WP_Customize_Nav_Menu_Item_Setting( $this->manager, 'nav_menu_item[-1]' );
     
    621622                        'section'  => 'add_menu',
    622623                        'settings' => array(),
    623624                ) ) );
     625
     626                $this->manager->add_setting( new WP_Customize_Filter_Setting( $this->manager, 'nav_menus_created_posts', array(
     627                        'transport' => 'postMessage',
     628                        'default'   => array(),
     629                ) ) );
    624630        }
    625631
    626632        /**
     
    655661                        foreach ( $post_types as $slug => $post_type ) {
    656662                                $item_types[] = array(
    657663                                        'title'  => $post_type->labels->name,
     664                                        'label'  => $post_type->labels->singular_name,
    658665                                        'type'   => 'post_type',
    659666                                        'object' => $post_type->name,
    660667                                );
     
    669676                                }
    670677                                $item_types[] = array(
    671678                                        'title'  => $taxonomy->labels->name,
     679                                        'label'  => $taxonomy->labels->singular_name,
    672680                                        'type'   => 'taxonomy',
    673681                                        'object' => $taxonomy->name,
    674682                                );
     
    688696        }
    689697
    690698        /**
     699         * Add a new `auto-draft` post.
     700         *
     701         * @access public
     702         * @since 4.6.0
     703         *
     704         * @param string $post_type The post type.
     705         * @param string $title     The post title.
     706         * @return WP_Post|WP_Error
     707         */
     708        public function insert_auto_draft_post( $post_type, $title ) {
     709
     710                $post_type_obj = get_post_type_object( $post_type );
     711                if ( ! $post_type_obj ) {
     712                        return new WP_Error( 'unknown_post_type', __( 'Unknown post type', 'customize-posts' ) );
     713                }
     714
     715                add_filter( 'wp_insert_post_empty_content', '__return_false' );
     716                $args = array(
     717                        'post_status' => 'auto-draft',
     718                        'post_type'   => $post_type,
     719                        'post_title'  => $title,
     720                        'post_name'  => sanitize_title( $title ), // Auto-drafts are allowed to have empty post_names, so we need to explicitly set it.
     721                );
     722                $r = wp_insert_post( wp_slash( $args ), true );
     723                remove_filter( 'wp_insert_post_empty_content', '__return_false' );
     724
     725                if ( is_wp_error( $r ) ) {
     726                        return $r;
     727                } else {
     728                        return get_post( $r );
     729                }
     730        }
     731
     732        /**
     733         * Ajax handler for adding a new auto-draft post.
     734         *
     735         * @action wp_ajax_customize-posts-insert-auto-draft
     736         * @access public
     737         * @since 4.6.0
     738         */
     739        public function ajax_add_new_auto_draft_post() {
     740                if ( ! check_ajax_referer( 'customize-menus', 'customize-menus-nonce' ) ) {
     741                        status_header( 400 );
     742                        wp_send_json_error( 'bad_nonce' );
     743                }
     744
     745                if ( ! current_user_can( 'customize' ) ) {
     746                        status_header( 403 );
     747                        wp_send_json_error( 'customize_not_allowed' );
     748                }
     749
     750                if ( empty( $_POST['params'] ) || ! is_array( $_POST['params'] ) ) {
     751                        status_header( 400 );
     752                        wp_send_json_error( 'missing_params' );
     753                }
     754
     755                $params = wp_unslash( $_POST['params'] );
     756
     757                if ( empty( $params['post_type'] ) ) {
     758                        status_header( 400 );
     759                        wp_send_json_error( 'missing_post_type_param' );
     760                }
     761
     762                $post_type_object = get_post_type_object( $params['post_type'] );
     763                if ( ! $post_type_object || ! current_user_can( $post_type_object->cap->create_posts ) ) {
     764                        status_header( 403 );
     765                        wp_send_json_error( 'insufficient_post_permissions' );
     766                }
     767
     768                if ( ! $params['title'] ) {
     769                        $params['title'] = '';
     770                }
     771
     772                $r = $this->insert_auto_draft_post( $post_type_object->name, $params['post_title'] );
     773                if ( is_wp_error( $r ) ) {
     774                        $error = $r;
     775                        if ( ! empty( $post_type_object->labels->singular_name ) ) {
     776                                $singular_name = $post_type_object->labels->singular_name;
     777                        } else {
     778                                $singular_name = __( 'Post' );
     779                        }
     780
     781                        $data = array(
     782                                /* translators: %1$s is the post type name and %2$s is the error message. */
     783                                'message' => sprintf( __( '%1$s could not be created: %2$s' ), $singular_name, $error->get_error_message() ),
     784                        );
     785                        wp_send_json_error( $data );
     786                } else {
     787                        $post = $r;
     788                        $data = array(
     789                                'postId' => $post->ID,
     790                                'url'    => get_permalink( $post->ID ),
     791                        );
     792                        wp_send_json_success( $data );
     793                }
     794        }
     795
     796        /**
    691797         * Print the JavaScript templates used to render Menu Customizer components.
    692798         *
    693799         * Templates are imported into the JS use wp.template.
     
    763869                                        <span class="spinner"></span>
    764870                                        <span class="clear-results"><span class="screen-reader-text"><?php _e( 'Clear Results' ); ?></span></span>
    765871                                </div>
    766                                 <ul class="accordion-section-content" data-type="search"></ul>
     872                                <ul class="accordion-section-content available-menu-items-list" data-type="search"></ul>
    767873                        </div>
    768874                        <div id="new-custom-menu-item" class="accordion-section">
    769875                                <h4 class="accordion-section-title" role="presentation">
     
    792898                                </div>
    793899                        </div>
    794900                        <?php
    795                         // Containers for per-post-type item browsing; items added with JS.
     901                        /**
     902                         * Filter the content types that do not allow new items to be created from nav menus.
     903                         *
     904                         * Types are formated as 'post_type'|'taxonomy' _ post_type_name; for example, 'taxonomy_post_format'.
     905                         * Taxonomies are not yet supported by this UI but will be in the future. Post types are only available
     906                         * here if `show_in_nav_menus` is true.
     907                         *
     908                         * @since 4.6.0
     909                         *
     910                         * @param array  $types  Array of disallowed types.
     911                         */
     912                        $disallowed_new_content_types = apply_filters( 'customize_nav_menus_disallow_new_content_types', array( 'taxonomy_post_format' ) );
     913
     914                        // Containers for per-post-type item browsing; items are added with JS.
    796915                        foreach ( $this->available_item_types() as $available_item_type ) {
    797916                                $id = sprintf( 'available-menu-items-%s-%s', $available_item_type['type'], $available_item_type['object'] );
    798917                                ?>
     
    808927                                                        <span class="toggle-indicator" aria-hidden="true"></span>
    809928                                                </button>
    810929                                        </h4>
    811                                         <ul class="accordion-section-content" data-type="<?php echo esc_attr( $available_item_type['type'] ); ?>" data-object="<?php echo esc_attr( $available_item_type['object'] ); ?>"></ul>
     930                                        <div class="accordion-section-content">
     931                                                <?php if ( 'post_type' === $available_item_type['type'] && ! in_array( $available_item_type['type'] . '_' . $available_item_type['object'], $disallowed_new_content_types ) ) : ?>
     932                                                        <?php $post_type_obj = get_post_type_object( $available_item_type['object'] ); ?>
     933                                                        <?php if ( current_user_can( $post_type_obj->cap->create_posts ) && current_user_can( $post_type_obj->cap->publish_posts ) ) : ?>
     934                                                                <div class="new-content-item">
     935                                                                        <input type="text" class="create-item-input" placeholder="<?php
     936                                                                        /* translators: %s: Singular title of post type or taxonomy */
     937                                                                        printf( __( 'Create New %s' ), $post_type_obj->labels->singular_name ); ?>">
     938                                                                        <button type="button" class="button add-content"><?php _e( 'Add' ); ?></button>
     939                                                                </div>
     940                                                        <?php endif; ?>
     941                                                <?php endif; ?>
     942                                                <ul class="available-menu-items-list" data-type="<?php echo esc_attr( $available_item_type['type'] ); ?>" data-object="<?php echo esc_attr( $available_item_type['object'] ); ?>" data-type_label="<?php echo esc_attr( $available_item_type['label'] ); ?>"></ul>
     943                                        </div>
    812944                                </div>
    813945                                <?php
    814946                        }
     
    8761008        }
    8771009
    8781010        /**
     1011         * Make the auto-draft status protected so that it can be queried.
     1012         *
     1013         * @since 4.6.0
     1014         * @access public
     1015         */
     1016        public function make_auto_draft_status_previewable() {
     1017                global $wp_post_statuses;
     1018                $wp_post_statuses['auto-draft']->protected = true;
     1019        }
     1020
     1021        /**
     1022         * Make the auto-draft status protected so that it can be queried.
     1023         *
     1024         * @since 4.6.0
     1025         * @access public
     1026         *
     1027         * @param WP_Customize_Setting $setting Customizer Setting object.
     1028         */
     1029        public function publish_auto_draft_posts( $setting ) {
     1030                $value = $setting->post_value();
     1031                if ( ! empty ( $value ) ) {
     1032                        global $wpdb;
     1033                        foreach ( $value as $index => $post_id ) {
     1034                                $post = get_post( $post_id );
     1035                                wp_publish_post( $post );
     1036                        }
     1037                }
     1038        }
     1039
     1040        /**
    8791041         * Keep track of the arguments that are being passed to wp_nav_menu().
    8801042         *
    8811043         * @since 4.3.0
  • src/wp-includes/class-wp-customize-setting.php

     
    497497        /**
    498498         * Fetch and sanitize the $_POST value for the setting.
    499499         *
     500         * During a save request prior to save, post_value() provides the new value while value() does not.
     501         *
    500502         * @since 3.4.0
    501503         *
    502504         * @param mixed $default A default value which is used as a fallback. Default is null.
  • src/wp-includes/script-loader.php

     
    461461        $scripts->add( 'customize-nav-menus', "/wp-admin/js/customize-nav-menus$suffix.js", array( 'jquery', 'wp-backbone', 'customize-controls', 'accordion', 'nav-menu' ), false, 1 );
    462462        $scripts->add( 'customize-preview-nav-menus', "/wp-includes/js/customize-preview-nav-menus$suffix.js", array( 'jquery', 'wp-util', 'customize-preview', 'customize-selective-refresh' ), false, 1 );
    463463
     464        $scripts->add( 'customize-posts', "/wp-admin/js/customize-posts$suffix.js", array( 'jquery', 'wp-backbone', 'customize-controls' ), false, 1 );
     465
    464466        $scripts->add( 'accordion', "/wp-admin/js/accordion$suffix.js", array( 'jquery' ), false, 1 );
    465467
    466468        $scripts->add( 'shortcode', "/wp-includes/js/shortcode$suffix.js", array( 'underscore' ), false, 1 );