<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[32806] trunk: Add menu management to the Customizer.</title>
</head>
<body>
<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; }
#msg dl a { font-weight: bold}
#msg dl a:link { color:#fc3; }
#msg dl a:active { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="https://core.trac.wordpress.org/changeset/32806">32806</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"https://core.trac.wordpress.org/changeset/32806","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>ocean90</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2015-06-16 22:07:08 +0000 (Tue, 16 Jun 2015)</dd>
</dl>
<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>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 <a href="https://core.trac.wordpress.org/ticket/32576">#32576</a>.</pre>
<h3>Modified Paths</h3>
<ul>
<li><a href="#trunkGruntfilejs">trunk/Gruntfile.js</a></li>
<li><a href="#trunksrcwpincludesclasswpcustomizecontrolphp">trunk/src/wp-includes/class-wp-customize-control.php</a></li>
<li><a href="#trunksrcwpincludesclasswpcustomizemanagerphp">trunk/src/wp-includes/class-wp-customize-manager.php</a></li>
<li><a href="#trunksrcwpincludesclasswpcustomizesectionphp">trunk/src/wp-includes/class-wp-customize-section.php</a></li>
<li><a href="#trunksrcwpincludesclasswpcustomizesettingphp">trunk/src/wp-includes/class-wp-customize-setting.php</a></li>
<li><a href="#trunksrcwpincludesscriptloaderphp">trunk/src/wp-includes/script-loader.php</a></li>
</ul>
<h3>Added Paths</h3>
<ul>
<li><a href="#trunksrcwpadmincsscustomizenavmenuscss">trunk/src/wp-admin/css/customize-nav-menus.css</a></li>
<li><a href="#trunksrcwpadminjscustomizenavmenusjs">trunk/src/wp-admin/js/customize-nav-menus.js</a></li>
<li><a href="#trunksrcwpincludesclasswpcustomizenavmenusphp">trunk/src/wp-includes/class-wp-customize-nav-menus.php</a></li>
<li><a href="#trunksrcwpincludescsscustomizepreviewcss">trunk/src/wp-includes/css/customize-preview.css</a></li>
<li><a href="#trunksrcwpincludesjscustomizepreviewnavmenusjs">trunk/src/wp-includes/js/customize-preview-nav-menus.js</a></li>
<li><a href="#trunktestsphpunittestscustomizenavmenuitemsettingphp">trunk/tests/phpunit/tests/customize/nav-menu-item-setting.php</a></li>
<li><a href="#trunktestsphpunittestscustomizenavmenusettingphp">trunk/tests/phpunit/tests/customize/nav-menu-setting.php</a></li>
<li><a href="#trunktestsphpunittestscustomizenavmenusphp">trunk/tests/phpunit/tests/customize/nav-menus.php</a></li>
</ul>
</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunkGruntfilejs"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/Gruntfile.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/Gruntfile.js 2015-06-16 21:32:53 UTC (rev 32805)
+++ trunk/Gruntfile.js 2015-06-16 22:07:08 UTC (rev 32806)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -141,7 +141,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> },
</span><span class="cx" style="display: block; padding: 0 10px"> cssmin: {
</span><span class="cx" style="display: block; padding: 0 10px"> options: {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- 'wp-admin': ['wp-admin', 'color-picker', 'customize-controls', 'customize-widgets', 'ie', 'install', 'login', 'press-this', 'deprecated-*']
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ 'wp-admin': ['wp-admin', 'color-picker', 'customize-controls', 'customize-widgets', 'customize-nav-menus', 'ie', 'install', 'login', 'press-this', 'deprecated-*']
</ins><span class="cx" style="display: block; padding: 0 10px"> },
</span><span class="cx" style="display: block; padding: 0 10px"> core: {
</span><span class="cx" style="display: block; padding: 0 10px"> expand: true,
</span></span></pre></div>
<a id="trunksrcwpadmincsscustomizenavmenuscss"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/src/wp-admin/css/customize-nav-menus.css</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/css/customize-nav-menus.css (rev 0)
+++ trunk/src/wp-admin/css/customize-nav-menus.css 2015-06-16 22:07:08 UTC (rev 32806)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,964 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+#accordion-section-menu_locations {
+ position: relative;
+ margin-bottom: 15px;
+}
+
+.menu-in-location,
+.menu-in-locations {
+ display: block;
+ color: #999;
+ font-weight: 600;
+ font-size: 10px;
+}
+
+#customize-controls .control-section .accordion-section-title:focus .menu-in-location,
+#customize-controls .control-section .accordion-section-title:hover .menu-in-location,
+#customize-controls .control-section .accordion-section-title:focus .menu-in-locations,
+#customize-controls .control-section .accordion-section-title:hover .menu-in-locations {
+ color: #fff;
+}
+
+.wp-customizer .menu-item-bar .menu-item-handle,
+.wp-customizer .menu-item-settings,
+.wp-customizer .menu-item-settings .description-thin {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+
+.wp-customizer .menu-item-bar {
+ margin: 0;
+}
+
+.wp-customizer .menu-item-bar .menu-item-handle {
+ max-width: 100%;
+ background: #fff;
+}
+
+.wp-customizer .menu-item-handle .item-title {
+ margin-right: 0;
+}
+
+.wp-customizer .menu-item-handle .item-type {
+ padding: 1px 15px 0 5px;
+ float: right;
+ text-align: right;
+}
+
+.wp-customizer .menu-item-settings {
+ max-width: 100%;
+ overflow: hidden;
+ padding: 10px;
+ background: #eee;
+ border: 1px solid #999;
+ border-top: none;
+}
+
+.wp-customizer .menu-item-settings .description-thin {
+ width: 100%;
+ height: auto;
+ margin: 0 0 8px 0;
+}
+
+.wp-customizer .menu-item-settings input[type="text"] {
+ width: 100%;
+}
+
+.wp-customizer .menu-item-settings .submitbox {
+ margin: 0;
+ padding: 0;
+}
+
+.wp-customizer .menu-item-settings .link-to-original {
+ padding: 5px 0;
+ border: none;
+ font-style: normal;
+ margin: 0;
+ width: 100%;
+}
+
+.wp-customizer .menu-item .submitbox .submitdelete {
+ display: block;
+ float: left;
+ margin: 6px 0 0;
+ padding: 0;
+ cursor: pointer;
+}
+
+.wp-customizer .menu-item .submitbox .submitdelete:focus {
+ -webkit-box-shadow: 0 0 0 1px #5b9dd9, 0 0 2px 1px rgba(30, 140, 190, .8);
+ box-shadow: 0 0 0 1px #5b9dd9, 0 0 2px 1px rgba(30, 140, 190, .8);
+}
+
+/* Menu-item reordering nav. */
+#customize-theme-controls button.reorder-toggle {
+ padding: 5px 8px;
+}
+
+.menu-item-reorder-nav {
+ display: none;
+ background-color: #fff;
+ position: absolute;
+ top: 0;
+ right: 0;
+}
+
+#customize-theme-controls .reordering .add-new-menu-item {
+ opacity: 0.2;
+ pointer-events: none;
+ cursor: not-allowed;
+}
+
+.menu-item-reorder-nav button {
+ position: relative;
+ overflow: hidden;
+ float: left;
+ display: block;
+ width: 30px;
+ height: 40px;
+ color: #82878c;
+ text-indent: -9999px;
+ cursor: pointer;
+ background: transparent;
+ border: none;
+ -webkit-box-shadow: none;
+ box-shadow: none;
+ outline: none;
+}
+
+.menu-item-reorder-nav button:before {
+ display: inline-block;
+ position: absolute;
+ top: 0;
+ right: 0;
+ width: 100%;
+ height: 100%;
+ font: normal 20px/40px dashicons;
+ text-align: center;
+ text-indent: 0;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+.menu-item-reorder-nav button:hover,
+.menu-item-reorder-nav button:focus {
+ color: #191e23;
+ background: #eee;
+}
+
+.menus-move-down:before {
+ content: '\f347';
+}
+
+.menus-move-up:before {
+ content: '\f343';
+}
+
+.menus-move-left:before {
+ content: '\f341';
+}
+
+.menus-move-right:before {
+ content: '\f345';
+}
+
+.move-up-disabled .menus-move-up,
+.move-down-disabled .menus-move-down,
+.move-right-disabled .menus-move-right,
+.move-left-disabled .menus-move-left,
+.menu-item-depth-0 .menus-move-left,
+.menu-item-depth-10 .menus-move-right {
+ color: #d5d5d5 !important;
+ background-color: #fff !important;
+ cursor: default;
+ pointer-events: none;
+}
+
+.menu-item-reorder-nav:before {
+ content: "";
+ display: block;
+ position: absolute;
+ left: -10px;
+ width: 10px;
+ height: 40px;
+ background: -webkit-linear-gradient(left, rgba(250,250,250,0) 0%,rgba(250,250,250,1) 100%);
+ background: -webkit-gradient(linear, left top, right top, from(rgba(250,250,250,0)), to(rgba(250,250,250,1)));
+ background: -webkit-linear-gradient(left, rgba(250,250,250,0) 0%, rgba(250,250,250,1) 100%);
+ background: linear-gradient(to right, rgba(250,250,250,0) 0%,rgba(250,250,250,1) 100%);
+}
+
+.reordering .menu-item .item-controls,
+.reordering .menu-item .item-type {
+ display: none;
+}
+
+.reordering .menu-item-reorder-nav {
+ display: block;
+}
+
+.customize-control input.menu-name-field {
+ width: 100%; /* Override the 98% default for customizer inputs, to align with the size of menu items. */
+ margin: 12px 0;
+}
+
+.wp-customizer .menu-item .item-edit {
+ position: absolute;
+ right: -19px;
+ top: 2px;
+ display: block;
+ width: 30px;
+ height: 38px;
+ margin-right: 0 !important;
+ text-indent: 100%;
+ outline: none;
+ overflow: hidden;
+ white-space: nowrap;
+ cursor: pointer;
+}
+
+.customize-control-nav_menu_item .item-edit:focus {
+ color: #0073aa;
+ -webkit-box-shadow: 0 0 0 1px #5b9dd9, 0 0 2px 1px rgba(30, 140, 190, .8);
+ box-shadow: 0 0 0 1px #5b9dd9, 0 0 2px 1px rgba(30, 140, 190, .8);
+}
+
+/* Duplicates `.nav-menus-php .item-edit:before {}` in common.css:2220. */
+.wp-customizer .menu-item .item-edit:before {
+ top: -1px;
+ right: 0;
+ content: '\f140';
+ border: none;
+ background: none;
+ font: normal 20px/1 dashicons;
+ speak: none;
+ display: block;
+ padding: 0;
+ text-indent: 0;
+ text-align: center;
+ position: relative;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ text-decoration: none !important;
+}
+
+.wp-customizer .menu-item.menu-item-edit-active .item-edit:before {
+ content: '\f142';
+}
+
+.wp-customizer .menu-item .item-edit:before {
+ line-height: 2;
+}
+
+/* Duplicates `.nav-menus-php .menu-item-edit-active .item-edit:before {}` in common.css:2271. */
+.wp-customizer .menu-item .menu-item-edit-active .item-edit:before {
+ content: '\f142';
+}
+
+.wp-customizer .menu-item-settings p.description {
+ font-style: normal;
+}
+
+.wp-customizer .menu-settings dl {
+ margin: 12px 0 0 0;
+ padding: 0;
+}
+
+.wp-customizer .menu-settings .checkbox-input {
+ margin-top: 8px;
+}
+
+.wp-customizer .menu-settings .menu-theme-locations {
+ border-top: 1px solid #ccc;
+}
+
+.wp-customizer .menu-settings {
+ margin-top: 36px;
+ border-top: none;
+}
+
+.menu-settings .customize-control-checkbox label {
+ line-height: 1;
+}
+
+/* @todo update selector or potentially remove */
+.menu-settings .customize-control.customize-control-checkbox {
+ margin-bottom: 8px; /* Override collapsing at smaller viewports. */
+}
+
+.customize-control-menu {
+ margin-top: 4px;
+}
+
+#customize-controls .customize-info.open.active-menu-screen-options .customize-help-toggle {
+ color: #555;
+}
+
+/* Screen Options */
+.customize-screen-options-toggle {
+ background: none;
+ border: none;
+ color: #555;
+ cursor: pointer;
+ padding: 20px;
+ position: absolute;
+ right: 31px;
+ top: 4px;
+}
+
+#customize-controls .customize-info .customize-help-toggle {
+ padding: 20px;
+}
+
+#customize-controls .customize-info .customize-help-toggle:before {
+ padding: 5px;
+}
+
+.customize-screen-options-toggle:hover,
+.customize-screen-options-toggle:active,
+.customize-screen-options-toggle:focus,
+.active-menu-screen-options .customize-screen-options-toggle,
+#customize-controls .customize-info.open.active-menu-screen-options .customize-help-toggle:hover,
+#customize-controls .customize-info.open.active-menu-screen-options .customize-help-toggle:active,
+#customize-controls .customize-info.open.active-menu-screen-options .customize-help-toggle:focus {
+ color: #0073aa;
+}
+
+.customize-screen-options-toggle:focus,
+#customize-controls .customize-info .customize-help-toggle:focus {
+ outline: none;
+ -webkit-box-shadow: 0 0 0 1px #5b9dd9, 0 0 2px 1px rgba(30, 140, 190, .8);
+ box-shadow: 0 0 0 1px #5b9dd9, 0 0 2px 1px rgba(30, 140, 190, .8);
+}
+
+.customize-screen-options-toggle:before {
+ -moz-osx-font-smoothing: grayscale;
+ border: none;
+ content: "\f111";
+ display: block;
+ font: 20px/1 "dashicons";
+ padding: 5px;
+ text-align: center;
+ text-decoration: none !important;
+ text-indent: 0;
+ left: 5px;
+ position: absolute;
+ top: 5px;
+}
+
+.wp-customizer #screen-options-wrap {
+ display: none;
+ background: #fff;
+ border-top: 1px solid #ddd;
+ padding: 4px 15px 0;
+}
+
+.wp-customizer .metabox-prefs label {
+ display: block;
+ padding-right: 0;
+ line-height: 30px;
+}
+
+#accordion-panel-menus .field-link-target,
+#accordion-panel-menus .field-attr-title,
+#accordion-panel-menus .field-css-classes,
+#accordion-panel-menus .field-xfn,
+#accordion-panel-menus .field-description {
+ display: none;
+}
+
+#accordion-panel-menus.field-link-target-active .field-link-target,
+#accordion-panel-menus.field-attr-title-active .field-attr-title,
+#accordion-panel-menus.field-css-classes-active .field-css-classes,
+#accordion-panel-menus.field-xfn-active .field-xfn,
+#accordion-panel-menus.field-description-active .field-description {
+ display: block;
+}
+
+
+/* Not exactly sure what broke this, or why it works without these styles in core. */
+.wp-customizer .wp-full-overlay a.collapse-sidebar {
+ position: fixed;
+ left: 0;
+}
+
+/* WARNING: The 20px factor is hard-coded in JS. */
+.menu-item-depth-0 { margin-left: 0; }
+.menu-item-depth-1 { margin-left: 20px; }
+.menu-item-depth-2 { margin-left: 40px; }
+.menu-item-depth-3 { margin-left: 60px; }
+.menu-item-depth-4 { margin-left: 80px; }
+.menu-item-depth-5 { margin-left: 100px; }
+.menu-item-depth-6 { margin-left: 120px; }
+.menu-item-depth-7 { margin-left: 140px; }
+.menu-item-depth-8 { margin-left: 160px; } /* Not likely to be used or useful beyond this depth */
+.menu-item-depth-9 { margin-left: 180px; }
+.menu-item-depth-10 { margin-left: 200px; }
+.menu-item-depth-11 { margin-left: 220px; }
+
+/* @todo handle .menu-item-settings width */
+.menu-item-depth-0 > .menu-item-bar { margin-right: 0; }
+.menu-item-depth-1 > .menu-item-bar { margin-right: 20px; }
+.menu-item-depth-2 > .menu-item-bar { margin-right: 40px; }
+.menu-item-depth-3 > .menu-item-bar { margin-right: 60px; }
+.menu-item-depth-4 > .menu-item-bar { margin-right: 80px; }
+.menu-item-depth-5 > .menu-item-bar { margin-right: 100px; }
+.menu-item-depth-6 > .menu-item-bar { margin-right: 120px; }
+.menu-item-depth-7 > .menu-item-bar { margin-right: 140px; }
+.menu-item-depth-8 > .menu-item-bar { margin-right: 160px; }
+.menu-item-depth-9 > .menu-item-bar { margin-right: 180px; }
+.menu-item-depth-10 > .menu-item-bar { margin-right: 200px; }
+.menu-item-depth-11 > .menu-item-bar { margin-right: 220px; }
+
+/* Submenu left margin. */
+/* @todo menu-item-transport seems entirely unused. */
+.menu-item-depth-0 .menu-item-transport { margin-left: 0; }
+.menu-item-depth-1 .menu-item-transport { margin-left: -20px; }
+.menu-item-depth-3 .menu-item-transport { margin-left: -60px; }
+.menu-item-depth-4 .menu-item-transport { margin-left: -80px; }
+.menu-item-depth-2 .menu-item-transport { margin-left: -40px; }
+.menu-item-depth-5 .menu-item-transport { margin-left: -100px; }
+.menu-item-depth-6 .menu-item-transport { margin-left: -120px; }
+.menu-item-depth-7 .menu-item-transport { margin-left: -140px; }
+.menu-item-depth-8 .menu-item-transport { margin-left: -160px; }
+.menu-item-depth-9 .menu-item-transport { margin-left: -180px; }
+.menu-item-depth-10 .menu-item-transport { margin-left: -200px; }
+.menu-item-depth-11 .menu-item-transport { margin-left: -220px; }
+
+/* WARNING: The 20px factor is hard-coded in JS. */
+.reordering .menu-item-depth-0 { margin-left: 0; }
+.reordering .menu-item-depth-1 { margin-left: 15px; }
+.reordering .menu-item-depth-2 { margin-left: 30px; }
+.reordering .menu-item-depth-3 { margin-left: 45px; }
+.reordering .menu-item-depth-4 { margin-left: 60px; }
+.reordering .menu-item-depth-5 { margin-left: 75px; }
+.reordering .menu-item-depth-6 { margin-left: 90px; }
+.reordering .menu-item-depth-7 { margin-left: 105px; }
+.reordering .menu-item-depth-8 { margin-left: 120px; } /* Not likely to be used or useful beyond this depth */
+.reordering .menu-item-depth-9 { margin-left: 135px; }
+.reordering .menu-item-depth-10 { margin-left: 150px; }
+.reordering .menu-item-depth-11 { margin-left: 165px; }
+
+.reordering .menu-item-depth-0 > .menu-item-bar { margin-right: 0; }
+.reordering .menu-item-depth-1 > .menu-item-bar { margin-right: 15px; }
+.reordering .menu-item-depth-2 > .menu-item-bar { margin-right: 30px; }
+.reordering .menu-item-depth-3 > .menu-item-bar { margin-right: 45px; }
+.reordering .menu-item-depth-4 > .menu-item-bar { margin-right: 60px; }
+.reordering .menu-item-depth-5 > .menu-item-bar { margin-right: 75px; }
+.reordering .menu-item-depth-6 > .menu-item-bar { margin-right: 90px; }
+.reordering .menu-item-depth-7 > .menu-item-bar { margin-right: 105px; }
+.reordering .menu-item-depth-8 > .menu-item-bar { margin-right: 120px; }
+.reordering .menu-item-depth-9 > .menu-item-bar { margin-right: 135px; }
+.reordering .menu-item-depth-10 > .menu-item-bar { margin-right: 150px; }
+.reordering .menu-item-depth-11 > .menu-item-bar { margin-right: 165px; }
+
+.control-section-nav_menu .menu .menu-item-edit-active {
+ margin-left: 0;
+}
+
+.control-section-nav_menu .menu .menu-item-edit-active .menu-item-bar {
+ margin-right: 0;
+}
+
+.control-section-nav_menu .menu .sortable-placeholder {
+ margin-top: 0;
+ margin-bottom: 1px;
+ max-width: -webkit-calc(100% - 2px);
+ max-width: calc(100% - 2px);
+ float: left;
+ display: list-item;
+ border-color: #a0a5aa;
+}
+
+.control-section-nav_menu .menu ul.menu-item-transport dl {
+ margin-top: 0;
+}
+
+/*
+ * Add-menu-items mode.
+ */
+.wp-full-overlay-main {
+ right: auto; /* This overrides a right: 0; which causes the preview to resize rather than slide off screen at the normal size. */
+ width: 100%;
+}
+
+.adding-menu-items .control-section {
+ opacity: .4;
+}
+
+.adding-menu-items .control-panel.control-section,
+.adding-menu-items .control-section.open {
+ opacity: 1;
+}
+
+/* Add-new button. */
+#customize-theme-controls .add-new-menu-item {
+ cursor: pointer;
+ float: right;
+ margin-left: 10px;
+ -webkit-transition: all 0.2s;
+ transition: all 0.2s;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ outline: none;
+}
+
+.add-new-menu-item:before {
+ content: "\f132";
+ display: inline-block;
+ position: relative;
+ left: -2px;
+ top: -1px;
+ font: normal 20px/1 'dashicons';
+ vertical-align: middle;
+ -webkit-transition: all 0.2s;
+ transition: all 0.2s;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+.adding-menu-items .add-new-menu-item,
+.adding-menu-items .add-new-menu-item:hover,
+.add-menu-toggle.open,
+.add-menu-toggle.open:hover {
+ background: #eee;
+ border-color: #929793;
+ color: #32373c;
+ -webkit-box-shadow: inset 0 2px 5px -3px rgba(0, 0, 0, 0.5);
+ box-shadow: inset 0 2px 5px -3px rgba(0, 0, 0, 0.5);
+}
+
+.adding-menu-items .add-new-menu-item:before,
+#accordion-section-add_menu .add-new-menu-item.open:before {
+ -webkit-transform: rotate(45deg);
+ -ms-transform: rotate(45deg);
+ transform: rotate(45deg);
+}
+
+.menu-item-bar .item-delete {
+ color: #a00;
+ position: absolute;
+ top: 2px;
+ right: -19px;
+ width: 30px;
+ height: 38px;
+ cursor: pointer;
+ display: none;
+}
+
+.menu-item-bar .item-delete:before {
+ content: "\f335";
+ font: normal 20px/1 dashicons;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ position: absolute;
+ top: 9px;
+ left: 5px;
+}
+
+.menu-item-bar .item-delete:hover,
+.menu-item-bar .item-delete:focus {
+ color: #f00;
+}
+
+.adding-menu-items .menu-item-bar .item-edit {
+ display: none;
+}
+
+.adding-menu-items .menu-item-bar .item-delete {
+ display: block;
+}
+
+#available-menu-items .item {
+ position: static;
+}
+
+#available-menu-items {
+ position: absolute;
+ overflow: hidden;
+ top: 0;
+ bottom: 0;
+ left: -301px;
+ width: 300px;
+ margin: 0;
+ z-index: 4;
+ background: #eee;
+ -webkit-transition: all 0.2s;
+ transition: all 0.2s;
+ border-right: 1px solid #ddd;
+}
+
+#available-menu-items.allow-scroll {
+ overflow-y: auto;
+}
+
+#available-menu-items .accordion-section-title {
+ border-left: none;
+ border-right: none;
+ background: #fff;
+}
+
+#available-menu-items .open .accordion-section-title {
+ background: #eee;
+}
+
+#available-menu-items .open .accordion-section-title:after {
+ content: '\f142';
+}
+
+#available-menu-items .accordion-section-content {
+ overflow-y: auto;
+ max-height: 200px; /* This gets set in JS to fit the screen size, and based on # of sections. */
+ background: transparent;
+}
+
+button.not-a-button {
+ background: transparent;
+ border: none;
+ -webkit-box-shadow: none;
+ box-shadow: none;
+ -webkit-border-radius: 0;
+ border-radius: 0;
+ outline: 0;
+ padding: 0;
+ margin: 0;
+}
+
+#available-menu-items .accordion-section-title button:focus:before {
+ display: block;
+ content: "";
+ width: 28px;
+ height: 32px;
+ position: absolute;
+ right: 5px;
+ top: 5px;
+ -webkit-box-shadow: 0 0 0 1px #5b9dd9, 0 0 2px 1px rgba(30, 140, 190, .8);
+ box-shadow: 0 0 0 1px #5b9dd9, 0 0 2px 1px rgba(30, 140, 190, .8);
+}
+
+#available-menu-items .accordion-section-content {
+ padding: 1px 15px 15px 15px;
+ min-height: 120px;
+ max-height: 290px;
+}
+
+#custom-menu-item-name.invalid,
+#custom-menu-item-url.invalid {
+ border: 1px solid #f00;
+}
+
+#available-menu-items .item-tpl {
+ position: relative;
+ padding: 20px 15px 20px 60px;
+ border-bottom: 1px solid #e4e4e4;
+ cursor: pointer;
+ display: none;
+}
+
+#available-menu-items .item-tpl:hover,
+#available-menu-items .item-tpl.selected {
+ background: #eee;
+}
+
+#available-menu-items .menu-item-handle .item-type {
+ padding-right: 0;
+}
+
+#available-menu-items .menu-item-handle .item-title {
+ padding-left: 20px;
+}
+
+#available-menu-items .menu-item-handle {
+ cursor: pointer;
+}
+
+#available-menu-items .item-top,
+#available-menu-items .item-top:hover {
+ border: none;
+ background: transparent;
+ -webkit-box-shadow: none;
+ box-shadow: none;
+}
+
+#available-menu-items .menu-item-handle {
+ -webkit-box-shadow: none;
+ box-shadow: none;
+ margin-top: -1px;
+}
+
+#available-menu-items .menu-item-handle:hover {
+ z-index: 1;
+}
+
+#available-menu-items .item-title h4 {
+ padding: 0 0 5px;
+ font-size: 14px;
+}
+
+#available-menu-items .item-add {
+ position: absolute;
+ top: 1px;
+ left: 1px;
+ color: #82878c;
+ width: 30px;
+ height: 38px;
+ cursor: pointer;
+}
+
+#available-menu-items .menu-item-handle .item-add:focus {
+ color: #23282d;
+ -webkit-box-shadow: 0 0 0 1px #5b9dd9, 0 0 2px 1px rgba(30, 140, 190, .8);
+ box-shadow: 0 0 0 1px #5b9dd9, 0 0 2px 1px rgba(30, 140, 190, .8);
+}
+
+#available-menu-items .item-add:before {
+ content: "\f132";
+ font: normal 20px/1 dashicons;
+ position: relative;
+ left: 2px;
+ top: 4px;
+}
+
+#available-menu-items .menu-item-handle.item-added .item-type,
+#available-menu-items .menu-item-handle.item-added .item-title,
+#available-menu-items .menu-item-handle.item-added:hover .item-add,
+#available-menu-items .menu-item-handle.item-added .item-add:focus {
+ color: #82878c;
+}
+
+#available-menu-items .menu-item-handle.item-added .item-add:before {
+ content: "\f147";
+}
+
+#available-menu-items .accordion-section-title.loading .spinner,
+#available-menu-items-search.loading .accordion-section-title .spinner {
+ visibility: visible;
+ margin: 0 20px;
+}
+
+#available-menu-items-search .spinner {
+ position: absolute;
+ top: 18px;
+ margin: 0 !important;
+ right: 20px;
+}
+
+#available-menu-items-search input {
+ padding: 6px 10px;
+ width: 100%;
+}
+
+#available-menu-items-search .accordion-section-title {
+ padding: 12px 15px;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+
+#available-menu-items-search .accordion-section-title:after {
+ display: none;
+}
+
+#available-menu-items-search .accordion-section-content:empty {
+ min-height: 0;
+ padding: 0;
+}
+
+#available-menu-items-search.loading .accordion-section-content div {
+ opacity: .5;
+}
+
+#available-menu-items-search.loading.loading-more .accordion-section-content div {
+ opacity: 1;
+}
+
+#customize-preview {
+ -webkit-transition: all 0.2s;
+ transition: all 0.2s;
+}
+
+body.adding-menu-items #available-menu-items {
+ left: 0;
+}
+
+body.adding-menu-items .wp-full-overlay-main {
+ left: 300px;
+}
+
+body.adding-menu-items #customize-preview {
+ opacity: 0.4;
+}
+
+.menu-item-handle .spinner {
+ display: none;
+ float: left;
+ margin: 0 8px 0 0;
+}
+
+.nav-menu-inserted-item-loading .spinner {
+ display: block;
+}
+
+.nav-menu-inserted-item-loading .menu-item-handle .item-type {
+ padding: 0 0 0 8px;
+}
+
+.nav-menu-inserted-item-loading .menu-item-handle,
+.added-menu-item .menu-item-handle.loading {
+ padding: 10px 15px 10px 8px;
+ cursor: default;
+ opacity: .5;
+ background: #fff;
+ color: #727773;
+}
+
+.added-menu-item .menu-item-handle {
+ -webkit-transition-property: opacity, background, color;
+ transition-property: opacity, background, color;
+ -webkit-transition-duration: 1.25s;
+ transition-duration: 1.25s;
+ -webkit-transition-timing-function: cubic-bezier( .25, -2.5, .75, 8 );
+ transition-timing-function: cubic-bezier( .25, -2.5, .75, 8 ); /* Replacement for .hide().fadeIn('slow') in JS to add emphasis when it's loaded. */
+}
+
+/* Add/delete Menus */
+
+/* @todo update selector */
+#accordion-section-add_menu {
+ margin: 15px 12px;
+}
+
+.new-menu-section-content {
+ display: none;
+ padding: 15px 0 0 0;
+ overflow: hidden;
+ clear: both;
+}
+
+/* @todo update selector */
+#accordion-section-add_menu .accordion-section-title {
+ padding-left: 45px;
+}
+
+/* @todo update selector */
+#accordion-section-add_menu .accordion-section-title:before {
+ font: normal 20px/1 dashicons;
+ position: absolute;
+ top: 12px;
+ left: 14px;
+ content: "\f132";
+}
+
+#create-new-menu-submit {
+ float: right;
+ margin: 0 0 12px 0;
+}
+
+.menu-delete-item {
+ display: block;
+ float: left;
+ padding: 1em 0;
+ width: 100%;
+}
+
+li.assigned-to-menu-location .menu-delete-item {
+ display: none;
+}
+
+li.assigned-to-menu-location .add-new-menu-item {
+ margin-bottom: 1em;
+}
+
+.menu-delete {
+ color: #a00;
+ cursor: pointer;
+ text-decoration: underline;
+}
+
+.menu-delete:hover,
+.menu-delete:focus {
+ color: #f00;
+ text-decoration: none;
+}
+
+.menu-delete:focus {
+ -webkit-box-shadow: 0 0 0 1px #5b9dd9, 0 0 2px 1px rgba(30, 140, 190, .8);
+ box-shadow: 0 0 0 1px #5b9dd9, 0 0 2px 1px rgba(30, 140, 190, .8);
+}
+
+.menu-item-handle {
+ margin-top: -1px;
+}
+.ui-sortable-disabled .menu-item-handle {
+ cursor: default;
+}
+
+.menu-item-handle:hover {
+ position: relative;
+ z-index: 10;
+ color: #0073aa;
+}
+
+.menu-item-handle:hover .item-type,
+.menu-item-handle:hover .item-edit,
+#available-menu-items .menu-item-handle:hover .item-add {
+ color: #0073aa;
+}
+
+.menu-item-edit-active .menu-item-handle {
+ border-color: #999;
+ border-bottom: none;
+}
+
+.customize-control-nav_menu_item {
+ margin-bottom: 0;
+}
+
+.customize-control-nav_menu {
+ margin-top: 12px;
+}
+
+#available-menu-items .customize-section-title {
+ display: none;
+}
+
+@media screen and ( max-width: 640px ) {
+ body.adding-menu-items div#available-menu-items {
+ top: 46px;
+ left: 0;
+ z-index: 10;
+ width: 100%;
+ }
+
+ #available-menu-items .customize-section-title {
+ display: block;
+ margin: 0;
+ }
+
+ #available-menu-items .customize-section-back {
+ height: 69px;
+ }
+
+ #available-menu-items .customize-section-title h3 {
+ font-size: 20px;
+ font-weight: 200;
+ padding: 9px 10px 12px 14px;
+ margin: 0;
+ line-height: 24px;
+ color: #555;
+ display: block;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+
+ #available-menu-items .customize-section-title .customize-action {
+ font-size: 13px;
+ display: block;
+ font-weight: 400;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+}
</ins></span></pre></div>
<a id="trunksrcwpadminjscustomizenavmenusjs"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/src/wp-admin/js/customize-nav-menus.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/js/customize-nav-menus.js (rev 0)
+++ trunk/src/wp-admin/js/customize-nav-menus.js 2015-06-16 22:07:08 UTC (rev 32806)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,2513 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+/* global _wpCustomizeNavMenusSettings, wpNavMenu, console */
+( function( api, wp, $ ) {
+ 'use strict';
+
+ /**
+ * Set up wpNavMenu for drag and drop.
+ */
+ wpNavMenu.originalInit = wpNavMenu.init;
+ wpNavMenu.options.menuItemDepthPerLevel = 20;
+ wpNavMenu.options.sortableItems = '.customize-control-nav_menu_item';
+ wpNavMenu.init = function() {
+ this.jQueryExtensions();
+ };
+
+ api.Menus = api.Menus || {};
+
+ // Link settings.
+ api.Menus.data = {
+ nonce: '',
+ itemTypes: {
+ taxonomies: {},
+ postTypes: {}
+ },
+ l10n: {},
+ menuItemTransport: 'postMessage',
+ phpIntMax: 0,
+ defaultSettingValues: {
+ nav_menu: {},
+ nav_menu_item: {}
+ }
+ };
+ if ( 'undefined' !== typeof _wpCustomizeNavMenusSettings ) {
+ $.extend( api.Menus.data, _wpCustomizeNavMenusSettings );
+ }
+
+ /**
+ * Newly-created Nav Menus and Nav Menu Items have negative integer IDs which
+ * serve as placeholders until Save & Publish happens.
+ *
+ * @return {number}
+ */
+ api.Menus.generatePlaceholderAutoIncrementId = function() {
+ return -Math.ceil( api.Menus.data.phpIntMax * Math.random() );
+ };
+
+ /**
+ * wp.customize.Menus.AvailableItemModel
+ *
+ * A single available menu item model. See PHP's WP_Customize_Nav_Menu_Item_Setting class.
+ *
+ * @constructor
+ * @augments Backbone.Model
+ */
+ api.Menus.AvailableItemModel = Backbone.Model.extend( $.extend(
+ {
+ id: null // This is only used by Backbone.
+ },
+ api.Menus.data.defaultSettingValues.nav_menu_item
+ ) );
+
+ /**
+ * wp.customize.Menus.AvailableItemCollection
+ *
+ * Collection for available menu item models.
+ *
+ * @constructor
+ * @augments Backbone.Model
+ */
+ api.Menus.AvailableItemCollection = Backbone.Collection.extend({
+ model: api.Menus.AvailableItemModel,
+
+ sort_key: 'order',
+
+ comparator: function( item ) {
+ return -item.get( this.sort_key );
+ },
+
+ sortByField: function( fieldName ) {
+ this.sort_key = fieldName;
+ this.sort();
+ }
+ });
+ api.Menus.availableMenuItems = new api.Menus.AvailableItemCollection( api.Menus.data.availableMenuItems );
+
+ /**
+ * wp.customize.Menus.AvailableMenuItemsPanelView
+ *
+ * View class for the available menu items panel.
+ *
+ * @constructor
+ * @augments wp.Backbone.View
+ * @augments Backbone.View
+ */
+ api.Menus.AvailableMenuItemsPanelView = wp.Backbone.View.extend({
+
+ el: '#available-menu-items',
+
+ events: {
+ 'input #menu-items-search': 'debounceSearch',
+ 'change #menu-items-search': 'debounceSearch',
+ 'click #menu-items-search': 'debounceSearch',
+ 'focus .menu-item-tpl': 'focus',
+ 'click .menu-item-tpl': '_submit',
+ 'keypress .menu-item-tpl': '_submit',
+ 'click #custom-menu-item-submit': '_submitLink',
+ 'keypress #custom-menu-item-name': '_submitLink',
+ 'keydown': 'keyboardAccessible'
+ },
+
+ // Cache current selected menu item.
+ selected: null,
+
+ // Cache menu control that opened the panel.
+ currentMenuControl: null,
+ debounceSearch: null,
+ $search: null,
+ searchTerm: '',
+ rendered: false,
+ pages: {},
+ sectionContent: '',
+ loading: false,
+
+ initialize: function() {
+ var self = this;
+
+ this.$search = $( '#menu-items-search' );
+ this.sectionContent = this.$el.find( '.accordion-section-content' );
+
+ this.debounceSearch = _.debounce( self.search, 250 );
+
+ _.bindAll( this, 'close' );
+
+ // If the available menu items panel is open and the customize controls are
+ // interacted with (other than an item being deleted), then close the
+ // available menu items panel. Also close on back button click.
+ $( '#customize-controls, .customize-section-back' ).on( 'click keydown', function( e ) {
+ var isDeleteBtn = $( e.target ).is( '.item-delete, .item-delete *' ),
+ isAddNewBtn = $( e.target ).is( '.add-new-menu-item, .add-new-menu-item *' );
+ if ( $( 'body' ).hasClass( 'adding-menu-items' ) && ! isDeleteBtn && ! isAddNewBtn ) {
+ self.close();
+ }
+ } );
+
+ this.$el.on( 'input', '#custom-menu-item-name.invalid, #custom-menu-item-url.invalid', function() {
+ $( this ).removeClass( 'invalid' );
+ });
+
+ // Load available items if it looks like we'll need them.
+ api.panel( 'nav_menus' ).container.bind( 'expanded', function() {
+ if ( ! self.rendered ) {
+ self.initList();
+ self.rendered = true;
+ }
+ });
+
+ // Load more items.
+ this.sectionContent.scroll( function() {
+ var totalHeight = self.$el.find( '.accordion-section.open .accordion-section-content' ).prop( 'scrollHeight' ),
+ visibleHeight = self.$el.find( '.accordion-section.open' ).height();
+ if ( ! self.loading && $( this ).scrollTop() > 3 / 4 * totalHeight - visibleHeight ) {
+ var type = $( this ).data( 'type' ),
+ obj_type = $( this ).data( 'obj_type' );
+ if ( 'search' === type ) {
+ if ( self.searchTerm ) {
+ self.doSearch( self.pages.search );
+ }
+ } else {
+ self.loadItems( type, obj_type );
+ }
+ }
+ });
+
+ // Close the panel if the URL in the preview changes
+ api.previewer.bind( 'url', this.close );
+ },
+
+ // Search input change handler.
+ search: function( event ) {
+ if ( ! event ) {
+ return;
+ }
+ // Manual accordion-opening behavior.
+ if ( this.searchTerm && ! $( '#available-menu-items-search' ).hasClass( 'open' ) ) {
+ $( '#available-menu-items .accordion-section-content' ).slideUp( 'fast' );
+ $( '#available-menu-items-search .accordion-section-content' ).slideDown( 'fast' );
+ $( '#available-menu-items .accordion-section.open' ).removeClass( 'open' );
+ $( '#available-menu-items-search' ).addClass( 'open' );
+ }
+ if ( '' === event.target.value ) {
+ $( '#available-menu-items-search' ).removeClass( 'open' );
+ }
+ if ( this.searchTerm === event.target.value ) {
+ return;
+ }
+ this.searchTerm = event.target.value;
+ this.pages.search = 1;
+ this.doSearch( 1 );
+ },
+
+ // Get search results.
+ doSearch: function( page ) {
+ var self = this, params,
+ $section = $( '#available-menu-items-search' ),
+ $content = $section.find( '.accordion-section-content' ),
+ itemTemplate = wp.template( 'available-menu-item' );
+
+ if ( self.currentRequest ) {
+ self.currentRequest.abort();
+ }
+
+ if ( page < 0 ) {
+ return;
+ } else if ( page > 1 ) {
+ $section.addClass( 'loading-more' );
+ } else if ( '' === self.searchTerm ) {
+ $content.html( '' );
+ return;
+ }
+
+ $section.addClass( 'loading' );
+ self.loading = true;
+ params = {
+ 'customize-menus-nonce': api.Menus.data.nonce,
+ 'wp_customize': 'on',
+ 'search': self.searchTerm,
+ 'page': page
+ };
+
+ self.currentRequest = wp.ajax.post( 'search-available-menu-items-customizer', params );
+
+ self.currentRequest.done(function( data ) {
+ var items;
+ if ( 1 === page ) {
+ // Clear previous results as it's a new search.
+ $content.empty();
+ }
+ $section.removeClass( 'loading loading-more' );
+ $section.addClass( 'open' );
+ self.loading = false;
+ items = new api.Menus.AvailableItemCollection( data.items );
+ self.collection.add( items.models );
+ items.each( function( menuItem ) {
+ $content.append( itemTemplate( menuItem.attributes ) );
+ } );
+ if ( 20 > items.length ) {
+ self.pages.search = -1; // Up to 20 posts and 20 terms in results, if <20, no more results for either.
+ } else {
+ self.pages.search = self.pages.search + 1;
+ }
+ });
+
+ self.currentRequest.fail(function( data ) {
+ $content.empty().append( $( '<p class="nothing-found"></p>' ).text( data.message ) );
+ wp.a11y.speak( data.message );
+ self.pages.search = -1;
+ });
+
+ self.currentRequest.always(function() {
+ $section.removeClass( 'loading loading-more' );
+ self.loading = false;
+ self.currentRequest = null;
+ });
+ },
+
+ // Render the individual items.
+ initList: function() {
+ var self = this;
+
+ // Render the template for each item by type.
+ _.each( api.Menus.data.itemTypes, function( typeObjects, type ) {
+ _.each( typeObjects, function( typeObject, slug ) {
+ if ( 'postTypes' === type ) {
+ type = 'post_type';
+ } else if ( 'taxonomies' === type ) {
+ type = 'taxonomy';
+ }
+ self.pages[ slug ] = 0; // @todo should prefix with type
+ self.loadItems( slug, type );
+ } );
+ } );
+ },
+
+ // Load available menu items.
+ loadItems: function( type, obj_type ) {
+ var self = this, params, request, itemTemplate;
+ itemTemplate = wp.template( 'available-menu-item' );
+
+ if ( 0 > self.pages[type] ) {
+ return;
+ }
+ $( '#available-menu-items-' + type + ' .accordion-section-title' ).addClass( 'loading' );
+ self.loading = true;
+ params = {
+ 'customize-menus-nonce': api.Menus.data.nonce,
+ 'wp_customize': 'on',
+ 'type': type,
+ 'obj_type': obj_type,
+ 'page': self.pages[ type ]
+ };
+ request = wp.ajax.post( 'load-available-menu-items-customizer', params );
+
+ request.done(function( data ) {
+ var items, typeInner;
+ items = data.items;
+ if ( 0 === items.length ) {
+ self.pages[ type ] = -1;
+ return;
+ }
+ items = new api.Menus.AvailableItemCollection( items ); // @todo Why is this collection created and then thrown away?
+ self.collection.add( items.models );
+ typeInner = $( '#available-menu-items-' + type + ' .accordion-section-content' );
+ items.each(function( menu_item ) {
+ typeInner.append( itemTemplate( menu_item.attributes ) );
+ });
+ self.pages[ type ] = self.pages[ type ] + 1;
+ });
+ request.fail(function( data ) {
+ if ( typeof console !== 'undefined' && console.error ) {
+ console.error( data );
+ }
+ });
+ request.always(function() {
+ $( '#available-menu-items-' + type + ' .accordion-section-title' ).removeClass( 'loading' );
+ self.loading = false;
+ });
+ },
+
+ // Adjust the height of each section of items to fit the screen.
+ itemSectionHeight: function() {
+ var sections, totalHeight, accordionHeight, diff;
+ totalHeight = window.innerHeight;
+ sections = this.$el.find( '.accordion-section-content' );
+ accordionHeight = 46 * ( 1 + sections.length ) - 16; // Magic numbers.
+ diff = totalHeight - accordionHeight;
+ if ( 120 < diff && 290 > diff ) {
+ sections.css( 'max-height', diff );
+ } else if ( 120 >= diff ) {
+ this.$el.addClass( 'allow-scroll' );
+ }
+ },
+
+ // Highlights a menu item.
+ select: function( menuitemTpl ) {
+ this.selected = $( menuitemTpl );
+ this.selected.siblings( '.menu-item-tpl' ).removeClass( 'selected' );
+ this.selected.addClass( 'selected' );
+ },
+
+ // Highlights a menu item on focus.
+ focus: function( event ) {
+ this.select( $( event.currentTarget ) );
+ },
+
+ // Submit handler for keypress and click on menu item.
+ _submit: function( event ) {
+ // Only proceed with keypress if it is Enter or Spacebar
+ if ( 'keypress' === event.type && ( 13 !== event.which && 32 !== event.which ) ) {
+ return;
+ }
+
+ this.submit( $( event.currentTarget ) );
+ },
+
+ // Adds a selected menu item to the menu.
+ submit: function( menuitemTpl ) {
+ var menuitemId, menu_item;
+
+ if ( ! menuitemTpl ) {
+ menuitemTpl = this.selected;
+ }
+
+ if ( ! menuitemTpl || ! this.currentMenuControl ) {
+ return;
+ }
+
+ this.select( menuitemTpl );
+
+ menuitemId = $( this.selected ).data( 'menu-item-id' );
+ menu_item = this.collection.findWhere( { id: menuitemId } );
+ if ( ! menu_item ) {
+ return;
+ }
+
+ this.currentMenuControl.addItemToMenu( menu_item.attributes );
+
+ $( menuitemTpl ).find( '.menu-item-handle' ).addClass( 'item-added' );
+ },
+
+ // Submit handler for keypress and click on custom menu item.
+ _submitLink: function( event ) {
+ // Only proceed with keypress if it is Enter.
+ if ( 'keypress' === event.type && 13 !== event.which ) {
+ return;
+ }
+
+ this.submitLink();
+ },
+
+ // Adds the custom menu item to the menu.
+ submitLink: function() {
+ var menuItem,
+ itemName = $( '#custom-menu-item-name' ),
+ itemUrl = $( '#custom-menu-item-url' );
+
+ if ( ! this.currentMenuControl ) {
+ return;
+ }
+
+ if ( '' === itemName.val() ) {
+ itemName.addClass( 'invalid' );
+ return;
+ } else if ( '' === itemUrl.val() || 'http://' === itemUrl.val() ) {
+ itemUrl.addClass( 'invalid' );
+ return;
+ }
+
+ menuItem = {
+ 'title': itemName.val(),
+ 'url': itemUrl.val(),
+ 'type': 'custom',
+ 'type_label': api.Menus.data.l10n.custom_label,
+ 'object': ''
+ };
+
+ this.currentMenuControl.addItemToMenu( menuItem );
+
+ // Reset the custom link form.
+ itemUrl.val( 'http://' );
+ itemName.val( '' );
+ },
+
+ // Opens the panel.
+ open: function( menuControl ) {
+ this.currentMenuControl = menuControl;
+
+ this.itemSectionHeight();
+
+ $( 'body' ).addClass( 'adding-menu-items' );
+
+ // Collapse all controls.
+ _( this.currentMenuControl.getMenuItemControls() ).each( function( control ) {
+ control.collapseForm();
+ } );
+
+ this.$el.find( '.selected' ).removeClass( 'selected' );
+
+ this.$search.focus();
+ },
+
+ // Closes the panel
+ close: function( options ) {
+ options = options || {};
+
+ if ( options.returnFocus && this.currentMenuControl ) {
+ this.currentMenuControl.container.find( '.add-new-menu-item' ).focus();
+ }
+
+ this.currentMenuControl = null;
+ this.selected = null;
+
+ $( 'body' ).removeClass( 'adding-menu-items' );
+ $( '#available-menu-items .menu-item-handle.item-added' ).removeClass( 'item-added' );
+
+ this.$search.val( '' );
+ },
+
+ // Add keyboard accessiblity to the panel
+ keyboardAccessible: function( event ) {
+ var isEnter = ( 13 === event.which ),
+ isEsc = ( 27 === event.which ),
+ isDown = ( 40 === event.which ),
+ isUp = ( 38 === event.which ),
+ isBackTab = ( 9 === event.which && event.shiftKey ),
+ selected = null,
+ firstVisible = this.$el.find( '> .menu-item-tpl:visible:first' ),
+ lastVisible = this.$el.find( '> .menu-item-tpl:visible:last' ),
+ isSearchFocused = $( event.target ).is( this.$search );
+
+ if ( isDown || isUp ) {
+ if ( isDown ) {
+ if ( isSearchFocused ) {
+ selected = firstVisible;
+ } else if ( this.selected && 0 !== this.selected.nextAll( '.menu-item-tpl:visible' ).length ) {
+ selected = this.selected.nextAll( '.menu-item-tpl:visible:first' );
+ }
+ } else if ( isUp ) {
+ if ( isSearchFocused ) {
+ selected = lastVisible;
+ } else if ( this.selected && 0 !== this.selected.prevAll( '.menu-item-tpl:visible' ).length ) {
+ selected = this.selected.prevAll( '.menu-item-tpl:visible:first' );
+ }
+ }
+
+ this.select( selected );
+
+ if ( selected ) {
+ selected.focus();
+ } else {
+ this.$search.focus();
+ }
+
+ return;
+ }
+
+ // If enter pressed but nothing entered, don't do anything
+ if ( isEnter && ! this.$search.val() ) {
+ return;
+ }
+
+ if ( isSearchFocused && isBackTab ) {
+ this.currentMenuControl.container.find( '.add-new-menu-item' ).focus();
+ event.preventDefault(); // Avoid additional back-tab.
+ } else if ( isEsc ) {
+ this.close( { returnFocus: true } );
+ }
+ }
+ });
+
+ /**
+ * wp.customize.Menus.MenusPanel
+ *
+ * Customizer panel for menus. This is used only for screen options management.
+ * Note that 'menus' must match the WP_Customize_Menu_Panel::$type.
+ *
+ * @constructor
+ * @augments wp.customize.Panel
+ */
+ api.Menus.MenusPanel = api.Panel.extend({
+
+ attachEvents: function() {
+ api.Panel.prototype.attachEvents.call( this );
+
+ var panel = this,
+ panelMeta = panel.container.find( '.panel-meta' ),
+ help = panelMeta.find( '.customize-help-toggle' ),
+ content = panelMeta.find( '.customize-panel-description' ),
+ options = $( '#screen-options-wrap' ),
+ button = panelMeta.find( '.customize-screen-options-toggle' );
+ button.on( 'click', function() {
+ // Hide description
+ if ( content.not( ':hidden' ) ) {
+ content.slideUp( 'fast' );
+ help.attr( 'aria-expanded', 'false' );
+ }
+
+ if ( 'true' === button.attr( 'aria-expanded' ) ) {
+ button.attr( 'aria-expanded', 'false' );
+ panelMeta.removeClass( 'open' );
+ panelMeta.removeClass( 'active-menu-screen-options' );
+ options.slideUp( 'fast' );
+ } else {
+ button.attr( 'aria-expanded', 'true' );
+ panelMeta.addClass( 'open' );
+ panelMeta.addClass( 'active-menu-screen-options' );
+ options.slideDown( 'fast' );
+ }
+
+ return false;
+ } );
+
+ // Help toggle
+ help.on( 'click', function() {
+ if ( 'true' === button.attr( 'aria-expanded' ) ) {
+ button.attr( 'aria-expanded', 'false' );
+ help.attr( 'aria-expanded', 'true' );
+ panelMeta.addClass( 'open' );
+ panelMeta.removeClass( 'active-menu-screen-options' );
+ options.slideUp( 'fast' );
+ content.slideDown( 'fast' );
+ }
+ } );
+ },
+
+ /**
+ * Show/hide/save screen options (columns). From common.js.
+ */
+ ready: function() {
+ var panel = this;
+ this.container.find( '.hide-column-tog' ).click( function() {
+ var $t = $( this ), column = $t.val();
+ if ( $t.prop( 'checked' ) ) {
+ panel.checked( column );
+ } else {
+ panel.unchecked( column );
+ }
+
+ panel.saveManageColumnsState();
+ });
+ this.container.find( '.hide-column-tog' ).each( function() {
+ var $t = $( this ), column = $t.val();
+ if ( $t.prop( 'checked' ) ) {
+ panel.checked( column );
+ } else {
+ panel.unchecked( column );
+ }
+ });
+ },
+
+ saveManageColumnsState: function() {
+ var hidden = this.hidden();
+ $.post( wp.ajax.settings.url, {
+ action: 'hidden-columns',
+ hidden: hidden,
+ screenoptionnonce: $( '#screenoptionnonce' ).val(),
+ page: 'nav-menus'
+ });
+ },
+
+ checked: function( column ) {
+ this.container.addClass( 'field-' + column + '-active' );
+ },
+
+ unchecked: function( column ) {
+ this.container.removeClass( 'field-' + column + '-active' );
+ },
+
+ hidden: function() {
+ this.hidden = function() {
+ return $( '.hide-column-tog' ).not( ':checked' ).map( function() {
+ var id = this.id;
+ return id.substring( id, id.length - 5 );
+ }).get().join( ',' );
+ };
+ }
+ } );
+
+ /**
+ * wp.customize.Menus.MenuSection
+ *
+ * Customizer section for menus. This is used only for lazy-loading child controls.
+ * Note that 'nav_menu' must match the WP_Customize_Menu_Section::$type.
+ *
+ * @constructor
+ * @augments wp.customize.Section
+ */
+ api.Menus.MenuSection = api.Section.extend({
+
+ /**
+ * @since Menu Customizer 0.3
+ *
+ * @param {String} id
+ * @param {Object} options
+ */
+ initialize: function( id, options ) {
+ var section = this;
+ api.Section.prototype.initialize.call( section, id, options );
+ section.deferred.initSortables = $.Deferred();
+ },
+
+ /**
+ *
+ */
+ ready: function() {
+ var section = this;
+
+ if ( 'undefined' === typeof section.params.menu_id ) {
+ throw new Error( 'params.menu_id was not defined' );
+ }
+
+ /*
+ * Since newly created sections won't be registered in PHP, we need to prevent the
+ * preview's sending of the activeSections to result in this control
+ * being deactivated when the preview refreshes. So we can hook onto
+ * the setting that has the same ID and its presence can dictate
+ * whether the section is active.
+ */
+ section.active.validate = function() {
+ if ( ! api.has( section.id ) ) {
+ return false;
+ }
+ return !! api( section.id ).get();
+ };
+
+ section.populateControls();
+
+ section.navMenuLocationSettings = {};
+ section.assignedLocations = new api.Value( [] );
+
+ api.each(function( setting, id ) {
+ var matches = id.match( /^nav_menu_locations\[(.+?)]/ );
+ if ( matches ) {
+ section.navMenuLocationSettings[ matches[1] ] = setting;
+ setting.bind( function() {
+ section.refreshAssignedLocations();
+ });
+ }
+ });
+
+ section.assignedLocations.bind(function( to ) {
+ section.updateAssignedLocationsInSectionTitle( to );
+ });
+
+ section.refreshAssignedLocations();
+ },
+
+ populateControls: function() {
+ var section = this, menuNameControlId, menuControl, menuNameControl;
+
+ // Add the control for managing the menu name.
+ menuNameControlId = section.id + '[name]';
+ menuNameControl = api.control( menuNameControlId );
+ if ( ! menuNameControl ) {
+ menuNameControl = new api.controlConstructor.nav_menu_name( menuNameControlId, {
+ params: {
+ type: 'nav_menu_name',
+ content: '<li id="customize-control-' + section.id.replace( '[', '-' ).replace( ']', '' ) + '-name" class="customize-control customize-control-nav_menu_name"></li>', // @todo core should do this for us
+ label: '',
+ active: true,
+ section: section.id,
+ priority: 0,
+ settings: {
+ 'default': section.id
+ }
+ }
+ } );
+ api.control.add( menuNameControl.id, menuNameControl );
+ menuNameControl.active.set( true );
+ }
+
+ // Add the menu control.
+ menuControl = api.control( section.id );
+ if ( ! menuControl ) {
+ menuControl = new api.controlConstructor.nav_menu( section.id, {
+ params: {
+ type: 'nav_menu',
+ content: '<li id="customize-control-' + section.id.replace( '[', '-' ).replace( ']', '' ) + '" class="customize-control customize-control-nav_menu"></li>', // @todo core should do this for us
+ section: section.id,
+ priority: 999,
+ active: true,
+ settings: {
+ 'default': section.id
+ },
+ menu_id: section.params.menu_id
+ }
+ } );
+ api.control.add( menuControl.id, menuControl );
+ menuControl.active.set( true );
+ }
+
+ },
+
+ /**
+ *
+ */
+ refreshAssignedLocations: function() {
+ var section = this,
+ menuTermId = section.params.menu_id,
+ currentAssignedLocations = [];
+ _.each( section.navMenuLocationSettings, function( setting, themeLocation ) {
+ if ( setting() === menuTermId ) {
+ currentAssignedLocations.push( themeLocation );
+ }
+ });
+ section.assignedLocations.set( currentAssignedLocations );
+ },
+
+ /**
+ * @param {array} themeLocations
+ */
+ updateAssignedLocationsInSectionTitle: function( themeLocations ) {
+ var section = this,
+ $title;
+
+ $title = section.container.find( '.accordion-section-title:first' );
+ $title.find( '.menu-in-location' ).remove();
+ _.each( themeLocations, function( themeLocation ) {
+ var $label = $( '<span class="menu-in-location"></span>' );
+ $label.text( api.Menus.data.l10n.menuLocation.replace( '%s', themeLocation ) );
+ $title.append( $label );
+ });
+
+ section.container.toggleClass( 'assigned-to-menu-location', 0 !== themeLocations.length );
+
+ },
+
+ onChangeExpanded: function( expanded, args ) {
+ var section = this;
+
+ if ( expanded ) {
+ wpNavMenu.menuList = section.container.find( '.accordion-section-content:first' );
+ wpNavMenu.targetList = wpNavMenu.menuList;
+
+ // Add attributes needed by wpNavMenu
+ $( '#menu-to-edit' ).removeAttr( 'id' );
+ wpNavMenu.menuList.attr( 'id', 'menu-to-edit' ).addClass( 'menu' );
+
+ _.each( api.section( section.id ).controls(), function( control ) {
+ if ( 'nav_menu_item' === control.params.type ) {
+ control.actuallyEmbed();
+ }
+ } );
+
+ if ( 'resolved' !== section.deferred.initSortables.state() ) {
+ wpNavMenu.initSortables(); // Depends on menu-to-edit ID being set above.
+ section.deferred.initSortables.resolve( wpNavMenu.menuList ); // Now MenuControl can extend the sortable.
+
+ // @todo Note that wp.customize.reflowPaneContents() is debounced, so this immediate change will show a slight flicker while priorities get updated.
+ api.control( 'nav_menu[' + String( section.params.menu_id ) + ']' ).reflowMenuItems();
+ }
+ }
+ api.Section.prototype.onChangeExpanded.call( section, expanded, args );
+ }
+ });
+
+ /**
+ * wp.customize.Menus.NewMenuSection
+ *
+ * Customizer section for new menus.
+ * Note that 'new_menu' must match the WP_Customize_New_Menu_Section::$type.
+ *
+ * @constructor
+ * @augments wp.customize.Section
+ */
+ api.Menus.NewMenuSection = api.Section.extend({
+
+ /**
+ * Add behaviors for the accordion section.
+ *
+ * @since Menu Customizer 0.3
+ */
+ attachEvents: function() {
+ var section = this;
+ this.container.on( 'click', '.add-menu-toggle', function() {
+ if ( section.expanded() ) {
+ section.collapse();
+ } else {
+ section.expand();
+ }
+ });
+ },
+
+ /**
+ * Update UI to reflect expanded state.
+ *
+ * @since 4.1.0
+ *
+ * @param {Boolean} expanded
+ */
+ onChangeExpanded: function( expanded ) {
+ var section = this,
+ button = section.container.find( '.add-menu-toggle' ),
+ content = section.container.find( '.new-menu-section-content' ),
+ customizer = section.container.closest( '.wp-full-overlay-sidebar-content' );
+ if ( expanded ) {
+ button.addClass( 'open' );
+ content.slideDown( 'fast', function() {
+ customizer.scrollTop( customizer.height() );
+ });
+ } else {
+ button.removeClass( 'open' );
+ content.slideUp( 'fast' );
+ }
+ }
+ });
+
+ /**
+ * wp.customize.Menus.MenuLocationControl
+ *
+ * Customizer control for menu locations (rendered as a <select>).
+ * Note that 'nav_menu_location' must match the WP_Customize_Nav_Menu_Location_Control::$type.
+ *
+ * @constructor
+ * @augments wp.customize.Control
+ */
+ api.Menus.MenuLocationControl = api.Control.extend({
+ initialize: function( id, options ) {
+ var control = this,
+ matches = id.match( /^nav_menu_locations\[(.+?)]/ );
+ control.themeLocation = matches[1];
+ api.Control.prototype.initialize.call( control, id, options );
+ },
+
+ ready: function() {
+ var control = this, navMenuIdRegex = /^nav_menu\[(-?\d+)]/;
+
+ // @todo It would be better if this was added directly on the setting itself, as opposed to the control.
+ control.setting.validate = function( value ) {
+ return parseInt( value, 10 );
+ };
+
+ // Add/remove menus from the available options when they are added and removed.
+ api.bind( 'add', function( setting ) {
+ var option, menuId, matches = setting.id.match( navMenuIdRegex );
+ if ( ! matches || false === setting() ) {
+ return;
+ }
+ menuId = matches[1];
+ option = new Option( setting().name, menuId );
+ control.container.find( 'select' ).append( option );
+ });
+ api.bind( 'remove', function( setting ) {
+ var menuId, matches = setting.id.match( navMenuIdRegex );
+ if ( ! matches ) {
+ return;
+ }
+ menuId = parseInt( matches[1], 10 );
+ if ( control.setting() === menuId ) {
+ control.setting.set( '' );
+ }
+ control.container.find( 'option[value=' + menuId + ']' ).remove();
+ });
+ api.bind( 'change', function( setting ) {
+ var menuId, matches = setting.id.match( navMenuIdRegex );
+ if ( ! matches ) {
+ return;
+ }
+ menuId = parseInt( matches[1], 10 );
+ if ( false === setting() ) {
+ if ( control.setting() === menuId ) {
+ control.setting.set( '' );
+ }
+ control.container.find( 'option[value=' + menuId + ']' ).remove();
+ } else {
+ control.container.find( 'option[value=' + menuId + ']' ).text( setting().name );
+ }
+ });
+ }
+ });
+
+ /**
+ * wp.customize.Menus.MenuItemControl
+ *
+ * Customizer control for menu items.
+ * Note that 'menu_item' must match the WP_Customize_Menu_Item_Control::$type.
+ *
+ * @constructor
+ * @augments wp.customize.Control
+ */
+ api.Menus.MenuItemControl = api.Control.extend({
+
+ /**
+ * @inheritdoc
+ */
+ initialize: function( id, options ) {
+ var control = this;
+ api.Control.prototype.initialize.call( control, id, options );
+ control.active.validate = function() {
+ return api.section( control.section() ).active();
+ };
+ },
+
+ /**
+ * @since Menu Customizer 0.3
+ *
+ * Override the embed() method to do nothing,
+ * so that the control isn't embedded on load,
+ * unless the containing section is already expanded.
+ */
+ embed: function() {
+ var control = this,
+ sectionId = control.section(),
+ section;
+ if ( ! sectionId ) {
+ return;
+ }
+ section = api.section( sectionId );
+ if ( section && section.expanded() ) {
+ control.actuallyEmbed();
+ }
+ },
+
+ /**
+ * This function is called in Section.onChangeExpanded() so the control
+ * will only get embedded when the Section is first expanded.
+ *
+ * @since Menu Customizer 0.3
+ */
+ actuallyEmbed: function() {
+ var control = this;
+ if ( 'resolved' === control.deferred.embedded.state() ) {
+ return;
+ }
+ control.renderContent();
+ control.deferred.embedded.resolve(); // This triggers control.ready().
+ },
+
+ /**
+ * Set up the control.
+ */
+ ready: function() {
+ if ( 'undefined' === typeof this.params.menu_item_id ) {
+ throw new Error( 'params.menu_item_id was not defined' );
+ }
+
+ this._setupControlToggle();
+ this._setupReorderUI();
+ this._setupUpdateUI();
+ this._setupRemoveUI();
+ this._setupLinksUI();
+ this._setupTitleUI();
+ },
+
+ /**
+ * Show/hide the settings when clicking on the menu item handle.
+ */
+ _setupControlToggle: function() {
+ var control = this;
+
+ this.container.find( '.menu-item-handle' ).on( 'click', function( e ) {
+ e.preventDefault();
+ e.stopPropagation();
+ var menuControl = control.getMenuControl();
+ if ( menuControl.isReordering || menuControl.isSorting ) {
+ return;
+ }
+ control.toggleForm();
+ } );
+ },
+
+ /**
+ * Set up the menu-item-reorder-nav
+ */
+ _setupReorderUI: function() {
+ var control = this, template, $reorderNav;
+
+ template = wp.template( 'menu-item-reorder-nav' );
+
+ // Add the menu item reordering elements to the menu item control.
+ control.container.find( '.item-controls' ).after( template );
+
+ // Handle clicks for up/down/left-right on the reorder nav.
+ $reorderNav = control.container.find( '.menu-item-reorder-nav' );
+ $reorderNav.find( '.menus-move-up, .menus-move-down, .menus-move-left, .menus-move-right' ).on( 'click', function() {
+ var moveBtn = $( this );
+ moveBtn.focus();
+
+ var isMoveUp = moveBtn.is( '.menus-move-up' ),
+ isMoveDown = moveBtn.is( '.menus-move-down' ),
+ isMoveLeft = moveBtn.is( '.menus-move-left' ),
+ isMoveRight = moveBtn.is( '.menus-move-right' );
+
+ if ( isMoveUp ) {
+ control.moveUp();
+ } else if ( isMoveDown ) {
+ control.moveDown();
+ } else if ( isMoveLeft ) {
+ control.moveLeft();
+ } else if ( isMoveRight ) {
+ control.moveRight();
+ }
+
+ moveBtn.focus(); // Re-focus after the container was moved.
+ } );
+ },
+
+ /**
+ * Set up event handlers for menu item updating.
+ */
+ _setupUpdateUI: function() {
+ var control = this,
+ settingValue = control.setting();
+
+ control.elements = {};
+ control.elements.url = new api.Element( control.container.find( '.edit-menu-item-url' ) );
+ control.elements.title = new api.Element( control.container.find( '.edit-menu-item-title' ) );
+ control.elements.attr_title = new api.Element( control.container.find( '.edit-menu-item-attr-title' ) );
+ control.elements.target = new api.Element( control.container.find( '.edit-menu-item-target' ) );
+ control.elements.classes = new api.Element( control.container.find( '.edit-menu-item-classes' ) );
+ control.elements.xfn = new api.Element( control.container.find( '.edit-menu-item-xfn' ) );
+ control.elements.description = new api.Element( control.container.find( '.edit-menu-item-description' ) );
+ // @todo allow other elements, added by plugins, to be automatically picked up here; allow additional values to be added to setting array.
+
+ _.each( control.elements, function( element, property ) {
+ element.bind(function( value ) {
+ if ( element.element.is( 'input[type=checkbox]' ) ) {
+ value = ( value ) ? element.element.val() : '';
+ }
+
+ var settingValue = control.setting();
+ if ( settingValue && settingValue[ property ] !== value ) {
+ settingValue = _.clone( settingValue );
+ settingValue[ property ] = value;
+ control.setting.set( settingValue );
+ }
+ });
+ if ( settingValue ) {
+ element.set( settingValue[ property ] );
+ }
+ });
+
+ control.setting.bind(function( to, from ) {
+ var itemId = control.params.menu_item_id,
+ followingSiblingItemControls = [],
+ childrenItemControls = [],
+ menuControl;
+
+ if ( false === to ) {
+ menuControl = api.control( 'nav_menu[' + String( from.nav_menu_term_id ) + ']' );
+ control.container.remove();
+
+ _.each( menuControl.getMenuItemControls(), function( otherControl ) {
+ if ( from.menu_item_parent === otherControl.setting().menu_item_parent && otherControl.setting().position > from.position ) {
+ followingSiblingItemControls.push( otherControl );
+ } else if ( otherControl.setting().menu_item_parent === itemId ) {
+ childrenItemControls.push( otherControl );
+ }
+ });
+
+ // Shift all following siblings by the number of children this item has.
+ _.each( followingSiblingItemControls, function( followingSiblingItemControl ) {
+ var value = _.clone( followingSiblingItemControl.setting() );
+ value.position += childrenItemControls.length;
+ followingSiblingItemControl.setting.set( value );
+ });
+
+ // Now move the children up to be the new subsequent siblings.
+ _.each( childrenItemControls, function( childrenItemControl, i ) {
+ var value = _.clone( childrenItemControl.setting() );
+ value.position = from.position + i;
+ value.menu_item_parent = from.menu_item_parent;
+ childrenItemControl.setting.set( value );
+ });
+
+ menuControl.debouncedReflowMenuItems();
+ } else {
+ // Update the elements' values to match the new setting properties.
+ _.each( to, function( value, key ) {
+ if ( control.elements[ key] ) {
+ control.elements[ key ].set( to[ key ] );
+ }
+ } );
+ control.container.find( '.menu-item-data-parent-id' ).val( to.menu_item_parent );
+
+ // Handle UI updates when the position or depth (parent) change.
+ if ( to.position !== from.position || to.menu_item_parent !== from.menu_item_parent ) {
+ control.getMenuControl().debouncedReflowMenuItems();
+ }
+ }
+ });
+ },
+
+ /**
+ * Set up event handlers for menu item deletion.
+ */
+ _setupRemoveUI: function() {
+ var control = this, $removeBtn;
+
+ // Configure delete button.
+ $removeBtn = control.container.find( '.item-delete' );
+
+ $removeBtn.on( 'click', function( e ) {
+ // Find an adjacent element to add focus to when this menu item goes away
+ var $adjacentFocusTarget;
+ if ( control.container.next().is( '.customize-control-nav_menu_item' ) ) {
+ if ( ! $( 'body' ).hasClass( 'adding-menu-items' ) ) {
+ $adjacentFocusTarget = control.container.next().find( '.item-edit:first' );
+ } else {
+ $adjacentFocusTarget = control.container.next().find( '.item-delete:first' );
+ }
+ } else if ( control.container.prev().is( '.customize-control-nav_menu_item' ) ) {
+ if ( ! $( 'body' ).hasClass( 'adding-menu-items' ) ) {
+ $adjacentFocusTarget = control.container.prev().find( '.item-edit:first' );
+ } else {
+ $adjacentFocusTarget = control.container.prev().find( '.item-delete:first' );
+ }
+ } else {
+ $adjacentFocusTarget = control.container.next( '.customize-control-nav_menu' ).find( '.add-new-menu-item' );
+ }
+
+ control.container.slideUp( function() {
+ control.setting.set( false );
+ wp.a11y.speak( api.Menus.data.l10n.itemDeleted );
+ $adjacentFocusTarget.focus(); // keyboard accessibility
+ } );
+ } );
+ },
+
+ _setupLinksUI: function() {
+ var $origBtn;
+
+ // Configure original link.
+ $origBtn = this.container.find( 'a.original-link' );
+
+ $origBtn.on( 'click', function( e ) {
+ e.preventDefault();
+ api.previewer.previewUrl( e.target.toString() );
+ } );
+ },
+
+ /**
+ * Update item handle title when changed.
+ */
+ _setupTitleUI: function() {
+ var control = this;
+
+ control.setting.bind( function( item ) {
+ if ( ! item ) {
+ return;
+ }
+
+ var titleEl = control.container.find( '.menu-item-title' );
+
+ // Don't update to an empty title.
+ if ( item.title ) {
+ titleEl
+ .text( item.title )
+ .removeClass( 'no-title' );
+ } else {
+ titleEl
+ .text( api.Menus.data.l10n.untitled )
+ .addClass( 'no-title' );
+ }
+ } );
+ },
+
+ /**
+ *
+ * @returns {number}
+ */
+ getDepth: function() {
+ var control = this, setting = control.setting(), depth = 0;
+ if ( ! setting ) {
+ return 0;
+ }
+ while ( setting && setting.menu_item_parent ) {
+ depth += 1;
+ control = api.control( 'nav_menu_item[' + setting.menu_item_parent + ']' );
+ if ( ! control ) {
+ break;
+ }
+ setting = control.setting();
+ }
+ return depth;
+ },
+
+ /**
+ * Amend the control's params with the data necessary for the JS template just in time.
+ */
+ renderContent: function() {
+ var control = this,
+ settingValue = control.setting(),
+ containerClasses;
+
+ control.params.title = settingValue.title || '';
+ control.params.depth = control.getDepth();
+ control.container.data( 'item-depth', control.params.depth );
+ containerClasses = [
+ 'menu-item',
+ 'menu-item-depth-' + String( control.params.depth ),
+ 'menu-item-' + settingValue.object,
+ 'menu-item-edit-inactive'
+ ];
+
+ if ( settingValue.invalid ) {
+ containerClasses.push( 'invalid' );
+ control.params.title = api.Menus.data.invalidTitleTpl.replace( '%s', control.params.title );
+ } else if ( 'draft' === settingValue.status ) {
+ containerClasses.push( 'pending' );
+ control.params.title = api.Menus.data.pendingTitleTpl.replace( '%s', control.params.title );
+ }
+
+ control.params.el_classes = containerClasses.join( ' ' );
+ control.params.item_type_label = api.Menus.getTypeLabel( settingValue.type, settingValue.object );
+ control.params.item_type = settingValue.type;
+ control.params.url = settingValue.url;
+ control.params.target = settingValue.target;
+ control.params.attr_title = settingValue.attr_title;
+ control.params.classes = _.isArray( settingValue.classes ) ? settingValue.classes.join( ' ' ) : settingValue.classes;
+ control.params.attr_title = settingValue.attr_title;
+ control.params.xfn = settingValue.xfn;
+ control.params.description = settingValue.description;
+ control.params.parent = settingValue.menu_item_parent;
+ control.params.original_title = settingValue.original_title || '';
+
+ control.container.addClass( control.params.el_classes );
+
+ api.Control.prototype.renderContent.call( control );
+ },
+
+ /***********************************************************************
+ * Begin public API methods
+ **********************************************************************/
+
+ /**
+ * @return {wp.customize.controlConstructor.nav_menu|null}
+ */
+ getMenuControl: function() {
+ var control = this, settingValue = control.setting();
+ if ( settingValue && settingValue.nav_menu_term_id ) {
+ return api.control( 'nav_menu[' + settingValue.nav_menu_term_id + ']' );
+ } else {
+ return null;
+ }
+ },
+
+ /**
+ * Expand the accordion section containing a control
+ */
+ expandControlSection: function() {
+ var $section = this.container.closest( '.accordion-section' );
+
+ if ( ! $section.hasClass( 'open' ) ) {
+ $section.find( '.accordion-section-title:first' ).trigger( 'click' );
+ }
+ },
+
+ /**
+ * Expand the menu item form control.
+ */
+ expandForm: function() {
+ this.toggleForm( true );
+ },
+
+ /**
+ * Collapse the menu item form control.
+ */
+ collapseForm: function() {
+ this.toggleForm( false );
+ },
+
+ /**
+ * Expand or collapse the menu item control.
+ *
+ * @param {boolean|undefined} [showOrHide] If not supplied, will be inverse of current visibility
+ */
+ toggleForm: function( showOrHide ) {
+ var self = this, $menuitem, $inside, complete;
+
+ $menuitem = this.container;
+ $inside = $menuitem.find( '.menu-item-settings:first' );
+ if ( 'undefined' === typeof showOrHide ) {
+ showOrHide = ! $inside.is( ':visible' );
+ }
+
+ // Already expanded or collapsed.
+ if ( $inside.is( ':visible' ) === showOrHide ) {
+ return;
+ }
+
+ if ( showOrHide ) {
+ // Close all other menu item controls before expanding this one.
+ api.control.each( function( otherControl ) {
+ if ( self.params.type === otherControl.params.type && self !== otherControl ) {
+ otherControl.collapseForm();
+ }
+ } );
+
+ complete = function() {
+ $menuitem
+ .removeClass( 'menu-item-edit-inactive' )
+ .addClass( 'menu-item-edit-active' );
+ self.container.trigger( 'expanded' );
+ };
+
+ $inside.slideDown( 'fast', complete );
+
+ self.container.trigger( 'expand' );
+ } else {
+ complete = function() {
+ $menuitem
+ .addClass( 'menu-item-edit-inactive' )
+ .removeClass( 'menu-item-edit-active' );
+ self.container.trigger( 'collapsed' );
+ };
+
+ self.container.trigger( 'collapse' );
+
+ $inside.slideUp( 'fast', complete );
+ }
+ },
+
+ /**
+ * Expand the containing menu section, expand the form, and focus on
+ * the first input in the control.
+ */
+ focus: function() {
+ this.expandControlSection();
+ this.expandForm();
+ this.container.find( '.menu-item-settings :focusable:first' ).focus();
+ },
+
+ /**
+ * Move menu item up one in the menu.
+ */
+ moveUp: function() {
+ this._changePosition( -1 );
+ wp.a11y.speak( api.Menus.data.l10n.movedUp );
+ },
+
+ /**
+ * Move menu item up one in the menu.
+ */
+ moveDown: function() {
+ this._changePosition( 1 );
+ wp.a11y.speak( api.Menus.data.l10n.movedDown );
+ },
+ /**
+ * Move menu item and all children up one level of depth.
+ */
+ moveLeft: function() {
+ this._changeDepth( -1 );
+ wp.a11y.speak( api.Menus.data.l10n.movedLeft );
+ },
+
+ /**
+ * Move menu item and children one level deeper, as a submenu of the previous item.
+ */
+ moveRight: function() {
+ this._changeDepth( 1 );
+ wp.a11y.speak( api.Menus.data.l10n.movedRight );
+ },
+
+ /**
+ * Note that this will trigger a UI update, causing child items to
+ * move as well and cardinal order class names to be updated.
+ *
+ * @private
+ *
+ * @param {Number} offset 1|-1
+ */
+ _changePosition: function( offset ) {
+ var control = this,
+ adjacentSetting,
+ settingValue = _.clone( control.setting() ),
+ siblingSettings = [],
+ realPosition;
+
+ if ( 1 !== offset && -1 !== offset ) {
+ throw new Error( 'Offset changes by 1 are only supported.' );
+ }
+
+ // Skip moving deleted items.
+ if ( ! control.setting() ) {
+ return;
+ }
+
+ // Locate the other items under the same parent (siblings).
+ _( control.getMenuControl().getMenuItemControls() ).each(function( otherControl ) {
+ if ( otherControl.setting().menu_item_parent === settingValue.menu_item_parent ) {
+ siblingSettings.push( otherControl.setting );
+ }
+ });
+ siblingSettings.sort(function( a, b ) {
+ return a().position - b().position;
+ });
+
+ realPosition = _.indexOf( siblingSettings, control.setting );
+ if ( -1 === realPosition ) {
+ throw new Error( 'Expected setting to be among siblings.' );
+ }
+
+ // Skip doing anything if the item is already at the edge in the desired direction.
+ if ( ( realPosition === 0 && offset < 0 ) || ( realPosition === siblingSettings.length - 1 && offset > 0 ) ) {
+ // @todo Should we allow a menu item to be moved up to break it out of a parent? Adopt with previous or following parent?
+ return;
+ }
+
+ // Update any adjacent menu item setting to take on this item's position.
+ adjacentSetting = siblingSettings[ realPosition + offset ];
+ if ( adjacentSetting ) {
+ adjacentSetting.set( $.extend(
+ _.clone( adjacentSetting() ),
+ {
+ position: settingValue.position
+ }
+ ) );
+ }
+
+ settingValue.position += offset;
+ control.setting.set( settingValue );
+ },
+
+ /**
+ * Note that this will trigger a UI update, causing child items to
+ * move as well and cardinal order class names to be updated.
+ *
+ * @private
+ *
+ * @param {Number} offset 1|-1
+ */
+ _changeDepth: function( offset ) {
+ if ( 1 !== offset && -1 !== offset ) {
+ throw new Error( 'Offset changes by 1 are only supported.' );
+ }
+ var control = this,
+ settingValue = _.clone( control.setting() ),
+ siblingControls = [],
+ realPosition,
+ siblingControl,
+ parentControl;
+
+ // Locate the other items under the same parent (siblings).
+ _( control.getMenuControl().getMenuItemControls() ).each(function( otherControl ) {
+ if ( otherControl.setting().menu_item_parent === settingValue.menu_item_parent ) {
+ siblingControls.push( otherControl );
+ }
+ });
+ siblingControls.sort(function( a, b ) {
+ return a.setting().position - b.setting().position;
+ });
+
+ realPosition = _.indexOf( siblingControls, control );
+ if ( -1 === realPosition ) {
+ throw new Error( 'Expected control to be among siblings.' );
+ }
+
+ if ( -1 === offset ) {
+ // Skip moving left an item that is already at the top level.
+ if ( ! settingValue.menu_item_parent ) {
+ return;
+ }
+
+ parentControl = api.control( 'nav_menu_item[' + settingValue.menu_item_parent + ']' );
+
+ // Make this control the parent of all the following siblings.
+ _( siblingControls ).chain().slice( realPosition ).each(function( siblingControl, i ) {
+ siblingControl.setting.set(
+ $.extend(
+ {},
+ siblingControl.setting(),
+ {
+ menu_item_parent: control.params.menu_item_id,
+ position: i
+ }
+ )
+ );
+ });
+
+ // Increase the positions of the parent item's subsequent children to make room for this one.
+ _( control.getMenuControl().getMenuItemControls() ).each(function( otherControl ) {
+ var otherControlSettingValue, isControlToBeShifted;
+ isControlToBeShifted = (
+ otherControl.setting().menu_item_parent === parentControl.setting().menu_item_parent &&
+ otherControl.setting().position > parentControl.setting().position
+ );
+ if ( isControlToBeShifted ) {
+ otherControlSettingValue = _.clone( otherControl.setting() );
+ otherControl.setting.set(
+ $.extend(
+ otherControlSettingValue,
+ { position: otherControlSettingValue.position + 1 }
+ )
+ );
+ }
+ });
+
+ // Make this control the following sibling of its parent item.
+ settingValue.position = parentControl.setting().position + 1;
+ settingValue.menu_item_parent = parentControl.setting().menu_item_parent;
+ control.setting.set( settingValue );
+
+ } else if ( 1 === offset ) {
+ // Skip moving right an item that doesn't have a previous sibling.
+ if ( realPosition === 0 ) {
+ return;
+ }
+
+ // Make the control the last child of the previous sibling.
+ siblingControl = siblingControls[ realPosition - 1 ];
+ settingValue.menu_item_parent = siblingControl.params.menu_item_id;
+ settingValue.position = 0;
+ _( control.getMenuControl().getMenuItemControls() ).each(function( otherControl ) {
+ if ( otherControl.setting().menu_item_parent === settingValue.menu_item_parent ) {
+ settingValue.position = Math.max( settingValue.position, otherControl.setting().position );
+ }
+ });
+ settingValue.position += 1;
+ control.setting.set( settingValue );
+ }
+ }
+ } );
+
+ /**
+ * wp.customize.Menus.MenuNameControl
+ *
+ * Customizer control for a nav menu's name.
+ *
+ * @constructor
+ * @augments wp.customize.Control
+ */
+ api.Menus.MenuNameControl = api.Control.extend({
+
+ ready: function() {
+ var control = this,
+ settingValue = control.setting();
+
+ /*
+ * Since the control is not registered in PHP, we need to prevent the
+ * preview's sending of the activeControls to result in this control
+ * being deactivated.
+ */
+ control.active.validate = function() {
+ return api.section( control.section() ).active();
+ };
+
+ control.nameElement = new api.Element( control.container.find( '.menu-name-field' ) );
+
+ control.nameElement.bind(function( value ) {
+ var settingValue = control.setting();
+ if ( settingValue && settingValue.name !== value ) {
+ settingValue = _.clone( settingValue );
+ settingValue.name = value;
+ control.setting.set( settingValue );
+ }
+ });
+ if ( settingValue ) {
+ control.nameElement.set( settingValue.name );
+ }
+
+ control.setting.bind(function( object ) {
+ if ( object ) {
+ control.nameElement.set( object.name );
+ }
+ });
+ }
+
+ });
+
+ /**
+ * wp.customize.Menus.MenuControl
+ *
+ * Customizer control for menus.
+ * Note that 'nav_menu' must match the WP_Menu_Customize_Control::$type
+ *
+ * @constructor
+ * @augments wp.customize.Control
+ */
+ api.Menus.MenuControl = api.Control.extend({
+ /**
+ * Set up the control.
+ */
+ ready: function() {
+ var control = this,
+ menuId = control.params.menu_id;
+
+ if ( 'undefined' === typeof this.params.menu_id ) {
+ throw new Error( 'params.menu_id was not defined' );
+ }
+
+ /*
+ * Since the control is not registered in PHP, we need to prevent the
+ * preview's sending of the activeControls to result in this control
+ * being deactivated.
+ */
+ control.active.validate = function() {
+ return api.section( control.section() ).active();
+ };
+
+ control.$controlSection = control.container.closest( '.control-section' );
+ control.$sectionContent = control.container.closest( '.accordion-section-content' );
+
+ this._setupModel();
+
+ api.section( control.section(), function( section ) {
+ section.deferred.initSortables.done(function( menuList ) {
+ control._setupSortable( menuList );
+ });
+ } );
+
+ this._setupAddition();
+ this._setupLocations();
+ this._setupTitle();
+
+ // Add menu to Custom Menu widgets.
+ if ( control.setting() ) {
+ api.control.each( function( widgetControl ) {
+ if ( ! widgetControl.extended( api.controlConstructor.widget_form ) || 'nav_menu' !== widgetControl.params.widget_id_base ) {
+ return;
+ }
+ var select = widgetControl.container.find( 'select' );
+ if ( select.find( 'option[value=' + String( menuId ) + ']' ).length === 0 ) {
+ select.append( new Option( control.setting().name, menuId ) );
+ }
+ } );
+ $( '#available-widgets-list .widget-inside:has(input.id_base[value=nav_menu]) select:first' ).append( new Option( control.setting().name, menuId ) );
+ }
+ },
+
+ /**
+ * Update ordering of menu item controls when the setting is updated.
+ */
+ _setupModel: function() {
+ var control = this,
+ menuId = control.params.menu_id;
+
+ control.elements = {};
+ control.elements.auto_add = new api.Element( control.container.find( 'input[type=checkbox].auto_add' ) );
+
+ control.elements.auto_add.bind(function( auto_add ) {
+ var settingValue = control.setting();
+ if ( settingValue && settingValue.auto_add !== auto_add ) {
+ settingValue = _.clone( settingValue );
+ settingValue.auto_add = auto_add;
+ control.setting.set( settingValue );
+ }
+ });
+ control.elements.auto_add.set( control.setting().auto_add );
+ control.setting.bind(function( object ) {
+ if ( ! object ) {
+ return;
+ }
+ control.elements.auto_add.set( object.auto_add );
+ });
+
+ control.setting.bind( function( to ) {
+ if ( false === to ) {
+ control._handleDeletion();
+ } else {
+ // Update names in the Custom Menu widgets.
+ api.control.each( function( widgetControl ) {
+ if ( ! widgetControl.extended( api.controlConstructor.widget_form ) || 'nav_menu' !== widgetControl.params.widget_id_base ) {
+ return;
+ }
+ var select = widgetControl.container.find( 'select' );
+ select.find( 'option[value=' + String( menuId ) + ']' ).text( to.name );
+ });
+ $( '#available-widgets-list .widget-inside:has(input.id_base[value=nav_menu]) select:first option[value=' + String( menuId ) + ']' ).text( to.name );
+ }
+ } );
+
+ control.container.find( '.menu-delete' ).on( 'click', function( event ) {
+ event.stopPropagation();
+ event.preventDefault();
+ control.setting.set( false );
+ });
+ },
+
+ /**
+ * Allow items in each menu to be re-ordered, and for the order to be previewed.
+ *
+ * Notice that the UI aspects here are handled by wpNavMenu.initSortables()
+ * which is called in MenuSection.onChangeExpanded()
+ *
+ * @param {object} menuList - The element that has sortable().
+ */
+ _setupSortable: function( menuList ) {
+ var control = this;
+
+ if ( ! menuList.is( control.$sectionContent ) ) {
+ throw new Error( 'Unexpected menuList.' );
+ }
+
+ menuList.on( 'sortstart', function() {
+ control.isSorting = true;
+ });
+
+ menuList.on( 'sortstop', function() {
+ setTimeout( function() { // Next tick.
+ var menuItemContainerIds = control.$sectionContent.sortable( 'toArray' ),
+ menuItemControls = [],
+ position = 0,
+ priority = 10;
+
+ control.isSorting = false;
+
+ _.each( menuItemContainerIds, function( menuItemContainerId ) {
+ var menuItemId, menuItemControl, matches;
+ matches = menuItemContainerId.match( /^customize-control-nav_menu_item-(-?\d+)$/, '' );
+ if ( ! matches ) {
+ return;
+ }
+ menuItemId = parseInt( matches[1], 10 );
+ menuItemControl = api.control( 'nav_menu_item[' + String( menuItemId ) + ']' );
+ if ( menuItemControl ) {
+ menuItemControls.push( menuItemControl );
+ }
+ } );
+
+ _.each( menuItemControls, function( menuItemControl ) {
+ if ( false === menuItemControl.setting() ) {
+ // Skip deleted items.
+ return;
+ }
+ var setting = _.clone( menuItemControl.setting() );
+ position += 1;
+ priority += 1;
+ setting.position = position;
+ menuItemControl.priority( priority );
+
+ // Note that wpNavMenu will be setting this .menu-item-data-parent-id input's value.
+ setting.menu_item_parent = parseInt( menuItemControl.container.find( '.menu-item-data-parent-id' ).val(), 10 );
+ if ( ! setting.menu_item_parent ) {
+ setting.menu_item_parent = 0;
+ }
+
+ menuItemControl.setting.set( setting );
+ });
+ });
+ });
+
+ control.isReordering = false;
+
+ /**
+ * Keyboard-accessible reordering.
+ */
+ this.container.find( '.reorder-toggle' ).on( 'click', function() {
+ control.toggleReordering( ! control.isReordering );
+ } );
+ },
+
+ /**
+ * Set up UI for adding a new menu item.
+ */
+ _setupAddition: function() {
+ var self = this;
+
+ this.container.find( '.add-new-menu-item' ).on( 'click', function( event ) {
+ if ( self.$sectionContent.hasClass( 'reordering' ) ) {
+ return;
+ }
+
+ if ( ! $( 'body' ).hasClass( 'adding-menu-items' ) ) {
+ api.Menus.availableMenuItemsPanel.open( self );
+ } else {
+ api.Menus.availableMenuItemsPanel.close();
+ event.stopPropagation();
+ }
+ } );
+ },
+
+ _handleDeletion: function() {
+ var control = this,
+ section,
+ menuId = control.params.menu_id,
+ removeSection;
+ section = api.section( control.section() );
+ removeSection = function() {
+ section.container.remove();
+ api.section.remove( section.id );
+ };
+
+ if ( section && section.expanded() ) {
+ section.collapse({
+ completeCallback: function() {
+ removeSection();
+ wp.a11y.speak( api.Menus.data.l10n.menuDeleted );
+ api.panel( 'nav_menus' ).focus();
+ }
+ });
+ } else {
+ removeSection();
+ }
+
+ // Remove the menu from any Custom Menu widgets.
+ api.control.each(function( widgetControl ) {
+ if ( ! widgetControl.extended( api.controlConstructor.widget_form ) || 'nav_menu' !== widgetControl.params.widget_id_base ) {
+ return;
+ }
+ var select = widgetControl.container.find( 'select' );
+ if ( select.val() === String( menuId ) ) {
+ select.prop( 'selectedIndex', 0 ).trigger( 'change' );
+ }
+ select.find( 'option[value=' + String( menuId ) + ']' ).remove();
+ });
+ $( '#available-widgets-list .widget-inside:has(input.id_base[value=nav_menu]) select:first option[value=' + String( menuId ) + ']' ).remove();
+ },
+
+ // Setup theme location checkboxes.
+ _setupLocations: function() {
+ var control = this;
+
+ control.container.find( '.assigned-menu-location' ).each(function() {
+ var container = $( this ),
+ checkbox = container.find( 'input[type=checkbox]' ),
+ element,
+ updateSelectedMenuLabel,
+ navMenuLocationSetting = api( 'nav_menu_locations[' + checkbox.data( 'location-id' ) + ']' );
+
+ updateSelectedMenuLabel = function( selectedMenuId ) {
+ var menuSetting = api( 'nav_menu[' + String( selectedMenuId ) + ']' );
+ if ( ! selectedMenuId || ! menuSetting || ! menuSetting() ) {
+ container.find( '.theme-location-set' ).hide();
+ } else {
+ container.find( '.theme-location-set' ).show().find( 'span' ).text( menuSetting().name );
+ }
+ };
+
+ element = new api.Element( checkbox );
+ element.set( navMenuLocationSetting.get() === control.params.menu_id );
+
+ checkbox.on( 'change', function() {
+ // Note: We can't use element.bind( function( checked ){ ... } ) here because it will trigger a change as well.
+ navMenuLocationSetting.set( this.checked ? control.params.menu_id : 0 );
+ } );
+
+ navMenuLocationSetting.bind(function( selectedMenuId ) {
+ element.set( selectedMenuId === control.params.menu_id );
+ updateSelectedMenuLabel( selectedMenuId );
+ });
+ updateSelectedMenuLabel( navMenuLocationSetting.get() );
+
+ });
+ },
+
+ /**
+ * Update Section Title as menu name is changed.
+ */
+ _setupTitle: function() {
+ var control = this;
+
+ control.setting.bind( function( menu ) {
+ if ( ! menu ) {
+ return;
+ }
+
+ // Empty names are not allowed (will not be saved), don't update to one.
+ if ( menu.name ) {
+ var section = control.container.closest( '.accordion-section' ),
+ menuId = control.params.menu_id,
+ controlTitle = section.find( '.accordion-section-title' ),
+ sectionTitle = section.find( '.customize-section-title h3' ),
+ location = section.find( '.menu-in-location' ),
+ action = sectionTitle.find( '.customize-action' );
+
+ // Update the control title
+ controlTitle.text( menu.name );
+ if ( location.length ) {
+ location.appendTo( controlTitle );
+ }
+
+ // Update the section title
+ sectionTitle.text( menu.name );
+ if ( action.length ) {
+ action.prependTo( sectionTitle );
+ }
+
+ // Update the nav menu name in location selects.
+ api.control.each( function( control ) {
+ if ( /^nav_menu_locations\[/.test( control.id ) ) {
+ control.container.find( 'option[value=' + menuId + ']' ).text( menu.name );
+ }
+ } );
+
+ // Update the nav menu name in all location checkboxes.
+ section.find( '.customize-control-checkbox input' ).each( function() {
+ if ( $( this ).prop( 'checked' ) ) {
+ $( '.current-menu-location-name-' + $( this ).data( 'location-id' ) ).text( menu.name );
+ }
+ } );
+ }
+ } );
+ },
+
+ /***********************************************************************
+ * Begin public API methods
+ **********************************************************************/
+
+ /**
+ * Enable/disable the reordering UI
+ *
+ * @param {Boolean} showOrHide to enable/disable reordering
+ */
+ toggleReordering: function( showOrHide ) {
+ showOrHide = Boolean( showOrHide );
+
+ if ( showOrHide === this.$sectionContent.hasClass( 'reordering' ) ) {
+ return;
+ }
+
+ this.isReordering = showOrHide;
+ this.$sectionContent.toggleClass( 'reordering', showOrHide );
+ this.$sectionContent.sortable( this.isReordering ? 'disable' : 'enable' );
+
+ if ( showOrHide ) {
+ _( this.getMenuItemControls() ).each( function( formControl ) {
+ formControl.collapseForm();
+ } );
+ }
+ },
+
+ /**
+ * @return {wp.customize.controlConstructor.nav_menu_item[]}
+ */
+ getMenuItemControls: function() {
+ var menuControl = this,
+ menuItemControls = [],
+ menuTermId = menuControl.params.menu_id;
+
+ api.control.each(function( control ) {
+ if ( 'nav_menu_item' === control.params.type && control.setting() && menuTermId === control.setting().nav_menu_term_id ) {
+ menuItemControls.push( control );
+ }
+ });
+
+ return menuItemControls;
+ },
+
+ /**
+ * Make sure that each menu item control has the proper depth.
+ */
+ reflowMenuItems: function() {
+ var menuControl = this,
+ menuSection = api.section( 'nav_menu[' + String( menuControl.params.menu_id ) + ']' ),
+ menuItemControls = menuControl.getMenuItemControls(),
+ reflowRecursively;
+
+ reflowRecursively = function( context ) {
+ var currentMenuItemControls = [],
+ thisParent = context.currentParent;
+ _.each( context.menuItemControls, function( menuItemControl ) {
+ if ( thisParent === menuItemControl.setting().menu_item_parent ) {
+ currentMenuItemControls.push( menuItemControl );
+ // @todo We could remove this item from menuItemControls now, for efficiency.
+ }
+ });
+ currentMenuItemControls.sort( function( a, b ) {
+ return a.setting().position - b.setting().position;
+ });
+
+ _.each( currentMenuItemControls, function( menuItemControl ) {
+ // Update position.
+ context.currentAbsolutePosition += 1;
+ menuItemControl.priority.set( context.currentAbsolutePosition ); // This will change the sort order.
+
+ // Update depth.
+ if ( ! menuItemControl.container.hasClass( 'menu-item-depth-' + String( context.currentDepth ) ) ) {
+ _.each( menuItemControl.container.prop( 'className' ).match( /menu-item-depth-\d+/g ), function( className ) {
+ menuItemControl.container.removeClass( className );
+ });
+ menuItemControl.container.addClass( 'menu-item-depth-' + String( context.currentDepth ) );
+ }
+ menuItemControl.container.data( 'item-depth', context.currentDepth );
+
+ // Process any children items.
+ context.currentDepth += 1;
+ context.currentParent = menuItemControl.params.menu_item_id;
+ reflowRecursively( context );
+ context.currentDepth -= 1;
+ context.currentParent = thisParent;
+ });
+
+ // Update class names for reordering controls.
+ if ( currentMenuItemControls.length ) {
+ _( currentMenuItemControls ).each(function( menuItemControl ) {
+ menuItemControl.container.removeClass( 'move-up-disabled move-down-disabled move-left-disabled move-right-disabled' );
+ });
+
+ currentMenuItemControls[0].container
+ .addClass( 'move-up-disabled' )
+ .addClass( 'move-right-disabled' )
+ .toggleClass( 'move-down-disabled', 1 === currentMenuItemControls.length );
+ currentMenuItemControls[ currentMenuItemControls.length - 1 ].container
+ .addClass( 'move-down-disabled' )
+ .toggleClass( 'move-up-disabled', 1 === currentMenuItemControls.length );
+ }
+ };
+
+ reflowRecursively( {
+ menuItemControls: menuItemControls,
+ currentParent: 0,
+ currentDepth: 0,
+ currentAbsolutePosition: 0
+ } );
+
+ menuSection.container.find( '.menu-item .menu-item-reorder-nav button' ).prop( 'tabIndex', 0 );
+ menuSection.container.find( '.menu-item.move-up-disabled .menus-move-up' ).prop( 'tabIndex', -1 );
+ menuSection.container.find( '.menu-item.move-down-disabled .menus-move-down' ).prop( 'tabIndex', -1 );
+ menuSection.container.find( '.menu-item.move-left-disabled .menus-move-left' ).prop( 'tabIndex', -1 );
+ menuSection.container.find( '.menu-item.move-right-disabled .menus-move-right' ).prop( 'tabIndex', -1 );
+
+ menuControl.container.find( '.reorder-toggle' ).toggle( menuItemControls.length > 1 );
+ },
+
+ /**
+ * Note that this function gets debounced so that when a lot of setting
+ * changes are made at once, for instance when moving a menu item that
+ * has child items, this function will only be called once all of the
+ * settings have been updated.
+ */
+ debouncedReflowMenuItems: _.debounce( function() {
+ this.reflowMenuItems.apply( this, arguments );
+ }, 0 ),
+
+ /**
+ * Add a new item to this menu.
+ *
+ * @param {object} item - Value for the nav_menu_item setting to be created.
+ * @returns {wp.customize.Menus.controlConstructor.nav_menu_item} The newly-created nav_menu_item control instance.
+ */
+ addItemToMenu: function( item ) {
+ var menuControl = this, customizeId, settingArgs, setting, menuItemControl, placeholderId, position = 0, priority = 10;
+
+ _.each( menuControl.getMenuItemControls(), function( control ) {
+ if ( false === control.setting() ) {
+ return;
+ }
+ priority = Math.max( priority, control.priority() );
+ if ( 0 === control.setting().menu_item_parent ) {
+ position = Math.max( position, control.setting().position );
+ }
+ });
+ position += 1;
+ priority += 1;
+
+ item = $.extend(
+ {},
+ api.Menus.data.defaultSettingValues.nav_menu_item,
+ item,
+ {
+ nav_menu_term_id: menuControl.params.menu_id,
+ original_title: item.title,
+ position: position
+ }
+ );
+ delete item.id; // only used by Backbone
+
+ placeholderId = api.Menus.generatePlaceholderAutoIncrementId();
+ customizeId = 'nav_menu_item[' + String( placeholderId ) + ']';
+ settingArgs = {
+ type: 'nav_menu_item',
+ transport: 'postMessage',
+ previewer: api.previewer
+ };
+ setting = api.create( customizeId, customizeId, {}, settingArgs );
+ setting.set( item ); // Change from initial empty object to actual item to mark as dirty.
+
+ // Add the menu item control.
+ menuItemControl = new api.controlConstructor.nav_menu_item( customizeId, {
+ params: {
+ type: 'nav_menu_item',
+ content: '<li id="customize-control-nav_menu_item-' + String( placeholderId ) + '" class="customize-control customize-control-nav_menu_item"></li>',
+ section: menuControl.id,
+ priority: priority,
+ active: true,
+ settings: {
+ 'default': customizeId
+ },
+ menu_item_id: placeholderId
+ },
+ previewer: api.previewer
+ } );
+
+ api.control.add( customizeId, menuItemControl );
+ setting.preview();
+ menuControl.debouncedReflowMenuItems();
+
+ wp.a11y.speak( api.Menus.data.l10n.itemAdded );
+
+ return menuItemControl;
+ }
+ } );
+
+ /**
+ * wp.customize.Menus.NewMenuControl
+ *
+ * Customizer control for creating new menus and handling deletion of existing menus.
+ * Note that 'new_menu' must match the WP_New_Menu_Customize_Control::$type.
+ *
+ * @constructor
+ * @augments wp.customize.Control
+ */
+ api.Menus.NewMenuControl = api.Control.extend({
+ /**
+ * Set up the control.
+ */
+ ready: function() {
+ this._bindHandlers();
+ },
+
+ _bindHandlers: function() {
+ var self = this,
+ name = $( '#customize-control-new_menu_name input' ),
+ submit = $( '#create-new-menu-submit' );
+ name.on( 'keydown', function( event ) {
+ if ( 13 === event.which ) { // Enter.
+ self.submit();
+ }
+ } );
+ submit.on( 'click', function( event ) {
+ self.submit();
+ event.stopPropagation();
+ event.preventDefault();
+ } );
+ },
+
+ /**
+ * Create the new menu with the name supplied.
+ *
+ * @returns {boolean}
+ */
+ submit: function() {
+
+ var control = this,
+ container = control.container.closest( '.accordion-section-new-menu' ),
+ nameInput = container.find( '.menu-name-field' ).first(),
+ name = nameInput.val(),
+ menuSection,
+ customizeId,
+ placeholderId = api.Menus.generatePlaceholderAutoIncrementId();
+
+ customizeId = 'nav_menu[' + String( placeholderId ) + ']';
+
+ // Register the menu control setting.
+ api.create( customizeId, customizeId, {}, {
+ type: 'nav_menu',
+ transport: 'postMessage',
+ previewer: api.previewer
+ } );
+ api( customizeId ).set( $.extend(
+ {},
+ api.Menus.data.defaultSettingValues.nav_menu,
+ {
+ name: name
+ }
+ ) );
+
+ /*
+ * Add the menu section (and its controls).
+ * Note that this will automatically create the required controls
+ * inside via the Section's ready method.
+ */
+ menuSection = new api.Menus.MenuSection( customizeId, {
+ params: {
+ id: customizeId,
+ panel: 'nav_menus',
+ title: name,
+ customizeAction: api.Menus.data.l10n.customizingMenus,
+ type: 'nav_menu',
+ priority: 10,
+ menu_id: placeholderId
+ }
+ } );
+ api.section.add( customizeId, menuSection );
+
+ // Clear name field.
+ nameInput.val( '' );
+
+ wp.a11y.speak( api.Menus.data.l10n.menuAdded );
+
+ // Focus on the new menu section.
+ api.section( customizeId ).focus(); // @todo should we focus on the new menu's control and open the add-items panel? Thinking user flow...
+ }
+ });
+
+ /**
+ * Extends wp.customize.controlConstructor with control constructor for
+ * menu_location, menu_item, nav_menu, and new_menu.
+ */
+ $.extend( api.controlConstructor, {
+ nav_menu_location: api.Menus.MenuLocationControl,
+ nav_menu_item: api.Menus.MenuItemControl,
+ nav_menu: api.Menus.MenuControl,
+ nav_menu_name: api.Menus.MenuNameControl,
+ new_menu: api.Menus.NewMenuControl
+ });
+
+ /**
+ * Extends wp.customize.panelConstructor with section constructor for menus.
+ */
+ $.extend( api.panelConstructor, {
+ menus: api.Menus.MenusPanel
+ });
+
+ /**
+ * Extends wp.customize.sectionConstructor with section constructor for menu.
+ */
+ $.extend( api.sectionConstructor, {
+ nav_menu: api.Menus.MenuSection,
+ new_menu: api.Menus.NewMenuSection
+ });
+
+ /**
+ * Init Customizer for menus.
+ */
+ api.bind( 'ready', function() {
+
+ // Set up the menu items panel.
+ api.Menus.availableMenuItemsPanel = new api.Menus.AvailableMenuItemsPanelView({
+ collection: api.Menus.availableMenuItems
+ });
+
+ api.bind( 'saved', function( data ) {
+ if ( data.nav_menu_updates || data.nav_menu_item_updates ) {
+ api.Menus.applySavedData( data );
+ }
+ } );
+
+ api.previewer.bind( 'refresh', function() {
+ api.previewer.refresh();
+ });
+ } );
+
+ /**
+ * When customize_save comes back with a success, make sure any inserted
+ * nav menus and items are properly re-added with their newly-assigned IDs.
+ *
+ * @param {object} data
+ * @param {array} data.nav_menu_updates
+ * @param {array} data.nav_menu_item_updates
+ */
+ api.Menus.applySavedData = function( data ) {
+
+ var insertedMenuIdMapping = {};
+
+ _( data.nav_menu_updates ).each(function( update ) {
+ var oldCustomizeId, newCustomizeId, oldSetting, newSetting, settingValue, oldSection, newSection;
+ if ( 'inserted' === update.status ) {
+ if ( ! update.previous_term_id ) {
+ throw new Error( 'Expected previous_term_id' );
+ }
+ if ( ! update.term_id ) {
+ throw new Error( 'Expected term_id' );
+ }
+ oldCustomizeId = 'nav_menu[' + String( update.previous_term_id ) + ']';
+ if ( ! api.has( oldCustomizeId ) ) {
+ throw new Error( 'Expected setting to exist: ' + oldCustomizeId );
+ }
+ oldSetting = api( oldCustomizeId );
+ if ( ! api.section.has( oldCustomizeId ) ) {
+ throw new Error( 'Expected control to exist: ' + oldCustomizeId );
+ }
+ oldSection = api.section( oldCustomizeId );
+
+ settingValue = oldSetting.get();
+ if ( ! settingValue ) {
+ throw new Error( 'Did not expect setting to be empty (deleted).' );
+ }
+ settingValue = _.clone( settingValue );
+
+ insertedMenuIdMapping[ update.previous_term_id ] = update.term_id;
+ newCustomizeId = 'nav_menu[' + String( update.term_id ) + ']';
+ newSetting = api.create( newCustomizeId, newCustomizeId, settingValue, {
+ type: 'nav_menu',
+ transport: 'postMessage',
+ previewer: api.previewer
+ } );
+
+ if ( oldSection.expanded() ) {
+ oldSection.collapse();
+ }
+
+ // Add the menu section.
+ newSection = new api.Menus.MenuSection( newCustomizeId, {
+ params: {
+ id: newCustomizeId,
+ panel: 'nav_menus',
+ title: settingValue.name,
+ customizeAction: api.Menus.data.l10n.customizingMenus,
+ type: 'nav_menu',
+ priority: oldSection.priority.get(),
+ active: true,
+ menu_id: update.term_id
+ }
+ } );
+
+ // Remove old setting and control.
+ oldSection.container.remove();
+ api.section.remove( oldCustomizeId );
+
+ // Add new control to take its place.
+ api.section.add( newCustomizeId, newSection );
+
+ // Delete the placeholder and preview the new setting.
+ oldSetting.callbacks.disable(); // Prevent setting triggering Customizer dirty state when set.
+ oldSetting.set( false );
+ oldSetting.preview();
+ newSetting.preview();
+
+ // Update nav_menu_locations to reference the new ID.
+ api.each( function( setting ) {
+ var wasSaved = api.state( 'saved' ).get();
+ if ( /^nav_menu_locations\[/.test( setting.id ) && setting.get() === update.previous_term_id ) {
+ setting.set( update.term_id );
+ setting._dirty = false; // Not dirty because this is has also just been done on server in WP_Customize_Nav_Menu_Setting::update().
+ api.state( 'saved' ).set( wasSaved );
+ setting.preview();
+ }
+ } );
+
+ if ( oldSection.expanded.get() ) {
+ // @todo This doesn't seem to be working.
+ newSection.expand();
+ }
+
+ // @todo Update the Custom Menu selects, ensuring the newly-inserted IDs are used for any that have selected a placeholder menu.
+ }
+ } );
+
+ _( data.nav_menu_item_updates ).each(function( update ) {
+ var oldCustomizeId, newCustomizeId, oldSetting, newSetting, settingValue, oldControl, newControl;
+ if ( 'inserted' === update.status ) {
+ if ( ! update.previous_post_id ) {
+ throw new Error( 'Expected previous_post_id' );
+ }
+ if ( ! update.post_id ) {
+ throw new Error( 'Expected post_id' );
+ }
+ oldCustomizeId = 'nav_menu_item[' + String( update.previous_post_id ) + ']';
+ if ( ! api.has( oldCustomizeId ) ) {
+ throw new Error( 'Expected setting to exist: ' + oldCustomizeId );
+ }
+ oldSetting = api( oldCustomizeId );
+ if ( ! api.control.has( oldCustomizeId ) ) {
+ throw new Error( 'Expected control to exist: ' + oldCustomizeId );
+ }
+ oldControl = api.control( oldCustomizeId );
+
+ settingValue = oldSetting.get();
+ if ( ! settingValue ) {
+ throw new Error( 'Did not expect setting to be empty (deleted).' );
+ }
+ settingValue = _.clone( settingValue );
+
+ // If the menu was also inserted, then make sure it uses the new menu ID for nav_menu_term_id.
+ if ( insertedMenuIdMapping[ settingValue.nav_menu_term_id ] ) {
+ settingValue.nav_menu_term_id = insertedMenuIdMapping[ settingValue.nav_menu_term_id ];
+ }
+
+ newCustomizeId = 'nav_menu_item[' + String( update.post_id ) + ']';
+ newSetting = api.create( newCustomizeId, newCustomizeId, settingValue, {
+ type: 'nav_menu_item',
+ transport: 'postMessage',
+ previewer: api.previewer
+ } );
+
+ // Add the menu control.
+ newControl = new api.controlConstructor.nav_menu_item( newCustomizeId, {
+ params: {
+ type: 'nav_menu_item',
+ content: '<li id="customize-control-nav_menu_item-' + String( update.post_id ) + '" class="customize-control customize-control-nav_menu_item"></li>',
+ menu_id: update.post_id,
+ section: 'nav_menu[' + String( settingValue.nav_menu_term_id ) + ']',
+ priority: oldControl.priority.get(),
+ active: true,
+ settings: {
+ 'default': newCustomizeId
+ },
+ menu_item_id: update.post_id
+ },
+ previewer: api.previewer
+ } );
+
+ // Remove old setting and control.
+ oldControl.container.remove();
+ api.control.remove( oldCustomizeId );
+
+ // Add new control to take its place.
+ api.control.add( newCustomizeId, newControl );
+
+ // Delete the placeholder and preview the new setting.
+ oldSetting.callbacks.disable(); // Prevent setting triggering Customizer dirty state when set.
+ oldSetting.set( false );
+ oldSetting.preview();
+ newSetting.preview();
+
+ newControl.container.toggleClass( 'menu-item-edit-inactive', oldControl.container.hasClass( 'menu-item-edit-inactive' ) );
+ }
+ });
+
+ // @todo trigger change event for each Custom Menu widget that was modified.
+ };
+
+ /**
+ * Focus a menu item control.
+ *
+ * @param {string} menuItemId
+ */
+ api.Menus.focusMenuItemControl = function( menuItemId ) {
+ var control = api.Menus.getMenuItemControl( menuItemId );
+
+ if ( control ) {
+ control.focus();
+ }
+ };
+
+ /**
+ * Get the control for a given menu.
+ *
+ * @param menuId
+ * @return {wp.customize.controlConstructor.menus[]}
+ */
+ api.Menus.getMenuControl = function( menuId ) {
+ return api.control( 'nav_menu[' + menuId + ']' );
+ };
+
+ /**
+ * Given a menu item type & object, get the label associated with it.
+ *
+ * @param {string} type
+ * @param {string} object
+ * @return {string}
+ */
+ api.Menus.getTypeLabel = function( type, object ) {
+ var label,
+ data = api.Menus.data;
+
+ if ( 'post_type' === type ) {
+ if ( data.itemTypes.postTypes[ object ] ) {
+ label = data.itemTypes.postTypes[ object ].label;
+ } else {
+ label = data.l10n.postTypeLabel;
+ }
+ } else if ( 'taxonomy' === type ) {
+ if ( data.itemTypes.taxonomies[ object ] ) {
+ label = data.itemTypes.taxonomies[ object ].label;
+ } else {
+ label = data.l10n.taxonomyTermLabel;
+ }
+ } else {
+ label = data.l10n.custom_label;
+ }
+
+ return label;
+ };
+
+ /**
+ * Given a menu item ID, get the control associated with it.
+ *
+ * @param {string} menuItemId
+ * @return {object|null}
+ */
+ api.Menus.getMenuItemControl = function( menuItemId ) {
+ return api.control( menuItemIdToSettingId( menuItemId ) );
+ };
+
+ /**
+ * @param {String} menuItemId
+ */
+ function menuItemIdToSettingId( menuItemId ) {
+ return 'nav_menu_item[' + menuItemId + ']';
+ }
+
+})( wp.customize, wp, jQuery );
</ins></span></pre></div>
<a id="trunksrcwpincludesclasswpcustomizecontrolphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/class-wp-customize-control.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/class-wp-customize-control.php 2015-06-16 21:32:53 UTC (rev 32805)
+++ trunk/src/wp-includes/class-wp-customize-control.php 2015-06-16 22:07:08 UTC (rev 32806)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1402,3 +1402,479 @@
</span><span class="cx" style="display: block; padding: 0 10px"> return $this->manager->widgets->is_widget_rendered( $this->widget_id );
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+/**
+ * Customize Nav Menus Panel Class
+ *
+ * Needed to add screen options.
+ *
+ * @since 4.3.0
+ */
+class WP_Customize_Nav_Menus_Panel extends WP_Customize_Panel {
+
+ /**
+ * Control type.
+ *
+ * @since 4.3.0
+ *
+ * @access public
+ * @var string
+ */
+ public $type = 'nav_menus';
+
+ /**
+ * Render screen options for Menus.
+ *
+ * @since 4.3.0
+ */
+ public function render_screen_options() {
+ // Essentially adds the screen options.
+ add_filter( 'manage_nav-menus_columns', array( $this, 'wp_nav_menu_manage_columns' ) );
+
+ // Display screen options.
+ $screen = WP_Screen::get( 'nav-menus.php' );
+ $screen->render_screen_options();
+ }
+
+ /**
+ * Returns the advanced options for the nav menus page.
+ *
+ * Link title attribute added as it's a relatively advanced concept for new users.
+ *
+ * @since 4.3.0
+ *
+ * @return array The advanced menu properties.
+ */
+ function wp_nav_menu_manage_columns() {
+ return array(
+ '_title' => __( 'Show advanced menu properties' ),
+ 'cb' => '<input type="checkbox" />',
+ 'link-target' => __( 'Link Target' ),
+ 'attr-title' => __( 'Title Attribute' ),
+ 'css-classes' => __( 'CSS Classes' ),
+ 'xfn' => __( 'Link Relationship (XFN)' ),
+ 'description' => __( 'Description' ),
+ );
+ }
+
+ /**
+ * An Underscore (JS) template for this panel's content (but not its container).
+ *
+ * Class variables for this panel class are available in the `data` JS object;
+ * export custom variables by overriding {@see WP_Customize_Panel::json()}.
+ *
+ * @since 4.3.0
+ *
+ * @see WP_Customize_Panel::print_template()
+ *
+ * @since 4.3.0
+ */
+ protected function content_template() {
+ ?>
+ <li class="panel-meta customize-info accordion-section <# if ( ! data.description ) { #> cannot-expand<# } #>">
+ <button type="button" class="customize-panel-back" tabindex="-1">
+ <span class="screen-reader-text"><?php _e( 'Back' ); ?></span>
+ </button>
+ <div class="accordion-section-title">
+ <span class="preview-notice">
+ <?php
+ /* translators: %s is the site/panel title in the Customizer */
+ printf( __( 'You are customizing %s' ), '<strong class="panel-title">{{ data.title }}</strong>' );
+ ?>
+ </span>
+ <button type="button" class="customize-screen-options-toggle" aria-expanded="false">
+ <span class="screen-reader-text"><?php _e( 'Menu Options' ); ?></span>
+ </button>
+ <button type="button" class="customize-help-toggle dashicons dashicons-editor-help" aria-expanded="false">
+ <span class="screen-reader-text"><?php _e( 'Help' ); ?></span>
+ </button>
+ </div>
+ <# if ( data.description ) { #>
+ <div class="description customize-panel-description">{{{ data.description }}}</div>
+ <# } #>
+ <?php $this->render_screen_options(); ?>
+ </li>
+ <?php
+ }
+}
+
+/**
+ * Customize Nav Menu Control Class
+ *
+ * @since 4.3.0
+ */
+class WP_Customize_Nav_Menu_Control extends WP_Customize_Control {
+
+ /**
+ * Control type.
+ *
+ * @since 4.3.0
+ *
+ * @access public
+ * @var string
+ */
+ public $type = 'nav_menu';
+
+ /**
+ * The nav menu setting.
+ *
+ * @since 4.3.0
+ *
+ * @var WP_Customize_Nav_Menu_Setting
+ */
+ public $setting;
+
+ /**
+ * Don't render the control's content - it uses a JS template instead.
+ *
+ * @since 4.3.0
+ */
+ public function render_content() {}
+
+ /**
+ * JS/Underscore template for the control UI.
+ *
+ * @since 4.3.0
+ */
+ public function content_template() {
+ ?>
+ <button type="button" class="button-secondary add-new-menu-item">
+ <?php _e( 'Add Items' ); ?>
+ </button>
+ <button type="button" class="not-a-button reorder-toggle">
+ <span class="reorder"><?php _ex( 'Reorder', 'Reorder menu items in Customizer' ); ?></span>
+ <span class="reorder-done"><?php _ex( 'Done', 'Cancel reordering menu items in Customizer' ); ?></span>
+ </button>
+ <span class="add-menu-item-loading spinner"></span>
+ <span class="menu-delete-item">
+ <button type="button" class="not-a-button menu-delete">
+ <?php _e( 'Delete menu' ); ?> <span class="screen-reader-text">{{ data.menu_name }}</span>
+ </button>
+ </span>
+ <?php if ( current_theme_supports( 'menus' ) ) : ?>
+ <ul class="menu-settings">
+ <li class="customize-control">
+ <span class="customize-control-title"><?php _e( 'Menu locations' ); ?></span>
+ </li>
+
+ <?php foreach ( get_registered_nav_menus() as $location => $description ) : ?>
+ <li class="customize-control customize-control-checkbox assigned-menu-location">
+ <label>
+ <input type="checkbox" data-menu-id="{{ data.menu_id }}" data-location-id="<?php echo esc_attr( $location ); ?>" class="menu-location" /> <?php echo $description; ?>
+ <span class="theme-location-set"><?php printf( _x( '(Current: %s)', 'Current menu location' ), '<span class="current-menu-location-name-' . esc_attr( $location ) . '"></span>' ); ?></span>
+ </label>
+ </li>
+ <?php endforeach; ?>
+
+ </ul>
+ <?php endif; ?>
+ <p>
+ <label>
+ <input type="checkbox" class="auto_add">
+ <?php _e( 'Automatically add new top-level pages to this menu.' ) ?>
+ </label>
+ </p>
+ <?php
+ }
+
+ /**
+ * Return params for this control.
+ *
+ * @since 4.3.0
+ *
+ * @return array
+ */
+ function json() {
+ $exported = parent::json();
+ $exported['menu_id'] = $this->setting->term_id;
+
+ return $exported;
+ }
+}
+
+/**
+ * Customize control to represent the name field for a given menu.
+ *
+ * @since 4.3.0
+ */
+class WP_Customize_Nav_Menu_Item_Control extends WP_Customize_Control {
+
+ /**
+ * Control type.
+ *
+ * @since 4.3.0
+ *
+ * @access public
+ * @var string
+ */
+ public $type = 'nav_menu_item';
+
+ /**
+ * The nav menu item setting.
+ *
+ * @since 4.3.0
+ *
+ * @var WP_Customize_Nav_Menu_Item_Setting
+ */
+ public $setting;
+
+ /**
+ * Constructor.
+ *
+ * @since 4.3.0
+ *
+ * @uses WP_Customize_Control::__construct()
+ *
+ * @param WP_Customize_Manager $manager An instance of the WP_Customize_Manager class.
+ * @param string $id The control ID.
+ * @param array $args Optional. Overrides class property defaults.
+ */
+ public function __construct( $manager, $id, $args = array() ) {
+ parent::__construct( $manager, $id, $args );
+ }
+
+ /**
+ * Don't render the control's content - it's rendered with a JS template.
+ *
+ * @since 4.3.0
+ */
+ public function render_content() {}
+
+ /**
+ * JS/Underscore template for the control UI.
+ *
+ * @since 4.3.0
+ */
+ public function content_template() {
+ ?>
+ <dl class="menu-item-bar">
+ <dt class="menu-item-handle">
+ <span class="item-type">{{ data.item_type_label }}</span>
+ <span class="item-title">
+ <span class="spinner"></span>
+ <span class="menu-item-title">{{ data.title }}</span>
+ </span>
+ <span class="item-controls">
+ <button type="button" class="not-a-button item-edit"><span class="screen-reader-text"><?php _e( 'Edit Menu Item' ); ?></span></button>
+ <button type="button" class="not-a-button item-delete submitdelete deletion"><span class="screen-reader-text"><?php _e( 'Remove Menu Item' ); ?></span></button>
+ </span>
+ </dt>
+ </dl>
+
+ <div class="menu-item-settings" id="menu-item-settings-{{ data.menu_item_id }}">
+ <# if ( 'custom' === data.item_type ) { #>
+ <p class="field-url description description-thin">
+ <label for="edit-menu-item-url-{{ data.menu_item_id }}">
+ <?php _e( 'URL' ); ?><br />
+ <input class="widefat code edit-menu-item-url" type="text" id="edit-menu-item-url-{{ data.menu_item_id }}" name="menu-item-url" />
+ </label>
+ </p>
+ <# } #>
+ <p class="description description-thin">
+ <label for="edit-menu-item-title-{{ data.menu_item_id }}">
+ <?php _e( 'Navigation Label' ); ?><br />
+ <input type="text" id="edit-menu-item-title-{{ data.menu_item_id }}" class="widefat edit-menu-item-title" name="menu-item-title" />
+ </label>
+ </p>
+ <p class="field-link-target description description-thin">
+ <label for="edit-menu-item-target-{{ data.menu_item_id }}">
+ <input type="checkbox" id="edit-menu-item-target-{{ data.menu_item_id }}" class="edit-menu-item-target" value="_blank" name="menu-item-target" />
+ <?php _e( 'Open link in a new tab' ); ?>
+ </label>
+ </p>
+ <p class="field-attr-title description description-thin">
+ <label for="edit-menu-item-attr-title-{{ data.menu_item_id }}">
+ <?php _e( 'Title Attribute' ); ?><br />
+ <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" />
+ </label>
+ </p>
+ <p class="field-css-classes description description-thin">
+ <label for="edit-menu-item-classes-{{ data.menu_item_id }}">
+ <?php _e( 'CSS Classes' ); ?><br />
+ <input type="text" id="edit-menu-item-classes-{{ data.menu_item_id }}" class="widefat code edit-menu-item-classes" name="menu-item-classes" />
+ </label>
+ </p>
+ <p class="field-xfn description description-thin">
+ <label for="edit-menu-item-xfn-{{ data.menu_item_id }}">
+ <?php _e( 'Link Relationship (XFN)' ); ?><br />
+ <input type="text" id="edit-menu-item-xfn-{{ data.menu_item_id }}" class="widefat code edit-menu-item-xfn" name="menu-item-xfn" />
+ </label>
+ </p>
+ <p class="field-description description description-thin">
+ <label for="edit-menu-item-description-{{ data.menu_item_id }}">
+ <?php _e( 'Description' ); ?><br />
+ <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>
+ <span class="description"><?php _e( 'The description will be displayed in the menu if the current theme supports it.' ); ?></span>
+ </label>
+ </p>
+
+ <div class="menu-item-actions description-thin submitbox">
+ <# if ( 'custom' != data.item_type && '' != data.original_title ) { #>
+ <p class="link-to-original">
+ <?php printf( __( 'Original: %s' ), '<a class="original-link" href="{{ data.url }}">{{{ data.original_title }}}</a>' ); ?>
+ </p>
+ <# } #>
+
+ <button type="button" class="not-a-button item-delete submitdelete deletion"><?php _e( 'Remove' ); ?></button>
+ <span class="spinner"></span>
+ </div>
+ <input type="hidden" name="menu-item-db-id[{{ data.menu_item_id }}]" class="menu-item-data-db-id" value="{{ data.menu_item_id }}" />
+ <input type="hidden" name="menu-item-parent-id[{{ data.menu_item_id }}]" class="menu-item-data-parent-id" value="{{ data.parent }}" />
+ </div><!-- .menu-item-settings-->
+ <ul class="menu-item-transport"></ul>
+ <?php
+ }
+
+ /**
+ * Return params for this control.
+ *
+ * @since 4.3.0
+ *
+ * @return array
+ */
+ function json() {
+ $exported = parent::json();
+ $exported['menu_item_id'] = $this->setting->post_id;
+
+ return $exported;
+ }
+}
+
+/**
+ * Customize Menu Location Control Class
+ *
+ * This custom control is only needed for JS.
+ *
+ * @since 4.3.0
+ */
+class WP_Customize_Nav_Menu_Location_Control extends WP_Customize_Control {
+
+ /**
+ * Control type.
+ *
+ * @since 4.3.0
+ *
+ * @access public
+ * @var string
+ */
+ public $type = 'nav_menu_location';
+
+ /**
+ * Location ID.
+ *
+ * @since 4.3.0
+ *
+ * @access public
+ * @var string
+ */
+ public $location_id = '';
+
+ /**
+ * Refresh the parameters passed to JavaScript via JSON.
+ *
+ * @since 4.3.0
+ *
+ * @uses WP_Customize_Control::to_json()
+ */
+ public function to_json() {
+ parent::to_json();
+ $this->json['locationId'] = $this->location_id;
+ }
+
+ /**
+ * Render content just like a normal select control.
+ *
+ * @since 4.3.0
+ */
+ public function render_content() {
+ if ( empty( $this->choices ) ) {
+ return;
+ }
+ ?>
+ <label>
+ <?php if ( ! empty( $this->label ) ) : ?>
+ <span class="customize-control-title"><?php echo esc_html( $this->label ); ?></span>
+ <?php endif; ?>
+
+ <?php if ( ! empty( $this->description ) ) : ?>
+ <span class="description customize-control-description"><?php echo $this->description; ?></span>
+ <?php endif; ?>
+
+ <select <?php $this->link(); ?>>
+ <?php
+ foreach ( $this->choices as $value => $label ) :
+ echo '<option value="' . esc_attr( $value ) . '"' . selected( $this->value(), $value, false ) . '>' . $label . '</option>';
+ endforeach;
+ ?>
+ </select>
+ </label>
+ <?php
+ }
+}
+
+/**
+ * Customize control to represent the name field for a given menu.
+ *
+ * @since 4.3.0
+ */
+class WP_Customize_Nav_Menu_Name_Control extends WP_Customize_Control {
+
+ /**
+ * Type of control, used by JS.
+ *
+ * @since 4.3.0
+ *
+ * @var string
+ */
+ public $type = 'nav_menu_name';
+
+ /**
+ * No-op since we're using JS template.
+ *
+ * @since 4.3.0
+ */
+ protected function render_content() {}
+
+ /**
+ * Render the Underscore template for this control.
+ *
+ * @since 4.3.0
+ */
+ protected function content_template() {
+ ?>
+ <label>
+ <input type="text" class="menu-name-field live-update-section-title" />
+ </label>
+ <?php
+ }
+}
+
+/**
+ * Customize control class for new menus.
+ *
+ * @since 4.3.0
+ */
+class WP_New_Menu_Customize_Control extends WP_Customize_Control {
+
+ /**
+ * Control type.
+ *
+ * @since 4.3.0
+ *
+ * @access public
+ * @var string
+ */
+ public $type = 'new_menu';
+
+ /**
+ * Render the control's content.
+ *
+ * @since 4.3.0
+ */
+ public function render_content() {
+ ?>
+ <button type="button" class="button button-primary" id="create-new-menu-submit"><?php _e( 'Create Menu' ); ?></button>
+ <span class="spinner"></span>
+ <?php
+ }
+}
</ins></span></pre></div>
<a id="trunksrcwpincludesclasswpcustomizemanagerphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/class-wp-customize-manager.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/class-wp-customize-manager.php 2015-06-16 21:32:53 UTC (rev 32805)
+++ trunk/src/wp-includes/class-wp-customize-manager.php 2015-06-16 22:07:08 UTC (rev 32806)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -49,6 +49,13 @@
</span><span class="cx" style="display: block; padding: 0 10px"> */
</span><span class="cx" style="display: block; padding: 0 10px"> public $widgets;
</span><span class="cx" style="display: block; padding: 0 10px">
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ /**
+ * Methods and properties deailing with managing nav menus in the Customizer.
+ *
+ * @var WP_Customize_Nav_Menus
+ */
+ public $nav_menus;
+
</ins><span class="cx" style="display: block; padding: 0 10px"> protected $settings = array();
</span><span class="cx" style="display: block; padding: 0 10px"> protected $containers = array();
</span><span class="cx" style="display: block; padding: 0 10px"> protected $panels = array();
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -104,8 +111,10 @@
</span><span class="cx" style="display: block; padding: 0 10px"> require_once( ABSPATH . WPINC . '/class-wp-customize-section.php' );
</span><span class="cx" style="display: block; padding: 0 10px"> require_once( ABSPATH . WPINC . '/class-wp-customize-control.php' );
</span><span class="cx" style="display: block; padding: 0 10px"> require_once( ABSPATH . WPINC . '/class-wp-customize-widgets.php' );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ require_once( ABSPATH . WPINC . '/class-wp-customize-nav-menus.php' );
</ins><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> $this->widgets = new WP_Customize_Widgets( $this );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $this->nav_menus = new WP_Customize_Nav_Menus( $this );
</ins><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> add_filter( 'wp_die_handler', array( $this, 'wp_die_handler' ) );
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1484,48 +1493,6 @@
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- /* Nav Menus */
-
- $locations = get_registered_nav_menus();
- $menus = wp_get_nav_menus();
- $num_locations = count( array_keys( $locations ) );
-
- if ( 1 == $num_locations ) {
- $description = __( 'Your theme supports one menu. Select which menu you would like to use.' );
- } else {
- $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 ) );
- }
-
- $this->add_section( 'nav', array(
- 'title' => __( 'Navigation' ),
- 'theme_supports' => 'menus',
- 'priority' => 100,
- 'description' => $description . "\n\n" . __( 'You can edit your menu content on the Menus screen in the Appearance section.' ),
- ) );
-
- if ( $menus ) {
- $choices = array( '' => __( '— Select —' ) );
- foreach ( $menus as $menu ) {
- $choices[ $menu->term_id ] = wp_html_excerpt( $menu->name, 40, '…' );
- }
-
- foreach ( $locations as $location => $description ) {
- $menu_setting_id = "nav_menu_locations[{$location}]";
-
- $this->add_setting( $menu_setting_id, array(
- 'sanitize_callback' => 'absint',
- 'theme_supports' => 'menus',
- ) );
-
- $this->add_control( $menu_setting_id, array(
- 'label' => $description,
- 'section' => 'nav',
- 'type' => 'select',
- 'choices' => $choices,
- ) );
- }
- }
-
</del><span class="cx" style="display: block; padding: 0 10px"> /* Static Front Page */
</span><span class="cx" style="display: block; padding: 0 10px"> // #WP19627
</span><span class="cx" style="display: block; padding: 0 10px">
</span></span></pre></div>
<a id="trunksrcwpincludesclasswpcustomizenavmenusphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/src/wp-includes/class-wp-customize-nav-menus.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/class-wp-customize-nav-menus.php (rev 0)
+++ trunk/src/wp-includes/class-wp-customize-nav-menus.php 2015-06-16 22:07:08 UTC (rev 32806)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,882 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * WordPress Customize Nav Menus classes
+ *
+ * @package WordPress
+ * @subpackage Customize
+ * @since 4.3.0
+ */
+
+/**
+ * Customize Nav Menus class.
+ *
+ * Implements menu management in the Customizer.
+ *
+ * @since 4.3.0
+ *
+ * @see WP_Customize_Manager
+ */
+final class WP_Customize_Nav_Menus {
+
+ /**
+ * WP_Customize_Manager instance.
+ *
+ * @since 4.3.0
+ *
+ * @access public
+ * @var WP_Customize_Manager
+ */
+ public $manager;
+
+ /**
+ * Previewed Menus.
+ *
+ * @since 4.3.0
+ *
+ * @access public
+ * @var array
+ */
+ public $previewed_menus;
+
+ /**
+ * Constructor.
+ *
+ * @since 4.3.0
+ *
+ * @access public
+ * @param object $manager An instance of the WP_Customize_Manager class.
+ */
+ public function __construct( $manager ) {
+ $this->previewed_menus = array();
+ $this->manager = $manager;
+
+ add_action( 'wp_ajax_load-available-menu-items-customizer', array( $this, 'ajax_load_available_items' ) );
+ add_action( 'wp_ajax_search-available-menu-items-customizer', array( $this, 'ajax_search_available_items' ) );
+ add_action( 'customize_controls_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
+ add_action( 'customize_register', array( $this, 'customize_register' ), 11 ); // Needs to run after core Navigation section is set up.
+ add_filter( 'customize_dynamic_setting_args', array( $this, 'filter_dynamic_setting_args' ), 10, 2 );
+ add_filter( 'customize_dynamic_setting_class', array( $this, 'filter_dynamic_setting_class' ), 10, 3 );
+ add_action( 'customize_controls_print_footer_scripts', array( $this, 'print_templates' ) );
+ add_action( 'customize_controls_print_footer_scripts', array( $this, 'available_items_template' ) );
+ add_action( 'customize_preview_init', array( $this, 'customize_preview_init' ) );
+ }
+
+ /**
+ * Ajax handler for loading available menu items.
+ *
+ * @since 4.3.0
+ */
+ public function ajax_load_available_items() {
+ check_ajax_referer( 'customize-menus', 'customize-menus-nonce' );
+
+ if ( ! current_user_can( 'edit_theme_options' ) ) {
+ wp_send_json_error( array( 'message' => __( 'Error: invalid user capabilities.' ) ) );
+ }
+ if ( empty( $_POST['obj_type'] ) || empty( $_POST['type'] ) ) {
+ wp_send_json_error( array( 'message' => __( 'Missing obj_type or type param.' ) ) );
+ }
+
+ $obj_type = sanitize_key( $_POST['obj_type'] );
+ if ( ! in_array( $obj_type, array( 'post_type', 'taxonomy' ) ) ) {
+ wp_send_json_error( array( 'message' => __( 'Invalid obj_type param: ' . $obj_type ) ) );
+ }
+ $taxonomy_or_post_type = sanitize_key( $_POST['type'] );
+ $page = isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 0;
+ $items = array();
+
+ if ( 'post_type' === $obj_type ) {
+ if ( ! get_post_type_object( $taxonomy_or_post_type ) ) {
+ wp_send_json_error( array( 'message' => __( 'Unknown post type.' ) ) );
+ }
+
+ if ( 0 === $page && 'page' === $taxonomy_or_post_type ) {
+ // Add "Home" link. Treat as a page, but switch to custom on add.
+ $items[] = array(
+ 'id' => 'home',
+ 'title' => _x( 'Home', 'nav menu home label' ),
+ 'type' => 'custom',
+ 'type_label' => __( 'Custom Link' ),
+ 'object' => '',
+ 'url' => home_url(),
+ );
+ }
+
+ $posts = get_posts( array(
+ 'numberposts' => 10,
+ 'offset' => 10 * $page,
+ 'orderby' => 'date',
+ 'order' => 'DESC',
+ 'post_type' => $taxonomy_or_post_type,
+ ) );
+ foreach ( $posts as $post ) {
+ $items[] = array(
+ 'id' => "post-{$post->ID}",
+ 'title' => html_entity_decode( get_the_title( $post ) ),
+ 'type' => 'post_type',
+ 'type_label' => get_post_type_object( $post->post_type )->labels->singular_name,
+ 'object' => $post->post_type,
+ 'object_id' => (int) $post->ID,
+ );
+ }
+ } else if ( 'taxonomy' === $obj_type ) {
+ $terms = get_terms( $taxonomy_or_post_type, array(
+ 'child_of' => 0,
+ 'exclude' => '',
+ 'hide_empty' => false,
+ 'hierarchical' => 1,
+ 'include' => '',
+ 'number' => 10,
+ 'offset' => 10 * $page,
+ 'order' => 'DESC',
+ 'orderby' => 'count',
+ 'pad_counts' => false,
+ ) );
+ if ( is_wp_error( $terms ) ) {
+ wp_send_json_error( array( 'message' => wp_strip_all_tags( $terms->get_error_message(), true ) ) );
+ }
+
+ foreach ( $terms as $term ) {
+ $items[] = array(
+ 'id' => "term-{$term->term_id}",
+ 'title' => html_entity_decode( $term->name ),
+ 'type' => 'taxonomy',
+ 'type_label' => get_taxonomy( $term->taxonomy )->labels->singular_name,
+ 'object' => $term->taxonomy,
+ 'object_id' => $term->term_id,
+ );
+ }
+ }
+
+ wp_send_json_success( array( 'items' => $items ) );
+ }
+
+ /**
+ * Ajax handler for searching available menu items.
+ *
+ * @since 4.3.0
+ */
+ public function ajax_search_available_items() {
+ check_ajax_referer( 'customize-menus', 'customize-menus-nonce' );
+
+ if ( ! current_user_can( 'edit_theme_options' ) ) {
+ wp_send_json_error( array( 'message' => __( 'Error: invalid user capabilities.' ) ) );
+ }
+ if ( empty( $_POST['search'] ) ) {
+ wp_send_json_error( array( 'message' => __( 'Error: missing search parameter.' ) ) );
+ }
+
+ $p = isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 0;
+ if ( $p < 1 ) {
+ $p = 1;
+ }
+
+ $s = sanitize_text_field( wp_unslash( $_POST['search'] ) );
+ $results = $this->search_available_items_query( array( 'pagenum' => $p, 's' => $s ) );
+
+ if ( empty( $results ) ) {
+ wp_send_json_error( array( 'message' => __( 'No results found.' ) ) );
+ } else {
+ wp_send_json_success( array( 'items' => $results ) );
+ }
+ }
+
+ /**
+ * Performs post queries for available-item searching.
+ *
+ * Based on WP_Editor::wp_link_query().
+ *
+ * @since 4.3.0
+ *
+ * @param array $args Optional. Accepts 'pagenum' and 's' (search) arguments.
+ * @return array Results.
+ */
+ public function search_available_items_query( $args = array() ) {
+ $results = array();
+
+ $post_type_objects = get_post_types( array( 'show_in_nav_menus' => true ), 'objects' );
+ $query = array(
+ 'post_type' => array_keys( $post_type_objects ),
+ 'suppress_filters' => true,
+ 'update_post_term_cache' => false,
+ 'update_post_meta_cache' => false,
+ 'post_status' => 'publish',
+ 'posts_per_page' => 20,
+ );
+
+ $args['pagenum'] = isset( $args['pagenum'] ) ? absint( $args['pagenum'] ) : 1;
+ $query['offset'] = $args['pagenum'] > 1 ? $query['posts_per_page'] * ( $args['pagenum'] - 1 ) : 0;
+
+ if ( isset( $args['s'] ) ) {
+ $query['s'] = $args['s'];
+ }
+
+ // Query posts.
+ $get_posts = new WP_Query( $query );
+
+ // Check if any posts were found.
+ if ( $get_posts->post_count ) {
+ foreach ( $get_posts->posts as $post ) {
+ $results[] = array(
+ 'id' => 'post-' . $post->ID,
+ 'type' => 'post_type',
+ 'type_label' => $post_type_objects[ $post->post_type ]->labels->singular_name,
+ 'object' => $post->post_type,
+ 'object_id' => intval( $post->ID ),
+ 'title' => html_entity_decode( get_the_title( $post ) ),
+ );
+ }
+ }
+
+ // Query taxonomy terms.
+ $taxonomies = get_taxonomies( array( 'show_in_nav_menus' => true ), 'names' );
+ $terms = get_terms( $taxonomies, array(
+ 'name__like' => $args['s'],
+ 'number' => 20,
+ 'offset' => 20 * ($args['pagenum'] - 1),
+ ) );
+
+ // Check if any taxonomies were found.
+ if ( ! empty( $terms ) ) {
+ foreach ( $terms as $term ) {
+ $results[] = array(
+ 'id' => 'term-' . $term->term_id,
+ 'type' => 'taxonomy',
+ 'type_label' => get_taxonomy( $term->taxonomy )->labels->singular_name,
+ 'object' => $term->taxonomy,
+ 'object_id' => intval( $term->term_id ),
+ 'title' => html_entity_decode( $term->name ),
+ );
+ }
+ }
+
+ return $results;
+ }
+
+ /**
+ * Enqueue scripts and styles for Customizer pane.
+ *
+ * @since 4.3.0
+ */
+ public function enqueue_scripts() {
+ wp_enqueue_style( 'customize-nav-menus' );
+ wp_enqueue_script( 'customize-nav-menus' );
+
+ $temp_nav_menu_setting = new WP_Customize_Nav_Menu_Setting( $this->manager, 'nav_menu[-1]' );
+ $temp_nav_menu_item_setting = new WP_Customize_Nav_Menu_Item_Setting( $this->manager, 'nav_menu_item[-1]' );
+
+ // Pass data to JS.
+ $settings = array(
+ 'nonce' => wp_create_nonce( 'customize-menus' ),
+ 'allMenus' => wp_get_nav_menus(),
+ 'itemTypes' => $this->available_item_types(),
+ 'l10n' => array(
+ 'untitled' => _x( '(no label)', 'Missing menu item navigation label.' ),
+ 'custom_label' => _x( 'Custom', 'Custom menu item type label.' ),
+ 'menuLocation' => _x( '(Currently set to: %s)', 'Current menu location.' ),
+ 'deleteWarn' => __( 'You are about to permanently delete this menu. "Cancel" to stop, "OK" to delete.' ),
+ 'itemAdded' => __( 'Menu item added' ),
+ 'itemDeleted' => __( 'Menu item deleted' ),
+ 'menuAdded' => __( 'Menu created' ),
+ 'menuDeleted' => __( 'Menu deleted' ),
+ 'movedUp' => __( 'Menu item moved up' ),
+ 'movedDown' => __( 'Menu item moved down' ),
+ 'movedLeft' => __( 'Menu item moved out of submenu' ),
+ 'movedRight' => __( 'Menu item is now a sub-item' ),
+ 'customizingMenus' => _x( 'Customizing ▸ Menus', '▸ is the unicode right-pointing triangle' ),
+ 'invalidTitleTpl' => __( '%s (Invalid)' ),
+ 'pendingTitleTpl' => __( '%s (Pending)' ),
+ 'taxonomyTermLabel' => __( 'Taxonomy' ),
+ 'postTypeLabel' => __( 'Post Type' ),
+ ),
+ 'menuItemTransport' => 'postMessage',
+ 'phpIntMax' => PHP_INT_MAX,
+ 'defaultSettingValues' => array(
+ 'nav_menu' => $temp_nav_menu_setting->default,
+ 'nav_menu_item' => $temp_nav_menu_item_setting->default,
+ ),
+ );
+
+ $data = sprintf( 'var _wpCustomizeNavMenusSettings = %s;', wp_json_encode( $settings ) );
+ wp_scripts()->add_data( 'customize-nav-menus', 'data', $data );
+
+ // This is copied from nav-menus.php, and it has an unfortunate object name of `menus`.
+ $nav_menus_l10n = array(
+ 'oneThemeLocationNoMenus' => null,
+ 'moveUp' => __( 'Move up one' ),
+ 'moveDown' => __( 'Move down one' ),
+ 'moveToTop' => __( 'Move to the top' ),
+ /* translators: %s: previous item name */
+ 'moveUnder' => __( 'Move under %s' ),
+ /* translators: %s: previous item name */
+ 'moveOutFrom' => __( 'Move out from under %s' ),
+ /* translators: %s: previous item name */
+ 'under' => __( 'Under %s' ),
+ /* translators: %s: previous item name */
+ 'outFrom' => __( 'Out from under %s' ),
+ /* translators: 1: item name, 2: item position, 3: total number of items */
+ 'menuFocus' => __( '%1$s. Menu item %2$d of %3$d.' ),
+ /* translators: 1: item name, 2: item position, 3: parent item name */
+ 'subMenuFocus' => __( '%1$s. Sub item number %2$d under %3$s.' ),
+ );
+ wp_localize_script( 'nav-menu', 'menus', $nav_menus_l10n );
+ }
+
+ /**
+ * Filter a dynamic setting's constructor args.
+ *
+ * For a dynamic setting to be registered, this filter must be employed
+ * to override the default false value with an array of args to pass to
+ * the WP_Customize_Setting constructor.
+ *
+ * @since 4.3.0
+ *
+ * @param false|array $setting_args The arguments to the WP_Customize_Setting constructor.
+ * @param string $setting_id ID for dynamic setting, usually coming from `$_POST['customized']`.
+ * @return array|false
+ */
+ public function filter_dynamic_setting_args( $setting_args, $setting_id ) {
+ if ( preg_match( WP_Customize_Nav_Menu_Setting::ID_PATTERN, $setting_id ) ) {
+ $setting_args = array(
+ 'type' => WP_Customize_Nav_Menu_Setting::TYPE,
+ );
+ } else if ( preg_match( WP_Customize_Nav_Menu_Item_Setting::ID_PATTERN, $setting_id ) ) {
+ $setting_args = array(
+ 'type' => WP_Customize_Nav_Menu_Item_Setting::TYPE,
+ );
+ }
+ return $setting_args;
+ }
+
+ /**
+ * Allow non-statically created settings to be constructed with custom WP_Customize_Setting subclass.
+ *
+ * @since 4.3.0
+ *
+ * @param string $setting_class WP_Customize_Setting or a subclass.
+ * @param string $setting_id ID for dynamic setting, usually coming from `$_POST['customized']`.
+ * @param array $setting_args WP_Customize_Setting or a subclass.
+ * @return string
+ */
+ public function filter_dynamic_setting_class( $setting_class, $setting_id, $setting_args ) {
+ unset( $setting_id );
+
+ if ( ! empty( $setting_args['type'] ) && WP_Customize_Nav_Menu_Setting::TYPE === $setting_args['type'] ) {
+ $setting_class = 'WP_Customize_Nav_Menu_Setting';
+ } else if ( ! empty( $setting_args['type'] ) && WP_Customize_Nav_Menu_Item_Setting::TYPE === $setting_args['type'] ) {
+ $setting_class = 'WP_Customize_Nav_Menu_Item_Setting';
+ }
+ return $setting_class;
+ }
+
+ /**
+ * Add the customizer settings and controls.
+ *
+ * @since 4.3.0
+ */
+ public function customize_register() {
+
+ // Require JS-rendered control types.
+ $this->manager->register_panel_type( 'WP_Customize_Nav_Menus_Panel' );
+ $this->manager->register_control_type( 'WP_Customize_Nav_Menu_Control' );
+ $this->manager->register_control_type( 'WP_Customize_Nav_Menu_Name_Control' );
+ $this->manager->register_control_type( 'WP_Customize_Nav_Menu_Item_Control' );
+
+ // Create a panel for Menus.
+ $this->manager->add_panel( new WP_Customize_Nav_Menus_Panel( $this->manager, 'nav_menus', array(
+ 'title' => __( 'Menus' ),
+ '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>',
+ 'priority' => 100,
+ // 'theme_supports' => 'menus|widgets', @todo allow multiple theme supports
+ ) ) );
+ $menus = wp_get_nav_menus();
+
+ // Menu loactions.
+ $locations = get_registered_nav_menus();
+ $num_locations = count( array_keys( $locations ) );
+ $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 ) );
+ $description .= '</p><p>' . __( 'You can also place menus in widget areas with the Custom Menu widget.' ) . '</p>';
+
+ $this->manager->add_section( 'menu_locations', array(
+ 'title' => __( 'Menu Locations' ),
+ 'panel' => 'nav_menus',
+ 'priority' => 5,
+ 'description' => $description,
+ ) );
+
+ // @todo if ( ! $menus ) : make a "default" menu
+ if ( $menus ) {
+ $choices = array( '0' => __( '— Select —' ) );
+ foreach ( $menus as $menu ) {
+ $choices[ $menu->term_id ] = wp_html_excerpt( $menu->name, 40, '…' );
+ }
+
+ foreach ( $locations as $location => $description ) {
+ $setting_id = "nav_menu_locations[{$location}]";
+
+ $setting = $this->manager->get_setting( $setting_id );
+ if ( $setting ) {
+ $setting->transport = 'postMessage';
+ remove_filter( "customize_sanitize_{$setting_id}", 'absint' );
+ add_filter( "customize_sanitize_{$setting_id}", array( $this, 'intval_base10' ) );
+ } else {
+ $this->manager->add_setting( $setting_id, array(
+ 'sanitize_callback' => array( $this, 'intval_base10' ),
+ 'theme_supports' => 'menus',
+ 'type' => 'theme_mod',
+ 'transport' => 'postMessage',
+ ) );
+ }
+
+ $this->manager->add_control( new WP_Customize_Nav_Menu_Location_Control( $this->manager, $setting_id, array(
+ 'label' => $description,
+ 'location_id' => $location,
+ 'section' => 'menu_locations',
+ 'choices' => $choices,
+ ) ) );
+ }
+ }
+
+ // Register each menu as a Customizer section, and add each menu item to each menu.
+ foreach ( $menus as $menu ) {
+ $menu_id = $menu->term_id;
+
+ // Create a section for each menu.
+ $section_id = 'nav_menu[' . $menu_id . ']';
+ $this->manager->add_section( new WP_Customize_Nav_Menu_Section( $this->manager, $section_id, array(
+ 'title' => html_entity_decode( $menu->name ),
+ 'priority' => 10,
+ 'panel' => 'nav_menus',
+ ) ) );
+
+ $nav_menu_setting_id = 'nav_menu[' . $menu_id . ']';
+ $this->manager->add_setting( new WP_Customize_Nav_Menu_Setting( $this->manager, $nav_menu_setting_id ) );
+
+ // Add the menu contents.
+ $menu_items = (array) wp_get_nav_menu_items( $menu_id );
+
+ foreach ( array_values( $menu_items ) as $i => $item ) {
+
+ // Create a setting for each menu item (which doesn't actually manage data, currently).
+ $menu_item_setting_id = 'nav_menu_item[' . $item->ID . ']';
+ $this->manager->add_setting( new WP_Customize_Nav_Menu_Item_Setting( $this->manager, $menu_item_setting_id ) );
+
+ // Create a control for each menu item.
+ $this->manager->add_control( new WP_Customize_Nav_Menu_Item_Control( $this->manager, $menu_item_setting_id, array(
+ 'label' => $item->title,
+ 'section' => $section_id,
+ 'priority' => 10 + $i,
+ ) ) );
+ }
+
+ // Note: other controls inside of this section get added dynamically in JS via the MenuSection.ready() function.
+ }
+
+ // Add the add-new-menu section and controls.
+ $this->manager->add_section( new WP_Customize_New_Menu_Section( $this->manager, 'add_menu', array(
+ 'title' => __( 'Add a Menu' ),
+ 'panel' => 'nav_menus',
+ 'priority' => 999,
+ ) ) );
+
+ $this->manager->add_setting( 'new_menu_name', array(
+ 'type' => 'new_menu',
+ 'default' => '',
+ 'transport' => 'postMessage',
+ ) );
+
+ $this->manager->add_control( 'new_menu_name', array(
+ 'label' => '',
+ 'section' => 'add_menu',
+ 'type' => 'text',
+ 'input_attrs' => array(
+ 'class' => 'menu-name-field',
+ 'placeholder' => __( 'New menu name' ),
+ ),
+ ) );
+
+ $this->manager->add_setting( 'create_new_menu', array(
+ 'type' => 'new_menu',
+ ) );
+
+ $this->manager->add_control( new WP_New_Menu_Customize_Control( $this->manager, 'create_new_menu', array(
+ 'section' => 'add_menu',
+ ) ) );
+ }
+
+ /**
+ * Get the base10 intval.
+ *
+ * This is used as a setting's sanitize_callback; we can't use just plain
+ * intval because the second argument is not what intval() expects.
+ *
+ * @since 4.3.0
+ *
+ * @param mixed $value Number to convert.
+ *
+ * @return int
+ */
+ function intval_base10( $value ) {
+ return intval( $value, 10 );
+ }
+
+ /**
+ * Return an array of all the available item types.
+ *
+ * @since 4.3.0
+ */
+ public function available_item_types() {
+ $items = array(
+ 'postTypes' => array(),
+ 'taxonomies' => array(),
+ );
+
+ $post_types = get_post_types( array( 'show_in_nav_menus' => true ), 'objects' );
+ foreach ( $post_types as $slug => $post_type ) {
+ $items['postTypes'][ $slug ] = array(
+ 'label' => $post_type->labels->singular_name,
+ );
+ }
+
+ $taxonomies = get_taxonomies( array( 'show_in_nav_menus' => true ), 'objects' );
+ foreach ( $taxonomies as $slug => $taxonomy ) {
+ if ( 'post_format' === $taxonomy && ! current_theme_supports( 'post-formats' ) ) {
+ continue;
+ }
+ $items['taxonomies'][ $slug ] = array(
+ 'label' => $taxonomy->labels->singular_name,
+ );
+ }
+ return $items;
+ }
+
+ /**
+ * Print the JavaScript templates used to render Menu Customizer components.
+ *
+ * Templates are imported into the JS use wp.template.
+ *
+ * @since 4.3.0
+ */
+ public function print_templates() {
+ ?>
+ <script type="text/html" id="tmpl-available-menu-item">
+ <div id="menu-item-tpl-{{ data.id }}" class="menu-item-tpl" data-menu-item-id="{{ data.id }}">
+ <dl class="menu-item-bar">
+ <dt class="menu-item-handle">
+ <span class="item-type">{{ data.type_label }}</span>
+ <span class="item-title">{{ data.title || wp.customize.Menus.data.l10n.untitled }}</span>
+ <button type="button" class="not-a-button item-add"><span class="screen-reader-text"><?php _e( 'Add Menu Item' ) ?></span></button>
+ </dt>
+ </dl>
+ </div>
+ </script>
+
+ <script type="text/html" id="tmpl-available-menu-item-type">
+ <div id="available-menu-items-{{ data.type }}" class="accordion-section">
+ <h4 class="accordion-section-title">{{ data.type_label }}</h4>
+ <div class="accordion-section-content">
+ </div>
+ </div>
+ </script>
+
+ <script type="text/html" id="tmpl-menu-item-reorder-nav">
+ <div class="menu-item-reorder-nav">
+ <?php
+ printf(
+ '<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>',
+ esc_html( 'Move up' ),
+ esc_html( 'Move down' ),
+ esc_html( 'Move one level up' ),
+ esc_html( 'Move one level down' )
+ );
+ ?>
+ </div>
+ </script>
+ <?php
+ }
+
+ /**
+ * Print the html template used to render the add-menu-item frame.
+ *
+ * @since 4.3.0
+ */
+ public function available_items_template() {
+ ?>
+ <div id="available-menu-items" class="accordion-container">
+ <div class="customize-section-title">
+ <button type="button" class="customize-section-back" tabindex="-1">
+ <span class="screen-reader-text"><?php _e( 'Back' ); ?></span>
+ </button>
+ <h3>
+ <span class="customize-action">
+ <?php
+ /* translators: ▸ is the unicode right-pointing triangle, and %s is the section title in the Customizer */
+ printf( __( 'Customizing ▸ %s' ), esc_html( $this->manager->get_panel( 'nav_menus' )->title ) );
+ ?>
+ </span>
+ <?php _e( 'Add Menu Items' ); ?>
+ </h3>
+ </div>
+ <div id="available-menu-items-search" class="accordion-section cannot-expand">
+ <div class="accordion-section-title">
+ <label class="screen-reader-text" for="menu-items-search"><?php _e( 'Search Menu Items' ); ?></label>
+ <input type="text" id="menu-items-search" placeholder="<?php esc_attr_e( 'Search menu items…' ) ?>" />
+ <span class="spinner"></span>
+ </div>
+ <div class="accordion-section-content" data-type="search"></div>
+ </div>
+ <div id="new-custom-menu-item" class="accordion-section">
+ <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>
+ <div class="accordion-section-content">
+ <input type="hidden" value="custom" id="custom-menu-item-type" name="menu-item[-1][menu-item-type]" />
+ <p id="menu-item-url-wrap">
+ <label class="howto" for="custom-menu-item-url">
+ <span><?php _e( 'URL' ); ?></span>
+ <input id="custom-menu-item-url" name="menu-item[-1][menu-item-url]" type="text" class="code menu-item-textbox" value="http://">
+ </label>
+ </p>
+ <p id="menu-item-name-wrap">
+ <label class="howto" for="custom-menu-item-name">
+ <span><?php _e( 'Link Text' ); ?></span>
+ <input id="custom-menu-item-name" name="menu-item[-1][menu-item-title]" type="text" class="regular-text menu-item-textbox">
+ </label>
+ </p>
+ <p class="button-controls">
+ <span class="add-to-menu">
+ <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">
+ <span class="spinner"></span>
+ </span>
+ </p>
+ </div>
+ </div>
+ <?php
+
+ // @todo: consider using add_meta_box/do_accordion_section and making screen-optional?
+ // Containers for per-post-type item browsing; items added with JS.
+ $post_types = get_post_types( array( 'show_in_nav_menus' => true ), 'object' );
+ if ( $post_types ) :
+ foreach ( $post_types as $type ) :
+ ?>
+ <div id="available-menu-items-<?php echo esc_attr( $type->name ); ?>" class="accordion-section">
+ <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>
+ <div class="accordion-section-content" data-type="<?php echo esc_attr( $type->name ); ?>" data-obj_type="post_type"></div>
+ </div>
+ <?php
+ endforeach;
+ endif;
+
+ $taxonomies = get_taxonomies( array( 'show_in_nav_menus' => true ), 'object' );
+ if ( $taxonomies ) :
+ foreach ( $taxonomies as $tax ) :
+ ?>
+ <div id="available-menu-items-<?php echo esc_attr( $tax->name ); ?>" class="accordion-section">
+ <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>
+ <div class="accordion-section-content" data-type="<?php echo esc_attr( $tax->name ); ?>" data-obj_type="taxonomy"></div>
+ </div>
+ <?php
+ endforeach;
+ endif;
+ ?>
+ </div><!-- #available-menu-items -->
+ <?php
+ }
+
+ // Start functionality specific to partial-refresh of menu changes in Customizer preview.
+ const RENDER_AJAX_ACTION = 'customize_render_menu_partial';
+ const RENDER_NONCE_POST_KEY = 'render-menu-nonce';
+ const RENDER_QUERY_VAR = 'wp_customize_menu_render';
+
+ /**
+ * The number of wp_nav_menu() calls which have happened in the preview.
+ *
+ * @since 4.3.0
+ *
+ * @var int
+ */
+ public $preview_nav_menu_instance_number = 0;
+
+ /**
+ * Nav menu args used for each instance.
+ *
+ * @since 4.3.0
+ *
+ * @var array
+ */
+ public $preview_nav_menu_instance_args = array();
+
+ /**
+ * Add hooks for the Customizer preview.
+ *
+ * @since 4.3.0
+ */
+ function customize_preview_init() {
+ add_action( 'template_redirect', array( $this, 'render_menu' ) );
+ add_action( 'wp_enqueue_scripts', array( $this, 'customize_preview_enqueue_deps' ) );
+
+ if ( ! isset( $_REQUEST[ self::RENDER_QUERY_VAR ] ) ) {
+ add_filter( 'wp_nav_menu_args', array( $this, 'filter_wp_nav_menu_args' ), 1000 );
+ add_filter( 'wp_nav_menu', array( $this, 'filter_wp_nav_menu' ), 10, 2 );
+ }
+ }
+
+ /**
+ * Keep track of the arguments that are being passed to wp_nav_menu().
+ *
+ * @since 4.3.0
+ *
+ * @see wp_nav_menu()
+ *
+ * @param array $args An array containing wp_nav_menu() arguments.
+ * @return array
+ */
+ function filter_wp_nav_menu_args( $args ) {
+ $this->preview_nav_menu_instance_number += 1;
+ $args['instance_number'] = $this->preview_nav_menu_instance_number;
+
+ $can_partial_refresh = (
+ $args['echo']
+ &&
+ is_string( $args['fallback_cb'] )
+ &&
+ is_string( $args['walker'] )
+ );
+ $args['can_partial_refresh'] = $can_partial_refresh;
+
+ if ( ! $can_partial_refresh ) {
+ unset( $args['fallback_cb'] );
+ unset( $args['walker'] );
+ }
+
+ ksort( $args );
+ $args['args_hash'] = $this->hash_nav_menu_args( $args );
+
+ $this->preview_nav_menu_instance_args[ $this->preview_nav_menu_instance_number ] = $args;
+ return $args;
+ }
+
+ /**
+ * Prepare wp_nav_menu() calls for partial refresh. Wraps output in container for refreshing.
+ *
+ * @since 4.3.0
+ *
+ * @see wp_nav_menu()
+ *
+ * @param string $nav_menu_content The HTML content for the navigation menu.
+ * @param object $args An object containing wp_nav_menu() arguments.
+ * @return null
+ */
+ function filter_wp_nav_menu( $nav_menu_content, $args ) {
+ if ( ! empty( $args->can_partial_refresh ) && ! empty( $args->instance_number ) ) {
+ $nav_menu_content = sprintf(
+ '<div id="partial-refresh-menu-container-%1$d" class="partial-refresh-menu-container" data-instance-number="%1$d">%2$s</div>',
+ $args->instance_number,
+ $nav_menu_content
+ );
+ }
+ return $nav_menu_content;
+ }
+
+ /**
+ * Hash (hmac) the arguments with the nonce and secret auth key to ensure they
+ * are not tampered with when submitted in the Ajax request.
+ *
+ * @since 4.3.0
+ *
+ * @param array $args The arguments to hash.
+ * @return string
+ */
+ function hash_nav_menu_args( $args ) {
+ return wp_hash( wp_create_nonce( self::RENDER_AJAX_ACTION ) . serialize( $args ) );
+ }
+
+ /**
+ * Enqueue scripts for the Customizer preview.
+ *
+ * @since 4.3.0
+ */
+ function customize_preview_enqueue_deps() {
+ wp_enqueue_script( 'customize-preview-nav-menus' );
+ wp_enqueue_style( 'customize-preview' );
+
+ add_action( 'wp_print_footer_scripts', array( $this, 'export_preview_data' ) );
+ }
+
+ /**
+ * Export data from PHP to JS.
+ *
+ * @since 4.3.0
+ */
+ function export_preview_data() {
+
+ // Why not wp_localize_script? Because we're not localizing, and it forces values into strings.
+ $exports = array(
+ 'renderQueryVar' => self::RENDER_QUERY_VAR,
+ 'renderNonceValue' => wp_create_nonce( self::RENDER_AJAX_ACTION ),
+ 'renderNoncePostKey' => self::RENDER_NONCE_POST_KEY,
+ 'requestUri' => '/',
+ 'theme' => array(
+ 'stylesheet' => $this->manager->get_stylesheet(),
+ 'active' => $this->manager->is_theme_active(),
+ ),
+ 'previewCustomizeNonce' => wp_create_nonce( 'preview-customize_' . $this->manager->get_stylesheet() ),
+ 'navMenuInstanceArgs' => $this->preview_nav_menu_instance_args,
+ );
+
+ if ( ! empty( $_SERVER['REQUEST_URI'] ) ) {
+ $exports['requestUri'] = esc_url_raw( home_url( wp_unslash( $_SERVER['REQUEST_URI'] ) ) );
+ }
+
+ printf( '<script>var _wpCustomizePreviewNavMenusExports = %s;</script>', wp_json_encode( $exports ) );
+ }
+
+ /**
+ * Render a specific menu via wp_nav_menu() using the supplied arguments.
+ *
+ * @since 4.3.0
+ *
+ * @see wp_nav_menu()
+ */
+ function render_menu() {
+ if ( empty( $_POST[ self::RENDER_QUERY_VAR ] ) ) {
+ return;
+ }
+
+ $this->manager->remove_preview_signature();
+
+ if ( empty( $_POST[ self::RENDER_NONCE_POST_KEY ] ) ) {
+ wp_send_json_error( 'missing_nonce_param' );
+ }
+
+ if ( ! is_customize_preview() ) {
+ wp_send_json_error( 'expected_customize_preview' );
+ }
+
+ if ( ! check_ajax_referer( self::RENDER_AJAX_ACTION, self::RENDER_NONCE_POST_KEY, false ) ) {
+ wp_send_json_error( 'nonce_check_fail' );
+ }
+
+ if ( ! current_user_can( 'edit_theme_options' ) ) {
+ wp_send_json_error( 'unauthorized' );
+ }
+
+ if ( ! isset( $_POST['wp_nav_menu_args'] ) ) {
+ wp_send_json_error( 'missing_param' );
+ }
+
+ if ( ! isset( $_POST['wp_nav_menu_args_hash'] ) ) {
+ wp_send_json_error( 'missing_param' );
+ }
+
+ $wp_nav_menu_args = json_decode( wp_unslash( $_POST['wp_nav_menu_args'] ), true );
+ if ( ! is_array( $wp_nav_menu_args ) ) {
+ wp_send_json_error( 'wp_nav_menu_args_not_array' );
+ }
+
+ $wp_nav_menu_args_hash = sanitize_text_field( wp_unslash( $_POST['wp_nav_menu_args_hash'] ) );
+ if ( ! hash_equals( $this->hash_nav_menu_args( $wp_nav_menu_args ), $wp_nav_menu_args_hash ) ) {
+ wp_send_json_error( 'wp_nav_menu_args_hash_mismatch' );
+ }
+
+ $wp_nav_menu_args['echo'] = false;
+ wp_send_json_success( wp_nav_menu( $wp_nav_menu_args ) );
+ }
+}
</ins></span></pre></div>
<a id="trunksrcwpincludesclasswpcustomizesectionphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/class-wp-customize-section.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/class-wp-customize-section.php 2015-06-16 21:32:53 UTC (rev 32805)
+++ trunk/src/wp-includes/class-wp-customize-section.php 2015-06-16 22:07:08 UTC (rev 32806)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -501,3 +501,74 @@
</span><span class="cx" style="display: block; padding: 0 10px"> return $this->manager->widgets->is_sidebar_rendered( $this->sidebar_id );
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+/**
+ * Customize Menu Section Class
+ *
+ * Custom section only needed in JS.
+ *
+ * @since 4.3.0
+ */
+class WP_Customize_Nav_Menu_Section extends WP_Customize_Section {
+
+ /**
+ * Control type.
+ *
+ * @since 4.3.0
+ *
+ * @access public
+ * @var string
+ */
+ public $type = 'nav_menu';
+
+ /**
+ * Get section params for JS.
+ *
+ * @since 4.3.0
+ *
+ * @return array
+ */
+ function json() {
+ $exported = parent::json();
+ $exported['menu_id'] = intval( preg_replace( '/^nav_menu\[(\d+)\]/', '$1', $this->id ) );
+
+ return $exported;
+ }
+}
+
+/**
+ * Customize Menu Section Class
+ *
+ * Implements the new-menu-ui toggle button instead of a regular section.
+ *
+ * @since 4.3.0
+ */
+class WP_Customize_New_Menu_Section extends WP_Customize_Section {
+
+ /**
+ * Control type.
+ *
+ * @since 4.3.0
+ *
+ * @access public
+ * @var string
+ */
+ public $type = 'new_menu';
+
+ /**
+ * Render the section, and the controls that have been added to it.
+ *
+ * @since 4.3.0
+ */
+ protected function render() {
+ ?>
+ <li id="accordion-section-<?php echo esc_attr( $this->id ); ?>" class="accordion-section-new-menu">
+ <button type="button" class="button-secondary add-new-menu-item add-menu-toggle">
+ <?php echo esc_html( $this->title ); ?>
+ <span class="screen-reader-text"><?php _e( 'Press return or enter to open' ); ?></span>
+ </button>
+ <ul class="new-menu-section-content"></ul>
+ </li>
+ <?php
+ }
+}
</ins></span></pre></div>
<a id="trunksrcwpincludesclasswpcustomizesettingphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/class-wp-customize-setting.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/class-wp-customize-setting.php 2015-06-16 21:32:53 UTC (rev 32805)
+++ trunk/src/wp-includes/class-wp-customize-setting.php 2015-06-16 22:07:08 UTC (rev 32806)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -630,3 +630,1142 @@
</span><span class="cx" style="display: block; padding: 0 10px"> remove_theme_mod( 'background_image_thumb' );
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+/**
+ * Customize Setting to represent a nav_menu.
+ *
+ * Subclass of WP_Customize_Setting to represent a nav_menu taxonomy term, and
+ * the IDs for the nav_menu_items associated with the nav menu.
+ *
+ * @since 4.3.0
+ *
+ * @see wp_get_nav_menu_items()
+ * @see WP_Customize_Setting
+ */
+class WP_Customize_Nav_Menu_Item_Setting extends WP_Customize_Setting {
+
+ const ID_PATTERN = '/^nav_menu_item\[(?P<id>-?\d+)\]$/';
+
+ const POST_TYPE = 'nav_menu_item';
+
+ const TYPE = 'nav_menu_item';
+
+ /**
+ * Setting type.
+ *
+ * @since 4.3.0
+ *
+ * @var string
+ */
+ public $type = self::TYPE;
+
+ /**
+ * Default setting value.
+ *
+ * @since 4.3.0
+ *
+ * @see wp_setup_nav_menu_item()
+ * @var array
+ */
+ public $default = array(
+ // The $menu_item_data for wp_update_nav_menu_item().
+ 'object_id' => 0,
+ 'object' => '', // Taxonomy name.
+ 'menu_item_parent' => 0, // A.K.A. menu-item-parent-id; note that post_parent is different, and not included.
+ 'position' => 0, // A.K.A. menu_order.
+ 'type' => 'custom', // Note that type_label is not included here.
+ 'title' => '',
+ 'url' => '',
+ 'target' => '',
+ 'attr_title' => '',
+ 'description' => '',
+ 'classes' => '',
+ 'xfn' => '',
+ 'status' => 'publish',
+ 'original_title' => '',
+ 'nav_menu_term_id' => 0, // This will be supplied as the $menu_id arg for wp_update_nav_menu_item().
+ // @todo also expose invalid?
+ );
+
+ /**
+ * Default transport.
+ *
+ * @since 4.3.0
+ *
+ * @var string
+ */
+ public $transport = 'postMessage';
+
+ /**
+ * The post ID represented by this setting instance. This is the db_id.
+ *
+ * A negative value represents a placeholder ID for a new menu not yet saved.
+ *
+ * @todo Should this be $db_id, and also use this for WP_Customize_Nav_Menu_Setting::$term_id
+ *
+ * @since 4.3.0
+ *
+ * @var int
+ */
+ public $post_id;
+
+ /**
+ * Previous (placeholder) post ID used before creating a new menu item.
+ *
+ * This value will be exported to JS via the customize_save_response filter
+ * so that JavaScript can update the settings to refer to the newly-assigned
+ * post ID. This value is always negative to indicate it does not refer to
+ * a real post.
+ *
+ * @since 4.3.0
+ *
+ * @see WP_Customize_Nav_Menu_Item_Setting::update()
+ * @see WP_Customize_Nav_Menu_Item_Setting::amend_customize_save_response()
+ *
+ * @var int
+ */
+ public $previous_post_id;
+
+ /**
+ * When previewing or updating a menu item, this stores the previous nav_menu_term_id
+ * which ensures that we can apply the proper filters.
+ *
+ * @since 4.3.0
+ *
+ * @var int
+ */
+ public $original_nav_menu_term_id;
+
+ /**
+ * Whether or not preview() was called.
+ *
+ * @since 4.3.0
+ *
+ * @var bool
+ */
+ protected $is_previewed = false;
+
+ /**
+ * Whether or not update() was called.
+ *
+ * @since 4.3.0
+ *
+ * @var bool
+ */
+ protected $is_updated = false;
+
+ /**
+ * Status for calling the update method, used in customize_save_response filter.
+ *
+ * When status is inserted, the placeholder post ID is stored in $previous_post_id.
+ * When status is error, the error is stored in $update_error.
+ *
+ * @since 4.3.0
+ *
+ * @see WP_Customize_Nav_Menu_Item_Setting::update()
+ * @see WP_Customize_Nav_Menu_Item_Setting::amend_customize_save_response()
+ *
+ * @var string updated|inserted|deleted|error
+ */
+ public $update_status;
+
+ /**
+ * Any error object returned by wp_update_nav_menu_item() when setting is updated.
+ *
+ * @since 4.3.0
+ *
+ * @see WP_Customize_Nav_Menu_Item_Setting::update()
+ * @see WP_Customize_Nav_Menu_Item_Setting::amend_customize_save_response()
+ *
+ * @var WP_Error
+ */
+ public $update_error;
+
+ /**
+ * Constructor.
+ *
+ * Any supplied $args override class property defaults.
+ *
+ * @since 4.3.0
+ *
+ * @param WP_Customize_Manager $manager Manager instance.
+ * @param string $id An specific ID of the setting. Can be a
+ * theme mod or option name.
+ * @param array $args Optional. Setting arguments.
+ * @throws Exception If $id is not valid for this setting type.
+ */
+ public function __construct( WP_Customize_Manager $manager, $id, array $args = array() ) {
+ if ( empty( $manager->nav_menus ) ) {
+ throw new Exception( 'Expected WP_Customize_Manager::$nav_menus to be set.' );
+ }
+
+ if ( ! preg_match( self::ID_PATTERN, $id, $matches ) ) {
+ throw new Exception( "Illegal widget setting ID: $id" );
+ }
+
+ $this->post_id = intval( $matches['id'] );
+
+ $menu = $this->value();
+ $this->original_nav_menu_term_id = $menu['nav_menu_term_id'];
+
+ parent::__construct( $manager, $id, $args );
+ }
+
+ /**
+ * Get the instance data for a given widget setting.
+ *
+ * @since 4.3.0
+ *
+ * @see wp_setup_nav_menu_item()
+ *
+ * @return array
+ */
+ public function value() {
+ if ( $this->is_previewed && $this->_previewed_blog_id === get_current_blog_id() ) {
+ $undefined = new stdClass(); // Symbol.
+ $post_value = $this->post_value( $undefined );
+
+ if ( $undefined === $post_value ) {
+ $value = $this->_original_value;
+ } else {
+ $value = $post_value;
+ }
+ } else {
+ $value = false;
+
+ // Note that a ID of less than one indicates a nav_menu not yet inserted.
+ if ( $this->post_id > 0 ) {
+ $post = get_post( $this->post_id );
+ if ( $post && self::POST_TYPE === $post->post_type ) {
+ $item = wp_setup_nav_menu_item( $post );
+ $value = wp_array_slice_assoc(
+ (array) $item,
+ array_keys( $this->default )
+ );
+ $value['position'] = $item->menu_order;
+ $value['status'] = $item->post_status;
+ $value['original_title'] = '';
+
+ $menus = wp_get_post_terms( $post->ID, WP_Customize_Nav_Menu_Setting::TAXONOMY, array(
+ 'fields' => 'ids',
+ ) );
+
+ if ( ! empty( $menus ) ) {
+ $value['nav_menu_term_id'] = array_shift( $menus );
+ } else {
+ $value['nav_menu_term_id'] = 0;
+ }
+
+ if ( 'post_type' === $value['type'] ) {
+ $original_title = get_the_title( $value['object_id'] );
+ } else if ( 'taxonomy' === $value['type'] ) {
+ $original_title = get_term_field( 'name', $value['object_id'], $value['object'], 'raw' );
+ if ( is_wp_error( $original_title ) ) {
+ $original_title = '';
+ }
+ }
+
+ if ( ! empty( $original_title ) ) {
+ $value['original_title'] = $original_title;
+ }
+ }
+ }
+
+ if ( ! is_array( $value ) ) {
+ $value = $this->default;
+ }
+ }
+
+ if ( is_array( $value ) ) {
+ foreach ( array( 'object_id', 'menu_item_parent', 'nav_menu_term_id' ) as $key ) {
+ $value[ $key ] = intval( $value[ $key ] );
+ }
+ }
+
+ return $value;
+ }
+
+ /**
+ * Handle previewing the setting.
+ *
+ * @since 4.3.0
+ *
+ * @see WP_Customize_Manager::post_value()
+ */
+ public function preview() {
+ if ( $this->is_previewed ) {
+ return;
+ }
+
+ $this->is_previewed = true;
+ $this->_original_value = $this->value();
+ $this->original_nav_menu_term_id = $this->_original_value['nav_menu_term_id'];
+ $this->_previewed_blog_id = get_current_blog_id();
+
+ add_filter( 'wp_get_nav_menu_items', array( $this, 'filter_wp_get_nav_menu_items' ), 10, 3 );
+
+ $sort_callback = array( __CLASS__, 'sort_wp_get_nav_menu_items' );
+ if ( ! has_filter( 'wp_get_nav_menu_items', $sort_callback ) ) {
+ add_filter( 'wp_get_nav_menu_items', array( __CLASS__, 'sort_wp_get_nav_menu_items' ), 1000, 3 );
+ }
+
+ // @todo Add get_post_metadata filters for plugins to add their data.
+ }
+
+ /**
+ * Filter the wp_get_nav_menu_items() result to supply the previewed menu items.
+ *
+ * @since 4.3.0
+ *
+ * @see wp_get_nav_menu_items()
+ *
+ * @param array $items An array of menu item post objects.
+ * @param object $menu The menu object.
+ * @param array $args An array of arguments used to retrieve menu item objects.
+ * @return array Array of menu items,
+ */
+ function filter_wp_get_nav_menu_items( $items, $menu, $args ) {
+ $this_item = $this->value();
+ $current_nav_menu_term_id = $this_item['nav_menu_term_id'];
+ unset( $this_item['nav_menu_term_id'] );
+
+ $should_filter = (
+ $menu->term_id === $this->original_nav_menu_term_id
+ ||
+ $menu->term_id === $current_nav_menu_term_id
+ );
+ if ( ! $should_filter ) {
+ return $items;
+ }
+
+ // Handle deleted menu item, or menu item moved to another menu.
+ $should_remove = (
+ false === $this_item
+ ||
+ (
+ $this->original_nav_menu_term_id === $menu->term_id
+ &&
+ $current_nav_menu_term_id !== $this->original_nav_menu_term_id
+ )
+ );
+ if ( $should_remove ) {
+ $filtered_items = array();
+ foreach ( $items as $item ) {
+ if ( $item->db_id !== $this->post_id ) {
+ $filtered_items[] = $item;
+ }
+ }
+ return $filtered_items;
+ }
+
+ $mutated = false;
+ $should_update = (
+ is_array( $this_item )
+ &&
+ $current_nav_menu_term_id === $menu->term_id
+ );
+ if ( $should_update ) {
+ foreach ( $items as $item ) {
+ if ( $item->db_id === $this->post_id ) {
+ foreach ( get_object_vars( $this->value_as_wp_post_nav_menu_item() ) as $key => $value ) {
+ $item->$key = $value;
+ }
+ $mutated = true;
+ }
+ }
+
+ // Not found so we have to append it..
+ if ( ! $mutated ) {
+ $items[] = $this->value_as_wp_post_nav_menu_item();
+ }
+ }
+
+ return $items;
+ }
+
+ /**
+ * Re-apply the tail logic also applied on $items by wp_get_nav_menu_items().
+ *
+ * @since 4.3.0
+ *
+ * @see wp_get_nav_menu_items()
+ *
+ * @param array $items An array of menu item post objects.
+ * @param object $menu The menu object.
+ * @param array $args An array of arguments used to retrieve menu item objects.
+ * @return array Array of menu items,
+ */
+ static function sort_wp_get_nav_menu_items( $items, $menu, $args ) {
+ // @todo We should probably re-apply some constraints imposed by $args.
+ unset( $args['include'] );
+
+ // Remove invalid items only in frontend.
+ if ( ! is_admin() ) {
+ $items = array_filter( $items, '_is_valid_nav_menu_item' );
+ }
+
+ if ( ARRAY_A === $args['output'] ) {
+ $GLOBALS['_menu_item_sort_prop'] = $args['output_key'];
+ usort( $items, '_sort_nav_menu_items' );
+ $i = 1;
+
+ foreach ( $items as $k => $item ) {
+ $items[ $k ]->$args['output_key'] = $i++;
+ }
+ }
+
+ return $items;
+ }
+
+ /**
+ * Get the value emulated into a WP_Post and set up as a nav_menu_item.
+ *
+ * @since 4.3.0
+ *
+ * @return WP_Post With {@see wp_setup_nav_menu_item()} applied.
+ */
+ public function value_as_wp_post_nav_menu_item() {
+ $item = (object) $this->value();
+ unset( $item->nav_menu_term_id );
+
+ $item->post_status = $item->status;
+ unset( $item->status );
+
+ $item->post_type = 'nav_menu_item';
+ $item->menu_order = $item->position;
+ unset( $item->position );
+
+ $item->post_author = get_current_user_id();
+
+ if ( $item->title ) {
+ $item->post_title = $item->title;
+ }
+
+ $item->ID = $this->post_id;
+ $post = new WP_Post( (object) $item );
+ $post = wp_setup_nav_menu_item( $post );
+
+ return $post;
+ }
+
+ /**
+ * Sanitize an input.
+ *
+ * Note that parent::sanitize() erroneously does wp_unslash() on $value, but
+ * we remove that in this override.
+ *
+ * @since 4.3.0
+ *
+ * @param array $menu_item_value The value to sanitize.
+ * @return array|false|null Null if an input isn't valid. False if it is marked for deletion. Otherwise the sanitized value.
+ */
+ public function sanitize( $menu_item_value ) {
+ // Menu is marked for deletion.
+ if ( false === $menu_item_value ) {
+ return $menu_item_value;
+ }
+
+ // Invalid.
+ if ( ! is_array( $menu_item_value ) ) {
+ return null;
+ }
+
+ $default = array(
+ 'object_id' => 0,
+ 'object' => '',
+ 'menu_item_parent' => 0,
+ 'position' => 0,
+ 'type' => 'custom',
+ 'title' => '',
+ 'url' => '',
+ 'target' => '',
+ 'attr_title' => '',
+ 'description' => '',
+ 'classes' => '',
+ 'xfn' => '',
+ 'status' => 'publish',
+ 'original_title' => '',
+ 'nav_menu_term_id' => 0,
+ );
+ $menu_item_value = array_merge( $default, $menu_item_value );
+ $menu_item_value = wp_array_slice_assoc( $menu_item_value, array_keys( $default ) );
+ $menu_item_value['position'] = max( 0, intval( $menu_item_value['position'] ) );
+
+ foreach ( array( 'object_id', 'menu_item_parent', 'nav_menu_term_id' ) as $key ) {
+ // Note we need to allow negative-integer IDs for previewed objects not inserted yet.
+ $menu_item_value[ $key ] = intval( $menu_item_value[ $key ] );
+ }
+
+ foreach ( array( 'type', 'object', 'target' ) as $key ) {
+ $menu_item_value[ $key ] = sanitize_key( $menu_item_value[ $key ] );
+ }
+
+ foreach ( array( 'xfn', 'classes' ) as $key ) {
+ $value = $menu_item_value[ $key ];
+ if ( ! is_array( $value ) ) {
+ $value = explode( ' ', $value );
+ }
+ $menu_item_value[ $key ] = implode( ' ', array_map( 'sanitize_html_class', $value ) );
+ }
+
+ foreach ( array( 'title', 'attr_title', 'description', 'original_title' ) as $key ) {
+ // @todo Should esc_attr() the attr_title as well?
+ $menu_item_value[ $key ] = sanitize_text_field( $menu_item_value[ $key ] );
+ }
+
+ $menu_item_value['url'] = esc_url_raw( $menu_item_value['url'] );
+ if ( ! get_post_status_object( $menu_item_value['status'] ) ) {
+ $menu_item_value['status'] = 'publish';
+ }
+
+ /** This filter is documented in wp-includes/class-wp-customize-setting.php */
+ return apply_filters( "customize_sanitize_{$this->id}", $menu_item_value, $this );
+ }
+
+ /**
+ * Create/update the nav_menu_item post for this setting.
+ *
+ * Any created menu items will have their assigned post IDs exported to the client
+ * via the customize_save_response filter. Likewise, any errors will be exported
+ * to the client via the customize_save_response() filter.
+ *
+ * To delete a menu, the client can send false as the value.
+ *
+ * @since 4.3.0
+ *
+ * @see wp_update_nav_menu_item()
+ *
+ * @param array|false $value The menu item array to update. If false, then the menu item will be deleted entirely.
+ * See {@see WP_Customize_Nav_Menu_Item_Setting::$default} for what the value should
+ * consist of.
+ * @return void
+ */
+ protected function update( $value ) {
+ if ( $this->is_updated ) {
+ return;
+ }
+
+ $this->is_updated = true;
+ $is_placeholder = ( $this->post_id < 0 );
+ $is_delete = ( false === $value );
+
+ add_filter( 'customize_save_response', array( $this, 'amend_customize_save_response' ) );
+
+ if ( $is_delete ) {
+ // If the current setting post is a placeholder, a delete request is a no-op.
+ if ( $is_placeholder ) {
+ $this->update_status = 'deleted';
+ } else {
+ $r = wp_delete_post( $this->post_id, true );
+
+ if ( false === $r ) {
+ $this->update_error = new WP_Error( 'delete_failure' );
+ $this->update_status = 'error';
+ } else {
+ $this->update_status = 'deleted';
+ }
+ // @todo send back the IDs for all associated nav menu items deleted, so these settings (and controls) can be removed from Customizer?
+ }
+ } else {
+
+ // Handle saving menu items for menus that are being newly-created.
+ if ( $value['nav_menu_term_id'] < 0 ) {
+ $nav_menu_setting_id = sprintf( 'nav_menu[%s]', $value['nav_menu_term_id'] );
+ $nav_menu_setting = $this->manager->get_setting( $nav_menu_setting_id );
+
+ if ( ! $nav_menu_setting || ! ( $nav_menu_setting instanceof WP_Customize_Nav_Menu_Setting ) ) {
+ $this->update_status = 'error';
+ $this->update_error = new WP_Error( 'unexpected_nav_menu_setting' );
+ return;
+ }
+
+ if ( false === $nav_menu_setting->save() ) {
+ $this->update_status = 'error';
+ $this->update_error = new WP_Error( 'nav_menu_setting_failure' );
+ }
+
+ if ( $nav_menu_setting->previous_term_id !== intval( $value['nav_menu_term_id'] ) ) {
+ $this->update_status = 'error';
+ $this->update_error = new WP_Error( 'unexpected_previous_term_id' );
+ return;
+ }
+
+ $value['nav_menu_term_id'] = $nav_menu_setting->term_id;
+ }
+
+ // Handle saving a nav menu item that is a child of a nav menu item being newly-created.
+ if ( $value['menu_item_parent'] < 0 ) {
+ $parent_nav_menu_item_setting_id = sprintf( 'nav_menu_item[%s]', $value['menu_item_parent'] );
+ $parent_nav_menu_item_setting = $this->manager->get_setting( $parent_nav_menu_item_setting_id );
+
+ if ( ! $parent_nav_menu_item_setting || ! ( $parent_nav_menu_item_setting instanceof WP_Customize_Nav_Menu_Item_Setting ) ) {
+ $this->update_status = 'error';
+ $this->update_error = new WP_Error( 'unexpected_nav_menu_item_setting' );
+ return;
+ }
+
+ if ( false === $parent_nav_menu_item_setting->save() ) {
+ $this->update_status = 'error';
+ $this->update_error = new WP_Error( 'nav_menu_item_setting_failure' );
+ }
+
+ if ( $parent_nav_menu_item_setting->previous_post_id !== intval( $value['menu_item_parent'] ) ) {
+ $this->update_status = 'error';
+ $this->update_error = new WP_Error( 'unexpected_previous_post_id' );
+ return;
+ }
+
+ $value['menu_item_parent'] = $parent_nav_menu_item_setting->post_id;
+ }
+
+ // Insert or update menu.
+ $menu_item_data = array(
+ 'menu-item-object-id' => $value['object_id'],
+ 'menu-item-object' => $value['object'],
+ 'menu-item-parent-id' => $value['menu_item_parent'],
+ 'menu-item-position' => $value['position'],
+ 'menu-item-type' => $value['type'],
+ 'menu-item-title' => $value['title'],
+ 'menu-item-url' => $value['url'],
+ 'menu-item-description' => $value['description'],
+ 'menu-item-attr-title' => $value['attr_title'],
+ 'menu-item-target' => $value['target'],
+ 'menu-item-classes' => $value['classes'],
+ 'menu-item-xfn' => $value['xfn'],
+ 'menu-item-status' => $value['status'],
+ );
+
+ $r = wp_update_nav_menu_item(
+ $value['nav_menu_term_id'],
+ $is_placeholder ? 0 : $this->post_id,
+ $menu_item_data
+ );
+
+ if ( is_wp_error( $r ) ) {
+ $this->update_status = 'error';
+ $this->update_error = $r;
+ } else {
+ if ( $is_placeholder ) {
+ $this->previous_post_id = $this->post_id;
+ $this->post_id = $r;
+ $this->update_status = 'inserted';
+ } else {
+ $this->update_status = 'updated';
+ }
+ }
+ }
+
+ }
+
+ /**
+ * Export data for the JS client.
+ *
+ * @since 4.3.0
+ *
+ * @see WP_Customize_Nav_Menu_Item_Setting::update()
+ *
+ * @param array $data Additional information passed back to the 'saved' event on `wp.customize`.
+ * @return array
+ */
+ function amend_customize_save_response( $data ) {
+ if ( ! isset( $data['nav_menu_item_updates'] ) ) {
+ $data['nav_menu_item_updates'] = array();
+ }
+
+ $data['nav_menu_item_updates'][] = array(
+ 'post_id' => $this->post_id,
+ 'previous_post_id' => $this->previous_post_id,
+ 'error' => $this->update_error ? $this->update_error->get_error_code() : null,
+ 'status' => $this->update_status,
+ );
+
+ return $data;
+ }
+}
+
+/**
+ * Customize Setting to represent a nav_menu.
+ *
+ * Subclass of WP_Customize_Setting to represent a nav_menu taxonomy term, and
+ * the IDs for the nav_menu_items associated with the nav menu.
+ *
+ * @since 4.3.0
+ *
+ * @see wp_get_nav_menu_object()
+ * @see WP_Customize_Setting
+ */
+class WP_Customize_Nav_Menu_Setting extends WP_Customize_Setting {
+
+ const ID_PATTERN = '/^nav_menu\[(?P<id>-?\d+)\]$/';
+
+ const TAXONOMY = 'nav_menu';
+
+ const TYPE = 'nav_menu';
+
+ /**
+ * Setting type.
+ *
+ * @since 4.3.0
+ *
+ * @var string
+ */
+ public $type = self::TYPE;
+
+ /**
+ * Default setting value.
+ *
+ * @since 4.3.0
+ *
+ * @see wp_get_nav_menu_object()
+ *
+ * @var array
+ */
+ public $default = array(
+ 'name' => '',
+ 'description' => '',
+ 'parent' => 0,
+ 'auto_add' => false,
+ );
+
+ /**
+ * Default transport.
+ *
+ * @since 4.3.0
+ *
+ * @var string
+ */
+ public $transport = 'postMessage';
+
+ /**
+ * The term ID represented by this setting instance.
+ *
+ * A negative value represents a placeholder ID for a new menu not yet saved.
+ *
+ * @since 4.3.0
+ *
+ * @var int
+ */
+ public $term_id;
+
+ /**
+ * Previous (placeholder) term ID used before creating a new menu.
+ *
+ * This value will be exported to JS via the customize_save_response filter
+ * so that JavaScript can update the settings to refer to the newly-assigned
+ * term ID. This value is always negative to indicate it does not refer to
+ * a real term.
+ *
+ * @since 4.3.0
+ *
+ * @see WP_Customize_Nav_Menu_Setting::update()
+ * @see WP_Customize_Nav_Menu_Setting::amend_customize_save_response()
+ *
+ * @var int
+ */
+ public $previous_term_id;
+
+ /**
+ * Whether or not preview() was called.
+ *
+ * @since 4.3.0
+ *
+ * @var bool
+ */
+ protected $is_previewed = false;
+
+ /**
+ * Whether or not update() was called.
+ *
+ * @since 4.3.0
+ *
+ * @var bool
+ */
+ protected $is_updated = false;
+
+ /**
+ * Status for calling the update method, used in customize_save_response filter.
+ *
+ * When status is inserted, the placeholder term ID is stored in $previous_term_id.
+ * When status is error, the error is stored in $update_error.
+ *
+ * @since 4.3.0
+ *
+ * @see WP_Customize_Nav_Menu_Setting::update()
+ * @see WP_Customize_Nav_Menu_Setting::amend_customize_save_response()
+ *
+ * @var string updated|inserted|deleted|error
+ */
+ public $update_status;
+
+ /**
+ * Any error object returned by wp_update_nav_menu_object() when setting is updated.
+ *
+ * @since 4.3.0
+ *
+ * @see WP_Customize_Nav_Menu_Setting::update()
+ * @see WP_Customize_Nav_Menu_Setting::amend_customize_save_response()
+ *
+ * @var WP_Error
+ */
+ public $update_error;
+
+ /**
+ * Constructor.
+ *
+ * Any supplied $args override class property defaults.
+ *
+ * @since 4.3.0
+ *
+ * @param WP_Customize_Manager $manager Manager instance.
+ * @param string $id An specific ID of the setting. Can be a
+ * theme mod or option name.
+ * @param array $args Optional. Setting arguments.
+ * @throws Exception If $id is not valid for this setting type.
+ */
+ public function __construct( WP_Customize_Manager $manager, $id, array $args = array() ) {
+ if ( empty( $manager->nav_menus ) ) {
+ throw new Exception( 'Expected WP_Customize_Manager::$nav_menus to be set.' );
+ }
+
+ if ( ! preg_match( self::ID_PATTERN, $id, $matches ) ) {
+ throw new Exception( "Illegal widget setting ID: $id" );
+ }
+
+ $this->term_id = intval( $matches['id'] );
+
+ parent::__construct( $manager, $id, $args );
+ }
+
+ /**
+ * Get the instance data for a given widget setting.
+ *
+ * @since 4.3.0
+ *
+ * @see wp_get_nav_menu_object()
+ *
+ * @return array
+ */
+ public function value() {
+ if ( $this->is_previewed && $this->_previewed_blog_id === get_current_blog_id() ) {
+ $undefined = new stdClass(); // Symbol.
+ $post_value = $this->post_value( $undefined );
+
+ if ( $undefined === $post_value ) {
+ $value = $this->_original_value;
+ } else {
+ $value = $post_value;
+ }
+ } else {
+ $value = false;
+
+ // Note that a term_id of less than one indicates a nav_menu not yet inserted.
+ if ( $this->term_id > 0 ) {
+ $term = wp_get_nav_menu_object( $this->term_id );
+
+ if ( $term ) {
+ $value = wp_array_slice_assoc( (array) $term, array_keys( $this->default ) );
+
+ $nav_menu_options = (array) get_option( 'nav_menu_options', array() );
+ $value['auto_add'] = false;
+
+ if ( isset( $nav_menu_options['auto_add'] ) && is_array( $nav_menu_options['auto_add'] ) ) {
+ $value['auto_add'] = in_array( $term->term_id, $nav_menu_options['auto_add'] );
+ }
+ }
+ }
+
+ if ( ! is_array( $value ) ) {
+ $value = $this->default;
+ }
+ }
+ return $value;
+ }
+
+ /**
+ * Handle previewing the setting.
+ *
+ * @since 4.3.0
+ *
+ * @see WP_Customize_Manager::post_value()
+ */
+ public function preview() {
+ if ( $this->is_previewed ) {
+ return;
+ }
+
+ $this->is_previewed = true;
+ $this->_original_value = $this->value();
+ $this->_previewed_blog_id = get_current_blog_id();
+
+ add_filter( 'wp_get_nav_menu_object', array( $this, 'filter_wp_get_nav_menu_object' ), 10, 2 );
+ add_filter( 'default_option_nav_menu_options', array( $this, 'filter_nav_menu_options' ) );
+ add_filter( 'option_nav_menu_options', array( $this, 'filter_nav_menu_options' ) );
+ }
+
+ /**
+ * Filter the wp_get_nav_menu_object() result to supply the previewed menu object.
+ *
+ * Requesting a nav_menu object by anything but ID is not supported.
+ *
+ * @since 4.3.0
+ *
+ * @see wp_get_nav_menu_object()
+ *
+ * @param object|null $menu_obj Object returned by wp_get_nav_menu_object().
+ * @param string $menu_id ID of the nav_menu term. Requests by slug or name will be ignored.
+ * @return object|null
+ */
+ function filter_wp_get_nav_menu_object( $menu_obj, $menu_id ) {
+ $ok = (
+ get_current_blog_id() === $this->_previewed_blog_id
+ &&
+ is_int( $menu_id )
+ &&
+ $menu_id === $this->term_id
+ );
+ if ( ! $ok ) {
+ return $menu_obj;
+ }
+
+ $setting_value = $this->value();
+
+ // Handle deleted menus.
+ if ( false === $setting_value ) {
+ return false;
+ }
+
+ // Handle sanitization failure by preventing short-circuiting.
+ if ( null === $setting_value ) {
+ return $menu_obj;
+ }
+
+ $menu_obj = (object) array_merge( array(
+ 'term_id' => $this->term_id,
+ 'term_taxonomy_id' => $this->term_id,
+ 'slug' => sanitize_title( $setting_value['name'] ),
+ 'count' => 0,
+ 'term_group' => 0,
+ 'taxonomy' => self::TAXONOMY,
+ 'filter' => 'raw',
+ ), $setting_value );
+
+ return $menu_obj;
+ }
+
+ /**
+ * Filter the nav_menu_options option to include this menu's auto_add preference.
+ *
+ * @since 4.3.0
+ *
+ * @param array $nav_menu_options Nav menu options including auto_add.
+ * @return array
+ */
+ function filter_nav_menu_options( $nav_menu_options ) {
+ if ( $this->_previewed_blog_id !== get_current_blog_id() ) {
+ return $nav_menu_options;
+ }
+
+ $menu = $this->value();
+ $nav_menu_options = $this->filter_nav_menu_options_value(
+ $nav_menu_options,
+ $this->term_id,
+ false === $menu ? false : $menu['auto_add']
+ );
+
+ return $nav_menu_options;
+ }
+
+ /**
+ * Sanitize an input.
+ *
+ * Note that parent::sanitize() erroneously does wp_unslash() on $value, but
+ * we remove that in this override.
+ *
+ * @since 4.3.0
+ *
+ * @param array $value The value to sanitize.
+ * @return array|false|null Null if an input isn't valid. False if it is marked for deletion. Otherwise the sanitized value.
+ */
+ public function sanitize( $value ) {
+ // Menu is marked for deletion.
+ if ( false === $value ) {
+ return $value;
+ }
+
+ // Invalid.
+ if ( ! is_array( $value ) ) {
+ return null;
+ }
+
+ $default = array(
+ 'name' => '',
+ 'description' => '',
+ 'parent' => 0,
+ 'auto_add' => false,
+ );
+ $value = array_merge( $default, $value );
+ $value = wp_array_slice_assoc( $value, array_keys( $default ) );
+
+ $value['name'] = trim( esc_html( $value['name'] ) ); // This sanitization code is used in wp-admin/nav-menus.php.
+ $value['description'] = sanitize_text_field( $value['description'] );
+ $value['parent'] = max( 0, intval( $value['parent'] ) );
+ $value['auto_add'] = ! empty( $value['auto_add'] );
+
+ /** This filter is documented in wp-includes/class-wp-customize-setting.php */
+ return apply_filters( "customize_sanitize_{$this->id}", $value, $this );
+ }
+
+ /**
+ * Create/update the nav_menu term for this setting.
+ *
+ * Any created menus will have their assigned term IDs exported to the client
+ * via the customize_save_response filter. Likewise, any errors will be exported
+ * to the client via the customize_save_response() filter.
+ *
+ * To delete a menu, the client can send false as the value.
+ *
+ * @since 4.3.0
+ *
+ * @see wp_update_nav_menu_object()
+ *
+ * @param array|false $value {
+ * The value to update. Note that slug cannot be updated via wp_update_nav_menu_object().
+ * If false, then the menu will be deleted entirely.
+ *
+ * @type string $name The name of the menu to save.
+ * @type string $description The term description. Default empty string.
+ * @type int $parent The id of the parent term. Default 0.
+ * @type bool $auto_add Whether pages will auto_add to this menu. Default false.
+ * }
+ * @return void
+ */
+ protected function update( $value ) {
+ if ( $this->is_updated ) {
+ return;
+ }
+
+ $this->is_updated = true;
+ $is_placeholder = ( $this->term_id < 0 );
+ $is_delete = ( false === $value );
+
+ add_filter( 'customize_save_response', array( $this, 'amend_customize_save_response' ) );
+
+ $auto_add = null;
+ if ( $is_delete ) {
+ // If the current setting term is a placeholder, a delete request is a no-op.
+ if ( $is_placeholder ) {
+ $this->update_status = 'deleted';
+ } else {
+ $r = wp_delete_nav_menu( $this->term_id );
+
+ if ( is_wp_error( $r ) ) {
+ $this->update_status = 'error';
+ $this->update_error = $r;
+ } else {
+ $this->update_status = 'deleted';
+ $auto_add = false;
+ }
+ }
+ } else {
+ // Insert or update menu.
+ $menu_data = wp_array_slice_assoc( $value, array( 'description', 'parent' ) );
+ if ( isset( $value['name'] ) ) {
+ $menu_data['menu-name'] = $value['name'];
+ }
+
+ $r = wp_update_nav_menu_object( $is_placeholder ? 0 : $this->term_id, $menu_data );
+ if ( is_wp_error( $r ) ) {
+ $this->update_status = 'error';
+ $this->update_error = $r;
+ } else {
+ if ( $is_placeholder ) {
+ $this->previous_term_id = $this->term_id;
+ $this->term_id = $r;
+ $this->update_status = 'inserted';
+ } else {
+ $this->update_status = 'updated';
+ }
+
+ $auto_add = $value['auto_add'];
+ }
+ }
+
+ if ( null !== $auto_add ) {
+ $nav_menu_options = $this->filter_nav_menu_options_value(
+ (array) get_option( 'nav_menu_options', array() ),
+ $this->term_id,
+ $auto_add
+ );
+ update_option( 'nav_menu_options', $nav_menu_options );
+ }
+
+ // Make sure that new menus assigned to nav menu locations use their new IDs.
+ if ( 'inserted' === $this->update_status ) {
+ foreach ( $this->manager->settings() as $setting ) {
+ if ( ! preg_match( '/^nav_menu_locations\[/', $setting->id ) ) {
+ continue;
+ }
+
+ $post_value = $setting->post_value( null );
+ if ( ! is_null( $post_value ) && $this->previous_term_id === intval( $post_value ) ) {
+ $this->manager->set_post_value( $setting->id, $this->term_id );
+ $setting->save();
+ }
+ }
+ }
+ }
+
+ /**
+ * Update a nav_menu_options array.
+ *
+ * @since 4.3.0
+ *
+ * @see WP_Customize_Nav_Menu_Setting::filter_nav_menu_options()
+ * @see WP_Customize_Nav_Menu_Setting::update()
+ *
+ * @param array $nav_menu_options Array as returned by get_option( 'nav_menu_options' ).
+ * @param int $menu_id The term ID for the given menu.
+ * @param bool $auto_add Whether to auto-add or not.
+ * @return array
+ */
+ protected function filter_nav_menu_options_value( $nav_menu_options, $menu_id, $auto_add ) {
+ $nav_menu_options = (array) $nav_menu_options;
+ if ( ! isset( $nav_menu_options['auto_add'] ) ) {
+ $nav_menu_options['auto_add'] = array();
+ }
+
+ $i = array_search( $menu_id, $nav_menu_options['auto_add'] );
+ if ( $auto_add && false === $i ) {
+ array_push( $nav_menu_options['auto_add'], $this->term_id );
+ } else if ( ! $auto_add && false !== $i ) {
+ array_splice( $nav_menu_options['auto_add'], $i, 1 );
+ }
+
+ return $nav_menu_options;
+ }
+
+ /**
+ * Export data for the JS client.
+ *
+ * @since 4.3.0
+ *
+ * @see WP_Customize_Nav_Menu_Setting::update()
+ *
+ * @param array $data Additional information passed back to the 'saved' event on `wp.customize`.
+ * @return array
+ */
+ function amend_customize_save_response( $data ) {
+ if ( ! isset( $data['nav_menu_updates'] ) ) {
+ $data['nav_menu_updates'] = array();
+ }
+
+ $data['nav_menu_updates'][] = array(
+ 'term_id' => $this->term_id,
+ 'previous_term_id' => $this->previous_term_id,
+ 'error' => $this->update_error ? $this->update_error->get_error_code() : null,
+ 'status' => $this->update_status,
+ );
+
+ return $data;
+ }
+}
</ins></span></pre></div>
<a id="trunksrcwpincludescsscustomizepreviewcss"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/src/wp-includes/css/customize-preview.css</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/css/customize-preview.css (rev 0)
+++ trunk/src/wp-includes/css/customize-preview.css 2015-06-16 22:07:08 UTC (rev 32806)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,6 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+.customize-partial-refreshing {
+ opacity: 0.25;
+ -webkit-transition: opacity 0.25s;
+ transition: opacity 0.25s;
+ cursor: progress;
+}
</ins></span></pre></div>
<a id="trunksrcwpincludesjscustomizepreviewnavmenusjs"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/src/wp-includes/js/customize-preview-nav-menus.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/js/customize-preview-nav-menus.js (rev 0)
+++ trunk/src/wp-includes/js/customize-preview-nav-menus.js 2015-06-16 22:07:08 UTC (rev 32806)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,242 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+/*global jQuery, JSON, _wpCustomizePreviewNavMenusExports, _ */
+
+wp.customize.menusPreview = ( function( $, api ) {
+ 'use strict';
+ var self;
+
+ self = {
+ renderQueryVar: null,
+ renderNonceValue: null,
+ renderNoncePostKey: null,
+ previewCustomizeNonce: null,
+ previewReady: $.Deferred(),
+ requestUri: '/',
+ theme: {
+ active: false,
+ stylesheet: ''
+ },
+ navMenuInstanceArgs: {},
+ refreshDebounceDelay: 200
+ };
+
+ api.bind( 'preview-ready', function() {
+ self.previewReady.resolve();
+ } );
+ self.previewReady.done( function() {
+ self.init();
+ } );
+
+ /**
+ * Bootstrap functionality.
+ */
+ self.init = function() {
+ var self = this;
+
+ if ( 'undefined' !== typeof _wpCustomizePreviewNavMenusExports ) {
+ $.extend( self, _wpCustomizePreviewNavMenusExports );
+ }
+
+ self.previewReady.done( function() {
+ api.each( function( setting, id ) {
+ setting.id = id;
+ self.bindListener( setting );
+ } );
+
+ api.preview.bind( 'setting', function( args ) {
+ var id, value, setting;
+ args = args.slice();
+ id = args.shift();
+ value = args.shift();
+ if ( ! api.has( id ) ) {
+ // Currently customize-preview.js is not creating settings for dynamically-created settings in the pane; so we have to do it
+ setting = api.create( id, value ); // @todo This should be in core
+ setting.id = id;
+ if ( self.bindListener( setting ) ) {
+ setting.callbacks.fireWith( setting, [ setting(), setting() ] );
+ }
+ }
+ } );
+ } );
+ };
+
+ /**
+ *
+ * @param {wp.customize.Value} setting
+ * @returns {boolean} Whether the setting was bound.
+ */
+ self.bindListener = function( setting ) {
+ var matches, themeLocation;
+
+ matches = setting.id.match( /^nav_menu\[(-?\d+)]$/ );
+ if ( matches ) {
+ setting.navMenuId = parseInt( matches[1], 10 );
+ setting.bind( self.onChangeNavMenuSetting );
+ return true;
+ }
+
+ matches = setting.id.match( /^nav_menu_item\[(-?\d+)]$/ );
+ if ( matches ) {
+ setting.navMenuItemId = parseInt( matches[1], 10 );
+ setting.bind( self.onChangeNavMenuItemSetting );
+ return true;
+ }
+
+ matches = setting.id.match( /^nav_menu_locations\[(.+?)]/ );
+ if ( matches ) {
+ themeLocation = matches[1];
+ setting.bind( function() {
+ self.refreshMenuLocation( themeLocation );
+ } );
+ return true;
+ }
+
+ return false;
+ };
+
+ /**
+ * Handle changing of a nav_menu setting.
+ *
+ * @this {wp.customize.Setting}
+ */
+ self.onChangeNavMenuSetting = function() {
+ var setting = this;
+ if ( ! setting.navMenuId ) {
+ throw new Error( 'Expected navMenuId property to be set.' );
+ }
+ self.refreshMenu( setting.navMenuId );
+ };
+
+ /**
+ * Handle changing of a nav_menu_item setting.
+ *
+ * @this {wp.customize.Setting}
+ * @param {object} to
+ * @param {object} from
+ */
+ self.onChangeNavMenuItemSetting = function( to, from ) {
+ if ( from && from.nav_menu_term_id && ( ! to || from.nav_menu_term_id !== to.nav_menu_term_id ) ) {
+ self.refreshMenu( from.nav_menu_term_id );
+ }
+ if ( to && to.nav_menu_term_id ) {
+ self.refreshMenu( to.nav_menu_term_id );
+ }
+ };
+
+ /**
+ * Update a given menu rendered in the preview.
+ *
+ * @param {int} menuId
+ */
+ self.refreshMenu = function( menuId ) {
+ var self = this, assignedLocations = [];
+
+ api.each(function( setting, id ) {
+ var matches = id.match( /^nav_menu_locations\[(.+?)]/ );
+ if ( matches && menuId === setting() ) {
+ assignedLocations.push( matches[1] );
+ }
+ });
+
+ _.each( self.navMenuInstanceArgs, function( navMenuArgs, instanceNumber ) {
+ if ( menuId === navMenuArgs.menu || -1 !== _.indexOf( assignedLocations, navMenuArgs.theme_location ) ) {
+ self.refreshMenuInstanceDebounced( instanceNumber );
+ }
+ } );
+ };
+
+ self.refreshMenuLocation = function( location ) {
+ var foundInstance = false;
+ _.each( self.navMenuInstanceArgs, function( navMenuArgs, instanceNumber ) {
+ if ( location === navMenuArgs.theme_location ) {
+ self.refreshMenuInstanceDebounced( instanceNumber );
+ foundInstance = true;
+ }
+ } );
+ if ( ! foundInstance ) {
+ api.preview.send( 'refresh' );
+ }
+ };
+
+ /**
+ * Update a specific instance of a given menu on the page.
+ *
+ * @param {int} instanceNumber
+ */
+ self.refreshMenuInstance = function( instanceNumber ) {
+ var self = this, data, customized, container, request, wpNavArgs, instance;
+
+ if ( ! self.navMenuInstanceArgs[ instanceNumber ] ) {
+ throw new Error( 'unknown_instance_number' );
+ }
+ instance = self.navMenuInstanceArgs[ instanceNumber ];
+
+ container = $( '#partial-refresh-menu-container-' + String( instanceNumber ) );
+
+ if ( ! instance.can_partial_refresh || 0 === container.length ) {
+ api.preview.send( 'refresh' );
+ return;
+ }
+
+ data = {
+ nonce: self.previewCustomizeNonce, // for Customize Preview
+ wp_customize: 'on'
+ };
+ if ( ! self.theme.active ) {
+ data.theme = self.theme.stylesheet;
+ }
+ data[ self.renderQueryVar ] = '1';
+ customized = {};
+ api.each( function( setting, id ) {
+ // @todo We need to limit this to just the menu items that are associated with this menu/location.
+ if ( /^(nav_menu|nav_menu_locations)/.test( id ) ) {
+ customized[ id ] = setting.get();
+ }
+ } );
+ data.customized = JSON.stringify( customized );
+ data[ self.renderNoncePostKey ] = self.renderNonceValue;
+
+ wpNavArgs = $.extend( {}, instance );
+ data.wp_nav_menu_args_hash = wpNavArgs.args_hash;
+ delete wpNavArgs.args_hash;
+ data.wp_nav_menu_args = JSON.stringify( wpNavArgs );
+
+ container.addClass( 'customize-partial-refreshing' );
+
+ request = wp.ajax.send( null, {
+ data: data,
+ url: self.requestUri
+ } );
+ request.done( function( data ) {
+ var eventParam;
+ container.empty().append( $( data ) );
+ eventParam = {
+ instanceNumber: instanceNumber,
+ wpNavArgs: wpNavArgs
+ };
+ $( document ).trigger( 'customize-preview-menu-refreshed', [ eventParam ] );
+ } );
+ request.fail( function() {
+ // @todo provide some indication for why
+ } );
+ request.always( function() {
+ container.removeClass( 'customize-partial-refreshing' );
+ } );
+ };
+
+ self.currentRefreshMenuInstanceDebouncedCalls = {};
+
+ self.refreshMenuInstanceDebounced = function( instanceNumber ) {
+ if ( self.currentRefreshMenuInstanceDebouncedCalls[ instanceNumber ] ) {
+ clearTimeout( self.currentRefreshMenuInstanceDebouncedCalls[ instanceNumber ] );
+ }
+ self.currentRefreshMenuInstanceDebouncedCalls[ instanceNumber ] = setTimeout(
+ function() {
+ self.refreshMenuInstance( instanceNumber );
+ },
+ self.refreshDebounceDelay
+ );
+ };
+
+ return self;
+
+}( jQuery, wp.customize ) );
</ins></span></pre></div>
<a id="trunksrcwpincludesscriptloaderphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/script-loader.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/script-loader.php 2015-06-16 21:32:53 UTC (rev 32805)
+++ trunk/src/wp-includes/script-loader.php 2015-06-16 22:07:08 UTC (rev 32806)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -406,6 +406,9 @@
</span><span class="cx" style="display: block; padding: 0 10px"> $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 );
</span><span class="cx" style="display: block; padding: 0 10px"> $scripts->add( 'customize-preview-widgets', "/wp-includes/js/customize-preview-widgets$suffix.js", array( 'jquery', 'wp-util', 'customize-preview' ), false, 1 );
</span><span class="cx" style="display: block; padding: 0 10px">
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $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 );
+ $scripts->add( 'customize-preview-nav-menus', "/wp-includes/js/customize-preview-nav-menus$suffix.js", array( 'jquery', 'wp-util', 'customize-preview' ), false, 1 );
+
</ins><span class="cx" style="display: block; padding: 0 10px"> $scripts->add( 'accordion', "/wp-admin/js/accordion$suffix.js", array( 'jquery' ), false, 1 );
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> $scripts->add( 'shortcode', "/wp-includes/js/shortcode$suffix.js", array( 'underscore' ), false, 1 );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -656,15 +659,16 @@
</span><span class="cx" style="display: block; padding: 0 10px"> $suffix = SCRIPT_DEBUG ? '' : '.min';
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> // Admin CSS
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- $styles->add( 'wp-admin', "/wp-admin/css/wp-admin$suffix.css", array( 'open-sans', 'dashicons' ) );
- $styles->add( 'login', "/wp-admin/css/login$suffix.css", array( 'buttons', 'open-sans', 'dashicons' ) );
- $styles->add( 'install', "/wp-admin/css/install$suffix.css", array( 'buttons', 'open-sans' ) );
- $styles->add( 'wp-color-picker', "/wp-admin/css/color-picker$suffix.css" );
- $styles->add( 'customize-controls', "/wp-admin/css/customize-controls$suffix.css", array( 'wp-admin', 'colors', 'ie', 'imgareaselect' ) );
- $styles->add( 'customize-widgets', "/wp-admin/css/customize-widgets$suffix.css", array( 'wp-admin', 'colors' ) );
- $styles->add( 'press-this', "/wp-admin/css/press-this$suffix.css", array( 'open-sans', 'buttons' ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $styles->add( 'wp-admin', "/wp-admin/css/wp-admin$suffix.css", array( 'open-sans', 'dashicons' ) );
+ $styles->add( 'login', "/wp-admin/css/login$suffix.css", array( 'buttons', 'open-sans', 'dashicons' ) );
+ $styles->add( 'install', "/wp-admin/css/install$suffix.css", array( 'buttons', 'open-sans' ) );
+ $styles->add( 'wp-color-picker', "/wp-admin/css/color-picker$suffix.css" );
+ $styles->add( 'customize-controls', "/wp-admin/css/customize-controls$suffix.css", array( 'wp-admin', 'colors', 'ie', 'imgareaselect' ) );
+ $styles->add( 'customize-widgets', "/wp-admin/css/customize-widgets$suffix.css", array( 'wp-admin', 'colors' ) );
+ $styles->add( 'customize-nav-menus', "/wp-admin/css/customize-nav-menus$suffix.css", array( 'wp-admin', 'colors' ) );
+ $styles->add( 'press-this', "/wp-admin/css/press-this$suffix.css", array( 'open-sans', 'buttons' ) );
</ins><span class="cx" style="display: block; padding: 0 10px">
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- $styles->add( 'ie', "/wp-admin/css/ie$suffix.css" );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $styles->add( 'ie', "/wp-admin/css/ie$suffix.css" );
</ins><span class="cx" style="display: block; padding: 0 10px"> $styles->add_data( 'ie', 'conditional', 'lte IE 7' );
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> // Common dependencies
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -673,11 +677,12 @@
</span><span class="cx" style="display: block; padding: 0 10px"> $styles->add( 'open-sans', $open_sans_font_url );
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> // Includes CSS
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- $styles->add( 'admin-bar', "/wp-includes/css/admin-bar$suffix.css", array( 'open-sans', 'dashicons' ) );
- $styles->add( 'wp-auth-check', "/wp-includes/css/wp-auth-check$suffix.css", array( 'dashicons' ) );
- $styles->add( 'editor-buttons', "/wp-includes/css/editor$suffix.css", array( 'dashicons' ) );
- $styles->add( 'media-views', "/wp-includes/css/media-views$suffix.css", array( 'buttons', 'dashicons', 'wp-mediaelement' ) );
- $styles->add( 'wp-pointer', "/wp-includes/css/wp-pointer$suffix.css", array( 'dashicons' ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $styles->add( 'admin-bar', "/wp-includes/css/admin-bar$suffix.css", array( 'open-sans', 'dashicons' ) );
+ $styles->add( 'wp-auth-check', "/wp-includes/css/wp-auth-check$suffix.css", array( 'dashicons' ) );
+ $styles->add( 'editor-buttons', "/wp-includes/css/editor$suffix.css", array( 'dashicons' ) );
+ $styles->add( 'media-views', "/wp-includes/css/media-views$suffix.css", array( 'buttons', 'dashicons', 'wp-mediaelement' ) );
+ $styles->add( 'wp-pointer', "/wp-includes/css/wp-pointer$suffix.css", array( 'dashicons' ) );
+ $styles->add( 'customize-preview', "/wp-includes/css/customize-preview$suffix.css" );
</ins><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> // External libraries and friends
</span><span class="cx" style="display: block; padding: 0 10px"> $styles->add( 'imgareaselect', '/wp-includes/js/imgareaselect/imgareaselect.css', array(), '0.9.8' );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -695,7 +700,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> // RTL CSS
</span><span class="cx" style="display: block; padding: 0 10px"> $rtl_styles = array(
</span><span class="cx" style="display: block; padding: 0 10px"> // wp-admin
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- 'wp-admin', 'install', 'wp-color-picker', 'customize-controls', 'customize-widgets', 'ie', 'login', 'press-this',
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ 'wp-admin', 'install', 'wp-color-picker', 'customize-controls', 'customize-widgets', 'customize-nav-menus', 'ie', 'login', 'press-this',
</ins><span class="cx" style="display: block; padding: 0 10px"> // wp-includes
</span><span class="cx" style="display: block; padding: 0 10px"> 'buttons', 'admin-bar', 'wp-auth-check', 'editor-buttons', 'media-views', 'wp-pointer',
</span><span class="cx" style="display: block; padding: 0 10px"> 'wp-jquery-ui-dialog',
</span></span></pre></div>
<a id="trunktestsphpunittestscustomizenavmenuitemsettingphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/customize/nav-menu-item-setting.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/customize/nav-menu-item-setting.php (rev 0)
+++ trunk/tests/phpunit/tests/customize/nav-menu-item-setting.php 2015-06-16 22:07:08 UTC (rev 32806)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,625 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Tests WP_Customize_Nav_Menu_Item_Setting.
+ *
+ * @group customize
+ */
+class Test_WP_Customize_Nav_Menu_Item_Setting extends WP_UnitTestCase {
+
+ /**
+ * Instance of WP_Customize_Manager which is reset for each test.
+ *
+ * @var WP_Customize_Manager
+ */
+ public $wp_customize;
+
+ /**
+ * Set up a test case.
+ *
+ * @see WP_UnitTestCase::setup()
+ */
+ function setUp() {
+ parent::setUp();
+ require_once ABSPATH . WPINC . '/class-wp-customize-manager.php';
+ wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );
+
+ global $wp_customize;
+ $this->wp_customize = new WP_Customize_Manager();
+ $wp_customize = $this->wp_customize;
+ }
+
+ /**
+ * Delete the $wp_customize global when cleaning up scope.
+ */
+ function clean_up_global_scope() {
+ global $wp_customize;
+ $wp_customize = null;
+ parent::clean_up_global_scope();
+ }
+
+ /**
+ * Test constants and statics.
+ */
+ function test_constants() {
+ do_action( 'customize_register', $this->wp_customize );
+ $this->assertTrue( post_type_exists( WP_Customize_Nav_Menu_Item_Setting::POST_TYPE ) );
+ }
+
+ /**
+ * Test constructor.
+ *
+ * @see WP_Customize_Nav_Menu_Item_Setting::__construct()
+ */
+ function test_construct() {
+ do_action( 'customize_register', $this->wp_customize );
+
+ $setting = new WP_Customize_Nav_Menu_Item_Setting( $this->wp_customize, 'nav_menu_item[123]' );
+ $this->assertEquals( 'nav_menu_item', $setting->type );
+ $this->assertEquals( 'postMessage', $setting->transport );
+ $this->assertEquals( 123, $setting->post_id );
+ $this->assertNull( $setting->previous_post_id );
+ $this->assertNull( $setting->update_status );
+ $this->assertNull( $setting->update_error );
+ $this->assertInternalType( 'array', $setting->default );
+
+ $default = array(
+ 'object_id' => 0,
+ 'object' => '',
+ 'menu_item_parent' => 0,
+ 'position' => 0,
+ 'type' => 'custom',
+ 'title' => '',
+ 'url' => '',
+ 'target' => '',
+ 'attr_title' => '',
+ 'description' => '',
+ 'classes' => '',
+ 'xfn' => '',
+ 'status' => 'publish',
+ 'original_title' => '',
+ 'nav_menu_term_id' => 0,
+ );
+ $this->assertEquals( $default, $setting->default );
+
+ $exception = null;
+ try {
+ $bad_setting = new WP_Customize_Nav_Menu_Item_Setting( $this->wp_customize, 'foo_bar_baz' );
+ unset( $bad_setting );
+ } catch ( Exception $e ) {
+ $exception = $e;
+ }
+ $this->assertInstanceOf( 'Exception', $exception );
+ }
+
+ /**
+ * Test empty constructor.
+ */
+ function test_construct_empty_menus() {
+ do_action( 'customize_register', $this->wp_customize );
+ $_wp_customize = $this->wp_customize;
+ unset($_wp_customize->nav_menus);
+
+ $exception = null;
+ try {
+ $bad_setting = new WP_Customize_Nav_Menu_Item_Setting( $_wp_customize, 'nav_menu_item[123]' );
+ unset( $bad_setting );
+ } catch ( Exception $e ) {
+ $exception = $e;
+ }
+ $this->assertInstanceOf( 'Exception', $exception );
+ }
+
+ /**
+ * Test constructor for placeholder (draft) menu.
+ *
+ * @see WP_Customize_Nav_Menu_Item_Setting::__construct()
+ */
+ function test_construct_placeholder() {
+ do_action( 'customize_register', $this->wp_customize );
+ $default = array(
+ 'title' => 'Lorem',
+ 'description' => 'ipsum',
+ 'menu_item_parent' => 123,
+ );
+ $setting = new WP_Customize_Nav_Menu_Item_Setting( $this->wp_customize, 'nav_menu_item[-5]', compact( 'default' ) );
+ $this->assertEquals( -5, $setting->post_id );
+ $this->assertNull( $setting->previous_post_id );
+ $this->assertEquals( $default, $setting->default );
+ }
+
+ /**
+ * Test value method with post.
+ *
+ * @see WP_Customize_Nav_Menu_Item_Setting::value()
+ */
+ function test_value_type_post_type() {
+ do_action( 'customize_register', $this->wp_customize );
+
+ $post_id = $this->factory->post->create( array( 'post_title' => 'Hello World' ) );
+
+ $menu_id = wp_create_nav_menu( 'Menu' );
+ $item_title = 'Greetings';
+ $item_id = wp_update_nav_menu_item( $menu_id, 0, array(
+ 'menu-item-type' => 'post_type',
+ 'menu-item-object' => 'post',
+ 'menu-item-object-id' => $post_id,
+ 'menu-item-title' => $item_title,
+ 'menu-item-status' => 'publish',
+ ) );
+
+ $post = get_post( $item_id );
+ $menu_item = wp_setup_nav_menu_item( $post );
+ $this->assertEquals( $item_title, $menu_item->title );
+
+ $setting_id = "nav_menu_item[$item_id]";
+ $setting = new WP_Customize_Nav_Menu_Item_Setting( $this->wp_customize, $setting_id );
+
+ $value = $setting->value();
+ $this->assertEquals( $menu_item->title, $value['title'] );
+ $this->assertEquals( $menu_item->type, $value['type'] );
+ $this->assertEquals( $menu_item->object_id, $value['object_id'] );
+ $this->assertEquals( $menu_id, $value['nav_menu_term_id'] );
+ $this->assertEquals( 'Hello World', $value['original_title'] );
+
+ $other_menu_id = wp_create_nav_menu( 'Menu2' );
+ wp_update_nav_menu_item( $other_menu_id, $item_id, array(
+ 'menu-item-title' => 'Hola',
+ ) );
+ $value = $setting->value();
+ $this->assertEquals( 'Hola', $value['title'] );
+ $this->assertEquals( $other_menu_id, $value['nav_menu_term_id'] );
+ }
+
+ /**
+ * Test value method with taxonomy.
+ *
+ * @see WP_Customize_Nav_Menu_Item_Setting::value()
+ */
+ function test_value_type_taxonomy() {
+ do_action( 'customize_register', $this->wp_customize );
+
+ $tax_id = $this->factory->category->create( array( 'name' => 'Salutations' ) );
+
+ $menu_id = wp_create_nav_menu( 'Menu' );
+ $item_title = 'Greetings';
+ $item_id = wp_update_nav_menu_item( $menu_id, 0, array(
+ 'menu-item-type' => 'taxonomy',
+ 'menu-item-object' => 'category',
+ 'menu-item-object-id' => $tax_id,
+ 'menu-item-title' => $item_title,
+ 'menu-item-status' => 'publish',
+ ) );
+
+ $post = get_post( $item_id );
+ $menu_item = wp_setup_nav_menu_item( $post );
+ $this->assertEquals( $item_title, $menu_item->title );
+
+ $setting_id = "nav_menu_item[$item_id]";
+ $setting = new WP_Customize_Nav_Menu_Item_Setting( $this->wp_customize, $setting_id );
+
+ $value = $setting->value();
+ $this->assertEquals( $menu_item->title, $value['title'] );
+ $this->assertEquals( $menu_item->type, $value['type'] );
+ $this->assertEquals( $menu_item->object_id, $value['object_id'] );
+ $this->assertEquals( $menu_id, $value['nav_menu_term_id'] );
+ $this->assertEquals( 'Salutations', $value['original_title'] );
+ }
+
+ /**
+ * Test value method returns zero for nav_menu_term_id when previewing a new menu.
+ *
+ * @see WP_Customize_Nav_Menu_Item_Setting::value()
+ */
+ function test_value_nav_menu_term_id_returns_zero() {
+ do_action( 'customize_register', $this->wp_customize );
+
+ $menu_id = -123;
+ $post_value = array(
+ 'name' => 'Secondary',
+ 'description' => '',
+ 'parent' => 0,
+ 'auto_add' => false,
+ );
+ $setting_id = "nav_menu[$menu_id]";
+ $menu = new WP_Customize_Nav_Menu_Setting( $this->wp_customize, $setting_id );
+
+ $this->wp_customize->set_post_value( $menu->id, $post_value );
+ $menu->preview();
+ $value = $menu->value();
+ $this->assertEquals( $post_value, $value );
+
+ $post_id = $this->factory->post->create( array( 'post_title' => 'Hello World' ) );
+ $item_id = wp_update_nav_menu_item( $menu_id, 0, array(
+ 'menu-item-type' => 'post_type',
+ 'menu-item-object' => 'post',
+ 'menu-item-object-id' => $post_id,
+ 'menu-item-title' => 'Hello World',
+ 'menu-item-status' => 'publish',
+ ) );
+
+ $post = get_post( $item_id );
+ $menu_item = wp_setup_nav_menu_item( $post );
+
+ $setting_id = "nav_menu_item[$item_id]";
+ $setting = new WP_Customize_Nav_Menu_Item_Setting( $this->wp_customize, $setting_id );
+ $value = $setting->value();
+ $this->assertEquals( 0, $value['nav_menu_term_id'] );
+ }
+
+ /**
+ * Test preview method for updated menu.
+ *
+ * @see WP_Customize_Nav_Menu_Item_Setting::preview()
+ */
+ function test_preview_updated() {
+ do_action( 'customize_register', $this->wp_customize );
+
+ $first_post_id = $this->factory->post->create( array( 'post_title' => 'Hello World' ) );
+ $second_post_id = $this->factory->post->create( array( 'post_title' => 'Hola Muno' ) );
+
+ $primary_menu_id = wp_create_nav_menu( 'Primary' );
+ $secondary_menu_id = wp_create_nav_menu( 'Secondary' );
+ $item_title = 'Greetings';
+ $item_id = wp_update_nav_menu_item( $primary_menu_id, 0, array(
+ 'menu-item-type' => 'post_type',
+ 'menu-item-object' => 'post',
+ 'menu-item-object-id' => $first_post_id,
+ 'menu-item-title' => $item_title,
+ 'menu-item-status' => 'publish',
+ ) );
+ $this->assertNotEmpty( wp_get_nav_menu_items( $primary_menu_id, array( 'post_status' => 'publish,draft' ) ) );
+
+ $post_value = array(
+ 'type' => 'post_type',
+ 'object' => 'post',
+ 'object_id' => $second_post_id,
+ 'title' => 'Saludos',
+ 'status' => 'publish',
+ 'nav_menu_term_id' => $secondary_menu_id,
+ );
+ $setting_id = "nav_menu_item[$item_id]";
+ $setting = new WP_Customize_Nav_Menu_Item_Setting( $this->wp_customize, $setting_id );
+ $this->wp_customize->set_post_value( $setting_id, $post_value );
+ unset( $post_value['nav_menu_term_id'] );
+ $setting->preview();
+
+ // Make sure the menu item appears in the new menu.
+ $this->assertNotContains( $item_id, wp_list_pluck( wp_get_nav_menu_items( $primary_menu_id ), 'db_id' ) );
+ $menu_items = wp_get_nav_menu_items( $secondary_menu_id );
+ $db_ids = wp_list_pluck( $menu_items, 'db_id' );
+ $this->assertContains( $item_id, $db_ids );
+ $i = array_search( $item_id, $db_ids );
+ $updated_item = $menu_items[ $i ];
+ $post_value['post_status'] = $post_value['status'];
+ unset( $post_value['status'] );
+ foreach ( $post_value as $key => $value ) {
+ $this->assertEquals( $value, $updated_item->$key, "Key $key mismatch" );
+ }
+ }
+
+ /**
+ * Test preview method for inserted menu.
+ *
+ * @see WP_Customize_Nav_Menu_Item_Setting::preview()
+ */
+ function test_preview_inserted() {
+ do_action( 'customize_register', $this->wp_customize );
+
+ $menu_id = wp_create_nav_menu( 'Primary' );
+ $post_id = $this->factory->post->create( array( 'post_title' => 'Hello World' ) );
+ $item_ids = array();
+ for ( $i = 0; $i < 5; $i += 1 ) {
+ $item_id = wp_update_nav_menu_item( $menu_id, 0, array(
+ 'menu-item-type' => 'post_type',
+ 'menu-item-object' => 'post',
+ 'menu-item-object-id' => $post_id,
+ 'menu-item-title' => "Item $i",
+ 'menu-item-status' => 'publish',
+ 'menu-item-position' => $i + 1,
+ ) );
+ $item_ids[] = $item_id;
+ }
+
+ $post_value = array(
+ 'type' => 'post_type',
+ 'object' => 'post',
+ 'object_id' => $post_id,
+ 'title' => 'Inserted item',
+ 'status' => 'publish',
+ 'nav_menu_term_id' => $menu_id,
+ 'position' => count( $item_ids ) + 1,
+ );
+
+ $new_item_id = -10;
+ $setting_id = "nav_menu_item[$new_item_id]";
+ $setting = new WP_Customize_Nav_Menu_Item_Setting( $this->wp_customize, $setting_id );
+ $this->wp_customize->set_post_value( $setting_id, $post_value );
+ unset( $post_value['nav_menu_term_id'] );
+
+ $current_items = wp_get_nav_menu_items( $menu_id );
+ $setting->preview();
+ $preview_items = wp_get_nav_menu_items( $menu_id );
+ $this->assertNotEquals( count( $current_items ), count( $preview_items ) );
+
+ $last_item = array_pop( $preview_items );
+ $this->assertEquals( $new_item_id, $last_item->db_id );
+ $post_value['post_status'] = $post_value['status'];
+ unset( $post_value['status'] );
+ $post_value['menu_order'] = $post_value['position'];
+ unset( $post_value['position'] );
+ foreach ( $post_value as $key => $value ) {
+ $this->assertEquals( $value, $last_item->$key, "Mismatch for $key property." );
+ }
+ }
+
+ /**
+ * Test preview method for deleted menu.
+ *
+ * @see WP_Customize_Nav_Menu_Item_Setting::preview()
+ */
+ function test_preview_deleted() {
+ do_action( 'customize_register', $this->wp_customize );
+
+ $menu_id = wp_create_nav_menu( 'Primary' );
+ $post_id = $this->factory->post->create( array( 'post_title' => 'Hello World' ) );
+ $item_ids = array();
+ for ( $i = 0; $i < 5; $i += 1 ) {
+ $item_id = wp_update_nav_menu_item( $menu_id, 0, array(
+ 'menu-item-type' => 'post_type',
+ 'menu-item-object' => 'post',
+ 'menu-item-object-id' => $post_id,
+ 'menu-item-title' => "Item $i",
+ 'menu-item-status' => 'publish',
+ 'menu-item-position' => $i + 1,
+ ) );
+ $item_ids[] = $item_id;
+ }
+
+ $delete_item_id = $item_ids[2];
+ $setting_id = "nav_menu_item[$delete_item_id]";
+ $setting = new WP_Customize_Nav_Menu_Item_Setting( $this->wp_customize, $setting_id );
+ $this->wp_customize->set_post_value( $setting_id, false );
+
+ $current_items = wp_get_nav_menu_items( $menu_id );
+ $this->assertContains( $delete_item_id, wp_list_pluck( $current_items, 'db_id' ) );
+ $setting->preview();
+ $preview_items = wp_get_nav_menu_items( $menu_id );
+ $this->assertNotEquals( count( $current_items ), count( $preview_items ) );
+ $this->assertContains( $delete_item_id, wp_list_pluck( $current_items, 'db_id' ) );
+ }
+
+ /**
+ * Test sanitize method.
+ *
+ * @see WP_Customize_Nav_Menu_Item_Setting::sanitize()
+ */
+ function test_sanitize() {
+ do_action( 'customize_register', $this->wp_customize );
+ $setting = new WP_Customize_Nav_Menu_Item_Setting( $this->wp_customize, 'nav_menu_item[123]' );
+
+ $this->assertNull( $setting->sanitize( 'not an array' ) );
+ $this->assertNull( $setting->sanitize( 123 ) );
+
+ $unsanitized = array(
+ 'object_id' => 'bad',
+ 'object' => '<b>hello</b>',
+ 'menu_item_parent' => 'asdasd',
+ 'position' => -123,
+ 'type' => 'custom<b>',
+ 'title' => 'Hi<script>alert(1)</script>',
+ 'url' => 'javascript:alert(1)',
+ 'target' => '" onclick="',
+ 'attr_title' => '<b>evil</b>',
+ 'description' => '<b>Hello world</b>',
+ 'classes' => 'hello " inject="',
+ 'xfn' => 'hello " inject="',
+ 'status' => 'forbidden',
+ 'original_title' => 'Hi<script>alert(1)</script>',
+ 'nav_menu_term_id' => 'heilo',
+ );
+
+ $sanitized = $setting->sanitize( $unsanitized );
+ $this->assertEqualSets( array_keys( $unsanitized ), array_keys( $sanitized ) );
+
+ $this->assertEquals( 0, $sanitized['object_id'] );
+ $this->assertEquals( 'bhellob', $sanitized['object'] );
+ $this->assertEquals( 0, $sanitized['menu_item_parent'] );
+ $this->assertEquals( 0, $sanitized['position'] );
+ $this->assertEquals( 'customb', $sanitized['type'] );
+ $this->assertEquals( 'Hi', $sanitized['title'] );
+ $this->assertEquals( '', $sanitized['url'] );
+ $this->assertEquals( 'onclick', $sanitized['target'] );
+ $this->assertEquals( 'evil', $sanitized['attr_title'] );
+ $this->assertEquals( 'Hello world', $sanitized['description'] );
+ $this->assertEquals( 'hello inject', $sanitized['classes'] );
+ $this->assertEquals( 'hello inject', $sanitized['xfn'] );
+ $this->assertEquals( 'publish', $sanitized['status'] );
+ $this->assertEquals( 'Hi', $sanitized['original_title'] );
+ $this->assertEquals( 0, $sanitized['nav_menu_term_id'] );
+ }
+
+ /**
+ * Test protected update() method via the save() method, for updated menu.
+ *
+ * @see WP_Customize_Nav_Menu_Item_Setting::update()
+ */
+ function test_save_updated() {
+ do_action( 'customize_register', $this->wp_customize );
+
+ $first_post_id = $this->factory->post->create( array( 'post_title' => 'Hello World' ) );
+ $second_post_id = $this->factory->post->create( array( 'post_title' => 'Hola Muno' ) );
+
+ $primary_menu_id = wp_create_nav_menu( 'Primary' );
+ $secondary_menu_id = wp_create_nav_menu( 'Secondary' );
+ $item_title = 'Greetings';
+ $item_id = wp_update_nav_menu_item( $primary_menu_id, 0, array(
+ 'menu-item-type' => 'post_type',
+ 'menu-item-object' => 'post',
+ 'menu-item-object-id' => $first_post_id,
+ 'menu-item-title' => $item_title,
+ 'menu-item-status' => 'publish',
+ ) );
+ $this->assertNotEmpty( wp_get_nav_menu_items( $primary_menu_id, array( 'post_status' => 'publish,draft' ) ) );
+
+ $post_value = array(
+ 'type' => 'post_type',
+ 'object' => 'post',
+ 'object_id' => $second_post_id,
+ 'title' => 'Saludos',
+ 'status' => 'publish',
+ 'nav_menu_term_id' => $secondary_menu_id,
+ );
+ $setting_id = "nav_menu_item[$item_id]";
+ $setting = new WP_Customize_Nav_Menu_Item_Setting( $this->wp_customize, $setting_id );
+ $this->wp_customize->set_post_value( $setting_id, $post_value );
+ unset( $post_value['nav_menu_term_id'] );
+ $setting->save();
+
+ // Make sure the menu item appears in the new menu.
+ $this->assertNotContains( $item_id, wp_list_pluck( wp_get_nav_menu_items( $primary_menu_id ), 'db_id' ) );
+ $menu_items = wp_get_nav_menu_items( $secondary_menu_id );
+ $db_ids = wp_list_pluck( $menu_items, 'db_id' );
+ $this->assertContains( $item_id, $db_ids );
+ $i = array_search( $item_id, $db_ids );
+ $updated_item = $menu_items[ $i ];
+ $post_value['post_status'] = $post_value['status'];
+ unset( $post_value['status'] );
+ foreach ( $post_value as $key => $value ) {
+ $this->assertEquals( $value, $updated_item->$key, "Key $key mismatch" );
+ }
+
+ // Verify the Ajax responses is being amended.
+ $save_response = apply_filters( 'customize_save_response', array() );
+ $this->assertArrayHasKey( 'nav_menu_item_updates', $save_response );
+ $update_result = array_shift( $save_response['nav_menu_item_updates'] );
+ $this->assertArrayHasKey( 'post_id', $update_result );
+ $this->assertArrayHasKey( 'previous_post_id', $update_result );
+ $this->assertArrayHasKey( 'error', $update_result );
+ $this->assertArrayHasKey( 'status', $update_result );
+
+ $this->assertEquals( $item_id, $update_result['post_id'] );
+ $this->assertNull( $update_result['previous_post_id'] );
+ $this->assertNull( $update_result['error'] );
+ $this->assertEquals( 'updated', $update_result['status'] );
+ }
+
+ /**
+ * Test protected update() method via the save() method, for inserted menu.
+ *
+ * @see WP_Customize_Nav_Menu_Item_Setting::update()
+ */
+ function test_save_inserted() {
+ do_action( 'customize_register', $this->wp_customize );
+
+ $menu_id = wp_create_nav_menu( 'Primary' );
+ $post_id = $this->factory->post->create( array( 'post_title' => 'Hello World' ) );
+ $item_ids = array();
+ for ( $i = 0; $i < 5; $i += 1 ) {
+ $item_id = wp_update_nav_menu_item( $menu_id, 0, array(
+ 'menu-item-type' => 'post_type',
+ 'menu-item-object' => 'post',
+ 'menu-item-object-id' => $post_id,
+ 'menu-item-title' => "Item $i",
+ 'menu-item-status' => 'publish',
+ 'menu-item-position' => $i + 1,
+ ) );
+ $item_ids[] = $item_id;
+ }
+
+ $post_value = array(
+ 'type' => 'post_type',
+ 'object' => 'post',
+ 'object_id' => $post_id,
+ 'title' => 'Inserted item',
+ 'status' => 'publish',
+ 'nav_menu_term_id' => $menu_id,
+ 'position' => count( $item_ids ) + 1,
+ );
+
+ $new_item_id = -10;
+ $setting_id = "nav_menu_item[$new_item_id]";
+ $setting = new WP_Customize_Nav_Menu_Item_Setting( $this->wp_customize, $setting_id );
+ $this->wp_customize->set_post_value( $setting_id, $post_value );
+ unset( $post_value['nav_menu_term_id'] );
+
+ $current_items = wp_get_nav_menu_items( $menu_id );
+ $setting->save();
+ $preview_items = wp_get_nav_menu_items( $menu_id );
+ $this->assertNotEquals( count( $current_items ), count( $preview_items ) );
+
+ $last_item = array_pop( $preview_items );
+ $this->assertEquals( $setting->post_id, $last_item->db_id );
+ $post_value['post_status'] = $post_value['status'];
+ unset( $post_value['status'] );
+ $post_value['menu_order'] = $post_value['position'];
+ unset( $post_value['position'] );
+ foreach ( $post_value as $key => $value ) {
+ $this->assertEquals( $value, $last_item->$key, "Mismatch for $key property." );
+ }
+
+ // Verify the Ajax responses is being amended.
+ $save_response = apply_filters( 'customize_save_response', array() );
+ $this->assertArrayHasKey( 'nav_menu_item_updates', $save_response );
+ $update_result = array_shift( $save_response['nav_menu_item_updates'] );
+ $this->assertArrayHasKey( 'post_id', $update_result );
+ $this->assertArrayHasKey( 'previous_post_id', $update_result );
+ $this->assertArrayHasKey( 'error', $update_result );
+ $this->assertArrayHasKey( 'status', $update_result );
+
+ $this->assertEquals( $setting->post_id, $update_result['post_id'] );
+ $this->assertEquals( $new_item_id, $update_result['previous_post_id'] );
+ $this->assertNull( $update_result['error'] );
+ $this->assertEquals( 'inserted', $update_result['status'] );
+ }
+
+ /**
+ * Test protected update() method via the save() method, for deleted menu.
+ *
+ * @see WP_Customize_Nav_Menu_Item_Setting::update()
+ */
+ function test_save_deleted() {
+ do_action( 'customize_register', $this->wp_customize );
+
+ $menu_id = wp_create_nav_menu( 'Primary' );
+ $post_id = $this->factory->post->create( array( 'post_title' => 'Hello World' ) );
+ $item_ids = array();
+ for ( $i = 0; $i < 5; $i += 1 ) {
+ $item_id = wp_update_nav_menu_item( $menu_id, 0, array(
+ 'menu-item-type' => 'post_type',
+ 'menu-item-object' => 'post',
+ 'menu-item-object-id' => $post_id,
+ 'menu-item-title' => "Item $i",
+ 'menu-item-status' => 'publish',
+ 'menu-item-position' => $i + 1,
+ ) );
+ $item_ids[] = $item_id;
+ }
+
+ $delete_item_id = $item_ids[2];
+ $setting_id = "nav_menu_item[$delete_item_id]";
+ $setting = new WP_Customize_Nav_Menu_Item_Setting( $this->wp_customize, $setting_id );
+ $this->wp_customize->set_post_value( $setting_id, false );
+
+ $current_items = wp_get_nav_menu_items( $menu_id );
+ $this->assertContains( $delete_item_id, wp_list_pluck( $current_items, 'db_id' ) );
+ $setting->save();
+ $preview_items = wp_get_nav_menu_items( $menu_id );
+ $this->assertNotEquals( count( $current_items ), count( $preview_items ) );
+ $this->assertContains( $delete_item_id, wp_list_pluck( $current_items, 'db_id' ) );
+
+ // Verify the Ajax responses is being amended.
+ $save_response = apply_filters( 'customize_save_response', array() );
+ $this->assertArrayHasKey( 'nav_menu_item_updates', $save_response );
+ $update_result = array_shift( $save_response['nav_menu_item_updates'] );
+ $this->assertArrayHasKey( 'post_id', $update_result );
+ $this->assertArrayHasKey( 'previous_post_id', $update_result );
+ $this->assertArrayHasKey( 'error', $update_result );
+ $this->assertArrayHasKey( 'status', $update_result );
+
+ $this->assertEquals( $delete_item_id, $update_result['post_id'] );
+ $this->assertNull( $update_result['previous_post_id'] );
+ $this->assertNull( $update_result['error'] );
+ $this->assertEquals( 'deleted', $update_result['status'] );
+ }
+
+}
</ins></span></pre></div>
<a id="trunktestsphpunittestscustomizenavmenusettingphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/customize/nav-menu-setting.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/customize/nav-menu-setting.php (rev 0)
+++ trunk/tests/phpunit/tests/customize/nav-menu-setting.php 2015-06-16 22:07:08 UTC (rev 32806)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,461 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+/**
+ * Tests WP_Customize_Nav_Menu_Setting.
+ *
+ * @group customize
+ */
+class Test_WP_Customize_Nav_Menu_Setting extends WP_UnitTestCase {
+
+ /**
+ * Instance of WP_Customize_Manager which is reset for each test.
+ *
+ * @var WP_Customize_Manager
+ */
+ public $wp_customize;
+
+ /**
+ * Set up a test case.
+ *
+ * @see WP_UnitTestCase::setup()
+ */
+ function setUp() {
+ parent::setUp();
+ require_once ABSPATH . WPINC . '/class-wp-customize-manager.php';
+ wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );
+
+ global $wp_customize;
+ $this->wp_customize = new WP_Customize_Manager();
+ $wp_customize = $this->wp_customize;
+ }
+
+ /**
+ * Delete the $wp_customize global when cleaning up scope.
+ */
+ function clean_up_global_scope() {
+ global $wp_customize;
+ $wp_customize = null;
+ parent::clean_up_global_scope();
+ }
+
+ /**
+ * Helper for getting the nav_menu_options option.
+ *
+ * @return array
+ */
+ function get_nav_menu_items_option() {
+ return get_option( 'nav_menu_options', array( 'auto_add' => array() ) );
+ }
+
+ /**
+ * Test constants and statics.
+ */
+ function test_constants() {
+ do_action( 'customize_register', $this->wp_customize );
+ $this->assertTrue( taxonomy_exists( WP_Customize_Nav_Menu_Setting::TAXONOMY ) );
+ }
+
+ /**
+ * Test constructor.
+ *
+ * @see WP_Customize_Nav_Menu_Setting::__construct()
+ */
+ function test_construct() {
+ do_action( 'customize_register', $this->wp_customize );
+
+ $setting = new WP_Customize_Nav_Menu_Setting( $this->wp_customize, 'nav_menu[123]' );
+ $this->assertEquals( 'nav_menu', $setting->type );
+ $this->assertEquals( 'postMessage', $setting->transport );
+ $this->assertEquals( 123, $setting->term_id );
+ $this->assertNull( $setting->previous_term_id );
+ $this->assertNull( $setting->update_status );
+ $this->assertNull( $setting->update_error );
+ $this->assertInternalType( 'array', $setting->default );
+ foreach ( array( 'name', 'description', 'parent' ) as $key ) {
+ $this->assertArrayHasKey( $key, $setting->default );
+ }
+ $this->assertEquals( '', $setting->default['name'] );
+ $this->assertEquals( '', $setting->default['description'] );
+ $this->assertEquals( 0, $setting->default['parent'] );
+
+ $exception = null;
+ try {
+ $bad_setting = new WP_Customize_Nav_Menu_Setting( $this->wp_customize, 'foo_bar_baz' );
+ unset( $bad_setting );
+ } catch ( Exception $e ) {
+ $exception = $e;
+ }
+ $this->assertInstanceOf( 'Exception', $exception );
+ }
+
+ /**
+ * Test empty constructor.
+ */
+ function test_construct_empty_menus() {
+ do_action( 'customize_register', $this->wp_customize );
+ $_wp_customize = $this->wp_customize;
+ unset($_wp_customize->nav_menus);
+
+ $exception = null;
+ try {
+ $bad_setting = new WP_Customize_Nav_Menu_Setting( $_wp_customize, 'nav_menu_item[123]' );
+ unset( $bad_setting );
+ } catch ( Exception $e ) {
+ $exception = $e;
+ }
+ $this->assertInstanceOf( 'Exception', $exception );
+ }
+
+ /**
+ * Test constructor for placeholder (draft) menu.
+ *
+ * @see WP_Customize_Nav_Menu_Setting::__construct()
+ */
+ function test_construct_placeholder() {
+ do_action( 'customize_register', $this->wp_customize );
+ $default = array(
+ 'name' => 'Lorem',
+ 'description' => 'ipsum',
+ 'parent' => 123,
+ );
+ $setting = new WP_Customize_Nav_Menu_Setting( $this->wp_customize, 'nav_menu[-5]', compact( 'default' ) );
+ $this->assertEquals( -5, $setting->term_id );
+ $this->assertEquals( $default, $setting->default );
+ }
+
+ /**
+ * Test value method.
+ *
+ * @see WP_Customize_Nav_Menu_Setting::value()
+ */
+ function test_value() {
+ do_action( 'customize_register', $this->wp_customize );
+
+ $menu_name = 'Test 123';
+ $parent_menu_id = wp_create_nav_menu( "Parent $menu_name" );
+ $description = 'Hello my world.';
+ $menu_id = wp_update_nav_menu_object( 0, array(
+ 'menu-name' => $menu_name,
+ 'parent' => $parent_menu_id,
+ 'description' => $description,
+ ) );
+
+ $setting_id = "nav_menu[$menu_id]";
+ $setting = new WP_Customize_Nav_Menu_Setting( $this->wp_customize, $setting_id );
+
+ $value = $setting->value();
+ $this->assertInternalType( 'array', $value );
+ foreach ( array( 'name', 'description', 'parent' ) as $key ) {
+ $this->assertArrayHasKey( $key, $value );
+ }
+ $this->assertEquals( $menu_name, $value['name'] );
+ $this->assertEquals( $description, $value['description'] );
+ $this->assertEquals( $parent_menu_id, $value['parent'] );
+
+ $new_menu_name = 'Foo';
+ wp_update_nav_menu_object( $menu_id, array( 'menu-name' => $new_menu_name ) );
+ $updated_value = $setting->value();
+ $this->assertEquals( $new_menu_name, $updated_value['name'] );
+ }
+
+ /**
+ * Test preview method for updated menu.
+ *
+ * @see WP_Customize_Nav_Menu_Setting::preview()
+ */
+ function test_preview_updated() {
+ do_action( 'customize_register', $this->wp_customize );
+
+ $menu_id = wp_update_nav_menu_object( 0, array(
+ 'menu-name' => 'Name 1',
+ 'description' => 'Description 1',
+ 'parent' => 0,
+ ) );
+ $setting_id = "nav_menu[$menu_id]";
+ $setting = new WP_Customize_Nav_Menu_Setting( $this->wp_customize, $setting_id );
+
+ $nav_menu_options = $this->get_nav_menu_items_option();
+ $this->assertNotContains( $menu_id, $nav_menu_options['auto_add'] );
+
+ $post_value = array(
+ 'name' => 'Name 2',
+ 'description' => 'Description 2',
+ 'parent' => 1,
+ 'auto_add' => true,
+ );
+ $this->wp_customize->set_post_value( $setting_id, $post_value );
+
+ $value = $setting->value();
+ $this->assertEquals( 'Name 1', $value['name'] );
+ $this->assertEquals( 'Description 1', $value['description'] );
+ $this->assertEquals( 0, $value['parent'] );
+
+ $term = (array) wp_get_nav_menu_object( $menu_id );
+
+ $this->assertEqualSets(
+ wp_array_slice_assoc( $value, array( 'name', 'description', 'parent' ) ),
+ wp_array_slice_assoc( $term, array( 'name', 'description', 'parent' ) )
+ );
+
+ $setting->preview();
+ $value = $setting->value();
+ $this->assertEquals( 'Name 2', $value['name'] );
+ $this->assertEquals( 'Description 2', $value['description'] );
+ $this->assertEquals( 1, $value['parent'] );
+ $term = (array) wp_get_nav_menu_object( $menu_id );
+ $this->assertEqualSets( $value, wp_array_slice_assoc( $term, array_keys( $value ) ) );
+
+ $menu_object = wp_get_nav_menu_object( $menu_id );
+ $this->assertEquals( (object) $term, $menu_object );
+ $this->assertEquals( $post_value['name'], $menu_object->name );
+
+ $nav_menu_options = get_option( 'nav_menu_options', array( 'auto_add' => array() ) );
+ $this->assertContains( $menu_id, $nav_menu_options['auto_add'] );
+ }
+
+ /**
+ * Test preview method for inserted menu.
+ *
+ * @see WP_Customize_Nav_Menu_Setting::preview()
+ */
+ function test_preview_inserted() {
+ do_action( 'customize_register', $this->wp_customize );
+
+ $menu_id = -123;
+ $post_value = array(
+ 'name' => 'New Menu Name 1',
+ 'description' => 'New Menu Description 1',
+ 'parent' => 0,
+ 'auto_add' => false,
+ );
+ $setting_id = "nav_menu[$menu_id]";
+ $setting = new WP_Customize_Nav_Menu_Setting( $this->wp_customize, $setting_id );
+
+ $this->wp_customize->set_post_value( $setting->id, $post_value );
+ $setting->preview();
+ $value = $setting->value();
+ $this->assertEquals( $post_value, $value );
+
+ $term = (array) wp_get_nav_menu_object( $menu_id );
+ $this->assertNotEmpty( $term );
+ $this->assertNotInstanceOf( 'WP_Error', $term );
+ $this->assertEqualSets( $post_value, wp_array_slice_assoc( $term, array_keys( $value ) ) );
+ $this->assertEquals( $menu_id, $term['term_id'] );
+ $this->assertEquals( $menu_id, $term['term_taxonomy_id'] );
+
+ $menu_object = wp_get_nav_menu_object( $menu_id );
+ $this->assertEquals( (object) $term, $menu_object );
+ $this->assertEquals( $post_value['name'], $menu_object->name );
+
+ $nav_menu_options = $this->get_nav_menu_items_option();
+ $this->assertNotContains( $menu_id, $nav_menu_options['auto_add'] );
+ }
+
+ /**
+ * Test preview method for deleted menu.
+ *
+ * @see WP_Customize_Nav_Menu_Setting::preview()
+ */
+ function test_preview_deleted() {
+ do_action( 'customize_register', $this->wp_customize );
+
+ $menu_id = wp_update_nav_menu_object( 0, array(
+ 'menu-name' => 'Name 1',
+ 'description' => 'Description 1',
+ 'parent' => 0,
+ ) );
+ $setting_id = "nav_menu[$menu_id]";
+ $setting = new WP_Customize_Nav_Menu_Setting( $this->wp_customize, $setting_id );
+ $nav_menu_options = $this->get_nav_menu_items_option();
+ $nav_menu_options['auto_add'][] = $menu_id;
+ update_option( 'nav_menu_options', $nav_menu_options );
+
+ $nav_menu_options = $this->get_nav_menu_items_option();
+ $this->assertContains( $menu_id, $nav_menu_options['auto_add'] );
+
+ $this->wp_customize->set_post_value( $setting_id, false );
+
+ $this->assertInternalType( 'array', $setting->value() );
+ $this->assertInternalType( 'object', wp_get_nav_menu_object( $menu_id ) );
+ $setting->preview();
+ $this->assertFalse( $setting->value() );
+ $this->assertFalse( wp_get_nav_menu_object( $menu_id ) );
+
+ $nav_menu_options = $this->get_nav_menu_items_option();
+ $this->assertNotContains( $menu_id, $nav_menu_options['auto_add'] );
+ }
+
+ /**
+ * Test sanitize method.
+ *
+ * @see WP_Customize_Nav_Menu_Setting::sanitize()
+ */
+ function test_sanitize() {
+ do_action( 'customize_register', $this->wp_customize );
+ $setting = new WP_Customize_Nav_Menu_Setting( $this->wp_customize, 'nav_menu[123]' );
+
+ $this->assertNull( $setting->sanitize( 'not an array' ) );
+ $this->assertNull( $setting->sanitize( 123 ) );
+
+ $value = array(
+ 'name' => ' Hello <b>world</b> ',
+ 'description' => "New\nline",
+ 'parent' => -12,
+ 'auto_add' => true,
+ 'extra' => 'ignored',
+ );
+ $sanitized = $setting->sanitize( $value );
+ $this->assertEquals( 'Hello <b>world</b>', $sanitized['name'] );
+ $this->assertEquals( 'New line', $sanitized['description'] );
+ $this->assertEquals( 0, $sanitized['parent'] );
+ $this->assertEquals( true, $sanitized['auto_add'] );
+ $this->assertEqualSets( array( 'name', 'description', 'parent', 'auto_add' ), array_keys( $sanitized ) );
+ }
+
+ /**
+ * Test protected update() method via the save() method, for updated menu.
+ *
+ * @see WP_Customize_Nav_Menu_Setting::update()
+ */
+ function test_save_updated() {
+ do_action( 'customize_register', $this->wp_customize );
+
+ $menu_id = wp_update_nav_menu_object( 0, array(
+ 'menu-name' => 'Name 1',
+ 'description' => 'Description 1',
+ 'parent' => 0,
+ ) );
+ $nav_menu_options = $this->get_nav_menu_items_option();
+ $nav_menu_options['auto_add'][] = $menu_id;
+ update_option( 'nav_menu_options', $nav_menu_options );
+
+ $setting_id = "nav_menu[$menu_id]";
+ $setting = new WP_Customize_Nav_Menu_Setting( $this->wp_customize, $setting_id );
+
+ $auto_add = false;
+ $new_value = array(
+ 'name' => 'Name 2',
+ 'description' => 'Description 2',
+ 'parent' => 1,
+ 'auto_add' => $auto_add,
+ );
+
+ $this->wp_customize->set_post_value( $setting_id, $new_value );
+ $setting->save();
+
+ $menu_object = wp_get_nav_menu_object( $menu_id );
+ foreach ( array( 'name', 'description', 'parent' ) as $key ) {
+ $this->assertEquals( $new_value[ $key ], $menu_object->$key );
+ }
+ $this->assertEqualSets(
+ wp_array_slice_assoc( $new_value, array( 'name', 'description', 'parent' ) ),
+ wp_array_slice_assoc( (array) $menu_object, array( 'name', 'description', 'parent' ) )
+ );
+ $this->assertEquals( $new_value, $setting->value() );
+
+ $save_response = apply_filters( 'customize_save_response', array() );
+ $this->assertArrayHasKey( 'nav_menu_updates', $save_response );
+ $update_result = array_shift( $save_response['nav_menu_updates'] );
+ $this->assertArrayHasKey( 'term_id', $update_result );
+ $this->assertArrayHasKey( 'previous_term_id', $update_result );
+ $this->assertArrayHasKey( 'error', $update_result );
+ $this->assertArrayHasKey( 'status', $update_result );
+
+ $this->assertEquals( $menu_id, $update_result['term_id'] );
+ $this->assertNull( $update_result['previous_term_id'] );
+ $this->assertNull( $update_result['error'] );
+ $this->assertEquals( 'updated', $update_result['status'] );
+
+ $nav_menu_options = $this->get_nav_menu_items_option();
+ $this->assertNotContains( $menu_id, $nav_menu_options['auto_add'] );
+ }
+
+ /**
+ * Test protected update() method via the save() method, for inserted menu.
+ *
+ * @see WP_Customize_Nav_Menu_Setting::update()
+ */
+ function test_save_inserted() {
+ do_action( 'customize_register', $this->wp_customize );
+
+ $menu_id = -123;
+ $post_value = array(
+ 'name' => 'New Menu Name 1',
+ 'description' => 'New Menu Description 1',
+ 'parent' => 0,
+ 'auto_add' => true,
+ );
+ $setting_id = "nav_menu[$menu_id]";
+ $setting = new WP_Customize_Nav_Menu_Setting( $this->wp_customize, $setting_id );
+
+ $this->wp_customize->set_post_value( $setting->id, $post_value );
+
+ $this->assertNull( $setting->previous_term_id );
+ $this->assertLessThan( 0, $setting->term_id );
+ $setting->save();
+ $this->assertEquals( $menu_id, $setting->previous_term_id );
+ $this->assertGreaterThan( 0, $setting->term_id );
+
+ $nav_menu_options = $this->get_nav_menu_items_option();
+ $this->assertContains( $setting->term_id, $nav_menu_options['auto_add'] );
+
+ $menu = wp_get_nav_menu_object( $setting->term_id );
+ unset( $post_value['auto_add'] );
+ $this->assertEqualSets( $post_value, wp_array_slice_assoc( (array) $menu, array_keys( $post_value ) ) );
+
+ $save_response = apply_filters( 'customize_save_response', array() );
+ $this->assertArrayHasKey( 'nav_menu_updates', $save_response );
+ $update_result = array_shift( $save_response['nav_menu_updates'] );
+ $this->assertArrayHasKey( 'term_id', $update_result );
+ $this->assertArrayHasKey( 'previous_term_id', $update_result );
+ $this->assertArrayHasKey( 'error', $update_result );
+ $this->assertArrayHasKey( 'status', $update_result );
+
+ $this->assertEquals( $menu->term_id, $update_result['term_id'] );
+ $this->assertEquals( $menu_id, $update_result['previous_term_id'] );
+ $this->assertNull( $update_result['error'] );
+ $this->assertEquals( 'inserted', $update_result['status'] );
+ }
+
+ /**
+ * Test protected update() method via the save() method, for deleted menu.
+ *
+ * @see WP_Customize_Nav_Menu_Setting::update()
+ */
+ function test_save_deleted() {
+ do_action( 'customize_register', $this->wp_customize );
+
+ $menu_name = 'Lorem Ipsum';
+ $menu_id = wp_create_nav_menu( $menu_name );
+ $setting_id = "nav_menu[$menu_id]";
+ $setting = new WP_Customize_Nav_Menu_Setting( $this->wp_customize, $setting_id );
+ $nav_menu_options = $this->get_nav_menu_items_option();
+ $nav_menu_options['auto_add'][] = $menu_id;
+ update_option( 'nav_menu_options', $nav_menu_options );
+
+ $menu = wp_get_nav_menu_object( $menu_id );
+ $this->assertEquals( $menu_name, $menu->name );
+
+ $this->wp_customize->set_post_value( $setting_id, false );
+ $setting->save();
+
+ $this->assertFalse( wp_get_nav_menu_object( $menu_id ) );
+
+ $save_response = apply_filters( 'customize_save_response', array() );
+ $this->assertArrayHasKey( 'nav_menu_updates', $save_response );
+ $update_result = array_shift( $save_response['nav_menu_updates'] );
+ $this->assertArrayHasKey( 'term_id', $update_result );
+ $this->assertArrayHasKey( 'previous_term_id', $update_result );
+ $this->assertArrayHasKey( 'error', $update_result );
+ $this->assertArrayHasKey( 'status', $update_result );
+
+ $this->assertEquals( $menu_id, $update_result['term_id'] );
+ $this->assertNull( $update_result['previous_term_id'] );
+ $this->assertNull( $update_result['error'] );
+ $this->assertEquals( 'deleted', $update_result['status'] );
+
+ $nav_menu_options = $this->get_nav_menu_items_option();
+ $this->assertNotContains( $menu_id, $nav_menu_options['auto_add'] );
+ }
+
+}
</ins></span></pre></div>
<a id="trunktestsphpunittestscustomizenavmenusphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/customize/nav-menus.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/customize/nav-menus.php (rev 0)
+++ trunk/tests/phpunit/tests/customize/nav-menus.php 2015-06-16 22:07:08 UTC (rev 32806)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,456 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+/**
+ * Tests WP_Customize_Nav_Menus.
+ *
+ * @group customize
+ */
+class Test_WP_Customize_Nav_Menus extends WP_UnitTestCase {
+
+ /**
+ * Instance of WP_Customize_Manager which is reset for each test.
+ *
+ * @var WP_Customize_Manager
+ */
+ public $wp_customize;
+
+ /**
+ * Set up a test case.
+ *
+ * @see WP_UnitTestCase::setup()
+ */
+ function setUp() {
+ parent::setUp();
+ require_once ABSPATH . WPINC . '/class-wp-customize-manager.php';
+ wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );
+ global $wp_customize;
+ $this->wp_customize = new WP_Customize_Manager();
+ $wp_customize = $this->wp_customize;
+ }
+
+ /**
+ * Delete the $wp_customize global when cleaning up scope.
+ */
+ function clean_up_global_scope() {
+ global $wp_customize;
+ $wp_customize = null;
+ parent::clean_up_global_scope();
+ }
+
+ /**
+ * Test constructor.
+ *
+ * @see WP_Customize_Nav_Menus::__construct()
+ */
+ function test_construct() {
+ do_action( 'customize_register', $this->wp_customize );
+ $menus = new WP_Customize_Nav_Menus( $this->wp_customize );
+ $this->assertInstanceOf( 'WP_Customize_Manager', $menus->manager );
+ }
+
+ /**
+ * Test the test_load_available_items_ajax method.
+ *
+ * @see WP_Customize_Nav_Menus::load_available_items_ajax()
+ */
+ function test_load_available_items_ajax() {
+
+ $this->markTestIncomplete( 'This test has not been implemented.' );
+
+ }
+
+ /**
+ * Test the search_available_items_ajax method.
+ *
+ * @see WP_Customize_Nav_Menus::search_available_items_ajax()
+ */
+ function test_search_available_items_ajax() {
+
+ $this->markTestIncomplete( 'This test has not been implemented.' );
+
+ }
+
+ /**
+ * Test the search_available_items_query method.
+ *
+ * @see WP_Customize_Nav_Menus::search_available_items_query()
+ */
+ function test_search_available_items_query() {
+ $menus = new WP_Customize_Nav_Menus( $this->wp_customize );
+
+ // Create posts
+ $post_ids = array();
+ $post_ids[] = $this->factory->post->create( array( 'post_title' => 'Search & Test' ) );
+ $post_ids[] = $this->factory->post->create( array( 'post_title' => 'Some Other Title' ) );
+
+ // Create terms
+ $term_ids = array();
+ $term_ids[] = $this->factory->category->create( array( 'name' => 'Dogs Are Cool' ) );
+ $term_ids[] = $this->factory->category->create( array( 'name' => 'Cats Drool' ) );
+
+ // Test empty results
+ $expected = array();
+ $results = $menus->search_available_items_query( array( 'pagenum' => 1, 's' => 'This Does NOT Exist' ) );
+ $this->assertEquals( $expected, $results );
+
+ // Test posts
+ foreach ( $post_ids as $post_id ) {
+ $expected = array(
+ 'id' => 'post-' . $post_id,
+ 'type' => 'post_type',
+ 'type_label' => get_post_type_object( 'post' )->labels->singular_name,
+ 'object' => 'post',
+ 'object_id' => intval( $post_id ),
+ 'title' => html_entity_decode( get_the_title( $post_id ) ),
+ );
+ wp_set_object_terms( $post_id, $term_ids, 'category' );
+ $search = $post_id === $post_ids[0] ? 'test & search' : 'other title';
+ $s = sanitize_text_field( wp_unslash( $search ) );
+ $results = $menus->search_available_items_query( array( 'pagenum' => 1, 's' => $s ) );
+ $this->assertEquals( $expected, $results[0] );
+ }
+
+ // Test terms
+ foreach ( $term_ids as $term_id ) {
+ $term = get_term_by( 'id', $term_id, 'category' );
+ $expected = array(
+ 'id' => 'term-' . $term_id,
+ 'type' => 'taxonomy',
+ 'type_label' => get_taxonomy( 'category' )->labels->singular_name,
+ 'object' => 'category',
+ 'object_id' => intval( $term_id ),
+ 'title' => $term->name,
+ );
+ $s = sanitize_text_field( wp_unslash( $term->name ) );
+ $results = $menus->search_available_items_query( array( 'pagenum' => 1, 's' => $s ) );
+ $this->assertEquals( $expected, $results[0] );
+ }
+ }
+
+ /**
+ * Test the enqueue method.
+ *
+ * @see WP_Customize_Nav_Menus::enqueue_scripts()
+ */
+ function test_enqueue_scripts() {
+ do_action( 'customize_register', $this->wp_customize );
+ $menus = new WP_Customize_Nav_Menus( $this->wp_customize );
+ $menus->enqueue_scripts();
+ $this->assertTrue( wp_script_is( 'customize-nav-menus' ) );
+ }
+
+ /**
+ * Test the filter_dynamic_setting_args method.
+ *
+ * @see WP_Customize_Nav_Menus::filter_dynamic_setting_args()
+ */
+ function test_filter_dynamic_setting_args() {
+ $menus = new WP_Customize_Nav_Menus( $this->wp_customize );
+
+ $expected = array( 'type' => 'nav_menu_item' );
+ $results = $menus->filter_dynamic_setting_args( $this->wp_customize, 'nav_menu_item[123]' );
+ $this->assertEquals( $expected, $results );
+
+ $expected = array( 'type' => 'nav_menu' );
+ $results = $menus->filter_dynamic_setting_args( $this->wp_customize, 'nav_menu[123]' );
+ $this->assertEquals( $expected, $results );
+ }
+
+ /**
+ * Test the filter_dynamic_setting_class method.
+ *
+ * @see WP_Customize_Nav_Menus::filter_dynamic_setting_class()
+ */
+ function test_filter_dynamic_setting_class() {
+ do_action( 'customize_register', $this->wp_customize );
+ $menus = new WP_Customize_Nav_Menus( $this->wp_customize );
+
+ $expected = 'WP_Customize_Nav_Menu_Item_Setting';
+ $results = $menus->filter_dynamic_setting_class( 'WP_Customize_Setting', 'nav_menu_item[123]', array( 'type' => 'nav_menu_item' ) );
+ $this->assertEquals( $expected, $results );
+
+ $expected = 'WP_Customize_Nav_Menu_Setting';
+ $results = $menus->filter_dynamic_setting_class( 'WP_Customize_Setting', 'nav_menu[123]', array( 'type' => 'nav_menu' ) );
+ $this->assertEquals( $expected, $results );
+ }
+
+ /**
+ * Test the customize_register method.
+ *
+ * @see WP_Customize_Nav_Menus::customize_register()
+ */
+ function test_customize_register() {
+ do_action( 'customize_register', $this->wp_customize );
+ $menu_id = wp_create_nav_menu( 'Primary' );
+ $post_id = $this->factory->post->create( array( 'post_title' => 'Hello World' ) );
+ $item_id = wp_update_nav_menu_item( $menu_id, 0, array(
+ 'menu-item-type' => 'post_type',
+ 'menu-item-object' => 'post',
+ 'menu-item-object-id' => $post_id,
+ 'menu-item-title' => 'Hello World',
+ 'menu-item-status' => 'publish',
+ ) );
+ $setting = new WP_Customize_Nav_Menu_Item_Setting( $this->wp_customize, "nav_menu_item[$item_id]" );
+ do_action( 'customize_register', $this->wp_customize );
+ $this->assertEquals( 'Primary', $this->wp_customize->get_section( "nav_menu[$menu_id]" )->title );
+ $this->assertEquals( 'Hello World', $this->wp_customize->get_control( "nav_menu_item[$item_id]" )->label );
+ }
+
+ /**
+ * Test the intval_base10 method.
+ *
+ * @see WP_Customize_Nav_Menus::intval_base10()
+ */
+ function test_intval_base10() {
+
+ $menus = new WP_Customize_Nav_Menus( $this->wp_customize );
+
+ $this->assertEquals( 2, $menus->intval_base10( 2 ) );
+ $this->assertEquals( 4, $menus->intval_base10( 4.1 ) );
+ $this->assertEquals( 4, $menus->intval_base10( '4' ) );
+ $this->assertEquals( 4, $menus->intval_base10( '04' ) );
+ $this->assertEquals( 42, $menus->intval_base10( +42 ) );
+ $this->assertEquals( -42, $menus->intval_base10( -42 ) );
+ $this->assertEquals( 26, $menus->intval_base10( 0x1A ) );
+ $this->assertEquals( 0, $menus->intval_base10( array() ) );
+ }
+
+ /**
+ * Test the available_item_types method.
+ *
+ * @see WP_Customize_Nav_Menus::available_item_types()
+ */
+ function test_available_item_types() {
+
+ $menus = new WP_Customize_Nav_Menus( $this->wp_customize );
+ $expected = array(
+ 'postTypes' => array(
+ 'post' => array( 'label' => 'Post' ),
+ 'page' => array( 'label' => 'Page' ),
+ ),
+ 'taxonomies' => array(
+ 'category' => array( 'label' => 'Category' ),
+ 'post_tag' => array( 'label' => 'Tag' ),
+ ),
+ );
+ if ( current_theme_supports( 'post-formats' ) ) {
+ $expected['taxonomies']['post_format'] = array( 'label' => 'Format' );
+ }
+ $this->assertEquals( $expected, $menus->available_item_types() );
+
+ register_taxonomy( 'wptests_tax', array( 'post' ), array( 'labels' => array( 'name' => 'Foo' ) ) );
+ $expected = array(
+ 'postTypes' => array(
+ 'post' => array( 'label' => 'Post' ),
+ 'page' => array( 'label' => 'Page' ),
+ ),
+ 'taxonomies' => array(
+ 'category' => array( 'label' => 'Category' ),
+ 'post_tag' => array( 'label' => 'Tag' ),
+ 'wptests_tax' => array( 'label' => 'Foo' ),
+ ),
+ );
+ if ( current_theme_supports( 'post-formats' ) ) {
+ $wptests_tax = array_pop( $expected['taxonomies'] );
+ $expected['taxonomies']['post_format'] = array( 'label' => 'Format' );
+ $expected['taxonomies']['wptests_tax'] = $wptests_tax;
+ }
+ $this->assertEquals( $expected, $menus->available_item_types() );
+
+ }
+
+ /**
+ * Test the print_templates method.
+ *
+ * @see WP_Customize_Nav_Menus::print_templates()
+ */
+ function test_print_templates() {
+ do_action( 'customize_register', $this->wp_customize );
+ $menus = new WP_Customize_Nav_Menus( $this->wp_customize );
+
+ ob_start();
+ $menus->print_templates();
+ $template = ob_get_clean();
+
+ $expected = sprintf(
+ '<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>',
+ esc_html( 'Move up' ),
+ esc_html( 'Move down' ),
+ esc_html( 'Move one level up' ),
+ esc_html( 'Move one level down' )
+ );
+
+ $this->assertContains( $expected, $template );
+ }
+
+ /**
+ * Test the available_items_template method.
+ *
+ * @see WP_Customize_Nav_Menus::available_items_template()
+ */
+ function test_available_items_template() {
+ do_action( 'customize_register', $this->wp_customize );
+ $menus = new WP_Customize_Nav_Menus( $this->wp_customize );
+
+ ob_start();
+ $menus->available_items_template();
+ $template = ob_get_clean();
+
+ $expected = sprintf( 'Customizing ▸ %s', esc_html( $this->wp_customize->get_panel( 'nav_menus' )->title ) );
+
+ $this->assertContains( $expected, $template );
+
+ $post_types = get_post_types( array( 'show_in_nav_menus' => true ), 'object' );
+ if ( $post_types ) {
+ foreach ( $post_types as $type ) {
+ $this->assertContains( 'available-menu-items-' . esc_attr( $type->name ), $template );
+ $this->assertContains( '<h4 class="accordion-section-title">' . esc_html( $type->label ), $template );
+ $this->assertContains( 'data-type="' . esc_attr( $type->name ) . '" data-obj_type="post_type"', $template );
+ }
+ }
+
+ $taxonomies = get_taxonomies( array( 'show_in_nav_menus' => true ), 'object' );
+ if ( $taxonomies ) {
+ foreach ( $taxonomies as $tax ) {
+ $this->assertContains( 'available-menu-items-' . esc_attr( $tax->name ), $template );
+ $this->assertContains( '<h4 class="accordion-section-title">' . esc_html( $tax->label ), $template );
+ $this->assertContains( 'data-type="' . esc_attr( $tax->name ) . '" data-obj_type="taxonomy"', $template );
+ }
+ }
+ }
+
+ /**
+ * Test the customize_preview_init method.
+ *
+ * @see WP_Customize_Nav_Menus::customize_preview_init()
+ */
+ function test_customize_preview_init() {
+ do_action( 'customize_register', $this->wp_customize );
+ $menus = new WP_Customize_Nav_Menus( $this->wp_customize );
+
+ $menus->customize_preview_init();
+ $this->assertEquals( 10, has_action( 'template_redirect', array( $menus, 'render_menu' ) ) );
+ $this->assertEquals( 10, has_action( 'wp_enqueue_scripts', array( $menus, 'customize_preview_enqueue_deps' ) ) );
+
+ if ( ! isset( $_REQUEST[ WP_Customize_Nav_Menus::RENDER_QUERY_VAR ] ) ) {
+ $this->assertEquals( 1000, has_filter( 'wp_nav_menu_args', array( $menus, 'filter_wp_nav_menu_args' ) ) );
+ $this->assertEquals( 10, has_filter( 'wp_nav_menu', array( $menus, 'filter_wp_nav_menu' ) ) );
+ }
+ }
+
+ /**
+ * Test the filter_wp_nav_menu_args method.
+ *
+ * @see WP_Customize_Nav_Menus::filter_wp_nav_menu_args()
+ */
+ function test_filter_wp_nav_menu_args() {
+ do_action( 'customize_register', $this->wp_customize );
+ $menus = new WP_Customize_Nav_Menus( $this->wp_customize );
+
+ $results = $menus->filter_wp_nav_menu_args( array(
+ 'echo' => true,
+ 'fallback_cb' => 'wp_page_menu',
+ 'walker' => '',
+ ) );
+ $this->assertEquals( 1, $results['can_partial_refresh'] );
+
+ $expected = array(
+ 'echo',
+ 'args_hash',
+ 'can_partial_refresh',
+ 'instance_number',
+ );
+ $results = $menus->filter_wp_nav_menu_args( array(
+ 'echo' => false,
+ 'fallback_cb' => 'wp_page_menu',
+ 'walker' => new Walker_Nav_Menu(),
+ ) );
+ $this->assertEqualSets( $expected, array_keys( $results ) );
+ $this->assertEquals( 0, $results['can_partial_refresh'] );
+ }
+
+ /**
+ * Test the filter_wp_nav_menu method.
+ *
+ * @see WP_Customize_Nav_Menus::filter_wp_nav_menu()
+ */
+ function test_filter_wp_nav_menu() {
+ do_action( 'customize_register', $this->wp_customize );
+ $menus = new WP_Customize_Nav_Menus( $this->wp_customize );
+
+ $args = $menus->filter_wp_nav_menu_args( array(
+ 'echo' => true,
+ 'fallback_cb' => 'wp_page_menu',
+ 'walker' => '',
+ ) );
+
+ ob_start();
+ wp_nav_menu( $args );
+ $nav_menu_content = ob_get_clean();
+
+ $object_args = json_decode( json_encode( $args ), false );
+ $result = $menus->filter_wp_nav_menu( $nav_menu_content, $object_args );
+ $expected = sprintf(
+ '<div id="partial-refresh-menu-container-%1$d" class="partial-refresh-menu-container" data-instance-number="%1$d">%2$s</div>',
+ $args['instance_number'],
+ $nav_menu_content
+ );
+ $this->assertEquals( $expected, $result );
+ }
+
+ /**
+ * Test the customize_preview_enqueue_deps method.
+ *
+ * @see WP_Customize_Nav_Menus::customize_preview_enqueue_deps()
+ */
+ function test_customize_preview_enqueue_deps() {
+ do_action( 'customize_register', $this->wp_customize );
+ $menus = new WP_Customize_Nav_Menus( $this->wp_customize );
+
+ $menus->customize_preview_enqueue_deps();
+
+ $this->assertTrue( wp_script_is( 'customize-preview-nav-menus' ) );
+ $this->assertEquals( 10, has_action( 'wp_print_footer_scripts', array( $menus, 'export_preview_data' ) ) );
+ }
+
+ /**
+ * Test the export_preview_data method.
+ *
+ * @see WP_Customize_Nav_Menus::export_preview_data()
+ */
+ function test_export_preview_data() {
+ do_action( 'customize_register', $this->wp_customize );
+ $menus = new WP_Customize_Nav_Menus( $this->wp_customize );
+
+ $request_uri = $_SERVER['REQUEST_URI'];
+
+ ob_start();
+ $_SERVER['REQUEST_URI'] = '/wp-admin';
+ $menus->export_preview_data();
+ $data = ob_get_clean();
+
+ $_SERVER['REQUEST_URI'] = $request_uri;
+
+ $this->assertContains( '_wpCustomizePreviewNavMenusExports', $data );
+ $this->assertContains( 'renderQueryVar', $data );
+ $this->assertContains( 'renderNonceValue', $data );
+ $this->assertContains( 'renderNoncePostKey', $data );
+ $this->assertContains( 'requestUri', $data );
+ $this->assertContains( 'theme', $data );
+ $this->assertContains( 'previewCustomizeNonce', $data );
+ $this->assertContains( 'navMenuInstanceArgs', $data );
+ $this->assertContains( 'requestUri', $data );
+
+ }
+
+ /**
+ * Test the render_menu method.
+ *
+ * @see WP_Customize_Nav_Menus::render_menu()
+ */
+ function test_render_menu() {
+
+ $this->markTestIncomplete( 'This test has not been implemented.' );
+ }
+
+}
</ins></span></pre>
</div>
</div>
</body>
</html>