WordPress.org

Make WordPress Core

Changeset 32806


Ignore:
Timestamp:
06/16/2015 10:07:08 PM (4 years ago)
Author:
ocean90
Message:

Add menu management to the Customizer.

This brings in the Menu Customizer plugin: https://wordpress.org/plugins/menu-customizer/.

props celloexpressions, westonruter, valendesigns, voldemortensen, ocean90, adamsilverstein, kucrut, jorbin, designsimply, afercia, davidakennedy, obenland.
see #32576.

Location:
trunk
Files:
8 added
6 edited

Legend:

Unmodified
Added
Removed
  • trunk/Gruntfile.js

    r32698 r32806  
    142142        cssmin: {
    143143            options: {
    144                 'wp-admin': ['wp-admin', 'color-picker', 'customize-controls', 'customize-widgets', 'ie', 'install', 'login', 'press-this', 'deprecated-*']
     144                'wp-admin': ['wp-admin', 'color-picker', 'customize-controls', 'customize-widgets', 'customize-nav-menus', 'ie', 'install', 'login', 'press-this', 'deprecated-*']
    145145            },
    146146            core: {
  • trunk/src/wp-includes/class-wp-customize-control.php

    r32657 r32806  
    14031403    }
    14041404}
     1405
     1406/**
     1407 * Customize Nav Menus Panel Class
     1408 *
     1409 * Needed to add screen options.
     1410 *
     1411 * @since 4.3.0
     1412 */
     1413class WP_Customize_Nav_Menus_Panel extends WP_Customize_Panel {
     1414
     1415    /**
     1416     * Control type.
     1417     *
     1418     * @since 4.3.0
     1419     *
     1420     * @access public
     1421     * @var string
     1422     */
     1423    public $type = 'nav_menus';
     1424
     1425    /**
     1426     * Render screen options for Menus.
     1427     *
     1428     * @since 4.3.0
     1429     */
     1430    public function render_screen_options() {
     1431        // Essentially adds the screen options.
     1432        add_filter( 'manage_nav-menus_columns', array( $this, 'wp_nav_menu_manage_columns' ) );
     1433
     1434        // Display screen options.
     1435        $screen = WP_Screen::get( 'nav-menus.php' );
     1436        $screen->render_screen_options();
     1437    }
     1438
     1439    /**
     1440     * Returns the advanced options for the nav menus page.
     1441     *
     1442     * Link title attribute added as it's a relatively advanced concept for new users.
     1443     *
     1444     * @since 4.3.0
     1445     *
     1446     * @return array The advanced menu properties.
     1447     */
     1448    function wp_nav_menu_manage_columns() {
     1449        return array(
     1450            '_title'      => __( 'Show advanced menu properties' ),
     1451            'cb'          => '<input type="checkbox" />',
     1452            'link-target' => __( 'Link Target' ),
     1453            'attr-title'  => __( 'Title Attribute' ),
     1454            'css-classes' => __( 'CSS Classes' ),
     1455            'xfn'         => __( 'Link Relationship (XFN)' ),
     1456            'description' => __( 'Description' ),
     1457        );
     1458    }
     1459
     1460    /**
     1461     * An Underscore (JS) template for this panel's content (but not its container).
     1462     *
     1463     * Class variables for this panel class are available in the `data` JS object;
     1464     * export custom variables by overriding {@see WP_Customize_Panel::json()}.
     1465     *
     1466     * @since 4.3.0
     1467     *
     1468     * @see WP_Customize_Panel::print_template()
     1469     *
     1470     * @since 4.3.0
     1471     */
     1472    protected function content_template() {
     1473        ?>
     1474        <li class="panel-meta customize-info accordion-section <# if ( ! data.description ) { #> cannot-expand<# } #>">
     1475            <button type="button" class="customize-panel-back" tabindex="-1">
     1476                <span class="screen-reader-text"><?php _e( 'Back' ); ?></span>
     1477            </button>
     1478            <div class="accordion-section-title">
     1479                <span class="preview-notice">
     1480                    <?php
     1481                        /* translators: %s is the site/panel title in the Customizer */
     1482                        printf( __( 'You are customizing %s' ), '<strong class="panel-title">{{ data.title }}</strong>' );
     1483                    ?>
     1484                </span>
     1485                <button type="button" class="customize-screen-options-toggle" aria-expanded="false">
     1486                    <span class="screen-reader-text"><?php _e( 'Menu Options' ); ?></span>
     1487                </button>
     1488                <button type="button" class="customize-help-toggle dashicons dashicons-editor-help" aria-expanded="false">
     1489                    <span class="screen-reader-text"><?php _e( 'Help' ); ?></span>
     1490                </button>
     1491            </div>
     1492            <# if ( data.description ) { #>
     1493            <div class="description customize-panel-description">{{{ data.description }}}</div>
     1494            <# } #>
     1495            <?php $this->render_screen_options(); ?>
     1496        </li>
     1497        <?php
     1498    }
     1499}
     1500
     1501/**
     1502 * Customize Nav Menu Control Class
     1503 *
     1504 * @since 4.3.0
     1505 */
     1506class WP_Customize_Nav_Menu_Control extends WP_Customize_Control {
     1507
     1508    /**
     1509     * Control type.
     1510     *
     1511     * @since 4.3.0
     1512     *
     1513     * @access public
     1514     * @var string
     1515     */
     1516    public $type = 'nav_menu';
     1517
     1518    /**
     1519     * The nav menu setting.
     1520     *
     1521     * @since 4.3.0
     1522     *
     1523     * @var WP_Customize_Nav_Menu_Setting
     1524     */
     1525    public $setting;
     1526
     1527    /**
     1528     * Don't render the control's content - it uses a JS template instead.
     1529     *
     1530     * @since 4.3.0
     1531     */
     1532    public function render_content() {}
     1533
     1534    /**
     1535     * JS/Underscore template for the control UI.
     1536     *
     1537     * @since 4.3.0
     1538     */
     1539    public function content_template() {
     1540        ?>
     1541        <button type="button" class="button-secondary add-new-menu-item">
     1542            <?php _e( 'Add Items' ); ?>
     1543        </button>
     1544        <button type="button" class="not-a-button reorder-toggle">
     1545            <span class="reorder"><?php _ex( 'Reorder', 'Reorder menu items in Customizer' ); ?></span>
     1546            <span class="reorder-done"><?php _ex( 'Done', 'Cancel reordering menu items in Customizer' ); ?></span>
     1547        </button>
     1548        <span class="add-menu-item-loading spinner"></span>
     1549        <span class="menu-delete-item">
     1550            <button type="button" class="not-a-button menu-delete">
     1551                <?php _e( 'Delete menu' ); ?> <span class="screen-reader-text">{{ data.menu_name }}</span>
     1552            </button>
     1553        </span>
     1554        <?php if ( current_theme_supports( 'menus' ) ) : ?>
     1555        <ul class="menu-settings">
     1556            <li class="customize-control">
     1557                <span class="customize-control-title"><?php _e( 'Menu locations' ); ?></span>
     1558            </li>
     1559
     1560            <?php foreach ( get_registered_nav_menus() as $location => $description ) : ?>
     1561            <li class="customize-control customize-control-checkbox assigned-menu-location">
     1562                <label>
     1563                    <input type="checkbox" data-menu-id="{{ data.menu_id }}" data-location-id="<?php echo esc_attr( $location ); ?>" class="menu-location" /> <?php echo $description; ?>
     1564                    <span class="theme-location-set"><?php printf( _x( '(Current: %s)', 'Current menu location' ), '<span class="current-menu-location-name-' . esc_attr( $location ) . '"></span>' ); ?></span>
     1565                </label>
     1566            </li>
     1567            <?php endforeach; ?>
     1568
     1569        </ul>
     1570        <?php endif; ?>
     1571        <p>
     1572            <label>
     1573                <input type="checkbox" class="auto_add">
     1574                <?php _e( 'Automatically add new top-level pages to this menu.' ) ?>
     1575            </label>
     1576        </p>
     1577        <?php
     1578    }
     1579
     1580    /**
     1581     * Return params for this control.
     1582     *
     1583     * @since 4.3.0
     1584     *
     1585     * @return array
     1586     */
     1587    function json() {
     1588        $exported            = parent::json();
     1589        $exported['menu_id'] = $this->setting->term_id;
     1590
     1591        return $exported;
     1592    }
     1593}
     1594
     1595/**
     1596 * Customize control to represent the name field for a given menu.
     1597 *
     1598 * @since 4.3.0
     1599 */
     1600class WP_Customize_Nav_Menu_Item_Control extends WP_Customize_Control {
     1601
     1602    /**
     1603     * Control type.
     1604     *
     1605     * @since 4.3.0
     1606     *
     1607     * @access public
     1608     * @var string
     1609     */
     1610    public $type = 'nav_menu_item';
     1611
     1612    /**
     1613     * The nav menu item setting.
     1614     *
     1615     * @since 4.3.0
     1616     *
     1617     * @var WP_Customize_Nav_Menu_Item_Setting
     1618     */
     1619    public $setting;
     1620
     1621    /**
     1622     * Constructor.
     1623     *
     1624     * @since 4.3.0
     1625     *
     1626     * @uses WP_Customize_Control::__construct()
     1627     *
     1628     * @param WP_Customize_Manager $manager An instance of the WP_Customize_Manager class.
     1629     * @param string               $id      The control ID.
     1630     * @param array                $args    Optional. Overrides class property defaults.
     1631     */
     1632    public function __construct( $manager, $id, $args = array() ) {
     1633        parent::__construct( $manager, $id, $args );
     1634    }
     1635
     1636    /**
     1637     * Don't render the control's content - it's rendered with a JS template.
     1638     *
     1639     * @since 4.3.0
     1640     */
     1641    public function render_content() {}
     1642
     1643    /**
     1644     * JS/Underscore template for the control UI.
     1645     *
     1646     * @since 4.3.0
     1647     */
     1648    public function content_template() {
     1649        ?>
     1650        <dl class="menu-item-bar">
     1651            <dt class="menu-item-handle">
     1652                <span class="item-type">{{ data.item_type_label }}</span>
     1653                <span class="item-title">
     1654                    <span class="spinner"></span>
     1655                    <span class="menu-item-title">{{ data.title }}</span>
     1656                </span>
     1657                <span class="item-controls">
     1658                    <button type="button" class="not-a-button item-edit"><span class="screen-reader-text"><?php _e( 'Edit Menu Item' ); ?></span></button>
     1659                    <button type="button" class="not-a-button item-delete submitdelete deletion"><span class="screen-reader-text"><?php _e( 'Remove Menu Item' ); ?></span></button>
     1660                </span>
     1661            </dt>
     1662        </dl>
     1663
     1664        <div class="menu-item-settings" id="menu-item-settings-{{ data.menu_item_id }}">
     1665            <# if ( 'custom' === data.item_type ) { #>
     1666            <p class="field-url description description-thin">
     1667                <label for="edit-menu-item-url-{{ data.menu_item_id }}">
     1668                    <?php _e( 'URL' ); ?><br />
     1669                    <input class="widefat code edit-menu-item-url" type="text" id="edit-menu-item-url-{{ data.menu_item_id }}" name="menu-item-url" />
     1670                </label>
     1671            </p>
     1672        <# } #>
     1673            <p class="description description-thin">
     1674                <label for="edit-menu-item-title-{{ data.menu_item_id }}">
     1675                    <?php _e( 'Navigation Label' ); ?><br />
     1676                    <input type="text" id="edit-menu-item-title-{{ data.menu_item_id }}" class="widefat edit-menu-item-title" name="menu-item-title" />
     1677                </label>
     1678            </p>
     1679            <p class="field-link-target description description-thin">
     1680                <label for="edit-menu-item-target-{{ data.menu_item_id }}">
     1681                    <input type="checkbox" id="edit-menu-item-target-{{ data.menu_item_id }}" class="edit-menu-item-target" value="_blank" name="menu-item-target" />
     1682                    <?php _e( 'Open link in a new tab' ); ?>
     1683                </label>
     1684            </p>
     1685            <p class="field-attr-title description description-thin">
     1686                <label for="edit-menu-item-attr-title-{{ data.menu_item_id }}">
     1687                    <?php _e( 'Title Attribute' ); ?><br />
     1688                    <input type="text" id="edit-menu-item-attr-title-{{ data.menu_item_id }}" class="widefat edit-menu-item-attr-title" name="menu-item-attr-title" />
     1689                </label>
     1690            </p>
     1691            <p class="field-css-classes description description-thin">
     1692                <label for="edit-menu-item-classes-{{ data.menu_item_id }}">
     1693                    <?php _e( 'CSS Classes' ); ?><br />
     1694                    <input type="text" id="edit-menu-item-classes-{{ data.menu_item_id }}" class="widefat code edit-menu-item-classes" name="menu-item-classes" />
     1695                </label>
     1696            </p>
     1697            <p class="field-xfn description description-thin">
     1698                <label for="edit-menu-item-xfn-{{ data.menu_item_id }}">
     1699                    <?php _e( 'Link Relationship (XFN)' ); ?><br />
     1700                    <input type="text" id="edit-menu-item-xfn-{{ data.menu_item_id }}" class="widefat code edit-menu-item-xfn" name="menu-item-xfn" />
     1701                </label>
     1702            </p>
     1703            <p class="field-description description description-thin">
     1704                <label for="edit-menu-item-description-{{ data.menu_item_id }}">
     1705                    <?php _e( 'Description' ); ?><br />
     1706                    <textarea id="edit-menu-item-description-{{ data.menu_item_id }}" class="widefat edit-menu-item-description" rows="3" cols="20" name="menu-item-description">{{ data.description }}</textarea>
     1707                    <span class="description"><?php _e( 'The description will be displayed in the menu if the current theme supports it.' ); ?></span>
     1708                </label>
     1709            </p>
     1710
     1711            <div class="menu-item-actions description-thin submitbox">
     1712                <# if ( 'custom' != data.item_type && '' != data.original_title ) { #>
     1713                <p class="link-to-original">
     1714                    <?php printf( __( 'Original: %s' ), '<a class="original-link" href="{{ data.url }}">{{{ data.original_title }}}</a>' ); ?>
     1715                </p>
     1716                <# } #>
     1717
     1718                <button type="button" class="not-a-button item-delete submitdelete deletion"><?php _e( 'Remove' ); ?></button>
     1719                <span class="spinner"></span>
     1720            </div>
     1721            <input type="hidden" name="menu-item-db-id[{{ data.menu_item_id }}]" class="menu-item-data-db-id" value="{{ data.menu_item_id }}" />
     1722            <input type="hidden" name="menu-item-parent-id[{{ data.menu_item_id }}]" class="menu-item-data-parent-id" value="{{ data.parent }}" />
     1723        </div><!-- .menu-item-settings-->
     1724        <ul class="menu-item-transport"></ul>
     1725        <?php
     1726    }
     1727
     1728    /**
     1729     * Return params for this control.
     1730     *
     1731     * @since 4.3.0
     1732     *
     1733     * @return array
     1734     */
     1735    function json() {
     1736        $exported                 = parent::json();
     1737        $exported['menu_item_id'] = $this->setting->post_id;
     1738
     1739        return $exported;
     1740    }
     1741}
     1742
     1743/**
     1744 * Customize Menu Location Control Class
     1745 *
     1746 * This custom control is only needed for JS.
     1747 *
     1748 * @since 4.3.0
     1749 */
     1750class WP_Customize_Nav_Menu_Location_Control extends WP_Customize_Control {
     1751
     1752    /**
     1753     * Control type.
     1754     *
     1755     * @since 4.3.0
     1756     *
     1757     * @access public
     1758     * @var string
     1759     */
     1760    public $type = 'nav_menu_location';
     1761
     1762    /**
     1763     * Location ID.
     1764     *
     1765     * @since 4.3.0
     1766     *
     1767     * @access public
     1768     * @var string
     1769     */
     1770    public $location_id = '';
     1771
     1772    /**
     1773     * Refresh the parameters passed to JavaScript via JSON.
     1774     *
     1775     * @since 4.3.0
     1776     *
     1777     * @uses WP_Customize_Control::to_json()
     1778     */
     1779    public function to_json() {
     1780        parent::to_json();
     1781        $this->json['locationId'] = $this->location_id;
     1782    }
     1783
     1784    /**
     1785     * Render content just like a normal select control.
     1786     *
     1787     * @since 4.3.0
     1788     */
     1789    public function render_content() {
     1790        if ( empty( $this->choices ) ) {
     1791            return;
     1792        }
     1793        ?>
     1794        <label>
     1795            <?php if ( ! empty( $this->label ) ) : ?>
     1796            <span class="customize-control-title"><?php echo esc_html( $this->label ); ?></span>
     1797            <?php endif; ?>
     1798
     1799            <?php if ( ! empty( $this->description ) ) : ?>
     1800            <span class="description customize-control-description"><?php echo $this->description; ?></span>
     1801            <?php endif; ?>
     1802
     1803            <select <?php $this->link(); ?>>
     1804                <?php
     1805                foreach ( $this->choices as $value => $label ) :
     1806                    echo '<option value="' . esc_attr( $value ) . '"' . selected( $this->value(), $value, false ) . '>' . $label . '</option>';
     1807                endforeach;
     1808                ?>
     1809            </select>
     1810        </label>
     1811        <?php
     1812    }
     1813}
     1814
     1815/**
     1816 * Customize control to represent the name field for a given menu.
     1817 *
     1818 * @since 4.3.0
     1819 */
     1820class WP_Customize_Nav_Menu_Name_Control extends WP_Customize_Control {
     1821
     1822    /**
     1823     * Type of control, used by JS.
     1824     *
     1825     * @since 4.3.0
     1826     *
     1827     * @var string
     1828     */
     1829    public $type = 'nav_menu_name';
     1830
     1831    /**
     1832     * No-op since we're using JS template.
     1833     *
     1834     * @since 4.3.0
     1835     */
     1836    protected function render_content() {}
     1837
     1838    /**
     1839     * Render the Underscore template for this control.
     1840     *
     1841     * @since 4.3.0
     1842     */
     1843    protected function content_template() {
     1844        ?>
     1845        <label>
     1846            <input type="text" class="menu-name-field live-update-section-title" />
     1847        </label>
     1848        <?php
     1849    }
     1850}
     1851
     1852/**
     1853 * Customize control class for new menus.
     1854 *
     1855 * @since 4.3.0
     1856 */
     1857class WP_New_Menu_Customize_Control extends WP_Customize_Control {
     1858
     1859    /**
     1860     * Control type.
     1861     *
     1862     * @since 4.3.0
     1863     *
     1864     * @access public
     1865     * @var string
     1866     */
     1867    public $type = 'new_menu';
     1868
     1869    /**
     1870     * Render the control's content.
     1871     *
     1872     * @since 4.3.0
     1873     */
     1874    public function render_content() {
     1875        ?>
     1876        <button type="button" class="button button-primary" id="create-new-menu-submit"><?php _e( 'Create Menu' ); ?></button>
     1877        <span class="spinner"></span>
     1878        <?php
     1879    }
     1880}
  • trunk/src/wp-includes/class-wp-customize-manager.php

    r32744 r32806  
    5050    public $widgets;
    5151
     52    /**
     53     * Methods and properties deailing with managing nav menus in the Customizer.
     54     *
     55     * @var WP_Customize_Nav_Menus
     56     */
     57    public $nav_menus;
     58
    5259    protected $settings   = array();
    5360    protected $containers = array();
     
    105112        require_once( ABSPATH . WPINC . '/class-wp-customize-control.php' );
    106113        require_once( ABSPATH . WPINC . '/class-wp-customize-widgets.php' );
     114        require_once( ABSPATH . WPINC . '/class-wp-customize-nav-menus.php' );
    107115
    108116        $this->widgets = new WP_Customize_Widgets( $this );
     117        $this->nav_menus = new WP_Customize_Nav_Menus( $this );
    109118
    110119        add_filter( 'wp_die_handler', array( $this, 'wp_die_handler' ) );
     
    14821491            foreach ( array( 'color', 'image', 'position_x', 'repeat', 'attachment' ) as $prop ) {
    14831492                $this->get_setting( 'background_' . $prop )->transport = 'postMessage';
    1484             }
    1485         }
    1486 
    1487         /* Nav Menus */
    1488 
    1489         $locations      = get_registered_nav_menus();
    1490         $menus          = wp_get_nav_menus();
    1491         $num_locations  = count( array_keys( $locations ) );
    1492 
    1493         if ( 1 == $num_locations ) {
    1494             $description = __( 'Your theme supports one menu. Select which menu you would like to use.' );
    1495         } else {
    1496             $description = sprintf( _n( 'Your theme supports %s menu. Select which menu appears in each location.', 'Your theme supports %s menus. Select which menu appears in each location.', $num_locations ), number_format_i18n( $num_locations ) );
    1497         }
    1498 
    1499         $this->add_section( 'nav', array(
    1500             'title'          => __( 'Navigation' ),
    1501             'theme_supports' => 'menus',
    1502             'priority'       => 100,
    1503             'description'    => $description . "\n\n" . __( 'You can edit your menu content on the Menus screen in the Appearance section.' ),
    1504         ) );
    1505 
    1506         if ( $menus ) {
    1507             $choices = array( '' => __( '&mdash; Select &mdash;' ) );
    1508             foreach ( $menus as $menu ) {
    1509                 $choices[ $menu->term_id ] = wp_html_excerpt( $menu->name, 40, '&hellip;' );
    1510             }
    1511 
    1512             foreach ( $locations as $location => $description ) {
    1513                 $menu_setting_id = "nav_menu_locations[{$location}]";
    1514 
    1515                 $this->add_setting( $menu_setting_id, array(
    1516                     'sanitize_callback' => 'absint',
    1517                     'theme_supports'    => 'menus',
    1518                 ) );
    1519 
    1520                 $this->add_control( $menu_setting_id, array(
    1521                     'label'   => $description,
    1522                     'section' => 'nav',
    1523                     'type'    => 'select',
    1524                     'choices' => $choices,
    1525                 ) );
    15261493            }
    15271494        }
  • trunk/src/wp-includes/class-wp-customize-section.php

    r32658 r32806  
    502502    }
    503503}
     504
     505/**
     506 * Customize Menu Section Class
     507 *
     508 * Custom section only needed in JS.
     509 *
     510 * @since 4.3.0
     511 */
     512class WP_Customize_Nav_Menu_Section extends WP_Customize_Section {
     513
     514    /**
     515     * Control type.
     516     *
     517     * @since 4.3.0
     518     *
     519     * @access public
     520     * @var string
     521     */
     522    public $type = 'nav_menu';
     523
     524    /**
     525     * Get section params for JS.
     526     *
     527     * @since 4.3.0
     528     *
     529     * @return array
     530     */
     531    function json() {
     532        $exported = parent::json();
     533        $exported['menu_id'] = intval( preg_replace( '/^nav_menu\[(\d+)\]/', '$1', $this->id ) );
     534
     535        return $exported;
     536    }
     537}
     538
     539/**
     540 * Customize Menu Section Class
     541 *
     542 * Implements the new-menu-ui toggle button instead of a regular section.
     543 *
     544 * @since 4.3.0
     545 */
     546class WP_Customize_New_Menu_Section extends WP_Customize_Section {
     547
     548    /**
     549     * Control type.
     550     *
     551     * @since 4.3.0
     552     *
     553     * @access public
     554     * @var string
     555     */
     556    public $type = 'new_menu';
     557
     558    /**
     559     * Render the section, and the controls that have been added to it.
     560     *
     561     * @since 4.3.0
     562     */
     563    protected function render() {
     564        ?>
     565        <li id="accordion-section-<?php echo esc_attr( $this->id ); ?>" class="accordion-section-new-menu">
     566            <button type="button" class="button-secondary add-new-menu-item add-menu-toggle">
     567                <?php echo esc_html( $this->title ); ?>
     568                <span class="screen-reader-text"><?php _e( 'Press return or enter to open' ); ?></span>
     569            </button>
     570            <ul class="new-menu-section-content"></ul>
     571        </li>
     572        <?php
     573    }
     574}
  • trunk/src/wp-includes/class-wp-customize-setting.php

    r32767 r32806  
    631631    }
    632632}
     633
     634/**
     635 * Customize Setting to represent a nav_menu.
     636 *
     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.
     639 *
     640 * @since 4.3.0
     641 *
     642 * @see wp_get_nav_menu_items()
     643 * @see WP_Customize_Setting
     644 */
     645class WP_Customize_Nav_Menu_Item_Setting extends WP_Customize_Setting {
     646
     647    const ID_PATTERN = '/^nav_menu_item\[(?P<id>-?\d+)\]$/';
     648
     649    const POST_TYPE = 'nav_menu_item';
     650
     651    const TYPE = 'nav_menu_item';
     652
     653    /**
     654     * Setting type.
     655     *
     656     * @since 4.3.0
     657     *
     658     * @var string
     659     */
     660    public $type = self::TYPE;
     661
     662    /**
     663     * Default setting value.
     664     *
     665     * @since 4.3.0
     666     *
     667     * @see wp_setup_nav_menu_item()
     668     * @var array
     669     */
     670    public $default = array(
     671        // The $menu_item_data for wp_update_nav_menu_item().
     672        'object_id'        => 0,
     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.
     677        'title'            => '',
     678        'url'              => '',
     679        'target'           => '',
     680        'attr_title'       => '',
     681        'description'      => '',
     682        'classes'          => '',
     683        'xfn'              => '',
     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?
     688    );
     689
     690    /**
     691     * Default transport.
     692     *
     693     * @since 4.3.0
     694     *
     695     * @var string
     696     */
     697    public $transport = 'postMessage';
     698
     699    /**
     700     * The post ID represented by this setting instance. This is the db_id.
     701     *
     702     * A negative value represents a placeholder ID for a new menu not yet saved.
     703     *
     704     * @todo Should this be $db_id, and also use this for WP_Customize_Nav_Menu_Setting::$term_id
     705     *
     706     * @since 4.3.0
     707     *
     708     * @var int
     709     */
     710    public $post_id;
     711
     712    /**
     713     * Previous (placeholder) post ID used before creating a new menu item.
     714     *
     715     * This value will be exported to JS via the customize_save_response filter
     716     * so that JavaScript can update the settings to refer to the newly-assigned
     717     * post ID. This value is always negative to indicate it does not refer to
     718     * a real post.
     719     *
     720     * @since 4.3.0
     721     *
     722     * @see WP_Customize_Nav_Menu_Item_Setting::update()
     723     * @see WP_Customize_Nav_Menu_Item_Setting::amend_customize_save_response()
     724     *
     725     * @var int
     726     */
     727    public $previous_post_id;
     728
     729    /**
     730     * When previewing or updating a menu item, this stores the previous nav_menu_term_id
     731     * which ensures that we can apply the proper filters.
     732     *
     733     * @since 4.3.0
     734     *
     735     * @var int
     736     */
     737    public $original_nav_menu_term_id;
     738
     739    /**
     740     * Whether or not preview() was called.
     741     *
     742     * @since 4.3.0
     743     *
     744     * @var bool
     745     */
     746    protected $is_previewed = false;
     747
     748    /**
     749     * Whether or not update() was called.
     750     *
     751     * @since 4.3.0
     752     *
     753     * @var bool
     754     */
     755    protected $is_updated = false;
     756
     757    /**
     758     * Status for calling the update method, used in customize_save_response filter.
     759     *
     760     * When status is inserted, the placeholder post ID is stored in $previous_post_id.
     761     * When status is error, the error is stored in $update_error.
     762     *
     763     * @since 4.3.0
     764     *
     765     * @see WP_Customize_Nav_Menu_Item_Setting::update()
     766     * @see WP_Customize_Nav_Menu_Item_Setting::amend_customize_save_response()
     767     *
     768     * @var string updated|inserted|deleted|error
     769     */
     770    public $update_status;
     771
     772    /**
     773     * Any error object returned by wp_update_nav_menu_item() when setting is updated.
     774     *
     775     * @since 4.3.0
     776     *
     777     * @see WP_Customize_Nav_Menu_Item_Setting::update()
     778     * @see WP_Customize_Nav_Menu_Item_Setting::amend_customize_save_response()
     779     *
     780     * @var WP_Error
     781     */
     782    public $update_error;
     783
     784    /**
     785     * Constructor.
     786     *
     787     * Any supplied $args override class property defaults.
     788     *
     789     * @since 4.3.0
     790     *
     791     * @param WP_Customize_Manager $manager Manager instance.
     792     * @param string               $id      An specific ID of the setting. Can be a
     793     *                                      theme mod or option name.
     794     * @param array                $args    Optional. Setting arguments.
     795     * @throws Exception If $id is not valid for this setting type.
     796     */
     797    public function __construct( WP_Customize_Manager $manager, $id, array $args = array() ) {
     798        if ( empty( $manager->nav_menus ) ) {
     799            throw new Exception( 'Expected WP_Customize_Manager::$nav_menus to be set.' );
     800        }
     801
     802        if ( ! preg_match( self::ID_PATTERN, $id, $matches ) ) {
     803            throw new Exception( "Illegal widget setting ID: $id" );
     804        }
     805
     806        $this->post_id = intval( $matches['id'] );
     807
     808        $menu = $this->value();
     809        $this->original_nav_menu_term_id = $menu['nav_menu_term_id'];
     810
     811        parent::__construct( $manager, $id, $args );
     812    }
     813
     814    /**
     815     * Get the instance data for a given widget setting.
     816     *
     817     * @since 4.3.0
     818     *
     819     * @see wp_setup_nav_menu_item()
     820     *
     821     * @return array
     822     */
     823    public function value() {
     824        if ( $this->is_previewed && $this->_previewed_blog_id === get_current_blog_id() ) {
     825            $undefined  = new stdClass(); // Symbol.
     826            $post_value = $this->post_value( $undefined );
     827
     828            if ( $undefined === $post_value ) {
     829                $value = $this->_original_value;
     830            } else {
     831                $value = $post_value;
     832            }
     833        } else {
     834            $value = false;
     835
     836            // Note that a ID of less than one indicates a nav_menu not yet inserted.
     837            if ( $this->post_id > 0 ) {
     838                $post = get_post( $this->post_id );
     839                if ( $post && self::POST_TYPE === $post->post_type ) {
     840                    $item  = wp_setup_nav_menu_item( $post );
     841                    $value = wp_array_slice_assoc(
     842                        (array) $item,
     843                        array_keys( $this->default )
     844                    );
     845                    $value['position']       = $item->menu_order;
     846                    $value['status']         = $item->post_status;
     847                    $value['original_title'] = '';
     848
     849                    $menus = wp_get_post_terms( $post->ID, WP_Customize_Nav_Menu_Setting::TAXONOMY, array(
     850                        'fields' => 'ids',
     851                    ) );
     852
     853                    if ( ! empty( $menus ) ) {
     854                        $value['nav_menu_term_id'] = array_shift( $menus );
     855                    } else {
     856                        $value['nav_menu_term_id'] = 0;
     857                    }
     858
     859                    if ( 'post_type' === $value['type'] ) {
     860                        $original_title = get_the_title( $value['object_id'] );
     861                    } else if ( 'taxonomy' === $value['type'] ) {
     862                        $original_title = get_term_field( 'name', $value['object_id'], $value['object'], 'raw' );
     863                        if ( is_wp_error( $original_title ) ) {
     864                            $original_title = '';
     865                        }
     866                    }
     867
     868                    if ( ! empty( $original_title ) ) {
     869                        $value['original_title'] = $original_title;
     870                    }
     871                }
     872            }
     873
     874            if ( ! is_array( $value ) ) {
     875                $value = $this->default;
     876            }
     877        }
     878
     879        if ( is_array( $value ) ) {
     880            foreach ( array( 'object_id', 'menu_item_parent', 'nav_menu_term_id' ) as $key ) {
     881                $value[ $key ] = intval( $value[ $key ] );
     882            }
     883        }
     884
     885        return $value;
     886    }
     887
     888    /**
     889     * Handle previewing the setting.
     890     *
     891     * @since 4.3.0
     892     *
     893     * @see WP_Customize_Manager::post_value()
     894     */
     895    public function preview() {
     896        if ( $this->is_previewed ) {
     897            return;
     898        }
     899
     900        $this->is_previewed              = true;
     901        $this->_original_value           = $this->value();
     902        $this->original_nav_menu_term_id = $this->_original_value['nav_menu_term_id'];
     903        $this->_previewed_blog_id        = get_current_blog_id();
     904
     905        add_filter( 'wp_get_nav_menu_items', array( $this, 'filter_wp_get_nav_menu_items' ), 10, 3 );
     906
     907        $sort_callback = array( __CLASS__, 'sort_wp_get_nav_menu_items' );
     908        if ( ! has_filter( 'wp_get_nav_menu_items', $sort_callback ) ) {
     909            add_filter( 'wp_get_nav_menu_items', array( __CLASS__, 'sort_wp_get_nav_menu_items' ), 1000, 3 );
     910        }
     911
     912        // @todo Add get_post_metadata filters for plugins to add their data.
     913    }
     914
     915    /**
     916     * Filter the wp_get_nav_menu_items() result to supply the previewed menu items.
     917     *
     918     * @since 4.3.0
     919     *
     920     * @see wp_get_nav_menu_items()
     921     *
     922     * @param array  $items An array of menu item post objects.
     923     * @param object $menu  The menu object.
     924     * @param array  $args  An array of arguments used to retrieve menu item objects.
     925     * @return array Array of menu items,
     926     */
     927    function filter_wp_get_nav_menu_items( $items, $menu, $args ) {
     928        $this_item = $this->value();
     929        $current_nav_menu_term_id = $this_item['nav_menu_term_id'];
     930        unset( $this_item['nav_menu_term_id'] );
     931
     932        $should_filter = (
     933            $menu->term_id === $this->original_nav_menu_term_id
     934            ||
     935            $menu->term_id === $current_nav_menu_term_id
     936        );
     937        if ( ! $should_filter ) {
     938            return $items;
     939        }
     940
     941        // Handle deleted menu item, or menu item moved to another menu.
     942        $should_remove = (
     943            false === $this_item
     944            ||
     945            (
     946                $this->original_nav_menu_term_id === $menu->term_id
     947                &&
     948                $current_nav_menu_term_id !== $this->original_nav_menu_term_id
     949            )
     950        );
     951        if ( $should_remove ) {
     952            $filtered_items = array();
     953            foreach ( $items as $item ) {
     954                if ( $item->db_id !== $this->post_id ) {
     955                    $filtered_items[] = $item;
     956                }
     957            }
     958            return $filtered_items;
     959        }
     960
     961        $mutated = false;
     962        $should_update = (
     963            is_array( $this_item )
     964            &&
     965            $current_nav_menu_term_id === $menu->term_id
     966        );
     967        if ( $should_update ) {
     968            foreach ( $items as $item ) {
     969                if ( $item->db_id === $this->post_id ) {
     970                    foreach ( get_object_vars( $this->value_as_wp_post_nav_menu_item() ) as $key => $value ) {
     971                        $item->$key = $value;
     972                    }
     973                    $mutated = true;
     974                }
     975            }
     976
     977            // Not found so we have to append it..
     978            if ( ! $mutated ) {
     979                $items[] = $this->value_as_wp_post_nav_menu_item();
     980            }
     981        }
     982
     983        return $items;
     984    }
     985
     986    /**
     987     * Re-apply the tail logic also applied on $items by wp_get_nav_menu_items().
     988     *
     989     * @since 4.3.0
     990     *
     991     * @see wp_get_nav_menu_items()
     992     *
     993     * @param array  $items An array of menu item post objects.
     994     * @param object $menu  The menu object.
     995     * @param array  $args  An array of arguments used to retrieve menu item objects.
     996     * @return array Array of menu items,
     997     */
     998    static function sort_wp_get_nav_menu_items( $items, $menu, $args ) {
     999        // @todo We should probably re-apply some constraints imposed by $args.
     1000        unset( $args['include'] );
     1001
     1002        // Remove invalid items only in frontend.
     1003        if ( ! is_admin() ) {
     1004            $items = array_filter( $items, '_is_valid_nav_menu_item' );
     1005        }
     1006
     1007        if ( ARRAY_A === $args['output'] ) {
     1008            $GLOBALS['_menu_item_sort_prop'] = $args['output_key'];
     1009            usort( $items, '_sort_nav_menu_items' );
     1010            $i = 1;
     1011
     1012            foreach ( $items as $k => $item ) {
     1013                $items[ $k ]->$args['output_key'] = $i++;
     1014            }
     1015        }
     1016
     1017        return $items;
     1018    }
     1019
     1020    /**
     1021     * Get the value emulated into a WP_Post and set up as a nav_menu_item.
     1022     *
     1023     * @since 4.3.0
     1024     *
     1025     * @return WP_Post With {@see wp_setup_nav_menu_item()} applied.
     1026     */
     1027    public function value_as_wp_post_nav_menu_item() {
     1028        $item = (object) $this->value();
     1029        unset( $item->nav_menu_term_id );
     1030
     1031        $item->post_status = $item->status;
     1032        unset( $item->status );
     1033
     1034        $item->post_type = 'nav_menu_item';
     1035        $item->menu_order = $item->position;
     1036        unset( $item->position );
     1037
     1038        $item->post_author = get_current_user_id();
     1039
     1040        if ( $item->title ) {
     1041            $item->post_title = $item->title;
     1042        }
     1043
     1044        $item->ID = $this->post_id;
     1045        $post = new WP_Post( (object) $item );
     1046        $post = wp_setup_nav_menu_item( $post );
     1047
     1048        return $post;
     1049    }
     1050
     1051    /**
     1052     * Sanitize an input.
     1053     *
     1054     * Note that parent::sanitize() erroneously does wp_unslash() on $value, but
     1055     * we remove that in this override.
     1056     *
     1057     * @since 4.3.0
     1058     *
     1059     * @param array $menu_item_value The value to sanitize.
     1060     * @return array|false|null Null if an input isn't valid. False if it is marked for deletion. Otherwise the sanitized value.
     1061     */
     1062    public function sanitize( $menu_item_value ) {
     1063        // Menu is marked for deletion.
     1064        if ( false === $menu_item_value ) {
     1065            return $menu_item_value;
     1066        }
     1067
     1068        // Invalid.
     1069        if ( ! is_array( $menu_item_value ) ) {
     1070            return null;
     1071        }
     1072
     1073        $default = array(
     1074            'object_id'        => 0,
     1075            'object'           => '',
     1076            'menu_item_parent' => 0,
     1077            'position'         => 0,
     1078            'type'             => 'custom',
     1079            'title'            => '',
     1080            'url'              => '',
     1081            'target'           => '',
     1082            'attr_title'       => '',
     1083            'description'      => '',
     1084            'classes'          => '',
     1085            'xfn'              => '',
     1086            'status'           => 'publish',
     1087            'original_title'   => '',
     1088            'nav_menu_term_id' => 0,
     1089        );
     1090        $menu_item_value = array_merge( $default, $menu_item_value );
     1091        $menu_item_value = wp_array_slice_assoc( $menu_item_value, array_keys( $default ) );
     1092        $menu_item_value['position'] = max( 0, intval( $menu_item_value['position'] ) );
     1093
     1094        foreach ( array( 'object_id', 'menu_item_parent', 'nav_menu_term_id' ) as $key ) {
     1095            // Note we need to allow negative-integer IDs for previewed objects not inserted yet.
     1096            $menu_item_value[ $key ] = intval( $menu_item_value[ $key ] );
     1097        }
     1098
     1099        foreach ( array( 'type', 'object', 'target' ) as $key ) {
     1100            $menu_item_value[ $key ] = sanitize_key( $menu_item_value[ $key ] );
     1101        }
     1102
     1103        foreach ( array( 'xfn', 'classes' ) as $key ) {
     1104            $value = $menu_item_value[ $key ];
     1105            if ( ! is_array( $value ) ) {
     1106                $value = explode( ' ', $value );
     1107            }
     1108            $menu_item_value[ $key ] = implode( ' ', array_map( 'sanitize_html_class', $value ) );
     1109        }
     1110
     1111        foreach ( array( 'title', 'attr_title', 'description', 'original_title' ) as $key ) {
     1112            // @todo Should esc_attr() the attr_title as well?
     1113            $menu_item_value[ $key ] = sanitize_text_field( $menu_item_value[ $key ] );
     1114        }
     1115
     1116        $menu_item_value['url'] = esc_url_raw( $menu_item_value['url'] );
     1117        if ( ! get_post_status_object( $menu_item_value['status'] ) ) {
     1118            $menu_item_value['status'] = 'publish';
     1119        }
     1120
     1121        /** This filter is documented in wp-includes/class-wp-customize-setting.php */
     1122        return apply_filters( "customize_sanitize_{$this->id}", $menu_item_value, $this );
     1123    }
     1124
     1125    /**
     1126     * Create/update the nav_menu_item post for this setting.
     1127     *
     1128     * Any created menu items will have their assigned post IDs exported to the client
     1129     * via the customize_save_response filter. Likewise, any errors will be exported
     1130     * to the client via the customize_save_response() filter.
     1131     *
     1132     * To delete a menu, the client can send false as the value.
     1133     *
     1134     * @since 4.3.0
     1135     *
     1136     * @see wp_update_nav_menu_item()
     1137     *
     1138     * @param array|false $value The menu item array to update. If false, then the menu item will be deleted entirely.
     1139     *                           See {@see WP_Customize_Nav_Menu_Item_Setting::$default} for what the value should
     1140     *                           consist of.
     1141     * @return void
     1142     */
     1143    protected function update( $value ) {
     1144        if ( $this->is_updated ) {
     1145            return;
     1146        }
     1147
     1148        $this->is_updated = true;
     1149        $is_placeholder   = ( $this->post_id < 0 );
     1150        $is_delete        = ( false === $value );
     1151
     1152        add_filter( 'customize_save_response', array( $this, 'amend_customize_save_response' ) );
     1153
     1154        if ( $is_delete ) {
     1155            // If the current setting post is a placeholder, a delete request is a no-op.
     1156            if ( $is_placeholder ) {
     1157                $this->update_status = 'deleted';
     1158            } else {
     1159                $r = wp_delete_post( $this->post_id, true );
     1160
     1161                if ( false === $r ) {
     1162                    $this->update_error  = new WP_Error( 'delete_failure' );
     1163                    $this->update_status = 'error';
     1164                } else {
     1165                    $this->update_status = 'deleted';
     1166                }
     1167                // @todo send back the IDs for all associated nav menu items deleted, so these settings (and controls) can be removed from Customizer?
     1168            }
     1169        } else {
     1170
     1171            // Handle saving menu items for menus that are being newly-created.
     1172            if ( $value['nav_menu_term_id'] < 0 ) {
     1173                $nav_menu_setting_id = sprintf( 'nav_menu[%s]', $value['nav_menu_term_id'] );
     1174                $nav_menu_setting    = $this->manager->get_setting( $nav_menu_setting_id );
     1175
     1176                if ( ! $nav_menu_setting || ! ( $nav_menu_setting instanceof WP_Customize_Nav_Menu_Setting ) ) {
     1177                    $this->update_status = 'error';
     1178                    $this->update_error  = new WP_Error( 'unexpected_nav_menu_setting' );
     1179                    return;
     1180                }
     1181
     1182                if ( false === $nav_menu_setting->save() ) {
     1183                    $this->update_status = 'error';
     1184                    $this->update_error  = new WP_Error( 'nav_menu_setting_failure' );
     1185                }
     1186
     1187                if ( $nav_menu_setting->previous_term_id !== intval( $value['nav_menu_term_id'] ) ) {
     1188                    $this->update_status = 'error';
     1189                    $this->update_error  = new WP_Error( 'unexpected_previous_term_id' );
     1190                    return;
     1191                }
     1192
     1193                $value['nav_menu_term_id'] = $nav_menu_setting->term_id;
     1194            }
     1195
     1196            // Handle saving a nav menu item that is a child of a nav menu item being newly-created.
     1197            if ( $value['menu_item_parent'] < 0 ) {
     1198                $parent_nav_menu_item_setting_id = sprintf( 'nav_menu_item[%s]', $value['menu_item_parent'] );
     1199                $parent_nav_menu_item_setting    = $this->manager->get_setting( $parent_nav_menu_item_setting_id );
     1200
     1201                if ( ! $parent_nav_menu_item_setting || ! ( $parent_nav_menu_item_setting instanceof WP_Customize_Nav_Menu_Item_Setting ) ) {
     1202                    $this->update_status = 'error';
     1203                    $this->update_error  = new WP_Error( 'unexpected_nav_menu_item_setting' );
     1204                    return;
     1205                }
     1206
     1207                if ( false === $parent_nav_menu_item_setting->save() ) {
     1208                    $this->update_status = 'error';
     1209                    $this->update_error  = new WP_Error( 'nav_menu_item_setting_failure' );
     1210                }
     1211
     1212                if ( $parent_nav_menu_item_setting->previous_post_id !== intval( $value['menu_item_parent'] ) ) {
     1213                    $this->update_status = 'error';
     1214                    $this->update_error  = new WP_Error( 'unexpected_previous_post_id' );
     1215                    return;
     1216                }
     1217
     1218                $value['menu_item_parent'] = $parent_nav_menu_item_setting->post_id;
     1219            }
     1220
     1221            // Insert or update menu.
     1222            $menu_item_data = array(
     1223                'menu-item-object-id'   => $value['object_id'],
     1224                'menu-item-object'      => $value['object'],
     1225                'menu-item-parent-id'   => $value['menu_item_parent'],
     1226                'menu-item-position'    => $value['position'],
     1227                'menu-item-type'        => $value['type'],
     1228                'menu-item-title'       => $value['title'],
     1229                'menu-item-url'         => $value['url'],
     1230                'menu-item-description' => $value['description'],
     1231                'menu-item-attr-title'  => $value['attr_title'],
     1232                'menu-item-target'      => $value['target'],
     1233                'menu-item-classes'     => $value['classes'],
     1234                'menu-item-xfn'         => $value['xfn'],
     1235                'menu-item-status'      => $value['status'],
     1236            );
     1237
     1238            $r = wp_update_nav_menu_item(
     1239                $value['nav_menu_term_id'],
     1240                $is_placeholder ? 0 : $this->post_id,
     1241                $menu_item_data
     1242            );
     1243
     1244            if ( is_wp_error( $r ) ) {
     1245                $this->update_status = 'error';
     1246                $this->update_error = $r;
     1247            } else {
     1248                if ( $is_placeholder ) {
     1249                    $this->previous_post_id = $this->post_id;
     1250                    $this->post_id = $r;
     1251                    $this->update_status = 'inserted';
     1252                } else {
     1253                    $this->update_status = 'updated';
     1254                }
     1255            }
     1256        }
     1257
     1258    }
     1259
     1260    /**
     1261     * Export data for the JS client.
     1262     *
     1263     * @since 4.3.0
     1264     *
     1265     * @see WP_Customize_Nav_Menu_Item_Setting::update()
     1266     *
     1267     * @param array $data Additional information passed back to the 'saved' event on `wp.customize`.
     1268     * @return array
     1269     */
     1270    function amend_customize_save_response( $data ) {
     1271        if ( ! isset( $data['nav_menu_item_updates'] ) ) {
     1272            $data['nav_menu_item_updates'] = array();
     1273        }
     1274
     1275        $data['nav_menu_item_updates'][] = array(
     1276            'post_id'          => $this->post_id,
     1277            'previous_post_id' => $this->previous_post_id,
     1278            'error'            => $this->update_error ? $this->update_error->get_error_code() : null,
     1279            'status'           => $this->update_status,
     1280        );
     1281
     1282        return $data;
     1283    }
     1284}
     1285
     1286/**
     1287 * Customize Setting to represent a nav_menu.
     1288 *
     1289 * Subclass of WP_Customize_Setting to represent a nav_menu taxonomy term, and
     1290 * the IDs for the nav_menu_items associated with the nav menu.
     1291 *
     1292 * @since 4.3.0
     1293 *
     1294 * @see wp_get_nav_menu_object()
     1295 * @see WP_Customize_Setting
     1296 */
     1297class WP_Customize_Nav_Menu_Setting extends WP_Customize_Setting {
     1298
     1299    const ID_PATTERN = '/^nav_menu\[(?P<id>-?\d+)\]$/';
     1300
     1301    const TAXONOMY = 'nav_menu';
     1302
     1303    const TYPE = 'nav_menu';
     1304
     1305    /**
     1306     * Setting type.
     1307     *
     1308     * @since 4.3.0
     1309     *
     1310     * @var string
     1311     */
     1312    public $type = self::TYPE;
     1313
     1314    /**
     1315     * Default setting value.
     1316     *
     1317     * @since 4.3.0
     1318     *
     1319     * @see wp_get_nav_menu_object()
     1320     *
     1321     * @var array
     1322     */
     1323    public $default = array(
     1324        'name'        => '',
     1325        'description' => '',
     1326        'parent'      => 0,
     1327        'auto_add'    => false,
     1328    );
     1329
     1330    /**
     1331     * Default transport.
     1332     *
     1333     * @since 4.3.0
     1334     *
     1335     * @var string
     1336     */
     1337    public $transport = 'postMessage';
     1338
     1339    /**
     1340     * The term ID represented by this setting instance.
     1341     *
     1342     * A negative value represents a placeholder ID for a new menu not yet saved.
     1343     *
     1344     * @since 4.3.0
     1345     *
     1346     * @var int
     1347     */
     1348    public $term_id;
     1349
     1350    /**
     1351     * Previous (placeholder) term ID used before creating a new menu.
     1352     *
     1353     * This value will be exported to JS via the customize_save_response filter
     1354     * so that JavaScript can update the settings to refer to the newly-assigned
     1355     * term ID. This value is always negative to indicate it does not refer to
     1356     * a real term.
     1357     *
     1358     * @since 4.3.0
     1359     *
     1360     * @see WP_Customize_Nav_Menu_Setting::update()
     1361     * @see WP_Customize_Nav_Menu_Setting::amend_customize_save_response()
     1362     *
     1363     * @var int
     1364     */
     1365    public $previous_term_id;
     1366
     1367    /**
     1368     * Whether or not preview() was called.
     1369     *
     1370     * @since 4.3.0
     1371     *
     1372     * @var bool
     1373     */
     1374    protected $is_previewed = false;
     1375
     1376    /**
     1377     * Whether or not update() was called.
     1378     *
     1379     * @since 4.3.0
     1380     *
     1381     * @var bool
     1382     */
     1383    protected $is_updated = false;
     1384
     1385    /**
     1386     * Status for calling the update method, used in customize_save_response filter.
     1387     *
     1388     * When status is inserted, the placeholder term ID is stored in $previous_term_id.
     1389     * When status is error, the error is stored in $update_error.
     1390     *
     1391     * @since 4.3.0
     1392     *
     1393     * @see WP_Customize_Nav_Menu_Setting::update()
     1394     * @see WP_Customize_Nav_Menu_Setting::amend_customize_save_response()
     1395     *
     1396     * @var string updated|inserted|deleted|error
     1397     */
     1398    public $update_status;
     1399
     1400    /**
     1401     * Any error object returned by wp_update_nav_menu_object() when setting is updated.
     1402     *
     1403     * @since 4.3.0
     1404     *
     1405     * @see WP_Customize_Nav_Menu_Setting::update()
     1406     * @see WP_Customize_Nav_Menu_Setting::amend_customize_save_response()
     1407     *
     1408     * @var WP_Error
     1409     */
     1410    public $update_error;
     1411
     1412    /**
     1413     * Constructor.
     1414     *
     1415     * Any supplied $args override class property defaults.
     1416     *
     1417     * @since 4.3.0
     1418     *
     1419     * @param WP_Customize_Manager $manager Manager instance.
     1420     * @param string               $id      An specific ID of the setting. Can be a
     1421     *                                      theme mod or option name.
     1422     * @param array                $args    Optional. Setting arguments.
     1423     * @throws Exception If $id is not valid for this setting type.
     1424     */
     1425    public function __construct( WP_Customize_Manager $manager, $id, array $args = array() ) {
     1426        if ( empty( $manager->nav_menus ) ) {
     1427            throw new Exception( 'Expected WP_Customize_Manager::$nav_menus to be set.' );
     1428        }
     1429
     1430        if ( ! preg_match( self::ID_PATTERN, $id, $matches ) ) {
     1431            throw new Exception( "Illegal widget setting ID: $id" );
     1432        }
     1433
     1434        $this->term_id = intval( $matches['id'] );
     1435
     1436        parent::__construct( $manager, $id, $args );
     1437    }
     1438
     1439    /**
     1440     * Get the instance data for a given widget setting.
     1441     *
     1442     * @since 4.3.0
     1443     *
     1444     * @see wp_get_nav_menu_object()
     1445     *
     1446     * @return array
     1447     */
     1448    public function value() {
     1449        if ( $this->is_previewed && $this->_previewed_blog_id === get_current_blog_id() ) {
     1450            $undefined  = new stdClass(); // Symbol.
     1451            $post_value = $this->post_value( $undefined );
     1452
     1453            if ( $undefined === $post_value ) {
     1454                $value = $this->_original_value;
     1455            } else {
     1456                $value = $post_value;
     1457            }
     1458        } else {
     1459            $value = false;
     1460
     1461            // Note that a term_id of less than one indicates a nav_menu not yet inserted.
     1462            if ( $this->term_id > 0 ) {
     1463                $term = wp_get_nav_menu_object( $this->term_id );
     1464
     1465                if ( $term ) {
     1466                    $value = wp_array_slice_assoc( (array) $term, array_keys( $this->default ) );
     1467
     1468                    $nav_menu_options  = (array) get_option( 'nav_menu_options', array() );
     1469                    $value['auto_add'] = false;
     1470
     1471                    if ( isset( $nav_menu_options['auto_add'] ) && is_array( $nav_menu_options['auto_add'] ) ) {
     1472                        $value['auto_add'] = in_array( $term->term_id, $nav_menu_options['auto_add'] );
     1473                    }
     1474                }
     1475            }
     1476
     1477            if ( ! is_array( $value ) ) {
     1478                $value = $this->default;
     1479            }
     1480        }
     1481        return $value;
     1482    }
     1483
     1484    /**
     1485     * Handle previewing the setting.
     1486     *
     1487     * @since 4.3.0
     1488     *
     1489     * @see WP_Customize_Manager::post_value()
     1490     */
     1491    public function preview() {
     1492        if ( $this->is_previewed ) {
     1493            return;
     1494        }
     1495
     1496        $this->is_previewed       = true;
     1497        $this->_original_value    = $this->value();
     1498        $this->_previewed_blog_id = get_current_blog_id();
     1499
     1500        add_filter( 'wp_get_nav_menu_object', array( $this, 'filter_wp_get_nav_menu_object' ), 10, 2 );
     1501        add_filter( 'default_option_nav_menu_options', array( $this, 'filter_nav_menu_options' ) );
     1502        add_filter( 'option_nav_menu_options', array( $this, 'filter_nav_menu_options' ) );
     1503    }
     1504
     1505    /**
     1506     * Filter the wp_get_nav_menu_object() result to supply the previewed menu object.
     1507     *
     1508     * Requesting a nav_menu object by anything but ID is not supported.
     1509     *
     1510     * @since 4.3.0
     1511     *
     1512     * @see wp_get_nav_menu_object()
     1513     *
     1514     * @param object|null $menu_obj Object returned by wp_get_nav_menu_object().
     1515     * @param string      $menu_id  ID of the nav_menu term. Requests by slug or name will be ignored.
     1516     * @return object|null
     1517     */
     1518    function filter_wp_get_nav_menu_object( $menu_obj, $menu_id ) {
     1519        $ok = (
     1520            get_current_blog_id() === $this->_previewed_blog_id
     1521            &&
     1522            is_int( $menu_id )
     1523            &&
     1524            $menu_id === $this->term_id
     1525        );
     1526        if ( ! $ok ) {
     1527            return $menu_obj;
     1528        }
     1529
     1530        $setting_value = $this->value();
     1531
     1532        // Handle deleted menus.
     1533        if ( false === $setting_value ) {
     1534            return false;
     1535        }
     1536
     1537        // Handle sanitization failure by preventing short-circuiting.
     1538        if ( null === $setting_value ) {
     1539            return $menu_obj;
     1540        }
     1541
     1542        $menu_obj = (object) array_merge( array(
     1543                'term_id'          => $this->term_id,
     1544                'term_taxonomy_id' => $this->term_id,
     1545                'slug'             => sanitize_title( $setting_value['name'] ),
     1546                'count'            => 0,
     1547                'term_group'       => 0,
     1548                'taxonomy'         => self::TAXONOMY,
     1549                'filter'           => 'raw',
     1550            ), $setting_value );
     1551
     1552        return $menu_obj;
     1553    }
     1554
     1555    /**
     1556     * Filter the nav_menu_options option to include this menu's auto_add preference.
     1557     *
     1558     * @since 4.3.0
     1559     *
     1560     * @param array $nav_menu_options Nav menu options including auto_add.
     1561     * @return array
     1562     */
     1563    function filter_nav_menu_options( $nav_menu_options ) {
     1564        if ( $this->_previewed_blog_id !== get_current_blog_id() ) {
     1565            return $nav_menu_options;
     1566        }
     1567
     1568        $menu = $this->value();
     1569        $nav_menu_options = $this->filter_nav_menu_options_value(
     1570            $nav_menu_options,
     1571            $this->term_id,
     1572            false === $menu ? false : $menu['auto_add']
     1573        );
     1574
     1575        return $nav_menu_options;
     1576    }
     1577
     1578    /**
     1579     * Sanitize an input.
     1580     *
     1581     * Note that parent::sanitize() erroneously does wp_unslash() on $value, but
     1582     * we remove that in this override.
     1583     *
     1584     * @since 4.3.0
     1585     *
     1586     * @param array $value The value to sanitize.
     1587     * @return array|false|null Null if an input isn't valid. False if it is marked for deletion. Otherwise the sanitized value.
     1588     */
     1589    public function sanitize( $value ) {
     1590        // Menu is marked for deletion.
     1591        if ( false === $value ) {
     1592            return $value;
     1593        }
     1594
     1595        // Invalid.
     1596        if ( ! is_array( $value ) ) {
     1597            return null;
     1598        }
     1599
     1600        $default = array(
     1601            'name'        => '',
     1602            'description' => '',
     1603            'parent'      => 0,
     1604            'auto_add'    => false,
     1605        );
     1606        $value = array_merge( $default, $value );
     1607        $value = wp_array_slice_assoc( $value, array_keys( $default ) );
     1608
     1609        $value['name']        = trim( esc_html( $value['name'] ) ); // This sanitization code is used in wp-admin/nav-menus.php.
     1610        $value['description'] = sanitize_text_field( $value['description'] );
     1611        $value['parent']      = max( 0, intval( $value['parent'] ) );
     1612        $value['auto_add']    = ! empty( $value['auto_add'] );
     1613
     1614        /** This filter is documented in wp-includes/class-wp-customize-setting.php */
     1615        return apply_filters( "customize_sanitize_{$this->id}", $value, $this );
     1616    }
     1617
     1618    /**
     1619     * Create/update the nav_menu term for this setting.
     1620     *
     1621     * Any created menus will have their assigned term IDs exported to the client
     1622     * via the customize_save_response filter. Likewise, any errors will be exported
     1623     * to the client via the customize_save_response() filter.
     1624     *
     1625     * To delete a menu, the client can send false as the value.
     1626     *
     1627     * @since 4.3.0
     1628     *
     1629     * @see wp_update_nav_menu_object()
     1630     *
     1631     * @param array|false $value {
     1632     *     The value to update. Note that slug cannot be updated via wp_update_nav_menu_object().
     1633     *     If false, then the menu will be deleted entirely.
     1634     *
     1635     *     @type string $name        The name of the menu to save.
     1636     *     @type string $description The term description. Default empty string.
     1637     *     @type int    $parent      The id of the parent term. Default 0.
     1638     *     @type bool   $auto_add    Whether pages will auto_add to this menu. Default false.
     1639     * }
     1640     * @return void
     1641     */
     1642    protected function update( $value ) {
     1643        if ( $this->is_updated ) {
     1644            return;
     1645        }
     1646
     1647        $this->is_updated = true;
     1648        $is_placeholder   = ( $this->term_id < 0 );
     1649        $is_delete        = ( false === $value );
     1650
     1651        add_filter( 'customize_save_response', array( $this, 'amend_customize_save_response' ) );
     1652
     1653        $auto_add = null;
     1654        if ( $is_delete ) {
     1655            // If the current setting term is a placeholder, a delete request is a no-op.
     1656            if ( $is_placeholder ) {
     1657                $this->update_status = 'deleted';
     1658            } else {
     1659                $r = wp_delete_nav_menu( $this->term_id );
     1660
     1661                if ( is_wp_error( $r ) ) {
     1662                    $this->update_status = 'error';
     1663                    $this->update_error  = $r;
     1664                } else {
     1665                    $this->update_status = 'deleted';
     1666                    $auto_add = false;
     1667                }
     1668            }
     1669        } else {
     1670            // Insert or update menu.
     1671            $menu_data = wp_array_slice_assoc( $value, array( 'description', 'parent' ) );
     1672            if ( isset( $value['name'] ) ) {
     1673                $menu_data['menu-name'] = $value['name'];
     1674            }
     1675
     1676            $r = wp_update_nav_menu_object( $is_placeholder ? 0 : $this->term_id, $menu_data );
     1677            if ( is_wp_error( $r ) ) {
     1678                $this->update_status = 'error';
     1679                $this->update_error  = $r;
     1680            } else {
     1681                if ( $is_placeholder ) {
     1682                    $this->previous_term_id = $this->term_id;
     1683                    $this->term_id          = $r;
     1684                    $this->update_status    = 'inserted';
     1685                } else {
     1686                    $this->update_status = 'updated';
     1687                }
     1688
     1689                $auto_add = $value['auto_add'];
     1690            }
     1691        }
     1692
     1693        if ( null !== $auto_add ) {
     1694            $nav_menu_options = $this->filter_nav_menu_options_value(
     1695                (array) get_option( 'nav_menu_options', array() ),
     1696                $this->term_id,
     1697                $auto_add
     1698            );
     1699            update_option( 'nav_menu_options', $nav_menu_options );
     1700        }
     1701
     1702        // Make sure that new menus assigned to nav menu locations use their new IDs.
     1703        if ( 'inserted' === $this->update_status ) {
     1704            foreach ( $this->manager->settings() as $setting ) {
     1705                if ( ! preg_match( '/^nav_menu_locations\[/', $setting->id ) ) {
     1706                    continue;
     1707                }
     1708
     1709                $post_value = $setting->post_value( null );
     1710                if ( ! is_null( $post_value ) && $this->previous_term_id === intval( $post_value ) ) {
     1711                    $this->manager->set_post_value( $setting->id, $this->term_id );
     1712                    $setting->save();
     1713                }
     1714            }
     1715        }
     1716    }
     1717
     1718    /**
     1719     * Update a nav_menu_options array.
     1720     *
     1721     * @since 4.3.0
     1722     *
     1723     * @see WP_Customize_Nav_Menu_Setting::filter_nav_menu_options()
     1724     * @see WP_Customize_Nav_Menu_Setting::update()
     1725     *
     1726     * @param array $nav_menu_options Array as returned by get_option( 'nav_menu_options' ).
     1727     * @param int   $menu_id          The term ID for the given menu.
     1728     * @param bool  $auto_add         Whether to auto-add or not.
     1729     * @return array
     1730     */
     1731    protected function filter_nav_menu_options_value( $nav_menu_options, $menu_id, $auto_add ) {
     1732        $nav_menu_options = (array) $nav_menu_options;
     1733        if ( ! isset( $nav_menu_options['auto_add'] ) ) {
     1734            $nav_menu_options['auto_add'] = array();
     1735        }
     1736
     1737        $i = array_search( $menu_id, $nav_menu_options['auto_add'] );
     1738        if ( $auto_add && false === $i ) {
     1739            array_push( $nav_menu_options['auto_add'], $this->term_id );
     1740        } else if ( ! $auto_add && false !== $i ) {
     1741            array_splice( $nav_menu_options['auto_add'], $i, 1 );
     1742        }
     1743
     1744        return $nav_menu_options;
     1745    }
     1746
     1747    /**
     1748     * Export data for the JS client.
     1749     *
     1750     * @since 4.3.0
     1751     *
     1752     * @see WP_Customize_Nav_Menu_Setting::update()
     1753     *
     1754     * @param array $data Additional information passed back to the 'saved' event on `wp.customize`.
     1755     * @return array
     1756     */
     1757    function amend_customize_save_response( $data ) {
     1758        if ( ! isset( $data['nav_menu_updates'] ) ) {
     1759            $data['nav_menu_updates'] = array();
     1760        }
     1761
     1762        $data['nav_menu_updates'][] = array(
     1763            'term_id'          => $this->term_id,
     1764            'previous_term_id' => $this->previous_term_id,
     1765            'error'            => $this->update_error ? $this->update_error->get_error_code() : null,
     1766            'status'           => $this->update_status,
     1767        );
     1768
     1769        return $data;
     1770    }
     1771}
  • trunk/src/wp-includes/script-loader.php

    r32779 r32806  
    407407    $scripts->add( 'customize-preview-widgets', "/wp-includes/js/customize-preview-widgets$suffix.js", array( 'jquery', 'wp-util', 'customize-preview' ), false, 1 );
    408408
     409    $scripts->add( 'customize-nav-menus', "/wp-admin/js/customize-nav-menus$suffix.js", array( 'jquery', 'wp-backbone', 'customize-controls', 'accordion', 'nav-menu', 'wp-a11y' ), false, 1 );
     410    $scripts->add( 'customize-preview-nav-menus', "/wp-includes/js/customize-preview-nav-menus$suffix.js", array( 'jquery', 'wp-util', 'customize-preview' ), false, 1 );
     411
    409412    $scripts->add( 'accordion', "/wp-admin/js/accordion$suffix.js", array( 'jquery' ), false, 1 );
    410413
     
    657660
    658661    // Admin CSS
    659     $styles->add( 'wp-admin',           "/wp-admin/css/wp-admin$suffix.css", array( 'open-sans', 'dashicons' ) );
    660     $styles->add( 'login',              "/wp-admin/css/login$suffix.css", array( 'buttons', 'open-sans', 'dashicons' ) );
    661     $styles->add( 'install',            "/wp-admin/css/install$suffix.css", array( 'buttons', 'open-sans' ) );
    662     $styles->add( 'wp-color-picker',    "/wp-admin/css/color-picker$suffix.css" );
    663     $styles->add( 'customize-controls', "/wp-admin/css/customize-controls$suffix.css", array( 'wp-admin', 'colors', 'ie', 'imgareaselect' ) );
    664     $styles->add( 'customize-widgets',  "/wp-admin/css/customize-widgets$suffix.css", array( 'wp-admin', 'colors' ) );
    665     $styles->add( 'press-this',         "/wp-admin/css/press-this$suffix.css", array( 'open-sans', 'buttons' ) );
    666 
    667     $styles->add( 'ie',                 "/wp-admin/css/ie$suffix.css" );
     662    $styles->add( 'wp-admin',            "/wp-admin/css/wp-admin$suffix.css", array( 'open-sans', 'dashicons' ) );
     663    $styles->add( 'login',               "/wp-admin/css/login$suffix.css", array( 'buttons', 'open-sans', 'dashicons' ) );
     664    $styles->add( 'install',             "/wp-admin/css/install$suffix.css", array( 'buttons', 'open-sans' ) );
     665    $styles->add( 'wp-color-picker',     "/wp-admin/css/color-picker$suffix.css" );
     666    $styles->add( 'customize-controls',  "/wp-admin/css/customize-controls$suffix.css", array( 'wp-admin', 'colors', 'ie', 'imgareaselect' ) );
     667    $styles->add( 'customize-widgets',   "/wp-admin/css/customize-widgets$suffix.css", array( 'wp-admin', 'colors' ) );
     668    $styles->add( 'customize-nav-menus', "/wp-admin/css/customize-nav-menus$suffix.css", array( 'wp-admin', 'colors' ) );
     669    $styles->add( 'press-this',          "/wp-admin/css/press-this$suffix.css", array( 'open-sans', 'buttons' ) );
     670
     671    $styles->add( 'ie', "/wp-admin/css/ie$suffix.css" );
    668672    $styles->add_data( 'ie', 'conditional', 'lte IE 7' );
    669673
     
    674678
    675679    // Includes CSS
    676     $styles->add( 'admin-bar',      "/wp-includes/css/admin-bar$suffix.css", array( 'open-sans', 'dashicons' ) );
    677     $styles->add( 'wp-auth-check',  "/wp-includes/css/wp-auth-check$suffix.css", array( 'dashicons' ) );
    678     $styles->add( 'editor-buttons', "/wp-includes/css/editor$suffix.css", array( 'dashicons' ) );
    679     $styles->add( 'media-views',    "/wp-includes/css/media-views$suffix.css", array( 'buttons', 'dashicons', 'wp-mediaelement' ) );
    680     $styles->add( 'wp-pointer',     "/wp-includes/css/wp-pointer$suffix.css", array( 'dashicons' ) );
     680    $styles->add( 'admin-bar',         "/wp-includes/css/admin-bar$suffix.css", array( 'open-sans', 'dashicons' ) );
     681    $styles->add( 'wp-auth-check',     "/wp-includes/css/wp-auth-check$suffix.css", array( 'dashicons' ) );
     682    $styles->add( 'editor-buttons',    "/wp-includes/css/editor$suffix.css", array( 'dashicons' ) );
     683    $styles->add( 'media-views',       "/wp-includes/css/media-views$suffix.css", array( 'buttons', 'dashicons', 'wp-mediaelement' ) );
     684    $styles->add( 'wp-pointer',        "/wp-includes/css/wp-pointer$suffix.css", array( 'dashicons' ) );
     685    $styles->add( 'customize-preview', "/wp-includes/css/customize-preview$suffix.css" );
    681686
    682687    // External libraries and friends
     
    696701    $rtl_styles = array(
    697702        // wp-admin
    698         'wp-admin', 'install', 'wp-color-picker', 'customize-controls', 'customize-widgets', 'ie', 'login', 'press-this',
     703        'wp-admin', 'install', 'wp-color-picker', 'customize-controls', 'customize-widgets', 'customize-nav-menus', 'ie', 'login', 'press-this',
    699704        // wp-includes
    700705        'buttons', 'admin-bar', 'wp-auth-check', 'editor-buttons', 'media-views', 'wp-pointer',
Note: See TracChangeset for help on using the changeset viewer.