Ticket #32576: 32576.diff
File 32576.diff, 81.4 KB (added by , 9 years ago) |
---|
-
src/wp-includes/class-wp-customize-control.php
1402 1402 return $this->manager->widgets->is_widget_rendered( $this->widget_id ); 1403 1403 } 1404 1404 } 1405 1406 /** 1407 * Customize Menu Panel Class 1408 * 1409 * Needed to add screen options. 1410 * 1411 * @since 4.3.0 1412 */ 1413 class WP_Customize_Menus_Panel extends WP_Customize_Panel { 1414 1415 /** 1416 * Control type. 1417 * 1418 * @access public 1419 * @var string 1420 */ 1421 public $type = 'menus'; 1422 1423 /** 1424 * Render screen options for Menus. 1425 */ 1426 public function render_screen_options() { 1427 // Essentially adds the screen options. 1428 add_filter( 'manage_nav-menus_columns', array( $this, 'wp_nav_menu_manage_columns' ) ); 1429 1430 // Display screen options. 1431 $screen = WP_Screen::get( 'nav-menus.php' ); 1432 $screen->render_screen_options(); 1433 } 1434 1435 /** 1436 * Returns the advanced options for the nav menus page. 1437 * 1438 * Link title attribute added as it's a relatively advanced concept for new users. 1439 * 1440 * @since 4.3.0 1441 * 1442 * @return array The advanced menu properties. 1443 */ 1444 function wp_nav_menu_manage_columns() { 1445 return array( 1446 '_title' => __( 'Show advanced menu properties' ), 1447 'cb' => '<input type="checkbox" />', 1448 'link-target' => __( 'Link Target' ), 1449 'attr-title' => __( 'Title Attribute' ), 1450 'css-classes' => __( 'CSS Classes' ), 1451 'xfn' => __( 'Link Relationship (XFN)' ), 1452 'description' => __( 'Description' ), 1453 ); 1454 } 1455 1456 /** 1457 * An Underscore (JS) template for this panel's content (but not its container). 1458 * 1459 * Class variables for this panel class are available in the `data` JS object; 1460 * export custom variables by overriding {@see WP_Customize_Panel::json()}. 1461 * 1462 * @see WP_Customize_Panel::print_template() 1463 * 1464 * @since 4.3.0 1465 */ 1466 protected function content_template() { 1467 ?> 1468 <li class="panel-meta customize-info accordion-section <# if ( ! data.description ) { #> cannot-expand<# } #>"> 1469 <button type="button" class="customize-panel-back" tabindex="-1"> 1470 <span class="screen-reader-text"><?php _e( 'Back' ); ?></span> 1471 </button> 1472 <div class="accordion-section-title"> 1473 <span class="preview-notice"> 1474 <?php 1475 /* translators: %s is the site/panel title in the Customizer */ 1476 printf( __( 'You are customizing %s' ), '<strong class="panel-title">{{ data.title }}</strong>' ); 1477 ?> 1478 </span> 1479 <button type="button" class="customize-screen-options-toggle" aria-expanded="false"> 1480 <span class="screen-reader-text"><?php _e( 'Menu Options' ); ?></span> 1481 </button> 1482 <button type="button" class="customize-help-toggle dashicons dashicons-editor-help" aria-expanded="false"> 1483 <span class="screen-reader-text"><?php _e( 'Help' ); ?></span> 1484 </button> 1485 </div> 1486 <# if ( data.description ) { #> 1487 <div class="description customize-panel-description">{{{ data.description }}}</div> 1488 <# } #> 1489 <?php $this->render_screen_options(); ?> 1490 </li> 1491 <?php 1492 } 1493 } 1494 1495 /** 1496 * Customize Nav Menu Control Class 1497 * 1498 * @since 4.3.0 1499 */ 1500 class WP_Customize_Nav_Menu_Control extends WP_Customize_Control { 1501 1502 /** 1503 * Control type 1504 * 1505 * @access public 1506 * @var string 1507 */ 1508 public $type = 'nav_menu'; 1509 1510 /** 1511 * The nav menu setting 1512 * 1513 * @var WP_Customize_Nav_Menu_Setting 1514 */ 1515 public $setting; 1516 1517 /** 1518 * Don't render the control's content - it uses a JS template instead. 1519 */ 1520 public function render_content() {} 1521 1522 /** 1523 * JS/Underscore template for the control UI. 1524 */ 1525 public function content_template() { 1526 ?> 1527 <button type="button" class="button-secondary add-new-menu-item"> 1528 <?php _e( 'Add Items' ); ?> 1529 </button> 1530 <button type="button" class="not-a-button reorder-toggle"> 1531 <span class="reorder"><?php _ex( 'Reorder', 'Reorder menu items in Customizer' ); ?></span> 1532 <span class="reorder-done"><?php _ex( 'Done', 'Cancel reordering menu items in Customizer' ); ?></span> 1533 </button> 1534 <span class="add-menu-item-loading spinner"></span> 1535 <span class="menu-delete-item"> 1536 <button type="button" class="not-a-button menu-delete"> 1537 <?php _e( 'Delete menu' ); ?> <span class="screen-reader-text">{{ data.menu_name }}</span> 1538 </button> 1539 </span> 1540 <?php if ( current_theme_supports( 'menus' ) ) : ?> 1541 <ul class="menu-settings"> 1542 <li class="customize-control"> 1543 <span class="customize-control-title"><?php _e( 'Menu locations' ); ?></span> 1544 </li> 1545 1546 <?php foreach ( get_registered_nav_menus() as $location => $description ) : ?> 1547 <li class="customize-control customize-control-checkbox assigned-menu-location"> 1548 <label> 1549 <input type="checkbox" data-menu-id="{{ data.menu_id }}" data-location-id="<?php echo esc_attr( $location ); ?>" class="menu-location" /> <?php echo $description; ?> 1550 <span class="theme-location-set"><?php printf( _x( '(Current: %s)', 'Current menu location' ), '<span class="current-menu-location-name-' . esc_attr( $location ) . '"></span>' ); ?></span> 1551 </label> 1552 </li> 1553 <?php endforeach; ?> 1554 1555 </ul> 1556 <?php endif; ?> 1557 <p> 1558 <label> 1559 <input type="checkbox" class="auto_add"> 1560 <?php _e( 'Automatically add new top-level pages to this menu.' ) ?> 1561 </label> 1562 </p> 1563 <?php 1564 } 1565 1566 /** 1567 * Return params for this control. 1568 * 1569 * @return array 1570 */ 1571 function json() { 1572 $exported = parent::json(); 1573 $exported['menu_id'] = $this->setting->term_id; 1574 return $exported; 1575 } 1576 } 1577 1578 /** 1579 * Customize control to represent the name field for a given menu. 1580 * 1581 * @since 4.3.0 1582 */ 1583 class WP_Customize_Nav_Menu_Item_Control extends WP_Customize_Control { 1584 1585 /** 1586 * Control type 1587 * 1588 * @access public 1589 * @var string 1590 */ 1591 public $type = 'nav_menu_item'; 1592 1593 /** 1594 * The nav menu item setting 1595 * 1596 * @var WP_Customize_Nav_Menu_Item_Setting 1597 */ 1598 public $setting; 1599 1600 /** 1601 * Constructor. 1602 * 1603 * @uses WP_Customize_Control::__construct() 1604 * 1605 * @param WP_Customize_Manager $manager An instance of the WP_Customize_Manager class. 1606 * @param string $id The control ID. 1607 * @param array $args Optional. Overrides class property defaults. 1608 */ 1609 public function __construct( $manager, $id, $args = array() ) { 1610 parent::__construct( $manager, $id, $args ); 1611 } 1612 1613 /** 1614 * Don't render the control's content - it's rendered with a JS template. 1615 */ 1616 public function render_content() {} 1617 1618 /** 1619 * JS/Underscore template for the control UI. 1620 */ 1621 public function content_template() { 1622 ?> 1623 <dl class="menu-item-bar"> 1624 <dt class="menu-item-handle"> 1625 <span class="item-type">{{ data.item_type_label }}</span> 1626 <span class="item-title"> 1627 <span class="spinner"></span> 1628 <span class="menu-item-title">{{ data.title }}</span> 1629 </span> 1630 <span class="item-controls"> 1631 <button type="button" class="not-a-button item-edit"><span class="screen-reader-text"><?php _e( 'Edit Menu Item' ); ?></span></button> 1632 <button type="button" class="not-a-button item-delete submitdelete deletion"><span class="screen-reader-text"><?php _e( 'Remove Menu Item' ); ?></span></button> 1633 </span> 1634 </dt> 1635 </dl> 1636 1637 <div class="menu-item-settings" id="menu-item-settings-{{ data.menu_item_id }}"> 1638 <# if ( 'custom' === data.item_type ) { #> 1639 <p class="field-url description description-thin"> 1640 <label for="edit-menu-item-url-{{ data.menu_item_id }}"> 1641 <?php _e( 'URL' ); ?><br /> 1642 <input class="widefat code edit-menu-item-url" type="text" id="edit-menu-item-url-{{ data.menu_item_id }}" name="menu-item-url" /> 1643 </label> 1644 </p> 1645 <# } #> 1646 <p class="description description-thin"> 1647 <label for="edit-menu-item-title-{{ data.menu_item_id }}"> 1648 <?php _e( 'Navigation Label' ); ?><br /> 1649 <input type="text" id="edit-menu-item-title-{{ data.menu_item_id }}" class="widefat edit-menu-item-title" name="menu-item-title" /> 1650 </label> 1651 </p> 1652 <p class="field-link-target description description-thin"> 1653 <label for="edit-menu-item-target-{{ data.menu_item_id }}"> 1654 <input type="checkbox" id="edit-menu-item-target-{{ data.menu_item_id }}" class="edit-menu-item-target" value="_blank" name="menu-item-target" /> 1655 <?php _e( 'Open link in a new tab' ); ?> 1656 </label> 1657 </p> 1658 <p class="field-attr-title description description-thin"> 1659 <label for="edit-menu-item-attr-title-{{ data.menu_item_id }}"> 1660 <?php _e( 'Title Attribute' ); ?><br /> 1661 <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" /> 1662 </label> 1663 </p> 1664 <p class="field-css-classes description description-thin"> 1665 <label for="edit-menu-item-classes-{{ data.menu_item_id }}"> 1666 <?php _e( 'CSS Classes' ); ?><br /> 1667 <input type="text" id="edit-menu-item-classes-{{ data.menu_item_id }}" class="widefat code edit-menu-item-classes" name="menu-item-classes" /> 1668 </label> 1669 </p> 1670 <p class="field-xfn description description-thin"> 1671 <label for="edit-menu-item-xfn-{{ data.menu_item_id }}"> 1672 <?php _e( 'Link Relationship (XFN)' ); ?><br /> 1673 <input type="text" id="edit-menu-item-xfn-{{ data.menu_item_id }}" class="widefat code edit-menu-item-xfn" name="menu-item-xfn" /> 1674 </label> 1675 </p> 1676 <p class="field-description description description-thin"> 1677 <label for="edit-menu-item-description-{{ data.menu_item_id }}"> 1678 <?php _e( 'Description' ); ?><br /> 1679 <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> 1680 <span class="description"><?php _e( 'The description will be displayed in the menu if the current theme supports it.' ); ?></span> 1681 </label> 1682 </p> 1683 1684 <div class="menu-item-actions description-thin submitbox"> 1685 <# if ( 'custom' != data.item_type && '' != data.original_title ) { #> 1686 <p class="link-to-original"> 1687 <?php printf( __( 'Original: %s' ), '<a class="original-link" href="{{ data.url }}">{{{ data.original_title }}}</a>' ); ?> 1688 </p> 1689 <# } #> 1690 1691 <button type="button" class="not-a-button item-delete submitdelete deletion"><?php _e( 'Remove' ); ?></button> 1692 <span class="spinner"></span> 1693 </div> 1694 <input type="hidden" name="menu-item-db-id[{{ data.menu_item_id }}]" class="menu-item-data-db-id" value="{{ data.menu_item_id }}" /> 1695 <input type="hidden" name="menu-item-parent-id[{{ data.menu_item_id }}]" class="menu-item-data-parent-id" value="{{ data.parent }}" /> 1696 </div><!-- .menu-item-settings--> 1697 <ul class="menu-item-transport"></ul> 1698 <?php 1699 } 1700 1701 /** 1702 * Return params for this control. 1703 * 1704 * @return array 1705 */ 1706 function json() { 1707 $exported = parent::json(); 1708 $exported['menu_item_id'] = $this->setting->post_id; 1709 return $exported; 1710 } 1711 } 1712 1713 /** 1714 * Customize Menu Location Control Class 1715 * 1716 * This custom control is only needed for JS. 1717 * 1718 * @since 4.3.0 1719 */ 1720 class WP_Customize_Nav_Menu_Location_Control extends WP_Customize_Control { 1721 1722 /** 1723 * Control type 1724 * 1725 * @access public 1726 * @var string 1727 */ 1728 public $type = 'nav_menu_location'; 1729 1730 /** 1731 * Location ID 1732 * 1733 * @access public 1734 * @var string 1735 */ 1736 public $location_id = ''; 1737 1738 /** 1739 * Refresh the parameters passed to JavaScript via JSON. 1740 * 1741 * @uses WP_Customize_Control::to_json() 1742 */ 1743 public function to_json() { 1744 parent::to_json(); 1745 $this->json['locationId'] = $this->location_id; 1746 } 1747 1748 /** 1749 * Render content just like a normal select control. 1750 */ 1751 public function render_content() { 1752 if ( empty( $this->choices ) ) { 1753 return; 1754 } 1755 ?> 1756 <label> 1757 <?php if ( ! empty( $this->label ) ) : ?> 1758 <span class="customize-control-title"><?php echo esc_html( $this->label ); ?></span> 1759 <?php endif; ?> 1760 1761 <?php if ( ! empty( $this->description ) ) : ?> 1762 <span class="description customize-control-description"><?php echo $this->description; ?></span> 1763 <?php endif; ?> 1764 1765 <select <?php $this->link(); ?>> 1766 <?php 1767 foreach ( $this->choices as $value => $label ) : 1768 echo '<option value="' . esc_attr( $value ) . '"' . selected( $this->value(), $value, false ) . '>' . $label . '</option>'; 1769 endforeach; 1770 ?> 1771 </select> 1772 </label> 1773 <?php 1774 } 1775 } 1776 1777 /** 1778 * Customize control to represent the name field for a given menu. 1779 * 1780 * @since 4.3.0 1781 */ 1782 class WP_Customize_Nav_Menu_Name_Control extends WP_Customize_Control { 1783 1784 /** 1785 * Type of control, used by JS. 1786 * 1787 * @var string 1788 */ 1789 public $type = 'nav_menu_name'; 1790 1791 /** 1792 * No-op since we're using JS template. 1793 */ 1794 protected function render_content() {} 1795 1796 /** 1797 * Render the Underscore template for this control. 1798 */ 1799 protected function content_template() { 1800 ?> 1801 <label> 1802 <input type="text" class="menu-name-field live-update-section-title" /> 1803 </label> 1804 <?php 1805 } 1806 } 1807 1808 /** 1809 * Customize control class for new menus. 1810 * 1811 * @since 4.3.0 1812 */ 1813 class WP_New_Menu_Customize_Control extends WP_Customize_Control { 1814 1815 /** 1816 * Control type. 1817 * 1818 * @access public 1819 * @var string 1820 */ 1821 public $type = 'new_menu'; 1822 1823 /** 1824 * Render the control's content. 1825 */ 1826 public function render_content() { 1827 ?> 1828 <button type="button" class="button button-primary" id="create-new-menu-submit"><?php _e( 'Create Menu' ); ?></button> 1829 <span class="spinner"></span> 1830 <?php 1831 } 1832 } -
src/wp-includes/class-wp-customize-menus.php
1 <?php 2 /** 3 * Base Customize Menus 4 * 5 * @package WordPress 6 * @subpackage Customize 7 */ 8 9 /** 10 * Base Customize Menus class which implements menu management in the Customizer. 11 * 12 * @since 4.3.0 13 */ 14 class WP_Customize_Menus { 15 16 /** 17 * WP_Customize_Manager instance. 18 * 19 * @access public 20 * @var WP_Customize_Manager 21 */ 22 public $manager; 23 24 /** 25 * Previewed Menus. 26 * 27 * @access public 28 * @var array 29 */ 30 public $previewed_menus; 31 32 /** 33 * Constructor 34 * 35 * @access public 36 * @param object $manager An instance of the WP_Customize_Manager class. 37 */ 38 public function __construct( $manager ) { 39 $this->previewed_menus = array(); 40 $this->manager = $manager; 41 42 add_action( 'wp_ajax_load-available-menu-items-customizer', array( $this, 'ajax_load_available_items' ) ); 43 add_action( 'wp_ajax_search-available-menu-items-customizer', array( $this, 'ajax_search_available_items' ) ); 44 add_action( 'customize_controls_enqueue_scripts', array( $this, 'enqueue_scripts' ) ); 45 add_action( 'customize_register', array( $this, 'customize_register' ), 11 ); // Needs to run after core Navigation section is set up. 46 add_filter( 'customize_dynamic_setting_args', array( $this, 'filter_dynamic_setting_args' ), 10, 2 ); 47 add_filter( 'customize_dynamic_setting_class', array( $this, 'filter_dynamic_setting_class' ), 10, 3 ); 48 add_action( 'customize_controls_print_footer_scripts', array( $this, 'print_templates' ) ); 49 add_action( 'customize_controls_print_footer_scripts', array( $this, 'available_items_template' ) ); 50 add_action( 'customize_preview_init', array( $this, 'customize_preview_init' ) ); 51 } 52 53 /** 54 * Ajax handler for loading available menu items. 55 * 56 * @access public 57 */ 58 public function ajax_load_available_items() { 59 check_ajax_referer( 'customize-menus', 'customize-menus-nonce' ); 60 61 if ( ! current_user_can( 'edit_theme_options' ) ) { 62 wp_send_json_error( array( 'message' => __( 'Error: invalid user capabilities.' ) ) ); 63 } 64 if ( empty( $_POST['obj_type'] ) || empty( $_POST['type'] ) ) { 65 wp_send_json_error( array( 'message' => __( 'Missing obj_type or type param.' ) ) ); 66 } 67 68 $obj_type = sanitize_key( $_POST['obj_type'] ); 69 if ( ! in_array( $obj_type, array( 'post_type', 'taxonomy' ) ) ) { 70 wp_send_json_error( array( 'message' => __( 'Invalid obj_type param: ' . $obj_type ) ) ); 71 } 72 $taxonomy_or_post_type = sanitize_key( $_POST['type'] ); 73 $page = isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 0; 74 $items = array(); 75 76 if ( 'post_type' === $obj_type ) { 77 if ( ! get_post_type_object( $taxonomy_or_post_type ) ) { 78 wp_send_json_error( array( 'message' => __( 'Unknown post type.' ) ) ); 79 } 80 81 if ( 0 === $page && 'page' === $taxonomy_or_post_type ) { 82 // Add "Home" link. Treat as a page, but switch to custom on add. 83 $items[] = array( 84 'id' => 'home', 85 'title' => _x( 'Home', 'nav menu home label' ), 86 'type' => 'custom', 87 'type_label' => __( 'Custom Link' ), 88 'object' => '', 89 'url' => home_url(), 90 ); 91 } 92 93 $posts = get_posts( array( 94 'numberposts' => 10, 95 'offset' => 10 * $page, 96 'orderby' => 'date', 97 'order' => 'DESC', 98 'post_type' => $taxonomy_or_post_type, 99 ) ); 100 foreach ( $posts as $post ) { 101 $items[] = array( 102 'id' => "post-{$post->ID}", 103 'title' => html_entity_decode( get_the_title( $post ), ENT_QUOTES, get_bloginfo( 'charset' ) ), 104 'type' => 'post_type', 105 'type_label' => get_post_type_object( $post->post_type )->labels->singular_name, 106 'object' => $post->post_type, 107 'object_id' => (int) $post->ID, 108 ); 109 } 110 } else if ( 'taxonomy' === $obj_type ) { 111 $terms = get_terms( $taxonomy_or_post_type, array( 112 'child_of' => 0, 113 'exclude' => '', 114 'hide_empty' => false, 115 'hierarchical' => 1, 116 'include' => '', 117 'number' => 10, 118 'offset' => 10 * $page, 119 'order' => 'DESC', 120 'orderby' => 'count', 121 'pad_counts' => false, 122 ) ); 123 if ( is_wp_error( $terms ) ) { 124 wp_send_json_error( array( 'message' => wp_strip_all_tags( $terms->get_error_message(), true ) ) ); 125 } 126 127 foreach ( $terms as $term ) { 128 $items[] = array( 129 'id' => "term-{$term->term_id}", 130 'title' => html_entity_decode( $term->name, ENT_QUOTES, get_bloginfo( 'charset' ) ), 131 'type' => 'taxonomy', 132 'type_label' => get_taxonomy( $term->taxonomy )->labels->singular_name, 133 'object' => $term->taxonomy, 134 'object_id' => $term->term_id, 135 ); 136 } 137 } 138 139 wp_send_json_success( array( 'items' => $items ) ); 140 } 141 142 /** 143 * Ajax handler for searching available menu items. 144 */ 145 public function ajax_search_available_items() { 146 check_ajax_referer( 'customize-menus', 'customize-menus-nonce' ); 147 148 if ( ! current_user_can( 'edit_theme_options' ) ) { 149 wp_send_json_error( array( 'message' => __( 'Error: invalid user capabilities.' ) ) ); 150 } 151 if ( empty( $_POST['search'] ) ) { 152 wp_send_json_error( array( 'message' => __( 'Error: missing search parameter.' ) ) ); 153 } 154 155 $p = isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 0; 156 if ( $p < 1 ) { 157 $p = 1; 158 } 159 160 $s = sanitize_text_field( wp_unslash( $_POST['search'] ) ); 161 $results = $this->search_available_items_query( array( 'pagenum' => $p, 's' => $s ) ); 162 163 if ( empty( $results ) ) { 164 wp_send_json_error( array( 'message' => __( 'No results found.' ) ) ); 165 } else { 166 wp_send_json_success( array( 'items' => $results ) ); 167 } 168 } 169 170 /** 171 * Performs post queries for available-item searching. 172 * 173 * Based on WP_Editor::wp_link_query(). 174 * 175 * @param array $args Optional. Accepts 'pagenum' and 's' (search) arguments. 176 * @return array Results. 177 */ 178 public function search_available_items_query( $args = array() ) { 179 $results = array(); 180 181 $post_type_objects = get_post_types( array( 'show_in_nav_menus' => true ), 'objects' ); 182 $query = array( 183 'post_type' => array_keys( $post_type_objects ), 184 'suppress_filters' => true, 185 'update_post_term_cache' => false, 186 'update_post_meta_cache' => false, 187 'post_status' => 'publish', 188 'posts_per_page' => 20, 189 ); 190 191 $args['pagenum'] = isset( $args['pagenum'] ) ? absint( $args['pagenum'] ) : 1; 192 $query['offset'] = $args['pagenum'] > 1 ? $query['posts_per_page'] * ( $args['pagenum'] - 1 ) : 0; 193 194 if ( isset( $args['s'] ) ) { 195 $query['s'] = $args['s']; 196 } 197 198 // Query posts. 199 $get_posts = new WP_Query( $query ); 200 201 // Check if any posts were found. 202 if ( $get_posts->post_count ) { 203 foreach ( $get_posts->posts as $post ) { 204 $results[] = array( 205 'id' => 'post-' . $post->ID, 206 'type' => 'post_type', 207 'type_label' => $post_type_objects[ $post->post_type ]->labels->singular_name, 208 'object' => $post->post_type, 209 'object_id' => intval( $post->ID ), 210 'title' => html_entity_decode( get_the_title( $post ), ENT_QUOTES, get_bloginfo( 'charset' ) ), 211 ); 212 } 213 } 214 215 // Query taxonomy terms. 216 $taxonomies = get_taxonomies( array( 'show_in_nav_menus' => true ), 'names' ); 217 $terms = get_terms( $taxonomies, array( 218 'name__like' => $args['s'], 219 'number' => 20, 220 'offset' => 20 * ($args['pagenum'] - 1), 221 ) ); 222 223 // Check if any taxonomies were found. 224 if ( ! empty( $terms ) ) { 225 foreach ( $terms as $term ) { 226 $results[] = array( 227 'id' => 'term-' . $term->term_id, 228 'type' => 'taxonomy', 229 'type_label' => get_taxonomy( $term->taxonomy )->labels->singular_name, 230 'object' => $term->taxonomy, 231 'object_id' => intval( $term->term_id ), 232 'title' => html_entity_decode( $term->name, ENT_QUOTES, get_bloginfo( 'charset' ) ), 233 ); 234 } 235 } 236 237 return $results; 238 } 239 240 /** 241 * Enqueue scripts and styles for Customizer pane. 242 * 243 * @since Menu Customizer 0.0 244 */ 245 public function enqueue_scripts() { 246 wp_enqueue_style( 'menu-customizer' ); 247 wp_enqueue_script( 'menu-customizer' ); 248 249 $temp_nav_menu_setting = new WP_Customize_Nav_Menu_Setting( $this->manager, 'nav_menu[-1]' ); 250 $temp_nav_menu_item_setting = new WP_Customize_Nav_Menu_Item_Setting( $this->manager, 'nav_menu_item[-1]' ); 251 252 // Pass data to JS. 253 $settings = array( 254 'nonce' => wp_create_nonce( 'customize-menus' ), 255 'allMenus' => wp_get_nav_menus(), 256 'itemTypes' => $this->available_item_types(), 257 'l10n' => array( 258 'untitled' => _x( '(no label)', 'Missing menu item navigation label.' ), 259 'custom_label' => _x( 'Custom', 'Custom menu item type label.' ), 260 'menuLocation' => _x( '(Currently set to: %s)', 'Current menu location.' ), 261 'deleteWarn' => __( 'You are about to permanently delete this menu. "Cancel" to stop, "OK" to delete.' ), 262 'itemAdded' => __( 'Menu item added' ), 263 'itemDeleted' => __( 'Menu item deleted' ), 264 'menuAdded' => __( 'Menu created' ), 265 'menuDeleted' => __( 'Menu deleted' ), 266 'movedUp' => __( 'Menu item moved up' ), 267 'movedDown' => __( 'Menu item moved down' ), 268 'movedLeft' => __( 'Menu item moved out of submenu' ), 269 'movedRight' => __( 'Menu item is now a sub-item' ), 270 'customizingMenus' => _x( 'Customizing ▸ Menus', '▸ is the unicode right-pointing triangle' ), 271 'invalidTitleTpl' => __( '%s (Invalid)' ), 272 'pendingTitleTpl' => __( '%s (Pending)' ), 273 'taxonomyTermLabel' => __( 'Taxonomy' ), 274 'postTypeLabel' => __( 'Post Type' ), 275 ), 276 'menuItemTransport' => 'postMessage', 277 'phpIntMax' => PHP_INT_MAX, 278 'defaultSettingValues' => array( 279 'nav_menu' => $temp_nav_menu_setting->default, 280 'nav_menu_item' => $temp_nav_menu_item_setting->default, 281 ), 282 ); 283 284 $data = sprintf( 'var _wpCustomizeMenusSettings = %s;', json_encode( $settings ) ); 285 wp_scripts()->add_data( 'menu-customizer', 'data', $data ); 286 287 // This is copied from nav-menus.php, and it has an unfortunate object name of `menus`. 288 $nav_menus_l10n = array( 289 'oneThemeLocationNoMenus' => null, 290 'moveUp' => __( 'Move up one' ), 291 'moveDown' => __( 'Move down one' ), 292 'moveToTop' => __( 'Move to the top' ), 293 /* translators: %s: previous item name */ 294 'moveUnder' => __( 'Move under %s' ), 295 /* translators: %s: previous item name */ 296 'moveOutFrom' => __( 'Move out from under %s' ), 297 /* translators: %s: previous item name */ 298 'under' => __( 'Under %s' ), 299 /* translators: %s: previous item name */ 300 'outFrom' => __( 'Out from under %s' ), 301 /* translators: 1: item name, 2: item position, 3: total number of items */ 302 'menuFocus' => __( '%1$s. Menu item %2$d of %3$d.' ), 303 /* translators: 1: item name, 2: item position, 3: parent item name */ 304 'subMenuFocus' => __( '%1$s. Sub item number %2$d under %3$s.' ), 305 ); 306 wp_localize_script( 'nav-menu', 'menus', $nav_menus_l10n ); 307 } 308 309 /** 310 * Filter a dynamic setting's constructor args. 311 * 312 * For a dynamic setting to be registered, this filter must be employed 313 * to override the default false value with an array of args to pass to 314 * the WP_Customize_Setting constructor. 315 * 316 * @param false|array $setting_args The arguments to the WP_Customize_Setting constructor. 317 * @param string $setting_id ID for dynamic setting, usually coming from `$_POST['customized']`. 318 * @return array|false 319 */ 320 public function filter_dynamic_setting_args( $setting_args, $setting_id ) { 321 if ( preg_match( WP_Customize_Nav_Menu_Setting::ID_PATTERN, $setting_id ) ) { 322 $setting_args = array( 323 'type' => WP_Customize_Nav_Menu_Setting::TYPE, 324 ); 325 } else if ( preg_match( WP_Customize_Nav_Menu_Item_Setting::ID_PATTERN, $setting_id ) ) { 326 $setting_args = array( 327 'type' => WP_Customize_Nav_Menu_Item_Setting::TYPE, 328 ); 329 } 330 return $setting_args; 331 } 332 333 /** 334 * Allow non-statically created settings to be constructed with custom WP_Customize_Setting subclass. 335 * 336 * @param string $setting_class WP_Customize_Setting or a subclass. 337 * @param string $setting_id ID for dynamic setting, usually coming from `$_POST['customized']`. 338 * @param array $setting_args WP_Customize_Setting or a subclass. 339 * @return string 340 */ 341 public function filter_dynamic_setting_class( $setting_class, $setting_id, $setting_args ) { 342 unset( $setting_id ); 343 344 if ( ! empty( $setting_args['type'] ) && WP_Customize_Nav_Menu_Setting::TYPE === $setting_args['type'] ) { 345 $setting_class = 'WP_Customize_Nav_Menu_Setting'; 346 } else if ( ! empty( $setting_args['type'] ) && WP_Customize_Nav_Menu_Item_Setting::TYPE === $setting_args['type'] ) { 347 $setting_class = 'WP_Customize_Nav_Menu_Item_Setting'; 348 } 349 return $setting_class; 350 } 351 352 /** 353 * Add the customizer settings and controls. 354 * 355 * @since Menu Customizer 0.0 356 */ 357 public function customize_register() { 358 359 // Require JS-rendered control types. 360 $this->manager->register_panel_type( 'WP_Customize_Menus_Panel' ); 361 $this->manager->register_control_type( 'WP_Customize_Nav_Menu_Control' ); 362 $this->manager->register_control_type( 'WP_Customize_Nav_Menu_Name_Control' ); 363 $this->manager->register_control_type( 'WP_Customize_Nav_Menu_Item_Control' ); 364 365 // Create a panel for Menus. 366 $this->manager->add_panel( new WP_Customize_Menus_Panel( $this->manager, 'menus', array( 367 'title' => __( 'Menus' ), 368 'description' => '<p>' . __( 'This panel is used for managing navigation menus for content you have already published on your site. You can create menus and add items for existing content such as pages, posts, categories, tags, formats, or custom links.' ) . '</p><p>' . __( 'Menus can be displayed in locations defined by your theme or in widget areas by adding a "Custom Menu" widget.' ) . '</p>', 369 'priority' => 30, 370 // 'theme_supports' => 'menus|widgets', @todo allow multiple theme supports 371 ) ) ); 372 $menus = wp_get_nav_menus(); 373 374 // Menu loactions. 375 $this->manager->remove_section( 'nav' ); // Remove old core section. @todo core merge remove corresponding code from WP_Customize_Manager::register_controls(). 376 $locations = get_registered_nav_menus(); 377 $num_locations = count( array_keys( $locations ) ); 378 $description = '<p>' . sprintf( _n( 'Your theme contains %s menu location. Select which menu you would like to use.', 'Your theme contains %s menu locations. Select which menu appears in each location.', $num_locations ), number_format_i18n( $num_locations ) ); 379 $description .= '</p><p>' . __( 'You can also place menus in widget areas with the Custom Menu widget.' ) . '</p>'; 380 381 $this->manager->add_section( 'menu_locations', array( 382 'title' => __( 'Menu Locations' ), 383 'panel' => 'menus', 384 'priority' => 5, 385 'description' => $description, 386 ) ); 387 388 // @todo if ( ! $menus ) : make a "default" menu 389 if ( $menus ) { 390 $choices = array( '0' => __( '— Select —' ) ); 391 foreach ( $menus as $menu ) { 392 $choices[ $menu->term_id ] = wp_html_excerpt( $menu->name, 40, '…' ); 393 } 394 395 foreach ( $locations as $location => $description ) { 396 $setting_id = "nav_menu_locations[{$location}]"; 397 398 $setting = $this->manager->get_setting( $setting_id ); 399 if ( $setting ) { 400 $setting->transport = 'postMessage'; 401 remove_filter( "customize_sanitize_{$setting_id}", 'absint' ); 402 add_filter( "customize_sanitize_{$setting_id}", array( $this, 'intval_base10' ) ); 403 } else { 404 $this->manager->add_setting( $setting_id, array( 405 'sanitize_callback' => array( $this, 'intval_base10' ), 406 'theme_supports' => 'menus', 407 'type' => 'theme_mod', 408 'transport' => 'postMessage', 409 ) ); 410 } 411 412 $this->manager->add_control( new WP_Customize_Nav_Menu_Location_Control( $this->manager, $setting_id, array( 413 'label' => $description, 414 'location_id' => $location, 415 'section' => 'menu_locations', 416 'choices' => $choices, 417 ) ) ); 418 } 419 } 420 421 // Register each menu as a Customizer section, and add each menu item to each menu. 422 foreach ( $menus as $menu ) { 423 $menu_id = $menu->term_id; 424 425 // Create a section for each menu. 426 $section_id = 'nav_menu[' . $menu_id . ']'; 427 $this->manager->add_section( new WP_Customize_Nav_Menu_Section( $this->manager, $section_id, array( 428 'title' => $menu->name, 429 'priority' => 10, 430 'panel' => 'menus', 431 ) ) ); 432 433 $nav_menu_setting_id = 'nav_menu[' . $menu_id . ']'; 434 $this->manager->add_setting( new WP_Customize_Nav_Menu_Setting( $this->manager, $nav_menu_setting_id ) ); 435 436 // Add the menu contents. 437 $menu_items = (array) wp_get_nav_menu_items( $menu_id ); 438 439 foreach ( array_values( $menu_items ) as $i => $item ) { 440 441 // Create a setting for each menu item (which doesn't actually manage data, currently). 442 $menu_item_setting_id = 'nav_menu_item[' . $item->ID . ']'; 443 $this->manager->add_setting( new WP_Customize_Nav_Menu_Item_Setting( $this->manager, $menu_item_setting_id ) ); 444 445 // Create a control for each menu item. 446 $this->manager->add_control( new WP_Customize_Nav_Menu_Item_Control( $this->manager, $menu_item_setting_id, array( 447 'label' => $item->title, 448 'section' => $section_id, 449 'priority' => 10 + $i, 450 ) ) ); 451 } 452 453 // Note: other controls inside of this section get added dynamically in JS via the MenuSection.ready() function. 454 } 455 456 // Add the add-new-menu section and controls. 457 $this->manager->add_section( new WP_Customize_New_Menu_Section( $this->manager, 'add_menu', array( 458 'title' => __( 'Add a Menu' ), 459 'panel' => 'menus', 460 'priority' => 999, 461 ) ) ); 462 463 $this->manager->add_setting( 'new_menu_name', array( 464 'type' => 'new_menu', 465 'default' => '', 466 'transport' => 'postMessage', 467 ) ); 468 469 $this->manager->add_control( 'new_menu_name', array( 470 'label' => '', 471 'section' => 'add_menu', 472 'type' => 'text', 473 'input_attrs' => array( 474 'class' => 'menu-name-field', 475 'placeholder' => __( 'New menu name' ), 476 ), 477 ) ); 478 479 $this->manager->add_setting( 'create_new_menu', array( 480 'type' => 'new_menu', 481 ) ); 482 483 $this->manager->add_control( new WP_New_Menu_Customize_Control( $this->manager, 'create_new_menu', array( 484 'section' => 'add_menu', 485 ) ) ); 486 } 487 488 /** 489 * Get the base10 intval. 490 * 491 * This is used as a setting's sanitize_callback; we can't use just plain 492 * intval because the second argument is not what intval() expects. 493 * 494 * @param mixed $value Number to convert. 495 * 496 * @return int 497 */ 498 function intval_base10( $value ) { 499 return intval( $value, 10 ); 500 } 501 502 /** 503 * Return an array of all the available item types. 504 * 505 * @since Menu Customizer 0.0 506 */ 507 public function available_item_types() { 508 $items = array( 509 'postTypes' => array(), 510 'taxonomies' => array(), 511 ); 512 513 $post_types = get_post_types( array( 'show_in_nav_menus' => true ), 'objects' ); 514 foreach ( $post_types as $slug => $post_type ) { 515 $items['postTypes'][ $slug ] = array( 516 'label' => $post_type->labels->singular_name, 517 ); 518 } 519 520 $taxonomies = get_taxonomies( array( 'show_in_nav_menus' => true ), 'objects' ); 521 foreach ( $taxonomies as $slug => $taxonomy ) { 522 if ( 'post_format' === $taxonomy && ! current_theme_supports( 'post-formats' ) ) { 523 continue; 524 } 525 $items['taxonomies'][ $slug ] = array( 526 'label' => $taxonomy->labels->singular_name, 527 ); 528 } 529 return $items; 530 } 531 532 /** 533 * Print the JavaScript templates used to render Menu Customizer components. 534 * 535 * Templates are imported into the JS use wp.template. 536 * 537 * @since Menu Customizer 0.0 538 */ 539 public function print_templates() { 540 ?> 541 <script type="text/html" id="tmpl-available-menu-item"> 542 <div id="menu-item-tpl-{{ data.id }}" class="menu-item-tpl" data-menu-item-id="{{ data.id }}"> 543 <dl class="menu-item-bar"> 544 <dt class="menu-item-handle"> 545 <span class="item-type">{{ data.type_label }}</span> 546 <span class="item-title">{{ data.title || wp.customize.Menus.data.l10n.untitled }}</span> 547 <button type="button" class="not-a-button item-add"><span class="screen-reader-text"><?php _e( 'Add Menu Item' ) ?></span></button> 548 </dt> 549 </dl> 550 </div> 551 </script> 552 553 <script type="text/html" id="tmpl-available-menu-item-type"> 554 <div id="available-menu-items-{{ data.type }}" class="accordion-section"> 555 <h4 class="accordion-section-title">{{ data.type_label }}</h4> 556 <div class="accordion-section-content"> 557 </div> 558 </div> 559 </script> 560 561 <script type="text/html" id="tmpl-menu-item-reorder-nav"> 562 <div class="menu-item-reorder-nav"> 563 <?php 564 printf( 565 '<button type="button" class="menus-move-up">%1$s</button><button type="button" class="menus-move-down">%2$s</button><button type="button" class="menus-move-left">%3$s</button><button type="button" class="menus-move-right">%4$s</button>', 566 esc_html__( 'Move up' ), 567 esc_html__( 'Move down' ), 568 esc_html__( 'Move one level up' ), 569 esc_html__( 'Move one level down' ) 570 ); 571 ?> 572 </div> 573 </script> 574 <?php 575 } 576 577 /** 578 * Print the html template used to render the add-menu-item frame. 579 */ 580 public function available_items_template() { 581 ?> 582 <div id="available-menu-items" class="accordion-container"> 583 <div class="customize-section-title"> 584 <button type="button" class="customize-section-back" tabindex="-1"> 585 <span class="screen-reader-text"><?php _e( 'Back' ); ?></span> 586 </button> 587 <h3> 588 <span class="customize-action"> 589 <?php 590 /* translators: ▸ is the unicode right-pointing triangle, and %s is the section title in the Customizer */ 591 printf( __( 'Customizing ▸ %s' ), esc_html( $this->manager->get_panel( 'menus' )->title ) ); 592 ?> 593 </span> 594 <?php _e( 'Add Menu Items' ); ?> 595 </h3> 596 </div> 597 <div id="available-menu-items-search" class="accordion-section cannot-expand"> 598 <div class="accordion-section-title"> 599 <label class="screen-reader-text" for="menu-items-search"><?php _e( 'Search Menu Items' ); ?></label> 600 <input type="text" id="menu-items-search" placeholder="<?php esc_attr_e( 'Search menu items…' ) ?>" /> 601 <span class="spinner"></span> 602 </div> 603 <div class="accordion-section-content" data-type="search"></div> 604 </div> 605 <div id="new-custom-menu-item" class="accordion-section"> 606 <h4 class="accordion-section-title"><?php _e( 'Links' ); ?><button type="button" class="not-a-button"><span class="screen-reader-text"><?php _e( 'Toggle' ); ?></span></button></h4> 607 <div class="accordion-section-content"> 608 <input type="hidden" value="custom" id="custom-menu-item-type" name="menu-item[-1][menu-item-type]" /> 609 <p id="menu-item-url-wrap"> 610 <label class="howto" for="custom-menu-item-url"> 611 <span><?php _e( 'URL' ); ?></span> 612 <input id="custom-menu-item-url" name="menu-item[-1][menu-item-url]" type="text" class="code menu-item-textbox" value="http://"> 613 </label> 614 </p> 615 <p id="menu-item-name-wrap"> 616 <label class="howto" for="custom-menu-item-name"> 617 <span><?php _e( 'Link Text' ); ?></span> 618 <input id="custom-menu-item-name" name="menu-item[-1][menu-item-title]" type="text" class="regular-text menu-item-textbox"> 619 </label> 620 </p> 621 <p class="button-controls"> 622 <span class="add-to-menu"> 623 <input type="submit" class="button-secondary submit-add-to-menu right" value="<?php esc_attr_e( 'Add to Menu' ); ?>" name="add-custom-menu-item" id="custom-menu-item-submit"> 624 <span class="spinner"></span> 625 </span> 626 </p> 627 </div> 628 </div> 629 <?php 630 631 // @todo: consider using add_meta_box/do_accordion_section and making screen-optional? 632 // Containers for per-post-type item browsing; items added with JS. 633 $post_types = get_post_types( array( 'show_in_nav_menus' => true ), 'object' ); 634 if ( $post_types ) : 635 foreach ( $post_types as $type ) : 636 ?> 637 <div id="available-menu-items-<?php echo esc_attr( $type->name ); ?>" class="accordion-section"> 638 <h4 class="accordion-section-title"><?php echo esc_html( $type->label ); ?> <span class="spinner"></span> <button type="button" class="not-a-button"><span class="screen-reader-text"><?php _e( 'Toggle' ); ?></span></button></h4> 639 <div class="accordion-section-content" data-type="<?php echo esc_attr( $type->name ); ?>" data-obj_type="post_type"></div> 640 </div> 641 <?php 642 endforeach; 643 endif; 644 645 $taxonomies = get_taxonomies( array( 'show_in_nav_menus' => true ), 'object' ); 646 if ( $taxonomies ) : 647 foreach ( $taxonomies as $tax ) : 648 ?> 649 <div id="available-menu-items-<?php echo esc_attr( $tax->name ); ?>" class="accordion-section"> 650 <h4 class="accordion-section-title"><?php echo esc_html( $tax->label ); ?> <span class="spinner"></span> <button type="button" class="not-a-button"><span class="screen-reader-text"><?php _e( 'Toggle' ); ?></span></button></h4> 651 <div class="accordion-section-content" data-type="<?php echo esc_attr( $tax->name ); ?>" data-obj_type="taxonomy"></div> 652 </div> 653 <?php 654 endforeach; 655 endif; 656 ?> 657 </div><!-- #available-menu-items --> 658 <?php 659 } 660 661 // Start functionality specific to partial-refresh of menu changes in Customizer preview. 662 const RENDER_AJAX_ACTION = 'customize_render_menu_partial'; 663 const RENDER_NONCE_POST_KEY = 'render-menu-nonce'; 664 const RENDER_QUERY_VAR = 'wp_customize_menu_render'; 665 666 /** 667 * The number of wp_nav_menu() calls which have happened in the preview. 668 * 669 * @var int 670 */ 671 public $preview_nav_menu_instance_number = 0; 672 673 /** 674 * Nav menu args used for each instance. 675 * 676 * @var array[] 677 */ 678 public $preview_nav_menu_instance_args = array(); 679 680 /** 681 * Add hooks for the Customizer preview. 682 */ 683 function customize_preview_init() { 684 add_action( 'template_redirect', array( $this, 'render_menu' ) ); 685 add_action( 'wp_enqueue_scripts', array( $this, 'customize_preview_enqueue_deps' ) ); 686 687 if ( ! isset( $_REQUEST[ self::RENDER_QUERY_VAR ] ) ) { 688 add_filter( 'wp_nav_menu_args', array( $this, 'filter_wp_nav_menu_args' ), 1000 ); 689 add_filter( 'wp_nav_menu', array( $this, 'filter_wp_nav_menu' ), 10, 2 ); 690 } 691 } 692 693 /** 694 * Keep track of the arguments that are being passed to wp_nav_menu(). 695 * 696 * @see wp_nav_menu() 697 * 698 * @param array $args An array containing wp_nav_menu() arguments. 699 * @return array 700 */ 701 function filter_wp_nav_menu_args( $args ) { 702 $this->preview_nav_menu_instance_number += 1; 703 $args['instance_number'] = $this->preview_nav_menu_instance_number; 704 705 $can_partial_refresh = ( 706 $args['echo'] 707 && 708 is_string( $args['fallback_cb'] ) 709 && 710 is_string( $args['walker'] ) 711 ); 712 $args['can_partial_refresh'] = $can_partial_refresh; 713 714 if ( ! $can_partial_refresh ) { 715 unset( $args['fallback_cb'] ); 716 unset( $args['walker'] ); 717 } 718 719 ksort( $args ); 720 $args['args_hash'] = $this->hash_nav_menu_args( $args ); 721 722 $this->preview_nav_menu_instance_args[ $this->preview_nav_menu_instance_number ] = $args; 723 return $args; 724 } 725 726 /** 727 * Prepare wp_nav_menu() calls for partial refresh. Wraps output in container for refreshing. 728 * 729 * @see wp_nav_menu() 730 * 731 * @param string $nav_menu_content The HTML content for the navigation menu. 732 * @param object $args An object containing wp_nav_menu() arguments. 733 * @return null 734 */ 735 function filter_wp_nav_menu( $nav_menu_content, $args ) { 736 if ( ! empty( $args->can_partial_refresh ) && ! empty( $args->instance_number ) ) { 737 $nav_menu_content = sprintf( 738 '<div id="partial-refresh-menu-container-%1$d" class="partial-refresh-menu-container" data-instance-number="%1$d">%2$s</div>', 739 $args->instance_number, 740 $nav_menu_content 741 ); 742 } 743 return $nav_menu_content; 744 } 745 746 /** 747 * Hash (hmac) the arguments with the nonce and secret auth key to ensure they 748 * are not tampered with when submitted in the Ajax request. 749 * 750 * @param array $args The arguments to hash. 751 * @return string 752 */ 753 function hash_nav_menu_args( $args ) { 754 return wp_hash( wp_create_nonce( self::RENDER_AJAX_ACTION ) . serialize( $args ) ); 755 } 756 757 /** 758 * Enqueue scripts for the Customizer preview. 759 */ 760 function customize_preview_enqueue_deps() { 761 wp_enqueue_script( 'customize-menus-preview' ); 762 wp_enqueue_style( 'customize-menus-preview' ); 763 764 add_action( 'wp_print_footer_scripts', array( $this, 'export_preview_data' ) ); 765 } 766 767 /** 768 * Export data from PHP to JS. 769 */ 770 function export_preview_data() { 771 772 // Why not wp_localize_script? Because we're not localizing, and it forces values into strings. 773 $exports = array( 774 'renderQueryVar' => self::RENDER_QUERY_VAR, 775 'renderNonceValue' => wp_create_nonce( self::RENDER_AJAX_ACTION ), 776 'renderNoncePostKey' => self::RENDER_NONCE_POST_KEY, 777 'requestUri' => '/', 778 'theme' => array( 779 'stylesheet' => $this->manager->get_stylesheet(), 780 'active' => $this->manager->is_theme_active(), 781 ), 782 'previewCustomizeNonce' => wp_create_nonce( 'preview-customize_' . $this->manager->get_stylesheet() ), 783 'navMenuInstanceArgs' => $this->preview_nav_menu_instance_args, 784 ); 785 786 if ( ! empty( $_SERVER['REQUEST_URI'] ) ) { 787 $exports['requestUri'] = esc_url_raw( home_url( wp_unslash( $_SERVER['REQUEST_URI'] ) ) ); 788 } 789 790 printf( '<script>var _wpCustomizePreviewMenusExports = %s;</script>', wp_json_encode( $exports ) ); 791 } 792 793 /** 794 * Render a specific menu via wp_nav_menu() using the supplied arguments. 795 * 796 * @see wp_nav_menu() 797 */ 798 function render_menu() { 799 if ( empty( $_POST[ self::RENDER_QUERY_VAR ] ) ) { 800 return; 801 } 802 803 $this->manager->remove_preview_signature(); 804 805 if ( empty( $_POST[ self::RENDER_NONCE_POST_KEY ] ) ) { 806 wp_send_json_error( 'missing_nonce_param' ); 807 } 808 if ( ! is_customize_preview() ) { 809 wp_send_json_error( 'expected_customize_preview' ); 810 } 811 if ( ! check_ajax_referer( self::RENDER_AJAX_ACTION, self::RENDER_NONCE_POST_KEY, false ) ) { 812 wp_send_json_error( 'nonce_check_fail' ); 813 } 814 if ( ! current_user_can( 'edit_theme_options' ) ) { 815 wp_send_json_error( 'unauthorized' ); 816 } 817 if ( ! isset( $_POST['wp_nav_menu_args'] ) ) { 818 wp_send_json_error( 'missing_param' ); 819 } 820 if ( ! isset( $_POST['wp_nav_menu_args_hash'] ) ) { 821 wp_send_json_error( 'missing_param' ); 822 } 823 824 $wp_nav_menu_args_hash = sanitize_text_field( wp_unslash( $_POST['wp_nav_menu_args_hash'] ) ); 825 $wp_nav_menu_args = json_decode( wp_unslash( $_POST['wp_nav_menu_args'] ), true ); 826 827 if ( json_last_error() ) { 828 wp_send_json_error( 'json_parse_error' ); 829 } 830 if ( ! is_array( $wp_nav_menu_args ) ) { 831 wp_send_json_error( 'wp_nav_menu_args_not_array' ); 832 } 833 if ( $this->hash_nav_menu_args( $wp_nav_menu_args ) !== $wp_nav_menu_args_hash ) { 834 wp_send_json_error( 'wp_nav_menu_args_hash_mismatch' ); 835 } 836 837 $wp_nav_menu_args['echo'] = false; 838 wp_send_json_success( wp_nav_menu( $wp_nav_menu_args ) ); 839 } 840 } -
src/wp-includes/class-wp-customize-section.php
501 501 return $this->manager->widgets->is_sidebar_rendered( $this->sidebar_id ); 502 502 } 503 503 } 504 505 /** 506 * Customize Menu Section Class 507 * 508 * Custom section only needed in JS. 509 * 510 * @since 4.3.0 511 */ 512 class WP_Customize_Nav_Menu_Section extends WP_Customize_Section { 513 514 /** 515 * Control type 516 * 517 * @access public 518 * @var string 519 */ 520 public $type = 'nav_menu'; 521 522 /** 523 * Get section params for JS. 524 * 525 * @return array 526 */ 527 function json() { 528 $exported = parent::json(); 529 $exported['menu_id'] = intval( preg_replace( '/^nav_menu\[(\d+)\]/', '$1', $this->id ) ); 530 531 return $exported; 532 } 533 } 534 535 /** 536 * Customize Menu Section Class 537 * 538 * Implements the new-menu-ui toggle button instead of a regular section. 539 * 540 * @since 4.3.0 541 */ 542 class WP_Customize_New_Menu_Section extends WP_Customize_Section { 543 544 /** 545 * Control type. 546 * 547 * @access public 548 * @var string 549 */ 550 public $type = 'new_menu'; 551 552 /** 553 * Render the section, and the controls that have been added to it. 554 */ 555 protected function render() { 556 ?> 557 <li id="accordion-section-<?php echo esc_attr( $this->id ); ?>" class="accordion-section-new-menu"> 558 <button type="button" class="button-secondary add-new-menu-item add-menu-toggle"> 559 <?php echo esc_html( $this->title ); ?> 560 <span class="screen-reader-text"><?php _e( 'Press return or enter to open' ); ?></span> 561 </button> 562 <ul class="new-menu-section-content"></ul> 563 </li> 564 <?php 565 } 566 } -
src/wp-includes/class-wp-customize-setting.php
630 630 remove_theme_mod( 'background_image_thumb' ); 631 631 } 632 632 } 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 */ 645 class 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 * @var string 657 */ 658 public $type = self::TYPE; 659 660 /** 661 * Default setting value. 662 * 663 * @see wp_setup_nav_menu_item() 664 * @var array 665 */ 666 public $default = array( 667 // The $menu_item_data for wp_update_nav_menu_item(). 668 'object_id' => 0, 669 'object' => '', // Taxonomy name. 670 'menu_item_parent' => 0, // A.K.A. menu-item-parent-id; note that post_parent is different, and not included. 671 'position' => 0, // A.K.A. menu_order. 672 'type' => 'custom', // Note that type_label is not included here. 673 'title' => '', 674 'url' => '', 675 'target' => '', 676 'attr_title' => '', 677 'description' => '', 678 'classes' => '', 679 'xfn' => '', 680 'status' => 'publish', 681 'original_title' => '', 682 'nav_menu_term_id' => 0, // This will be supplied as the $menu_id arg for wp_update_nav_menu_item(). 683 // @todo also expose invalid? 684 ); 685 686 /** 687 * Default transport. 688 * 689 * @var string 690 */ 691 public $transport = 'postMessage'; 692 693 /** 694 * The post ID represented by this setting instance. This is the db_id. 695 * 696 * A negative value represents a placeholder ID for a new menu not yet saved. 697 * 698 * @todo Should this be $db_id, and also use this for WP_Customize_Nav_Menu_Setting::$term_id 699 * 700 * @var int 701 */ 702 public $post_id; 703 704 /** 705 * Previous (placeholder) post ID used before creating a new menu item. 706 * 707 * This value will be exported to JS via the customize_save_response filter 708 * so that JavaScript can update the settings to refer to the newly-assigned 709 * post ID. This value is always negative to indicate it does not refer to 710 * a real post. 711 * 712 * @see WP_Customize_Nav_Menu_Item_Setting::update() 713 * @see WP_Customize_Nav_Menu_Item_Setting::amend_customize_save_response() 714 * 715 * @var int 716 */ 717 public $previous_post_id; 718 719 /** 720 * When previewing or updating a menu item, this stores the previous nav_menu_term_id 721 * which ensures that we can apply the proper filters. 722 * 723 * @var int 724 */ 725 public $original_nav_menu_term_id; 726 727 /** 728 * Whether or not preview() was called. 729 * 730 * @var bool 731 */ 732 protected $is_previewed = false; 733 734 /** 735 * Whether or not update() was called. 736 * 737 * @var bool 738 */ 739 protected $is_updated = false; 740 741 /** 742 * Status for calling the update method, used in customize_save_response filter. 743 * 744 * When status is inserted, the placeholder post ID is stored in $previous_post_id. 745 * When status is error, the error is stored in $update_error. 746 * 747 * @see WP_Customize_Nav_Menu_Item_Setting::update() 748 * @see WP_Customize_Nav_Menu_Item_Setting::amend_customize_save_response() 749 * 750 * @var string updated|inserted|deleted|error 751 */ 752 public $update_status; 753 754 /** 755 * Any error object returned by wp_update_nav_menu_item() when setting is updated. 756 * 757 * @see WP_Customize_Nav_Menu_Item_Setting::update() 758 * @see WP_Customize_Nav_Menu_Item_Setting::amend_customize_save_response() 759 * 760 * @var WP_Error 761 */ 762 public $update_error; 763 764 /** 765 * Constructor. 766 * 767 * Any supplied $args override class property defaults. 768 * 769 * @param WP_Customize_Manager $manager Manager instance. 770 * @param string $id An specific ID of the setting. Can be a 771 * theme mod or option name. 772 * @param array $args Optional. Setting arguments. 773 * @throws Exception If $id is not valid for this setting type. 774 */ 775 public function __construct( WP_Customize_Manager $manager, $id, array $args = array() ) { 776 if ( empty( $manager->menus ) ) { 777 throw new Exception( 'Expected WP_Customize_Manager::$menus to be set.' ); 778 } 779 780 if ( ! preg_match( self::ID_PATTERN, $id, $matches ) ) { 781 throw new Exception( "Illegal widget setting ID: $id" ); 782 } 783 784 $this->post_id = intval( $matches['id'] ); 785 786 $menu = $this->value(); 787 $this->original_nav_menu_term_id = $menu['nav_menu_term_id']; 788 789 parent::__construct( $manager, $id, $args ); 790 } 791 792 /** 793 * Get the instance data for a given widget setting. 794 * 795 * @see wp_setup_nav_menu_item() 796 * @return array 797 */ 798 public function value() { 799 if ( $this->is_previewed && $this->_previewed_blog_id === get_current_blog_id() ) { 800 $undefined = new stdClass(); // Symbol. 801 $post_value = $this->post_value( $undefined ); 802 803 if ( $undefined === $post_value ) { 804 $value = $this->_original_value; 805 } else { 806 $value = $post_value; 807 } 808 } else { 809 $value = false; 810 811 // Note that a ID of less than one indicates a nav_menu not yet inserted. 812 if ( $this->post_id > 0 ) { 813 $post = get_post( $this->post_id ); 814 if ( $post && self::POST_TYPE === $post->post_type ) { 815 $item = wp_setup_nav_menu_item( $post ); 816 $value = wp_array_slice_assoc( 817 (array) $item, 818 array_keys( $this->default ) 819 ); 820 $value['position'] = $item->menu_order; 821 $value['status'] = $item->post_status; 822 $value['original_title'] = ''; 823 824 $menus = wp_get_post_terms( $post->ID, WP_Customize_Nav_Menu_Setting::TAXONOMY, array( 825 'fields' => 'ids', 826 ) ); 827 828 if ( ! empty( $menus ) ) { 829 $value['nav_menu_term_id'] = array_shift( $menus ); 830 } else { 831 $value['nav_menu_term_id'] = 0; 832 } 833 834 if ( 'post_type' === $value['type'] ) { 835 $original_title = get_the_title( $value['object_id'] ); 836 } else if ( 'taxonomy' === $value['type'] ) { 837 $original_title = get_term_field( 'name', $value['object_id'], $value['object'], 'raw' ); 838 if ( is_wp_error( $original_title ) ) { 839 $original_title = ''; 840 } 841 } 842 843 if ( ! empty( $original_title ) ) { 844 $value['original_title'] = $original_title; 845 } 846 } 847 } 848 849 if ( ! is_array( $value ) ) { 850 $value = $this->default; 851 } 852 } 853 854 if ( is_array( $value ) ) { 855 foreach ( array( 'object_id', 'menu_item_parent', 'nav_menu_term_id' ) as $key ) { 856 $value[ $key ] = intval( $value[ $key ] ); 857 } 858 } 859 860 return $value; 861 } 862 863 /** 864 * Handle previewing the setting. 865 * 866 * @see WP_Customize_Manager::post_value() 867 */ 868 public function preview() { 869 if ( $this->is_previewed ) { 870 return; 871 } 872 873 $this->is_previewed = true; 874 $this->_original_value = $this->value(); 875 $this->original_nav_menu_term_id = $this->_original_value['nav_menu_term_id']; 876 $this->_previewed_blog_id = get_current_blog_id(); 877 878 add_filter( 'wp_get_nav_menu_items', array( $this, 'filter_wp_get_nav_menu_items' ), 10, 3 ); 879 880 $sort_callback = array( __CLASS__, 'sort_wp_get_nav_menu_items' ); 881 if ( ! has_filter( 'wp_get_nav_menu_items', $sort_callback ) ) { 882 add_filter( 'wp_get_nav_menu_items', array( __CLASS__, 'sort_wp_get_nav_menu_items' ), 1000, 3 ); 883 } 884 885 // @todo Add get_post_metadata filters for plugins to add their data. 886 } 887 888 /** 889 * Filter the wp_get_nav_menu_items() result to supply the previewed menu items. 890 * 891 * @see wp_get_nav_menu_items() 892 * @param array $items An array of menu item post objects. 893 * @param object $menu The menu object. 894 * @param array $args An array of arguments used to retrieve menu item objects. 895 * @return array 896 */ 897 function filter_wp_get_nav_menu_items( $items, $menu, $args ) { 898 $this_item = $this->value(); 899 $current_nav_menu_term_id = $this_item['nav_menu_term_id']; 900 unset( $this_item['nav_menu_term_id'] ); 901 902 $should_filter = ( 903 $menu->term_id === $this->original_nav_menu_term_id 904 || 905 $menu->term_id === $current_nav_menu_term_id 906 ); 907 if ( ! $should_filter ) { 908 return $items; 909 } 910 911 // Handle deleted menu item, or menu item moved to another menu. 912 $should_remove = ( 913 false === $this_item 914 || 915 ( 916 $this->original_nav_menu_term_id === $menu->term_id 917 && 918 $current_nav_menu_term_id !== $this->original_nav_menu_term_id 919 ) 920 ); 921 if ( $should_remove ) { 922 $filtered_items = array(); 923 foreach ( $items as $item ) { 924 if ( $item->db_id !== $this->post_id ) { 925 $filtered_items[] = $item; 926 } 927 } 928 return $filtered_items; 929 } 930 931 $mutated = false; 932 $should_update = ( 933 is_array( $this_item ) 934 && 935 $current_nav_menu_term_id === $menu->term_id 936 ); 937 if ( $should_update ) { 938 foreach ( $items as $item ) { 939 if ( $item->db_id === $this->post_id ) { 940 foreach ( get_object_vars( $this->value_as_wp_post_nav_menu_item() ) as $key => $value ) { 941 $item->$key = $value; 942 } 943 $mutated = true; 944 } 945 } 946 947 // Not found so we have to append it.. 948 if ( ! $mutated ) { 949 $items[] = $this->value_as_wp_post_nav_menu_item(); 950 } 951 } 952 953 return $items; 954 } 955 956 /** 957 * Re-apply the tail logic also applied on $items by wp_get_nav_menu_items(). 958 * 959 * @see wp_get_nav_menu_items() 960 * 961 * @param array $items An array of menu item post objects. 962 * @param object $menu The menu object. 963 * @param array $args An array of arguments used to retrieve menu item objects. 964 * @return array 965 */ 966 static function sort_wp_get_nav_menu_items( $items, $menu, $args ) { 967 // @todo We should probably re-apply some constraints imposed by $args. 968 unset( $args['include'] ); 969 970 // Remove invalid items only in frontend. 971 if ( ! is_admin() ) { 972 $items = array_filter( $items, '_is_valid_nav_menu_item' ); 973 } 974 975 if ( ARRAY_A === $args['output'] ) { 976 $GLOBALS['_menu_item_sort_prop'] = $args['output_key']; 977 usort( $items, '_sort_nav_menu_items' ); 978 $i = 1; 979 980 foreach ( $items as $k => $item ) { 981 $items[ $k ]->$args['output_key'] = $i++; 982 } 983 } 984 985 return $items; 986 } 987 988 /** 989 * Get the value emulated into a WP_Post and set up as a nav_menu_item. 990 * 991 * @return WP_Post With {@see wp_setup_nav_menu_item()} applied. 992 */ 993 public function value_as_wp_post_nav_menu_item() { 994 $item = (object) $this->value(); 995 unset( $item->nav_menu_term_id ); 996 997 $item->post_status = $item->status; 998 unset( $item->status ); 999 1000 $item->post_type = 'nav_menu_item'; 1001 $item->menu_order = $item->position; 1002 unset( $item->position ); 1003 1004 $item->post_author = get_current_user_id(); 1005 1006 if ( $item->title ) { 1007 $item->post_title = $item->title; 1008 } 1009 1010 $item->ID = $this->post_id; 1011 $post = new WP_Post( (object) $item ); 1012 $post = wp_setup_nav_menu_item( $post ); 1013 1014 return $post; 1015 } 1016 1017 /** 1018 * Sanitize an input. 1019 * 1020 * Note that parent::sanitize() erroneously does wp_unslash() on $value, but 1021 * we remove that in this override. 1022 * 1023 * @param array $menu_item_value The value to sanitize. 1024 * @return array|false|null Null if an input isn't valid. False if it is marked for deletion. Otherwise the sanitized value. 1025 */ 1026 public function sanitize( $menu_item_value ) { 1027 // Menu is marked for deletion. 1028 if ( false === $menu_item_value ) { 1029 return $menu_item_value; 1030 } 1031 1032 // Invalid. 1033 if ( ! is_array( $menu_item_value ) ) { 1034 return null; 1035 } 1036 1037 $default = array( 1038 'object_id' => 0, 1039 'object' => '', 1040 'menu_item_parent' => 0, 1041 'position' => 0, 1042 'type' => 'custom', 1043 'title' => '', 1044 'url' => '', 1045 'target' => '', 1046 'attr_title' => '', 1047 'description' => '', 1048 'classes' => '', 1049 'xfn' => '', 1050 'status' => 'publish', 1051 'original_title' => '', 1052 'nav_menu_term_id' => 0, 1053 ); 1054 $menu_item_value = array_merge( $default, $menu_item_value ); 1055 $menu_item_value = wp_array_slice_assoc( $menu_item_value, array_keys( $default ) ); 1056 $menu_item_value['position'] = max( 0, intval( $menu_item_value['position'] ) ); 1057 1058 foreach ( array( 'object_id', 'menu_item_parent', 'nav_menu_term_id' ) as $key ) { 1059 // Note we need to allow negative-integer IDs for previewed objects not inserted yet. 1060 $menu_item_value[ $key ] = intval( $menu_item_value[ $key ] ); 1061 } 1062 1063 foreach ( array( 'type', 'object', 'target' ) as $key ) { 1064 $menu_item_value[ $key ] = sanitize_key( $menu_item_value[ $key ] ); 1065 } 1066 1067 foreach ( array( 'xfn', 'classes' ) as $key ) { 1068 $value = $menu_item_value[ $key ]; 1069 if ( ! is_array( $value ) ) { 1070 $value = explode( ' ', $value ); 1071 } 1072 $menu_item_value[ $key ] = implode( ' ', array_map( 'sanitize_html_class', $value ) ); 1073 } 1074 1075 foreach ( array( 'title', 'attr_title', 'description', 'original_title' ) as $key ) { 1076 // @todo Should esc_attr() the attr_title as well? 1077 $menu_item_value[ $key ] = sanitize_text_field( $menu_item_value[ $key ] ); 1078 } 1079 1080 $menu_item_value['url'] = esc_url_raw( $menu_item_value['url'] ); 1081 if ( ! get_post_status_object( $menu_item_value['status'] ) ) { 1082 $menu_item_value['status'] = 'publish'; 1083 } 1084 1085 /** This filter is documented in wp-includes/class-wp-customize-setting.php */ 1086 return apply_filters( "customize_sanitize_{$this->id}", $menu_item_value, $this ); 1087 } 1088 1089 /** 1090 * Create/update the nav_menu_item post for this setting. 1091 * 1092 * Any created menu items will have their assigned post IDs exported to the client 1093 * via the customize_save_response filter. Likewise, any errors will be exported 1094 * to the client via the customize_save_response() filter. 1095 * 1096 * To delete a menu, the client can send false as the value. 1097 * 1098 * @see wp_update_nav_menu_item() 1099 * 1100 * @param array|false $value The menu item array to update. If false, then the menu item will be deleted entirely. 1101 * See {@see WP_Customize_Nav_Menu_Item_Setting::$default} for what the value should 1102 * consist of. 1103 * @return void 1104 */ 1105 protected function update( $value ) { 1106 if ( $this->is_updated ) { 1107 return; 1108 } 1109 1110 $this->is_updated = true; 1111 $is_placeholder = ( $this->post_id < 0 ); 1112 $is_delete = ( false === $value ); 1113 1114 add_filter( 'customize_save_response', array( $this, 'amend_customize_save_response' ) ); 1115 1116 if ( $is_delete ) { 1117 // If the current setting post is a placeholder, a delete request is a no-op. 1118 if ( $is_placeholder ) { 1119 $this->update_status = 'deleted'; 1120 } else { 1121 $r = wp_delete_post( $this->post_id, true ); 1122 1123 if ( false === $r ) { 1124 $this->update_error = new WP_Error( 'delete_failure' ); 1125 $this->update_status = 'error'; 1126 } else { 1127 $this->update_status = 'deleted'; 1128 } 1129 // @todo send back the IDs for all associated nav menu items deleted, so these settings (and controls) can be removed from Customizer? 1130 } 1131 } else { 1132 1133 // Handle saving menu items for menus that are being newly-created. 1134 if ( $value['nav_menu_term_id'] < 0 ) { 1135 $nav_menu_setting_id = sprintf( 'nav_menu[%s]', $value['nav_menu_term_id'] ); 1136 $nav_menu_setting = $this->manager->get_setting( $nav_menu_setting_id ); 1137 1138 if ( ! $nav_menu_setting || ! ( $nav_menu_setting instanceof WP_Customize_Nav_Menu_Setting ) ) { 1139 $this->update_status = 'error'; 1140 $this->update_error = new WP_Error( 'unexpected_nav_menu_setting' ); 1141 return; 1142 } 1143 1144 if ( false === $nav_menu_setting->save() ) { 1145 $this->update_status = 'error'; 1146 $this->update_error = new WP_Error( 'nav_menu_setting_failure' ); 1147 } 1148 1149 if ( $nav_menu_setting->previous_term_id !== intval( $value['nav_menu_term_id'] ) ) { 1150 $this->update_status = 'error'; 1151 $this->update_error = new WP_Error( 'unexpected_previous_term_id' ); 1152 return; 1153 } 1154 1155 $value['nav_menu_term_id'] = $nav_menu_setting->term_id; 1156 } 1157 1158 // Handle saving a nav menu item that is a child of a nav menu item being newly-created. 1159 if ( $value['menu_item_parent'] < 0 ) { 1160 $parent_nav_menu_item_setting_id = sprintf( 'nav_menu_item[%s]', $value['menu_item_parent'] ); 1161 $parent_nav_menu_item_setting = $this->manager->get_setting( $parent_nav_menu_item_setting_id ); 1162 1163 if ( ! $parent_nav_menu_item_setting || ! ( $parent_nav_menu_item_setting instanceof WP_Customize_Nav_Menu_Item_Setting ) ) { 1164 $this->update_status = 'error'; 1165 $this->update_error = new WP_Error( 'unexpected_nav_menu_item_setting' ); 1166 return; 1167 } 1168 1169 if ( false === $parent_nav_menu_item_setting->save() ) { 1170 $this->update_status = 'error'; 1171 $this->update_error = new WP_Error( 'nav_menu_item_setting_failure' ); 1172 } 1173 1174 if ( $parent_nav_menu_item_setting->previous_post_id !== intval( $value['menu_item_parent'] ) ) { 1175 $this->update_status = 'error'; 1176 $this->update_error = new WP_Error( 'unexpected_previous_post_id' ); 1177 return; 1178 } 1179 1180 $value['menu_item_parent'] = $parent_nav_menu_item_setting->post_id; 1181 } 1182 1183 // Insert or update menu. 1184 $menu_item_data = array( 1185 'menu-item-object-id' => $value['object_id'], 1186 'menu-item-object' => $value['object'], 1187 'menu-item-parent-id' => $value['menu_item_parent'], 1188 'menu-item-position' => $value['position'], 1189 'menu-item-type' => $value['type'], 1190 'menu-item-title' => $value['title'], 1191 'menu-item-url' => $value['url'], 1192 'menu-item-description' => $value['description'], 1193 'menu-item-attr-title' => $value['attr_title'], 1194 'menu-item-target' => $value['target'], 1195 'menu-item-classes' => $value['classes'], 1196 'menu-item-xfn' => $value['xfn'], 1197 'menu-item-status' => $value['status'], 1198 ); 1199 1200 $r = wp_update_nav_menu_item( 1201 $value['nav_menu_term_id'], 1202 $is_placeholder ? 0 : $this->post_id, 1203 $menu_item_data 1204 ); 1205 1206 if ( is_wp_error( $r ) ) { 1207 $this->update_status = 'error'; 1208 $this->update_error = $r; 1209 } else { 1210 if ( $is_placeholder ) { 1211 $this->previous_post_id = $this->post_id; 1212 $this->post_id = $r; 1213 $this->update_status = 'inserted'; 1214 } else { 1215 $this->update_status = 'updated'; 1216 } 1217 } 1218 } 1219 1220 } 1221 1222 /** 1223 * Export data for the JS client. 1224 * 1225 * @see WP_Customize_Nav_Menu_Item_Setting::update() 1226 * 1227 * @param array $data Additional information passed back to the 'saved' event on `wp.customize`. 1228 * @return array 1229 */ 1230 function amend_customize_save_response( $data ) { 1231 if ( ! isset( $data['nav_menu_item_updates'] ) ) { 1232 $data['nav_menu_item_updates'] = array(); 1233 } 1234 1235 $data['nav_menu_item_updates'][] = array( 1236 'post_id' => $this->post_id, 1237 'previous_post_id' => $this->previous_post_id, 1238 'error' => $this->update_error ? $this->update_error->get_error_code() : null, 1239 'status' => $this->update_status, 1240 ); 1241 1242 return $data; 1243 } 1244 } 1245 1246 /** 1247 * Customize Setting to represent a nav_menu. 1248 * 1249 * Subclass of WP_Customize_Setting to represent a nav_menu taxonomy term, and 1250 * the IDs for the nav_menu_items associated with the nav menu. 1251 * 1252 * @since 4.3.0 1253 * 1254 * @see wp_get_nav_menu_object() 1255 * @see WP_Customize_Setting 1256 */ 1257 class WP_Customize_Nav_Menu_Setting extends WP_Customize_Setting { 1258 1259 const ID_PATTERN = '/^nav_menu\[(?P<id>-?\d+)\]$/'; 1260 1261 const TAXONOMY = 'nav_menu'; 1262 1263 const TYPE = 'nav_menu'; 1264 1265 /** 1266 * Setting type. 1267 * 1268 * @var string 1269 */ 1270 public $type = self::TYPE; 1271 1272 /** 1273 * Default setting value; 1274 * 1275 * @see wp_get_nav_menu_object() 1276 * 1277 * @var array 1278 */ 1279 public $default = array( 1280 'name' => '', 1281 'description' => '', 1282 'parent' => 0, 1283 'auto_add' => false, 1284 ); 1285 1286 /** 1287 * Default transport. 1288 * 1289 * @var string 1290 */ 1291 public $transport = 'postMessage'; 1292 1293 /** 1294 * The term ID represented by this setting instance. 1295 * 1296 * A negative value represents a placeholder ID for a new menu not yet saved. 1297 * 1298 * @var int 1299 */ 1300 public $term_id; 1301 1302 /** 1303 * Previous (placeholder) term ID used before creating a new menu. 1304 * 1305 * This value will be exported to JS via the customize_save_response filter 1306 * so that JavaScript can update the settings to refer to the newly-assigned 1307 * term ID. This value is always negative to indicate it does not refer to 1308 * a real term. 1309 * 1310 * @see WP_Customize_Nav_Menu_Setting::update() 1311 * @see WP_Customize_Nav_Menu_Setting::amend_customize_save_response() 1312 * 1313 * @var int 1314 */ 1315 public $previous_term_id; 1316 1317 /** 1318 * Whether or not preview() was called. 1319 * 1320 * @var bool 1321 */ 1322 protected $is_previewed = false; 1323 1324 /** 1325 * Whether or not update() was called. 1326 * 1327 * @var bool 1328 */ 1329 protected $is_updated = false; 1330 1331 /** 1332 * Status for calling the update method, used in customize_save_response filter. 1333 * 1334 * When status is inserted, the placeholder term ID is stored in $previous_term_id. 1335 * When status is error, the error is stored in $update_error. 1336 * 1337 * @see WP_Customize_Nav_Menu_Setting::update() 1338 * @see WP_Customize_Nav_Menu_Setting::amend_customize_save_response() 1339 * 1340 * @var string updated|inserted|deleted|error 1341 */ 1342 public $update_status; 1343 1344 /** 1345 * Any error object returned by wp_update_nav_menu_object() when setting is updated. 1346 * 1347 * @see WP_Customize_Nav_Menu_Setting::update() 1348 * @see WP_Customize_Nav_Menu_Setting::amend_customize_save_response() 1349 * 1350 * @var WP_Error 1351 */ 1352 public $update_error; 1353 1354 /** 1355 * Constructor. 1356 * 1357 * Any supplied $args override class property defaults. 1358 * 1359 * @param WP_Customize_Manager $manager Manager instance. 1360 * @param string $id An specific ID of the setting. Can be a 1361 * theme mod or option name. 1362 * @param array $args Optional. Setting arguments. 1363 * @throws Exception If $id is not valid for this setting type. 1364 */ 1365 public function __construct( WP_Customize_Manager $manager, $id, array $args = array() ) { 1366 if ( empty( $manager->menus ) ) { 1367 throw new Exception( 'Expected WP_Customize_Manager::$menus to be set.' ); 1368 } 1369 1370 if ( ! preg_match( self::ID_PATTERN, $id, $matches ) ) { 1371 throw new Exception( "Illegal widget setting ID: $id" ); 1372 } 1373 1374 $this->term_id = intval( $matches['id'] ); 1375 1376 parent::__construct( $manager, $id, $args ); 1377 } 1378 1379 /** 1380 * Get the instance data for a given widget setting. 1381 * 1382 * @see wp_get_nav_menu_object() 1383 * @return array 1384 */ 1385 public function value() { 1386 if ( $this->is_previewed && $this->_previewed_blog_id === get_current_blog_id() ) { 1387 $undefined = new stdClass(); // Symbol. 1388 $post_value = $this->post_value( $undefined ); 1389 1390 if ( $undefined === $post_value ) { 1391 $value = $this->_original_value; 1392 } else { 1393 $value = $post_value; 1394 } 1395 } else { 1396 $value = false; 1397 1398 // Note that a term_id of less than one indicates a nav_menu not yet inserted. 1399 if ( $this->term_id > 0 ) { 1400 $term = wp_get_nav_menu_object( $this->term_id ); 1401 1402 if ( $term ) { 1403 $value = wp_array_slice_assoc( (array) $term, array_keys( $this->default ) ); 1404 1405 $nav_menu_options = (array) get_option( 'nav_menu_options', array() ); 1406 $value['auto_add'] = false; 1407 1408 if ( isset( $nav_menu_options['auto_add'] ) && is_array( $nav_menu_options['auto_add'] ) ) { 1409 $value['auto_add'] = in_array( $term->term_id, $nav_menu_options['auto_add'] ); 1410 } 1411 } 1412 } 1413 1414 if ( ! is_array( $value ) ) { 1415 $value = $this->default; 1416 } 1417 } 1418 return $value; 1419 } 1420 1421 /** 1422 * Handle previewing the setting. 1423 * 1424 * @see WP_Customize_Manager::post_value() 1425 */ 1426 public function preview() { 1427 if ( $this->is_previewed ) { 1428 return; 1429 } 1430 1431 $this->is_previewed = true; 1432 $this->_original_value = $this->value(); 1433 $this->_previewed_blog_id = get_current_blog_id(); 1434 1435 add_filter( 'wp_get_nav_menu_object', array( $this, 'filter_wp_get_nav_menu_object' ), 10, 2 ); 1436 add_filter( 'default_option_nav_menu_options', array( $this, 'filter_nav_menu_options' ) ); 1437 add_filter( 'option_nav_menu_options', array( $this, 'filter_nav_menu_options' ) ); 1438 } 1439 1440 /** 1441 * Filter the wp_get_nav_menu_object() result to supply the previewed menu object. 1442 * 1443 * Requesting a nav_menu object by anything but ID is not supported. 1444 * 1445 * @see wp_get_nav_menu_object() 1446 * 1447 * @param object|null $menu_obj Object returned by wp_get_nav_menu_object(). 1448 * @param string $menu_id ID of the nav_menu term. Requests by slug or name will be ignored. 1449 * @return object|null 1450 */ 1451 function filter_wp_get_nav_menu_object( $menu_obj, $menu_id ) { 1452 $ok = ( 1453 get_current_blog_id() === $this->_previewed_blog_id 1454 && 1455 is_int( $menu_id ) 1456 && 1457 $menu_id === $this->term_id 1458 ); 1459 if ( ! $ok ) { 1460 return $menu_obj; 1461 } 1462 1463 $setting_value = $this->value(); 1464 1465 // Handle deleted menus. 1466 if ( false === $setting_value ) { 1467 return false; 1468 } 1469 1470 // Handle sanitization failure by preventing short-circuiting. 1471 if ( null === $setting_value ) { 1472 return $menu_obj; 1473 } 1474 1475 $menu_obj = (object) array_merge( array( 1476 'term_id' => $this->term_id, 1477 'term_taxonomy_id' => $this->term_id, 1478 'slug' => sanitize_title( $setting_value['name'] ), 1479 'count' => 0, 1480 'term_group' => 0, 1481 'taxonomy' => self::TAXONOMY, 1482 'filter' => 'raw', 1483 ), $setting_value ); 1484 1485 return $menu_obj; 1486 } 1487 1488 /** 1489 * Filter the nav_menu_options option to include this menu's auto_add preference. 1490 * 1491 * @param array $nav_menu_options Nav menu options including auto_add. 1492 * @return array 1493 */ 1494 function filter_nav_menu_options( $nav_menu_options ) { 1495 if ( $this->_previewed_blog_id !== get_current_blog_id() ) { 1496 return $nav_menu_options; 1497 } 1498 1499 $menu = $this->value(); 1500 $nav_menu_options = $this->filter_nav_menu_options_value( 1501 $nav_menu_options, 1502 $this->term_id, 1503 false === $menu ? false : $menu['auto_add'] 1504 ); 1505 1506 return $nav_menu_options; 1507 } 1508 1509 /** 1510 * Sanitize an input. 1511 * 1512 * Note that parent::sanitize() erroneously does wp_unslash() on $value, but 1513 * we remove that in this override. 1514 * 1515 * @param array $value The value to sanitize. 1516 * @return array|false|null Null if an input isn't valid. False if it is marked for deletion. Otherwise the sanitized value. 1517 */ 1518 public function sanitize( $value ) { 1519 // Menu is marked for deletion. 1520 if ( false === $value ) { 1521 return $value; 1522 } 1523 1524 // Invalid. 1525 if ( ! is_array( $value ) ) { 1526 return null; 1527 } 1528 1529 $default = array( 1530 'name' => '', 1531 'description' => '', 1532 'parent' => 0, 1533 'auto_add' => false, 1534 ); 1535 $value = array_merge( $default, $value ); 1536 $value = wp_array_slice_assoc( $value, array_keys( $default ) ); 1537 1538 $value['name'] = trim( esc_html( $value['name'] ) ); // This sanitization code is used in wp-admin/nav-menus.php. 1539 $value['description'] = sanitize_text_field( $value['description'] ); 1540 $value['parent'] = max( 0, intval( $value['parent'] ) ); 1541 $value['auto_add'] = ! empty( $value['auto_add'] ); 1542 1543 /** This filter is documented in wp-includes/class-wp-customize-setting.php */ 1544 return apply_filters( "customize_sanitize_{$this->id}", $value, $this ); 1545 } 1546 1547 /** 1548 * Create/update the nav_menu term for this setting. 1549 * 1550 * Any created menus will have their assigned term IDs exported to the client 1551 * via the customize_save_response filter. Likewise, any errors will be exported 1552 * to the client via the customize_save_response() filter. 1553 * 1554 * To delete a menu, the client can send false as the value. 1555 * 1556 * @see wp_update_nav_menu_object() 1557 * 1558 * @param array|false $value { 1559 * The value to update. Note that slug cannot be updated via wp_update_nav_menu_object(). 1560 * If false, then the menu will be deleted entirely. 1561 * 1562 * @type string $name The name of the menu to save. 1563 * @type string $description The term description. Default empty string. 1564 * @type int $parent The id of the parent term. Default 0. 1565 * @type bool $auto_add Whether pages will auto_add to this menu. Default false. 1566 * } 1567 * @return void 1568 */ 1569 protected function update( $value ) { 1570 if ( $this->is_updated ) { 1571 return; 1572 } 1573 1574 $this->is_updated = true; 1575 $is_placeholder = ( $this->term_id < 0 ); 1576 $is_delete = ( false === $value ); 1577 1578 add_filter( 'customize_save_response', array( $this, 'amend_customize_save_response' ) ); 1579 1580 $auto_add = null; 1581 if ( $is_delete ) { 1582 // If the current setting term is a placeholder, a delete request is a no-op. 1583 if ( $is_placeholder ) { 1584 $this->update_status = 'deleted'; 1585 } else { 1586 $r = wp_delete_nav_menu( $this->term_id ); 1587 1588 if ( is_wp_error( $r ) ) { 1589 $this->update_status = 'error'; 1590 $this->update_error = $r; 1591 } else { 1592 $this->update_status = 'deleted'; 1593 $auto_add = false; 1594 } 1595 } 1596 } else { 1597 // Insert or update menu. 1598 $menu_data = wp_array_slice_assoc( $value, array( 'description', 'parent' ) ); 1599 if ( isset( $value['name'] ) ) { 1600 $menu_data['menu-name'] = $value['name']; 1601 } 1602 1603 $r = wp_update_nav_menu_object( $is_placeholder ? 0 : $this->term_id, $menu_data ); 1604 if ( is_wp_error( $r ) ) { 1605 $this->update_status = 'error'; 1606 $this->update_error = $r; 1607 } else { 1608 if ( $is_placeholder ) { 1609 $this->previous_term_id = $this->term_id; 1610 $this->term_id = $r; 1611 $this->update_status = 'inserted'; 1612 } else { 1613 $this->update_status = 'updated'; 1614 } 1615 1616 $auto_add = $value['auto_add']; 1617 } 1618 } 1619 1620 if ( null !== $auto_add ) { 1621 $nav_menu_options = $this->filter_nav_menu_options_value( 1622 (array) get_option( 'nav_menu_options', array() ), 1623 $this->term_id, 1624 $auto_add 1625 ); 1626 update_option( 'nav_menu_options', $nav_menu_options ); 1627 } 1628 1629 // Make sure that new menus assigned to nav menu locations use their new IDs. 1630 if ( 'inserted' === $this->update_status ) { 1631 foreach ( $this->manager->settings() as $setting ) { 1632 if ( ! preg_match( '/^nav_menu_locations\[/', $setting->id ) ) { 1633 continue; 1634 } 1635 1636 $post_value = $setting->post_value( null ); 1637 if ( ! is_null( $post_value ) && $this->previous_term_id === intval( $post_value ) ) { 1638 $this->manager->set_post_value( $setting->id, $this->term_id ); 1639 $setting->save(); 1640 } 1641 } 1642 } 1643 } 1644 1645 /** 1646 * Update a nav_menu_options array. 1647 * 1648 * @see WP_Customize_Nav_Menu_Setting::filter_nav_menu_options() 1649 * @see WP_Customize_Nav_Menu_Setting::update() 1650 * 1651 * @param array $nav_menu_options Array as returned by get_option( 'nav_menu_options' ). 1652 * @param int $menu_id The term ID for the given menu. 1653 * @param bool $auto_add Whether to auto-add or not. 1654 * @return array 1655 */ 1656 protected function filter_nav_menu_options_value( $nav_menu_options, $menu_id, $auto_add ) { 1657 $nav_menu_options = (array) $nav_menu_options; 1658 if ( ! isset( $nav_menu_options['auto_add'] ) ) { 1659 $nav_menu_options['auto_add'] = array(); 1660 } 1661 1662 $i = array_search( $menu_id, $nav_menu_options['auto_add'] ); 1663 if ( $auto_add && false === $i ) { 1664 array_push( $nav_menu_options['auto_add'], $this->term_id ); 1665 } else if ( ! $auto_add && false !== $i ) { 1666 array_splice( $nav_menu_options['auto_add'], $i, 1 ); 1667 } 1668 1669 return $nav_menu_options; 1670 } 1671 1672 /** 1673 * Export data for the JS client. 1674 * 1675 * @see WP_Customize_Nav_Menu_Setting::update() 1676 * 1677 * @param array $data Additional information passed back to the 'saved' event on `wp.customize`. 1678 * @return array 1679 */ 1680 function amend_customize_save_response( $data ) { 1681 if ( ! isset( $data['nav_menu_updates'] ) ) { 1682 $data['nav_menu_updates'] = array(); 1683 } 1684 1685 $data['nav_menu_updates'][] = array( 1686 'term_id' => $this->term_id, 1687 'previous_term_id' => $this->previous_term_id, 1688 'error' => $this->update_error ? $this->update_error->get_error_code() : null, 1689 'status' => $this->update_status, 1690 ); 1691 1692 return $data; 1693 } 1694 } -
src/wp-includes/script-loader.php
406 406 $scripts->add( 'customize-widgets', "/wp-admin/js/customize-widgets$suffix.js", array( 'jquery', 'jquery-ui-sortable', 'jquery-ui-droppable', 'wp-backbone', 'customize-controls' ), false, 1 ); 407 407 $scripts->add( 'customize-preview-widgets', "/wp-includes/js/customize-preview-widgets$suffix.js", array( 'jquery', 'wp-util', 'customize-preview' ), false, 1 ); 408 408 409 $scripts->add( 'menu-customizer', "/wp-admin/js/menu-customizer$suffix.js", array( 'jquery', 'wp-backbone', 'customize-controls', 'accordion', 'nav-menu', 'wp-a11y' ) ); 410 $scripts->add( 'customize-menus-preview', "/wp-admin/js/customize-menus-preview$suffix.js", array( 'customize-preview', 'wp-util' ), false, 1 ); 411 409 412 $scripts->add( 'accordion', "/wp-admin/js/accordion$suffix.js", array( 'jquery' ), false, 1 ); 410 413 411 414 $scripts->add( 'shortcode', "/wp-includes/js/shortcode$suffix.js", array( 'underscore' ), false, 1 ); … … 656 659 $suffix = SCRIPT_DEBUG ? '' : '.min'; 657 660 658 661 // 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' ) ); 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( 'menu-customizer', "/wp-admin/css/menu-customizer$suffix.css" ); 669 $styles->add( 'customize-menus-preview', "/wp-admin/css/customize-menus-preview$suffix.css" ); 670 $styles->add( 'press-this', "/wp-admin/css/press-this$suffix.css", array( 'open-sans', 'buttons' ) ); 666 671 667 672 $styles->add( 'ie', "/wp-admin/css/ie$suffix.css" ); 668 673 $styles->add_data( 'ie', 'conditional', 'lte IE 7' );