Make WordPress Core

Changeset 32806


Ignore:
Timestamp:
06/16/2015 10:07:08 PM (11 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.