Make WordPress Core

Ticket #32576: 32576.4.diff

File 32576.4.diff, 195.8 KB (added by obenland, 10 years ago)
  • Gruntfile.js

     
    141141                },
    142142                cssmin: {
    143143                        options: {
    144                                 'wp-admin': ['wp-admin', 'color-picker', 'customize-controls', 'customize-widgets', 'ie', 'install', 'login', 'press-this', 'deprecated-*']
     144                                'wp-admin': ['wp-admin', 'color-picker', 'customize-controls', 'customize-widgets', 'customize-nav-menus', 'ie', 'install', 'login', 'press-this', 'deprecated-*']
    145145                        },
    146146                        core: {
    147147                                expand: true,
  • src/wp-admin/css/customize-nav-menus.css

     
     1/**
     2 * CSS for the Menu Customizer.
     3 *
     4 * Many styles are re-used from the existing Menu screen; these rules make
     5 * a few adaptations so that things fit better in the Customizer.
     6 */
     7
     8#accordion-section-menu_locations {
     9        position: relative;
     10        margin-bottom: 15px;
     11}
     12
     13.menu-in-location,
     14.menu-in-locations {
     15        display: block;
     16        color: #999;
     17        font-weight: 600;
     18        font-size: 10px;
     19}
     20
     21#customize-controls .control-section .accordion-section-title:focus .menu-in-location,
     22#customize-controls .control-section .accordion-section-title:hover .menu-in-location,
     23#customize-controls .control-section .accordion-section-title:focus .menu-in-locations,
     24#customize-controls .control-section .accordion-section-title:hover .menu-in-locations {
     25        color: #fff;
     26}
     27
     28.wp-customizer .menu-item-bar .menu-item-handle,
     29.wp-customizer .menu-item-settings,
     30.wp-customizer .menu-item-settings .description-thin {
     31        -webkit-box-sizing: border-box;
     32        -moz-box-sizing: border-box;
     33        box-sizing: border-box;
     34}
     35
     36.wp-customizer .menu-item-bar {
     37        margin: 0;
     38}
     39
     40.wp-customizer .menu-item-bar .menu-item-handle {
     41        max-width: 100%;
     42        background: #fff;
     43}
     44
     45.wp-customizer .menu-item-handle .item-title {
     46        margin-right: 0;
     47}
     48
     49.wp-customizer .menu-item-handle .item-type {
     50        padding: 1px 15px 0 5px;
     51        float: right;
     52        text-align: right;
     53}
     54
     55.wp-customizer .menu-item-settings {
     56        max-width: 100%;
     57        overflow: hidden;
     58        padding: 10px;
     59        background: #eee;
     60        border: 1px solid #999;
     61        border-top: none;
     62}
     63
     64.wp-customizer .menu-item-settings .description-thin {
     65        width: 100%;
     66        height: auto;
     67        margin: 0 0 8px 0;
     68}
     69
     70.wp-customizer .menu-item-settings input[type="text"] {
     71        width: 100%;
     72}
     73
     74.wp-customizer .menu-item-settings .submitbox {
     75        margin: 0;
     76        padding: 0;
     77}
     78
     79.wp-customizer .menu-item-settings .link-to-original {
     80        padding: 5px 0;
     81        border: none;
     82        font-style: normal;
     83        margin: 0;
     84        width: 100%;
     85}
     86
     87.wp-customizer .menu-item .submitbox .submitdelete {
     88        display: block;
     89        float: left;
     90        margin: 6px 0 0;
     91        padding: 0;
     92        cursor: pointer;
     93}
     94
     95.wp-customizer .menu-item .submitbox .submitdelete:focus {
     96        -webkit-box-shadow: 0 0 0 1px #5b9dd9, 0 0 2px 1px rgba(30, 140, 190, .8);
     97        box-shadow: 0 0 0 1px #5b9dd9, 0 0 2px 1px rgba(30, 140, 190, .8);
     98}
     99
     100/* Menu-item reordering nav. */
     101#customize-theme-controls button.reorder-toggle {
     102        padding: 5px 8px;
     103}
     104
     105.menu-item-reorder-nav {
     106        display: none;
     107        background-color: #fff;
     108        position: absolute;
     109        top: 0;
     110        right: 0;
     111}
     112
     113#customize-theme-controls .reordering .add-new-menu-item {
     114        opacity: 0.2;
     115        pointer-events: none;
     116        cursor: not-allowed;
     117}
     118
     119.menu-item-reorder-nav button {
     120        position: relative;
     121        overflow: hidden;
     122        float: left;
     123        display: block;
     124        width: 30px;
     125        height: 40px;
     126        color: #82878c;
     127        text-indent: -9999px;
     128        cursor: pointer;
     129        background: transparent;
     130        border: none;
     131        -webkit-box-shadow: none;
     132        box-shadow: none;
     133        outline: none;
     134}
     135
     136.menu-item-reorder-nav button:before {
     137        display: inline-block;
     138        position: absolute;
     139        top: 0;
     140        right: 0;
     141        width: 100%;
     142        height: 100%;
     143        font: normal 20px/40px dashicons;
     144        text-align: center;
     145        text-indent: 0;
     146        -webkit-font-smoothing: antialiased;
     147        -moz-osx-font-smoothing: grayscale;
     148}
     149
     150.menu-item-reorder-nav button:hover,
     151.menu-item-reorder-nav button:focus {
     152        color: #191e23;
     153        background: #eee;
     154}
     155
     156.menus-move-down:before {
     157        content: '\f347';
     158}
     159
     160.menus-move-up:before {
     161        content: '\f343';
     162}
     163
     164.menus-move-left:before {
     165        content: '\f341';
     166}
     167
     168.menus-move-right:before {
     169        content: '\f345';
     170}
     171
     172.move-up-disabled .menus-move-up,
     173.move-down-disabled .menus-move-down,
     174.move-right-disabled .menus-move-right,
     175.move-left-disabled .menus-move-left,
     176.menu-item-depth-0 .menus-move-left,
     177.menu-item-depth-10 .menus-move-right {
     178        color: #d5d5d5 !important;
     179        background-color: #fff !important;
     180        cursor: default;
     181        pointer-events: none;
     182}
     183
     184.menu-item-reorder-nav:before {
     185        content: "";
     186        display: block;
     187        position: absolute;
     188        left: -10px;
     189        width: 10px;
     190        height: 40px;
     191        background: -webkit-linear-gradient(left, rgba(250,250,250,0) 0%,rgba(250,250,250,1) 100%);
     192        background: -webkit-gradient(linear, left top, right top, from(rgba(250,250,250,0)), to(rgba(250,250,250,1)));
     193        background: -webkit-linear-gradient(left, rgba(250,250,250,0) 0%, rgba(250,250,250,1) 100%);
     194        background: linear-gradient(to right, rgba(250,250,250,0) 0%,rgba(250,250,250,1) 100%);
     195}
     196
     197.reordering .menu-item .item-controls,
     198.reordering .menu-item .item-type {
     199        display: none;
     200}
     201
     202.reordering .menu-item-reorder-nav {
     203        display: block;
     204}
     205
     206.customize-control input.menu-name-field {
     207        width: 100%; /* Override the 98% default for customizer inputs, to align with the size of menu items. */
     208        margin: 12px 0;
     209}
     210
     211.wp-customizer .menu-item .item-edit {
     212        position: absolute;
     213        right: -19px;
     214        top: 2px;
     215        display: block;
     216        width: 30px;
     217        height: 38px;
     218        margin-right: 0 !important;
     219        text-indent: 100%;
     220        outline: none;
     221        overflow: hidden;
     222        white-space: nowrap;
     223        cursor: pointer;
     224}
     225
     226.customize-control-nav_menu_item .item-edit:focus {
     227        color: #0073aa;
     228        -webkit-box-shadow: 0 0 0 1px #5b9dd9, 0 0 2px 1px rgba(30, 140, 190, .8);
     229        box-shadow: 0 0 0 1px #5b9dd9, 0 0 2px 1px rgba(30, 140, 190, .8);
     230}
     231
     232/* Duplicates `.nav-menus-php .item-edit:before {}` in common.css:2220. */
     233.wp-customizer .menu-item .item-edit:before {
     234        top: -1px;
     235        right: 0;
     236        content: '\f140';
     237        border: none;
     238        background: none;
     239        font: normal 20px/1 dashicons;
     240        speak: none;
     241        display: block;
     242        padding: 0;
     243        text-indent: 0;
     244        text-align: center;
     245        position: relative;
     246        -webkit-font-smoothing: antialiased;
     247        -moz-osx-font-smoothing: grayscale;
     248        text-decoration: none !important;
     249}
     250
     251.wp-customizer .menu-item.menu-item-edit-active .item-edit:before {
     252        content: '\f142';
     253}
     254
     255.wp-customizer .menu-item .item-edit:before {
     256        line-height: 2;
     257}
     258
     259/* Duplicates `.nav-menus-php .menu-item-edit-active .item-edit:before {}` in common.css:2271. */
     260.wp-customizer .menu-item .menu-item-edit-active .item-edit:before {
     261        content: '\f142';
     262}
     263
     264.wp-customizer .menu-item-settings p.description {
     265        font-style: normal;
     266}
     267
     268.wp-customizer .menu-settings dl {
     269        margin: 12px 0 0 0;
     270        padding: 0;
     271}
     272
     273.wp-customizer .menu-settings .checkbox-input {
     274        margin-top: 8px;
     275}
     276
     277.wp-customizer .menu-settings .menu-theme-locations {
     278        border-top: 1px solid #ccc;
     279}
     280
     281.wp-customizer .menu-settings {
     282        margin-top: 36px;
     283        border-top: none;
     284}
     285
     286.menu-settings .customize-control-checkbox label {
     287        line-height: 1;
     288}
     289
     290/* @todo update selector or potentially remove */
     291.menu-settings .customize-control.customize-control-checkbox {
     292        margin-bottom: 8px; /* Override collapsing at smaller viewports. */
     293}
     294
     295.customize-control-menu {
     296        margin-top: 4px;
     297}
     298
     299#customize-controls .customize-info.open.active-menu-screen-options .customize-help-toggle {
     300        color: #555;
     301}
     302
     303/* Screen Options */
     304.customize-screen-options-toggle {
     305        background: none;
     306        border: none;
     307        color: #555;
     308        cursor: pointer;
     309        padding: 20px;
     310        position: absolute;
     311        right: 31px;
     312        top: 4px;
     313}
     314
     315#customize-controls .customize-info .customize-help-toggle {
     316        padding: 20px;
     317}
     318
     319#customize-controls .customize-info .customize-help-toggle:before {
     320        padding: 5px;
     321}
     322
     323.customize-screen-options-toggle:hover,
     324.customize-screen-options-toggle:active,
     325.customize-screen-options-toggle:focus,
     326.active-menu-screen-options .customize-screen-options-toggle,
     327#customize-controls .customize-info.open.active-menu-screen-options .customize-help-toggle:hover,
     328#customize-controls .customize-info.open.active-menu-screen-options .customize-help-toggle:active,
     329#customize-controls .customize-info.open.active-menu-screen-options .customize-help-toggle:focus {
     330        color: #0073aa;
     331}
     332
     333.customize-screen-options-toggle:focus,
     334#customize-controls .customize-info .customize-help-toggle:focus {
     335        outline: none;
     336        -webkit-box-shadow: 0 0 0 1px #5b9dd9, 0 0 2px 1px rgba(30, 140, 190, .8);
     337        box-shadow: 0 0 0 1px #5b9dd9, 0 0 2px 1px rgba(30, 140, 190, .8);
     338}
     339
     340.customize-screen-options-toggle:before {
     341        -moz-osx-font-smoothing: grayscale;
     342        border: none;
     343        content: "\f111";
     344        display: block;
     345        font: 20px/1 "dashicons";
     346        padding: 5px;
     347        text-align: center;
     348        text-decoration: none !important;
     349        text-indent: 0;
     350        left: 5px;
     351        position: absolute;
     352        top: 5px;
     353}
     354
     355.wp-customizer #screen-options-wrap {
     356        display: none;
     357        background: #fff;
     358        border-top: 1px solid #ddd;
     359        padding: 4px 15px 0;
     360}
     361
     362.wp-customizer .metabox-prefs label {
     363        display: block;
     364        padding-right: 0;
     365        line-height: 30px;
     366}
     367
     368#accordion-panel-menus .field-link-target,
     369#accordion-panel-menus .field-attr-title,
     370#accordion-panel-menus .field-css-classes,
     371#accordion-panel-menus .field-xfn,
     372#accordion-panel-menus .field-description {
     373        display: none;
     374}
     375
     376#accordion-panel-menus.field-link-target-active .field-link-target,
     377#accordion-panel-menus.field-attr-title-active .field-attr-title,
     378#accordion-panel-menus.field-css-classes-active .field-css-classes,
     379#accordion-panel-menus.field-xfn-active .field-xfn,
     380#accordion-panel-menus.field-description-active .field-description {
     381        display: block;
     382}
     383
     384
     385/* Not exactly sure what broke this, or why it works without these styles in core. */
     386.wp-customizer .wp-full-overlay a.collapse-sidebar {
     387        position: fixed;
     388        left: 0;
     389}
     390
     391/* WARNING: The 20px factor is hard-coded in JS. */
     392.menu-item-depth-0  { margin-left: 0;     }
     393.menu-item-depth-1  { margin-left: 20px;  }
     394.menu-item-depth-2  { margin-left: 40px;  }
     395.menu-item-depth-3  { margin-left: 60px;  }
     396.menu-item-depth-4  { margin-left: 80px;  }
     397.menu-item-depth-5  { margin-left: 100px; }
     398.menu-item-depth-6  { margin-left: 120px; }
     399.menu-item-depth-7  { margin-left: 140px; }
     400.menu-item-depth-8  { margin-left: 160px; } /* Not likely to be used or useful beyond this depth */
     401.menu-item-depth-9  { margin-left: 180px; }
     402.menu-item-depth-10 { margin-left: 200px; }
     403.menu-item-depth-11 { margin-left: 220px; }
     404
     405/* @todo handle .menu-item-settings width */
     406.menu-item-depth-0  > .menu-item-bar { margin-right: 0;     }
     407.menu-item-depth-1  > .menu-item-bar { margin-right: 20px;  }
     408.menu-item-depth-2  > .menu-item-bar { margin-right: 40px;  }
     409.menu-item-depth-3  > .menu-item-bar { margin-right: 60px;  }
     410.menu-item-depth-4  > .menu-item-bar { margin-right: 80px;  }
     411.menu-item-depth-5  > .menu-item-bar { margin-right: 100px; }
     412.menu-item-depth-6  > .menu-item-bar { margin-right: 120px; }
     413.menu-item-depth-7  > .menu-item-bar { margin-right: 140px; }
     414.menu-item-depth-8  > .menu-item-bar { margin-right: 160px; }
     415.menu-item-depth-9  > .menu-item-bar { margin-right: 180px; }
     416.menu-item-depth-10 > .menu-item-bar { margin-right: 200px; }
     417.menu-item-depth-11 > .menu-item-bar { margin-right: 220px; }
     418
     419/* Submenu left margin. */
     420/* @todo menu-item-transport seems entirely unused. */
     421.menu-item-depth-0  .menu-item-transport { margin-left: 0;      }
     422.menu-item-depth-1  .menu-item-transport { margin-left: -20px;  }
     423.menu-item-depth-3  .menu-item-transport { margin-left: -60px;  }
     424.menu-item-depth-4  .menu-item-transport { margin-left: -80px;  }
     425.menu-item-depth-2  .menu-item-transport { margin-left: -40px;  }
     426.menu-item-depth-5  .menu-item-transport { margin-left: -100px; }
     427.menu-item-depth-6  .menu-item-transport { margin-left: -120px; }
     428.menu-item-depth-7  .menu-item-transport { margin-left: -140px; }
     429.menu-item-depth-8  .menu-item-transport { margin-left: -160px; }
     430.menu-item-depth-9  .menu-item-transport { margin-left: -180px; }
     431.menu-item-depth-10 .menu-item-transport { margin-left: -200px; }
     432.menu-item-depth-11 .menu-item-transport { margin-left: -220px; }
     433
     434/* WARNING: The 20px factor is hard-coded in JS. */
     435.reordering .menu-item-depth-0  { margin-left: 0;     }
     436.reordering .menu-item-depth-1  { margin-left: 15px;  }
     437.reordering .menu-item-depth-2  { margin-left: 30px;  }
     438.reordering .menu-item-depth-3  { margin-left: 45px;  }
     439.reordering .menu-item-depth-4  { margin-left: 60px;  }
     440.reordering .menu-item-depth-5  { margin-left: 75px;  }
     441.reordering .menu-item-depth-6  { margin-left: 90px;  }
     442.reordering .menu-item-depth-7  { margin-left: 105px; }
     443.reordering .menu-item-depth-8  { margin-left: 120px; } /* Not likely to be used or useful beyond this depth */
     444.reordering .menu-item-depth-9  { margin-left: 135px; }
     445.reordering .menu-item-depth-10 { margin-left: 150px; }
     446.reordering .menu-item-depth-11 { margin-left: 165px; }
     447
     448.reordering .menu-item-depth-0  > .menu-item-bar { margin-right: 0;     }
     449.reordering .menu-item-depth-1  > .menu-item-bar { margin-right: 15px;  }
     450.reordering .menu-item-depth-2  > .menu-item-bar { margin-right: 30px;  }
     451.reordering .menu-item-depth-3  > .menu-item-bar { margin-right: 45px;  }
     452.reordering .menu-item-depth-4  > .menu-item-bar { margin-right: 60px;  }
     453.reordering .menu-item-depth-5  > .menu-item-bar { margin-right: 75px;  }
     454.reordering .menu-item-depth-6  > .menu-item-bar { margin-right: 90px;  }
     455.reordering .menu-item-depth-7  > .menu-item-bar { margin-right: 105px; }
     456.reordering .menu-item-depth-8  > .menu-item-bar { margin-right: 120px; }
     457.reordering .menu-item-depth-9  > .menu-item-bar { margin-right: 135px; }
     458.reordering .menu-item-depth-10 > .menu-item-bar { margin-right: 150px; }
     459.reordering .menu-item-depth-11 > .menu-item-bar { margin-right: 165px; }
     460
     461.control-section-nav_menu .menu .menu-item-edit-active {
     462        margin-left: 0;
     463}
     464
     465.control-section-nav_menu .menu .menu-item-edit-active .menu-item-bar {
     466        margin-right: 0;
     467}
     468
     469.control-section-nav_menu .menu .sortable-placeholder {
     470        margin-top: 0;
     471        margin-bottom: 1px;
     472        max-width: -webkit-calc(100% - 2px);
     473        max-width: calc(100% - 2px);
     474        float: left;
     475        display: list-item;
     476        border-color: #a0a5aa;
     477}
     478
     479.control-section-nav_menu .menu ul.menu-item-transport dl {
     480        margin-top: 0;
     481}
     482
     483/*
     484 * Add-menu-items mode.
     485 */
     486.wp-full-overlay-main {
     487        right: auto; /* This overrides a right: 0; which causes the preview to resize rather than slide off screen at the normal size. */
     488        width: 100%;
     489}
     490
     491.adding-menu-items .control-section {
     492        opacity: .4;
     493}
     494
     495.adding-menu-items .control-panel.control-section,
     496.adding-menu-items .control-section.open {
     497        opacity: 1;
     498}
     499
     500/* Add-new button. */
     501#customize-theme-controls .add-new-menu-item {
     502        cursor: pointer;
     503        float: right;
     504        margin-left: 10px;
     505        -webkit-transition: all 0.2s;
     506        transition: all 0.2s;
     507        -webkit-user-select: none;
     508        -moz-user-select: none;
     509        -ms-user-select: none;
     510        user-select: none;
     511        outline: none;
     512}
     513
     514.add-new-menu-item:before {
     515        content: "\f132";
     516        display: inline-block;
     517        position: relative;
     518        left: -2px;
     519        top: -1px;
     520        font: normal 20px/1 'dashicons';
     521        vertical-align: middle;
     522        -webkit-transition: all 0.2s;
     523        transition: all 0.2s;
     524        -webkit-font-smoothing: antialiased;
     525        -moz-osx-font-smoothing: grayscale;
     526}
     527
     528.adding-menu-items .add-new-menu-item,
     529.adding-menu-items .add-new-menu-item:hover,
     530.add-menu-toggle.open,
     531.add-menu-toggle.open:hover {
     532        background: #eee;
     533        border-color: #929793;
     534        color: #32373c;
     535        -webkit-box-shadow: inset 0 2px 5px -3px rgba(0, 0, 0, 0.5);
     536        box-shadow: inset 0 2px 5px -3px rgba(0, 0, 0, 0.5);
     537}
     538
     539.adding-menu-items .add-new-menu-item:before,
     540#accordion-section-add_menu .add-new-menu-item.open:before {
     541        -webkit-transform: rotate(45deg);
     542        -ms-transform: rotate(45deg);
     543        transform: rotate(45deg);
     544}
     545
     546.menu-item-bar .item-delete {
     547        color: #a00;
     548        position: absolute;
     549        top: 2px;
     550        right: -19px;
     551        width: 30px;
     552        height: 38px;
     553        cursor: pointer;
     554        display: none;
     555}
     556
     557.menu-item-bar .item-delete:before {
     558        content: "\f335";
     559        font: normal 20px/1 dashicons;
     560        -webkit-font-smoothing: antialiased;
     561        -moz-osx-font-smoothing: grayscale;
     562        position: absolute;
     563        top: 9px;
     564        left: 5px;
     565}
     566
     567.menu-item-bar .item-delete:hover,
     568.menu-item-bar .item-delete:focus {
     569        color: #f00;
     570}
     571
     572.adding-menu-items .menu-item-bar .item-edit {
     573        display: none;
     574}
     575
     576.adding-menu-items .menu-item-bar .item-delete {
     577        display: block;
     578}
     579
     580#available-menu-items .item {
     581        position: static;
     582}
     583
     584#available-menu-items {
     585        position: absolute;
     586        overflow: hidden;
     587        top: 0;
     588        bottom: 0;
     589        left: -301px;
     590        width: 300px;
     591        margin: 0;
     592        z-index: 4;
     593        background: #eee;
     594        -webkit-transition: all 0.2s;
     595        transition: all 0.2s;
     596        border-right: 1px solid #ddd;
     597}
     598
     599#available-menu-items.allow-scroll {
     600        overflow-y: auto;
     601}
     602
     603#available-menu-items .accordion-section-title {
     604        border-left: none;
     605        border-right: none;
     606        background: #fff;
     607}
     608
     609#available-menu-items .open .accordion-section-title {
     610        background: #eee;
     611}
     612
     613#available-menu-items .open .accordion-section-title:after {
     614        content: '\f142';
     615}
     616
     617#available-menu-items .accordion-section-content {
     618        overflow-y: auto;
     619        max-height: 200px; /* This gets set in JS to fit the screen size, and based on # of sections. */
     620        background: transparent;
     621}
     622
     623button.not-a-button {
     624        background: transparent;
     625        border: none;
     626        -webkit-box-shadow: none;
     627        box-shadow: none;
     628        -webkit-border-radius: 0;
     629        border-radius: 0;
     630        outline: 0;
     631        padding: 0;
     632        margin: 0;
     633}
     634
     635#available-menu-items .accordion-section-title button:focus:before {
     636        display: block;
     637        content: "";
     638        width: 28px;
     639        height: 32px;
     640        position: absolute;
     641        right: 5px;
     642        top: 5px;
     643        -webkit-box-shadow: 0 0 0 1px #5b9dd9, 0 0 2px 1px rgba(30, 140, 190, .8);
     644        box-shadow: 0 0 0 1px #5b9dd9, 0 0 2px 1px rgba(30, 140, 190, .8);
     645}
     646
     647#available-menu-items .accordion-section-content {
     648        padding: 1px 15px 15px 15px;
     649        min-height: 120px;
     650        max-height: 290px;
     651}
     652
     653#custom-menu-item-name.invalid,
     654#custom-menu-item-url.invalid {
     655        border: 1px solid #f00;
     656}
     657
     658#available-menu-items .item-tpl {
     659        position: relative;
     660        padding: 20px 15px 20px 60px;
     661        border-bottom: 1px solid #e4e4e4;
     662        cursor: pointer;
     663        display: none;
     664}
     665
     666#available-menu-items .item-tpl:hover,
     667#available-menu-items .item-tpl.selected {
     668        background: #eee;
     669}
     670
     671#available-menu-items .menu-item-handle .item-type {
     672        padding-right: 0;
     673}
     674
     675#available-menu-items .menu-item-handle .item-title {
     676        padding-left: 20px;
     677}
     678
     679#available-menu-items .menu-item-handle {
     680        cursor: pointer;
     681}
     682
     683#available-menu-items .item-top,
     684#available-menu-items .item-top:hover {
     685        border: none;
     686        background: transparent;
     687        -webkit-box-shadow: none;
     688        box-shadow: none;
     689}
     690
     691#available-menu-items .menu-item-handle {
     692        -webkit-box-shadow: none;
     693        box-shadow: none;
     694        margin-top: -1px;
     695}
     696
     697#available-menu-items .menu-item-handle:hover {
     698        z-index: 1;
     699}
     700
     701#available-menu-items .item-title h4 {
     702        padding: 0 0 5px;
     703        font-size: 14px;
     704}
     705
     706#available-menu-items .item-add {
     707        position: absolute;
     708        top: 1px;
     709        left: 1px;
     710        color: #82878c;
     711        width: 30px;
     712        height: 38px;
     713        cursor: pointer;
     714}
     715
     716#available-menu-items .menu-item-handle .item-add:focus {
     717        color: #23282d;
     718        -webkit-box-shadow: 0 0 0 1px #5b9dd9, 0 0 2px 1px rgba(30, 140, 190, .8);
     719        box-shadow: 0 0 0 1px #5b9dd9, 0 0 2px 1px rgba(30, 140, 190, .8);
     720}
     721
     722#available-menu-items .item-add:before {
     723        content: "\f132";
     724        font: normal 20px/1 dashicons;
     725        position: relative;
     726        left: 2px;
     727        top: 4px;
     728}
     729
     730#available-menu-items .menu-item-handle.item-added .item-type,
     731#available-menu-items .menu-item-handle.item-added .item-title,
     732#available-menu-items .menu-item-handle.item-added:hover .item-add,
     733#available-menu-items .menu-item-handle.item-added .item-add:focus {
     734        color: #82878c;
     735}
     736
     737#available-menu-items .menu-item-handle.item-added .item-add:before {
     738        content: "\f147";
     739}
     740
     741#available-menu-items .accordion-section-title.loading .spinner,
     742#available-menu-items-search.loading .accordion-section-title .spinner {
     743        visibility: visible;
     744        margin: 0 20px;
     745}
     746
     747#available-menu-items-search .spinner {
     748        position: absolute;
     749        top: 18px;
     750        margin: 0 !important;
     751        right: 20px;
     752}
     753
     754#available-menu-items-search input {
     755        padding: 6px 10px;
     756        width: 100%;
     757}
     758
     759#available-menu-items-search .accordion-section-title {
     760        padding: 12px 15px;
     761        -webkit-box-sizing: border-box;
     762        -moz-box-sizing: border-box;
     763        box-sizing: border-box;
     764}
     765
     766#available-menu-items-search .accordion-section-title:after {
     767        display: none;
     768}
     769
     770#available-menu-items-search .accordion-section-content:empty {
     771        min-height: 0;
     772        padding: 0;
     773}
     774
     775#available-menu-items-search.loading .accordion-section-content div {
     776        opacity: .5;
     777}
     778
     779#available-menu-items-search.loading.loading-more .accordion-section-content div {
     780        opacity: 1;
     781}
     782
     783#customize-preview {
     784        -webkit-transition: all 0.2s;
     785        transition: all 0.2s;
     786}
     787
     788body.adding-menu-items #available-menu-items {
     789        left: 0;
     790}
     791
     792body.adding-menu-items .wp-full-overlay-main {
     793        left: 300px;
     794}
     795
     796body.adding-menu-items #customize-preview {
     797        opacity: 0.4;
     798}
     799
     800.menu-item-handle .spinner {
     801        display: none;
     802        float: left;
     803        margin: 0 8px 0 0;
     804}
     805
     806.nav-menu-inserted-item-loading .spinner {
     807        display: block;
     808}
     809
     810.nav-menu-inserted-item-loading .menu-item-handle .item-type {
     811        padding: 0 0 0 8px;
     812}
     813
     814.nav-menu-inserted-item-loading .menu-item-handle,
     815.added-menu-item .menu-item-handle.loading {
     816        padding: 10px 15px 10px 8px;
     817        cursor: default;
     818        opacity: .5;
     819        background: #fff;
     820        color: #727773;
     821}
     822
     823.added-menu-item .menu-item-handle {
     824        -webkit-transition-property: opacity, background, color;
     825        transition-property: opacity, background, color;
     826        -webkit-transition-duration: 1.25s;
     827        transition-duration: 1.25s;
     828        -webkit-transition-timing-function: cubic-bezier( .25, -2.5, .75, 8 );
     829        transition-timing-function: cubic-bezier( .25, -2.5, .75, 8 ); /* Replacement for .hide().fadeIn('slow') in JS to add emphasis when it's loaded. */
     830}
     831
     832/* Add/delete Menus */
     833
     834/* @todo update selector */
     835#accordion-section-add_menu {
     836        margin: 15px 12px;
     837}
     838
     839.new-menu-section-content {
     840        display: none;
     841        padding: 15px 0 0 0;
     842        overflow: hidden;
     843        clear: both;
     844}
     845
     846/* @todo update selector */
     847#accordion-section-add_menu .accordion-section-title {
     848        padding-left: 45px;
     849}
     850
     851/* @todo update selector */
     852#accordion-section-add_menu .accordion-section-title:before {
     853        font: normal 20px/1 dashicons;
     854        position: absolute;
     855        top: 12px;
     856        left: 14px;
     857        content: "\f132";
     858}
     859
     860#create-new-menu-submit {
     861        float: right;
     862        margin: 0 0 12px 0;
     863}
     864
     865.menu-delete-item {
     866        display: block;
     867        float: left;
     868        padding: 1em 0;
     869        width: 100%;
     870}
     871
     872li.assigned-to-menu-location .menu-delete-item {
     873  display: none;
     874}
     875
     876li.assigned-to-menu-location .add-new-menu-item {
     877  margin-bottom: 1em;
     878}
     879
     880.menu-delete {
     881        color: #a00;
     882        cursor: pointer;
     883        text-decoration: underline;
     884}
     885
     886.menu-delete:hover,
     887.menu-delete:focus {
     888        color: #f00;
     889        text-decoration: none;
     890}
     891
     892.menu-delete:focus {
     893        -webkit-box-shadow: 0 0 0 1px #5b9dd9, 0 0 2px 1px rgba(30, 140, 190, .8);
     894        box-shadow: 0 0 0 1px #5b9dd9, 0 0 2px 1px rgba(30, 140, 190, .8);
     895}
     896
     897.menu-item-handle {
     898        margin-top: -1px;
     899}
     900.ui-sortable-disabled .menu-item-handle {
     901        cursor: default;
     902}
     903
     904.menu-item-handle:hover {
     905        position: relative;
     906        z-index: 10;
     907        color: #0073aa;
     908}
     909
     910.menu-item-handle:hover .item-type,
     911.menu-item-handle:hover .item-edit,
     912#available-menu-items .menu-item-handle:hover .item-add {
     913        color: #0073aa;
     914}
     915
     916.menu-item-edit-active .menu-item-handle {
     917        border-color: #999;
     918        border-bottom: none;
     919}
     920
     921.customize-control-nav_menu_item {
     922        margin-bottom: 0;
     923}
     924
     925.customize-control-nav_menu {
     926        margin-top: 12px;
     927}
     928
     929#available-menu-items .customize-section-title {
     930        display: none;
     931}
     932
     933@media screen and ( max-width: 640px ) {
     934        body.adding-menu-items div#available-menu-items {
     935                top: 46px;
     936                left: 0;
     937                z-index: 10;
     938                width: 100%;
     939        }
     940
     941        #available-menu-items .customize-section-title {
     942                display: block;
     943                margin: 0;
     944        }
     945
     946        #available-menu-items .customize-section-back {
     947                height: 69px;
     948        }
     949
     950        #available-menu-items .customize-section-title h3 {
     951                font-size: 20px;
     952                font-weight: 200;
     953                padding: 9px 10px 12px 14px;
     954                margin: 0;
     955                line-height: 24px;
     956                color: #555;
     957                display: block;
     958                overflow: hidden;
     959                white-space: nowrap;
     960                text-overflow: ellipsis;
     961        }
     962
     963        #available-menu-items .customize-section-title .customize-action {
     964                font-size: 13px;
     965                display: block;
     966                font-weight: 400;
     967                overflow: hidden;
     968                white-space: nowrap;
     969                text-overflow: ellipsis;
     970        }
     971}
  • src/wp-admin/js/customize-nav-menus.js

     
     1/* global _wpCustomizeNavMenusSettings, wpNavMenu, console */
     2( function( api, wp, $ ) {
     3        'use strict';
     4
     5        /**
     6         * Set up wpNavMenu for drag and drop.
     7         */
     8        wpNavMenu.originalInit = wpNavMenu.init;
     9        wpNavMenu.options.menuItemDepthPerLevel = 20;
     10        wpNavMenu.options.sortableItems         = '.customize-control-nav_menu_item';
     11        wpNavMenu.init = function() {
     12                this.jQueryExtensions();
     13        };
     14
     15        api.Menus = api.Menus || {};
     16
     17        // Link settings.
     18        api.Menus.data = {
     19                nonce: '',
     20                itemTypes: {
     21                        taxonomies: {},
     22                        postTypes: {}
     23                },
     24                l10n: {},
     25                menuItemTransport: 'postMessage',
     26                phpIntMax: 0,
     27                defaultSettingValues: {
     28                        nav_menu: {},
     29                        nav_menu_item: {}
     30                }
     31        };
     32        if ( 'undefined' !== typeof _wpCustomizeNavMenusSettings ) {
     33                $.extend( api.Menus.data, _wpCustomizeNavMenusSettings );
     34        }
     35
     36        /**
     37         * Newly-created Nav Menus and Nav Menu Items have negative integer IDs which
     38         * serve as placeholders until Save & Publish happens.
     39         *
     40         * @return {number}
     41         */
     42        api.Menus.generatePlaceholderAutoIncrementId = function() {
     43                return -Math.ceil( api.Menus.data.phpIntMax * Math.random() );
     44        };
     45
     46        /**
     47         * wp.customize.Menus.AvailableItemModel
     48         *
     49         * A single available menu item model. See PHP's WP_Customize_Nav_Menu_Item_Setting class.
     50         *
     51         * @constructor
     52         * @augments Backbone.Model
     53         */
     54        api.Menus.AvailableItemModel = Backbone.Model.extend( $.extend(
     55                {
     56                        id: null // This is only used by Backbone.
     57                },
     58                api.Menus.data.defaultSettingValues.nav_menu_item
     59        ) );
     60
     61        /**
     62         * wp.customize.Menus.AvailableItemCollection
     63         *
     64         * Collection for available menu item models.
     65         *
     66         * @constructor
     67         * @augments Backbone.Model
     68         */
     69        api.Menus.AvailableItemCollection = Backbone.Collection.extend({
     70                model: api.Menus.AvailableItemModel,
     71
     72                sort_key: 'order',
     73
     74                comparator: function( item ) {
     75                        return -item.get( this.sort_key );
     76                },
     77
     78                sortByField: function( fieldName ) {
     79                        this.sort_key = fieldName;
     80                        this.sort();
     81                }
     82        });
     83        api.Menus.availableMenuItems = new api.Menus.AvailableItemCollection( api.Menus.data.availableMenuItems );
     84
     85        /**
     86         * wp.customize.Menus.AvailableMenuItemsPanelView
     87         *
     88         * View class for the available menu items panel.
     89         *
     90         * @constructor
     91         * @augments wp.Backbone.View
     92         * @augments Backbone.View
     93         */
     94        api.Menus.AvailableMenuItemsPanelView = wp.Backbone.View.extend({
     95
     96                el: '#available-menu-items',
     97
     98                events: {
     99                        'input #menu-items-search': 'debounceSearch',
     100                        'change #menu-items-search': 'debounceSearch',
     101                        'click #menu-items-search': 'debounceSearch',
     102                        'focus .menu-item-tpl': 'focus',
     103                        'click .menu-item-tpl': '_submit',
     104                        'keypress .menu-item-tpl': '_submit',
     105                        'click #custom-menu-item-submit': '_submitLink',
     106                        'keypress #custom-menu-item-name': '_submitLink',
     107                        'keydown': 'keyboardAccessible'
     108                },
     109
     110                // Cache current selected menu item.
     111                selected: null,
     112
     113                // Cache menu control that opened the panel.
     114                currentMenuControl: null,
     115                debounceSearch: null,
     116                $search: null,
     117                searchTerm: '',
     118                rendered: false,
     119                pages: {},
     120                sectionContent: '',
     121                loading: false,
     122
     123                initialize: function() {
     124                        var self = this;
     125
     126                        this.$search = $( '#menu-items-search' );
     127                        this.sectionContent = this.$el.find( '.accordion-section-content' );
     128
     129                        this.debounceSearch = _.debounce( self.search, 250 );
     130
     131                        _.bindAll( this, 'close' );
     132
     133                        // If the available menu items panel is open and the customize controls are
     134                        // interacted with (other than an item being deleted), then close the
     135                        // available menu items panel. Also close on back button click.
     136                        $( '#customize-controls, .customize-section-back' ).on( 'click keydown', function( e ) {
     137                                var isDeleteBtn = $( e.target ).is( '.item-delete, .item-delete *' ),
     138                                        isAddNewBtn = $( e.target ).is( '.add-new-menu-item, .add-new-menu-item *' );
     139                                if ( $( 'body' ).hasClass( 'adding-menu-items' ) && ! isDeleteBtn && ! isAddNewBtn ) {
     140                                        self.close();
     141                                }
     142                        } );
     143
     144                        this.$el.on( 'input', '#custom-menu-item-name.invalid, #custom-menu-item-url.invalid', function() {
     145                                $( this ).removeClass( 'invalid' );
     146                        });
     147
     148                        // Load available items if it looks like we'll need them.
     149                        api.panel( 'menus' ).container.bind( 'expanded', function() {
     150                                if ( ! self.rendered ) {
     151                                        self.initList();
     152                                        self.rendered = true;
     153                                }
     154                        });
     155
     156                        // Load more items.
     157                        this.sectionContent.scroll( function() {
     158                                var totalHeight = self.$el.find( '.accordion-section.open .accordion-section-content' ).prop( 'scrollHeight' ),
     159                                    visibleHeight = self.$el.find( '.accordion-section.open' ).height();
     160                                if ( ! self.loading && $( this ).scrollTop() > 3 / 4 * totalHeight - visibleHeight ) {
     161                                        var type = $( this ).data( 'type' ),
     162                                            obj_type = $( this ).data( 'obj_type' );
     163                                        if ( 'search' === type ) {
     164                                                if ( self.searchTerm ) {
     165                                                        self.doSearch( self.pages.search );
     166                                                }
     167                                        } else {
     168                                                self.loadItems( type, obj_type );
     169                                        }
     170                                }
     171                        });
     172
     173                        // Close the panel if the URL in the preview changes
     174                        api.previewer.bind( 'url', this.close );
     175                },
     176
     177                // Search input change handler.
     178                search: function( event ) {
     179                        if ( ! event ) {
     180                                return;
     181                        }
     182                        // Manual accordion-opening behavior.
     183                        if ( this.searchTerm && ! $( '#available-menu-items-search' ).hasClass( 'open' ) ) {
     184                                $( '#available-menu-items .accordion-section-content' ).slideUp( 'fast' );
     185                                $( '#available-menu-items-search .accordion-section-content' ).slideDown( 'fast' );
     186                                $( '#available-menu-items .accordion-section.open' ).removeClass( 'open' );
     187                                $( '#available-menu-items-search' ).addClass( 'open' );
     188                        }
     189                        if ( '' === event.target.value ) {
     190                                $( '#available-menu-items-search' ).removeClass( 'open' );
     191                        }
     192                        if ( this.searchTerm === event.target.value ) {
     193                                return;
     194                        }
     195                        this.searchTerm = event.target.value;
     196                        this.pages.search = 1;
     197                        this.doSearch( 1 );
     198                },
     199
     200                // Get search results.
     201                doSearch: function( page ) {
     202                        var self = this, params,
     203                            $section = $( '#available-menu-items-search' ),
     204                            $content = $section.find( '.accordion-section-content' ),
     205                            itemTemplate = wp.template( 'available-menu-item' );
     206
     207                        if ( self.currentRequest ) {
     208                                self.currentRequest.abort();
     209                        }
     210
     211                        if ( page < 0 ) {
     212                                return;
     213                        } else if ( page > 1 ) {
     214                                $section.addClass( 'loading-more' );
     215                        } else if ( '' === self.searchTerm ) {
     216                                $content.html( '' );
     217                                return;
     218                        }
     219
     220                        $section.addClass( 'loading' );
     221                        self.loading = true;
     222                        params = {
     223                                'customize-menus-nonce': api.Menus.data.nonce,
     224                                'wp_customize': 'on',
     225                                'search': self.searchTerm,
     226                                'page': page
     227                        };
     228
     229                        self.currentRequest = wp.ajax.post( 'search-available-menu-items-customizer', params );
     230
     231                        self.currentRequest.done(function( data ) {
     232                                var items;
     233                                if ( 1 === page ) {
     234                                        // Clear previous results as it's a new search.
     235                                        $content.empty();
     236                                }
     237                                $section.removeClass( 'loading loading-more' );
     238                                $section.addClass( 'open' );
     239                                self.loading = false;
     240                                items = new api.Menus.AvailableItemCollection( data.items );
     241                                self.collection.add( items.models );
     242                                items.each( function( menuItem ) {
     243                                        $content.append( itemTemplate( menuItem.attributes ) );
     244                                } );
     245                                if ( 20 > items.length ) {
     246                                        self.pages.search = -1; // Up to 20 posts and 20 terms in results, if <20, no more results for either.
     247                                } else {
     248                                        self.pages.search = self.pages.search + 1;
     249                                }
     250                        });
     251
     252                        self.currentRequest.fail(function( data ) {
     253                                $content.empty().append( $( '<p class="nothing-found"></p>' ).text( data.message ) );
     254                                wp.a11y.speak( data.message );
     255                                self.pages.search = -1;
     256                        });
     257
     258                        self.currentRequest.always(function() {
     259                                $section.removeClass( 'loading loading-more' );
     260                                self.loading = false;
     261                                self.currentRequest = null;
     262                        });
     263                },
     264
     265                // Render the individual items.
     266                initList: function() {
     267                        var self = this;
     268
     269                        // Render the template for each item by type.
     270                        _.each( api.Menus.data.itemTypes, function( typeObjects, type ) {
     271                                _.each( typeObjects, function( typeObject, slug ) {
     272                                        if ( 'postTypes' === type ) {
     273                                                type = 'post_type';
     274                                        } else if ( 'taxonomies' === type ) {
     275                                                type = 'taxonomy';
     276                                        }
     277                                        self.pages[ slug ] = 0; // @todo should prefix with type
     278                                        self.loadItems( slug, type );
     279                                } );
     280                        } );
     281                },
     282
     283                // Load available menu items.
     284                loadItems: function( type, obj_type ) {
     285                        var self = this, params, request, itemTemplate;
     286                        itemTemplate = wp.template( 'available-menu-item' );
     287
     288                        if ( 0 > self.pages[type] ) {
     289                                return;
     290                        }
     291                        $( '#available-menu-items-' + type + ' .accordion-section-title' ).addClass( 'loading' );
     292                        self.loading = true;
     293                        params = {
     294                                'customize-menus-nonce': api.Menus.data.nonce,
     295                                'wp_customize': 'on',
     296                                'type': type,
     297                                'obj_type': obj_type,
     298                                'page': self.pages[ type ]
     299                        };
     300                        request = wp.ajax.post( 'load-available-menu-items-customizer', params );
     301
     302                        request.done(function( data ) {
     303                                var items, typeInner;
     304                                items = data.items;
     305                                if ( 0 === items.length ) {
     306                                        self.pages[ type ] = -1;
     307                                        return;
     308                                }
     309                                items = new api.Menus.AvailableItemCollection( items ); // @todo Why is this collection created and then thrown away?
     310                                self.collection.add( items.models );
     311                                typeInner = $( '#available-menu-items-' + type + ' .accordion-section-content' );
     312                                items.each(function( menu_item ) {
     313                                        typeInner.append( itemTemplate( menu_item.attributes ) );
     314                                });
     315                                self.pages[ type ] = self.pages[ type ] + 1;
     316                        });
     317                        request.fail(function( data ) {
     318                                if ( typeof console !== 'undefined' && console.error ) {
     319                                        console.error( data );
     320                                }
     321                        });
     322                        request.always(function() {
     323                                $( '#available-menu-items-' + type + ' .accordion-section-title' ).removeClass( 'loading' );
     324                                self.loading = false;
     325                        });
     326                },
     327
     328                // Adjust the height of each section of items to fit the screen.
     329                itemSectionHeight: function() {
     330                        var sections, totalHeight, accordionHeight, diff;
     331                        totalHeight = window.innerHeight;
     332                        sections = this.$el.find( '.accordion-section-content' );
     333                        accordionHeight =  46 * ( 1 + sections.length ) - 16; // Magic numbers.
     334                        diff = totalHeight - accordionHeight;
     335                        if ( 120 < diff && 290 > diff ) {
     336                                sections.css( 'max-height', diff );
     337                        } else if ( 120 >= diff ) {
     338                                this.$el.addClass( 'allow-scroll' );
     339                        }
     340                },
     341
     342                // Highlights a menu item.
     343                select: function( menuitemTpl ) {
     344                        this.selected = $( menuitemTpl );
     345                        this.selected.siblings( '.menu-item-tpl' ).removeClass( 'selected' );
     346                        this.selected.addClass( 'selected' );
     347                },
     348
     349                // Highlights a menu item on focus.
     350                focus: function( event ) {
     351                        this.select( $( event.currentTarget ) );
     352                },
     353
     354                // Submit handler for keypress and click on menu item.
     355                _submit: function( event ) {
     356                        // Only proceed with keypress if it is Enter or Spacebar
     357                        if ( 'keypress' === event.type && ( 13 !== event.which && 32 !== event.which ) ) {
     358                                return;
     359                        }
     360
     361                        this.submit( $( event.currentTarget ) );
     362                },
     363
     364                // Adds a selected menu item to the menu.
     365                submit: function( menuitemTpl ) {
     366                        var menuitemId, menu_item;
     367
     368                        if ( ! menuitemTpl ) {
     369                                menuitemTpl = this.selected;
     370                        }
     371
     372                        if ( ! menuitemTpl || ! this.currentMenuControl ) {
     373                                return;
     374                        }
     375
     376                        this.select( menuitemTpl );
     377
     378                        menuitemId = $( this.selected ).data( 'menu-item-id' );
     379                        menu_item = this.collection.findWhere( { id: menuitemId } );
     380                        if ( ! menu_item ) {
     381                                return;
     382                        }
     383
     384                        this.currentMenuControl.addItemToMenu( menu_item.attributes );
     385
     386                        $( menuitemTpl ).find( '.menu-item-handle' ).addClass( 'item-added' );
     387                },
     388
     389                // Submit handler for keypress and click on custom menu item.
     390                _submitLink: function( event ) {
     391                        // Only proceed with keypress if it is Enter.
     392                        if ( 'keypress' === event.type && 13 !== event.which ) {
     393                                return;
     394                        }
     395
     396                        this.submitLink();
     397                },
     398
     399                // Adds the custom menu item to the menu.
     400                submitLink: function() {
     401                        var menuItem,
     402                                itemName = $( '#custom-menu-item-name' ),
     403                                itemUrl = $( '#custom-menu-item-url' );
     404
     405                        if ( ! this.currentMenuControl ) {
     406                                return;
     407                        }
     408
     409                        if ( '' === itemName.val() ) {
     410                                itemName.addClass( 'invalid' );
     411                                return;
     412                        } else if ( '' === itemUrl.val() || 'http://' === itemUrl.val() ) {
     413                                itemUrl.addClass( 'invalid' );
     414                                return;
     415                        }
     416
     417                        menuItem = {
     418                                'title': itemName.val(),
     419                                'url': itemUrl.val(),
     420                                'type': 'custom',
     421                                'type_label': api.Menus.data.l10n.custom_label,
     422                                'object': ''
     423                        };
     424
     425                        this.currentMenuControl.addItemToMenu( menuItem );
     426
     427                        // Reset the custom link form.
     428                        itemUrl.val( 'http://' );
     429                        itemName.val( '' );
     430                },
     431
     432                // Opens the panel.
     433                open: function( menuControl ) {
     434                        this.currentMenuControl = menuControl;
     435
     436                        this.itemSectionHeight();
     437
     438                        $( 'body' ).addClass( 'adding-menu-items' );
     439
     440                        // Collapse all controls.
     441                        _( this.currentMenuControl.getMenuItemControls() ).each( function( control ) {
     442                                control.collapseForm();
     443                        } );
     444
     445                        this.$el.find( '.selected' ).removeClass( 'selected' );
     446
     447                        this.$search.focus();
     448                },
     449
     450                // Closes the panel
     451                close: function( options ) {
     452                        options = options || {};
     453
     454                        if ( options.returnFocus && this.currentMenuControl ) {
     455                                this.currentMenuControl.container.find( '.add-new-menu-item' ).focus();
     456                        }
     457
     458                        this.currentMenuControl = null;
     459                        this.selected = null;
     460
     461                        $( 'body' ).removeClass( 'adding-menu-items' );
     462                        $( '#available-menu-items .menu-item-handle.item-added' ).removeClass( 'item-added' );
     463
     464                        this.$search.val( '' );
     465                },
     466
     467                // Add keyboard accessiblity to the panel
     468                keyboardAccessible: function( event ) {
     469                        var isEnter = ( 13 === event.which ),
     470                                isEsc = ( 27 === event.which ),
     471                                isDown = ( 40 === event.which ),
     472                                isUp = ( 38 === event.which ),
     473                                isBackTab = ( 9 === event.which && event.shiftKey ),
     474                                selected = null,
     475                                firstVisible = this.$el.find( '> .menu-item-tpl:visible:first' ),
     476                                lastVisible = this.$el.find( '> .menu-item-tpl:visible:last' ),
     477                                isSearchFocused = $( event.target ).is( this.$search );
     478
     479                        if ( isDown || isUp ) {
     480                                if ( isDown ) {
     481                                        if ( isSearchFocused ) {
     482                                                selected = firstVisible;
     483                                        } else if ( this.selected && 0 !== this.selected.nextAll( '.menu-item-tpl:visible' ).length ) {
     484                                                selected = this.selected.nextAll( '.menu-item-tpl:visible:first' );
     485                                        }
     486                                } else if ( isUp ) {
     487                                        if ( isSearchFocused ) {
     488                                                selected = lastVisible;
     489                                        } else if ( this.selected && 0 !== this.selected.prevAll( '.menu-item-tpl:visible' ).length ) {
     490                                                selected = this.selected.prevAll( '.menu-item-tpl:visible:first' );
     491                                        }
     492                                }
     493
     494                                this.select( selected );
     495
     496                                if ( selected ) {
     497                                        selected.focus();
     498                                } else {
     499                                        this.$search.focus();
     500                                }
     501
     502                                return;
     503                        }
     504
     505                        // If enter pressed but nothing entered, don't do anything
     506                        if ( isEnter && ! this.$search.val() ) {
     507                                return;
     508                        }
     509
     510                        if ( isSearchFocused && isBackTab ) {
     511                                this.currentMenuControl.container.find( '.add-new-menu-item' ).focus();
     512                                event.preventDefault(); // Avoid additional back-tab.
     513                        } else if ( isEsc ) {
     514                                this.close( { returnFocus: true } );
     515                        }
     516                }
     517        });
     518
     519        /**
     520         * wp.customize.Menus.MenusPanel
     521         *
     522         * Customizer panel for menus. This is used only for screen options management.
     523         * Note that 'menus' must match the WP_Customize_Menu_Panel::$type.
     524         *
     525         * @constructor
     526         * @augments wp.customize.Panel
     527         */
     528        api.Menus.MenusPanel = api.Panel.extend({
     529
     530                attachEvents: function() {
     531                        api.Panel.prototype.attachEvents.call( this );
     532
     533                        var panel = this,
     534                                panelMeta = panel.container.find( '.panel-meta' ),
     535                                help = panelMeta.find( '.customize-help-toggle' ),
     536                                content = panelMeta.find( '.customize-panel-description' ),
     537                                options = $( '#screen-options-wrap' ),
     538                                button = panelMeta.find( '.customize-screen-options-toggle' );
     539                        button.on( 'click', function() {
     540                                // Hide description
     541                                if ( content.not( ':hidden' ) ) {
     542                                        content.slideUp( 'fast' );
     543                                        help.attr( 'aria-expanded', 'false' );
     544                                }
     545
     546                                if ( 'true' === button.attr( 'aria-expanded' ) ) {
     547                                        button.attr( 'aria-expanded', 'false' );
     548                                        panelMeta.removeClass( 'open' );
     549                                        panelMeta.removeClass( 'active-menu-screen-options' );
     550                                        options.slideUp( 'fast' );
     551                                } else {
     552                                        button.attr( 'aria-expanded', 'true' );
     553                                        panelMeta.addClass( 'open' );
     554                                        panelMeta.addClass( 'active-menu-screen-options' );
     555                                        options.slideDown( 'fast' );
     556                                }
     557
     558                                return false;
     559                        } );
     560
     561                        // Help toggle
     562                        help.on( 'click', function() {
     563                                if ( 'true' === button.attr( 'aria-expanded' ) ) {
     564                                        button.attr( 'aria-expanded', 'false' );
     565                                        help.attr( 'aria-expanded', 'true' );
     566                                        panelMeta.addClass( 'open' );
     567                                        panelMeta.removeClass( 'active-menu-screen-options' );
     568                                        options.slideUp( 'fast' );
     569                                        content.slideDown( 'fast' );
     570                                }
     571                        } );
     572                },
     573
     574                /**
     575                 * Show/hide/save screen options (columns). From common.js.
     576                 */
     577                ready: function() {
     578                        var panel = this;
     579                        this.container.find( '.hide-column-tog' ).click( function() {
     580                                var $t = $( this ), column = $t.val();
     581                                if ( $t.prop( 'checked' ) ) {
     582                                        panel.checked( column );
     583                                } else {
     584                                        panel.unchecked( column );
     585                                }
     586
     587                                panel.saveManageColumnsState();
     588                        });
     589                        this.container.find( '.hide-column-tog' ).each( function() {
     590                        var $t = $( this ), column = $t.val();
     591                                if ( $t.prop( 'checked' ) ) {
     592                                        panel.checked( column );
     593                                } else {
     594                                        panel.unchecked( column );
     595                                }
     596                        });
     597                },
     598
     599                saveManageColumnsState: function() {
     600                        var hidden = this.hidden();
     601                        $.post( wp.ajax.settings.url, {
     602                                action: 'hidden-columns',
     603                                hidden: hidden,
     604                                screenoptionnonce: $( '#screenoptionnonce' ).val(),
     605                                page: 'nav-menus'
     606                        });
     607                },
     608
     609                checked: function( column ) {
     610                        this.container.addClass( 'field-' + column + '-active' );
     611                },
     612
     613                unchecked: function( column ) {
     614                        this.container.removeClass( 'field-' + column + '-active' );
     615                },
     616
     617                hidden: function() {
     618                        this.hidden = function() {
     619                                return $( '.hide-column-tog' ).not( ':checked' ).map( function() {
     620                                        var id = this.id;
     621                                        return id.substring( id, id.length - 5 );
     622                                }).get().join( ',' );
     623                        };
     624                }
     625        } );
     626
     627        /**
     628         * wp.customize.Menus.MenuSection
     629         *
     630         * Customizer section for menus. This is used only for lazy-loading child controls.
     631         * Note that 'nav_menu' must match the WP_Customize_Menu_Section::$type.
     632         *
     633         * @constructor
     634         * @augments wp.customize.Section
     635         */
     636        api.Menus.MenuSection = api.Section.extend({
     637
     638                /**
     639                 * @since Menu Customizer 0.3
     640                 *
     641                 * @param {String} id
     642                 * @param {Object} options
     643                 */
     644                initialize: function( id, options ) {
     645                        var section = this;
     646                        api.Section.prototype.initialize.call( section, id, options );
     647                        section.deferred.initSortables = $.Deferred();
     648                },
     649
     650                /**
     651                 *
     652                 */
     653                ready: function() {
     654                        var section = this;
     655
     656                        if ( 'undefined' === typeof section.params.menu_id ) {
     657                                throw new Error( 'params.menu_id was not defined' );
     658                        }
     659
     660                        /*
     661                         * Since newly created sections won't be registered in PHP, we need to prevent the
     662                         * preview's sending of the activeSections to result in this control
     663                         * being deactivated when the preview refreshes. So we can hook onto
     664                         * the setting that has the same ID and its presence can dictate
     665                         * whether the section is active.
     666                         */
     667                        section.active.validate = function() {
     668                                if ( ! api.has( section.id ) ) {
     669                                        return false;
     670                                }
     671                                return !! api( section.id ).get();
     672                        };
     673
     674                        section.populateControls();
     675
     676                        section.navMenuLocationSettings = {};
     677                        section.assignedLocations = new api.Value( [] );
     678
     679                        api.each(function( setting, id ) {
     680                                var matches = id.match( /^nav_menu_locations\[(.+?)]/ );
     681                                if ( matches ) {
     682                                        section.navMenuLocationSettings[ matches[1] ] = setting;
     683                                        setting.bind( function() {
     684                                                section.refreshAssignedLocations();
     685                                        });
     686                                }
     687                        });
     688
     689                        section.assignedLocations.bind(function( to ) {
     690                                section.updateAssignedLocationsInSectionTitle( to );
     691                        });
     692
     693                        section.refreshAssignedLocations();
     694                },
     695
     696                populateControls: function() {
     697                        var section = this, menuNameControlId, menuControl, menuNameControl;
     698
     699                        // Add the control for managing the menu name.
     700                        menuNameControlId = section.id + '[name]';
     701                        menuNameControl = api.control( menuNameControlId );
     702                        if ( ! menuNameControl ) {
     703                                menuNameControl = new api.controlConstructor.nav_menu_name( menuNameControlId, {
     704                                        params: {
     705                                                type: 'nav_menu_name',
     706                                                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
     707                                                label: '',
     708                                                active: true,
     709                                                section: section.id,
     710                                                priority: 0,
     711                                                settings: {
     712                                                        'default': section.id
     713                                                }
     714                                        }
     715                                } );
     716                                api.control.add( menuNameControl.id, menuNameControl );
     717                                menuNameControl.active.set( true );
     718                        }
     719
     720                        // Add the menu control.
     721                        menuControl = api.control( section.id );
     722                        if ( ! menuControl ) {
     723                                menuControl = new api.controlConstructor.nav_menu( section.id, {
     724                                        params: {
     725                                                type: 'nav_menu',
     726                                                content: '<li id="customize-control-' + section.id.replace( '[', '-' ).replace( ']', '' ) + '" class="customize-control customize-control-nav_menu"></li>', // @todo core should do this for us
     727                                                section: section.id,
     728                                                priority: 999,
     729                                                active: true,
     730                                                settings: {
     731                                                        'default': section.id
     732                                                },
     733                                                menu_id: section.params.menu_id
     734                                        }
     735                                } );
     736                                api.control.add( menuControl.id, menuControl );
     737                                menuControl.active.set( true );
     738                        }
     739
     740                },
     741
     742                /**
     743                 *
     744                 */
     745                refreshAssignedLocations: function() {
     746                        var section = this,
     747                                menuTermId = section.params.menu_id,
     748                                currentAssignedLocations = [];
     749                        _.each( section.navMenuLocationSettings, function( setting, themeLocation ) {
     750                                if ( setting() === menuTermId ) {
     751                                        currentAssignedLocations.push( themeLocation );
     752                                }
     753                        });
     754                        section.assignedLocations.set( currentAssignedLocations );
     755                },
     756
     757                /**
     758                 * @param {array} themeLocations
     759                 */
     760                updateAssignedLocationsInSectionTitle: function( themeLocations ) {
     761                        var section = this,
     762                                $title;
     763
     764                        $title = section.container.find( '.accordion-section-title:first' );
     765                        $title.find( '.menu-in-location' ).remove();
     766                        _.each( themeLocations, function( themeLocation ) {
     767                                var $label = $( '<span class="menu-in-location"></span>' );
     768                                $label.text( api.Menus.data.l10n.menuLocation.replace( '%s', themeLocation ) );
     769                                $title.append( $label );
     770                        });
     771
     772                        section.container.toggleClass( 'assigned-to-menu-location', 0 !== themeLocations.length );
     773
     774                },
     775
     776                onChangeExpanded: function( expanded, args ) {
     777                        var section = this;
     778
     779                        if ( expanded ) {
     780                                wpNavMenu.menuList = section.container.find( '.accordion-section-content:first' );
     781                                wpNavMenu.targetList = wpNavMenu.menuList;
     782
     783                                // Add attributes needed by wpNavMenu
     784                                $( '#menu-to-edit' ).removeAttr( 'id' );
     785                                wpNavMenu.menuList.attr( 'id', 'menu-to-edit' ).addClass( 'menu' );
     786
     787                                _.each( api.section( section.id ).controls(), function( control ) {
     788                                        if ( 'nav_menu_item' === control.params.type ) {
     789                                                control.actuallyEmbed();
     790                                        }
     791                                } );
     792
     793                                if ( 'resolved' !== section.deferred.initSortables.state() ) {
     794                                        wpNavMenu.initSortables(); // Depends on menu-to-edit ID being set above.
     795                                        section.deferred.initSortables.resolve( wpNavMenu.menuList ); // Now MenuControl can extend the sortable.
     796
     797                                        // @todo Note that wp.customize.reflowPaneContents() is debounced, so this immediate change will show a slight flicker while priorities get updated.
     798                                        api.control( 'nav_menu[' + String( section.params.menu_id ) + ']' ).reflowMenuItems();
     799                                }
     800                        }
     801                        api.Section.prototype.onChangeExpanded.call( section, expanded, args );
     802                }
     803        });
     804
     805        /**
     806         * wp.customize.Menus.NewMenuSection
     807         *
     808         * Customizer section for new menus.
     809         * Note that 'new_menu' must match the WP_Customize_New_Menu_Section::$type.
     810         *
     811         * @constructor
     812         * @augments wp.customize.Section
     813         */
     814        api.Menus.NewMenuSection = api.Section.extend({
     815
     816                /**
     817                 * Add behaviors for the accordion section.
     818                 *
     819                 * @since Menu Customizer 0.3
     820                 */
     821                attachEvents: function() {
     822                        var section = this;
     823                        this.container.on( 'click', '.add-menu-toggle', function() {
     824                                if ( section.expanded() ) {
     825                                        section.collapse();
     826                                } else {
     827                                        section.expand();
     828                                }
     829                        });
     830                },
     831
     832                /**
     833                 * Update UI to reflect expanded state.
     834                 *
     835                 * @since 4.1.0
     836                 *
     837                 * @param {Boolean} expanded
     838                 */
     839                onChangeExpanded: function( expanded ) {
     840                        var section = this,
     841                                button = section.container.find( '.add-menu-toggle' ),
     842                                content = section.container.find( '.new-menu-section-content' ),
     843                                customizer = section.container.closest( '.wp-full-overlay-sidebar-content' );
     844                        if ( expanded ) {
     845                                button.addClass( 'open' );
     846                                content.slideDown( 'fast', function() {
     847                                        customizer.scrollTop( customizer.height() );
     848                                });
     849                        } else {
     850                                button.removeClass( 'open' );
     851                                content.slideUp( 'fast' );
     852                        }
     853                }
     854        });
     855
     856        /**
     857         * wp.customize.Menus.MenuLocationControl
     858         *
     859         * Customizer control for menu locations (rendered as a <select>).
     860         * Note that 'nav_menu_location' must match the WP_Customize_Nav_Menu_Location_Control::$type.
     861         *
     862         * @constructor
     863         * @augments wp.customize.Control
     864         */
     865        api.Menus.MenuLocationControl = api.Control.extend({
     866                initialize: function( id, options ) {
     867                        var control = this,
     868                                matches = id.match( /^nav_menu_locations\[(.+?)]/ );
     869                        control.themeLocation = matches[1];
     870                        api.Control.prototype.initialize.call( control, id, options );
     871                },
     872
     873                ready: function() {
     874                        var control = this, navMenuIdRegex = /^nav_menu\[(-?\d+)]/;
     875
     876                        // @todo It would be better if this was added directly on the setting itself, as opposed to the control.
     877                        control.setting.validate = function( value ) {
     878                                return parseInt( value, 10 );
     879                        };
     880
     881                        // Add/remove menus from the available options when they are added and removed.
     882                        api.bind( 'add', function( setting ) {
     883                                var option, menuId, matches = setting.id.match( navMenuIdRegex );
     884                                if ( ! matches || false === setting() ) {
     885                                        return;
     886                                }
     887                                menuId = matches[1];
     888                                option = new Option( setting().name, menuId );
     889                                control.container.find( 'select' ).append( option );
     890                        });
     891                        api.bind( 'remove', function( setting ) {
     892                                var menuId, matches = setting.id.match( navMenuIdRegex );
     893                                if ( ! matches ) {
     894                                        return;
     895                                }
     896                                menuId = parseInt( matches[1], 10 );
     897                                if ( control.setting() === menuId ) {
     898                                        control.setting.set( '' );
     899                                }
     900                                control.container.find( 'option[value=' + menuId + ']' ).remove();
     901                        });
     902                        api.bind( 'change', function( setting ) {
     903                                var menuId, matches = setting.id.match( navMenuIdRegex );
     904                                if ( ! matches ) {
     905                                        return;
     906                                }
     907                                menuId = parseInt( matches[1], 10 );
     908                                if ( false === setting() ) {
     909                                        if ( control.setting() === menuId ) {
     910                                                control.setting.set( '' );
     911                                        }
     912                                        control.container.find( 'option[value=' + menuId + ']' ).remove();
     913                                } else {
     914                                        control.container.find( 'option[value=' + menuId + ']' ).text( setting().name );
     915                                }
     916                        });
     917                }
     918        });
     919
     920        /**
     921         * wp.customize.Menus.MenuItemControl
     922         *
     923         * Customizer control for menu items.
     924         * Note that 'menu_item' must match the WP_Customize_Menu_Item_Control::$type.
     925         *
     926         * @constructor
     927         * @augments wp.customize.Control
     928         */
     929        api.Menus.MenuItemControl = api.Control.extend({
     930
     931                /**
     932                 * @inheritdoc
     933                 */
     934                initialize: function( id, options ) {
     935                        var control = this;
     936                        api.Control.prototype.initialize.call( control, id, options );
     937                        control.active.validate = function() {
     938                                return api.section( control.section() ).active();
     939                        };
     940                },
     941
     942                /**
     943                 * @since Menu Customizer 0.3
     944                 *
     945                 * Override the embed() method to do nothing,
     946                 * so that the control isn't embedded on load,
     947                 * unless the containing section is already expanded.
     948                 */
     949                embed: function() {
     950                        var control = this,
     951                                sectionId = control.section(),
     952                                section;
     953                        if ( ! sectionId ) {
     954                                return;
     955                        }
     956                        section = api.section( sectionId );
     957                        if ( section && section.expanded() ) {
     958                                control.actuallyEmbed();
     959                        }
     960                },
     961
     962                /**
     963                 * This function is called in Section.onChangeExpanded() so the control
     964                 * will only get embedded when the Section is first expanded.
     965                 *
     966                 * @since Menu Customizer 0.3
     967                 */
     968                actuallyEmbed: function() {
     969                        var control = this;
     970                        if ( 'resolved' === control.deferred.embedded.state() ) {
     971                                return;
     972                        }
     973                        control.renderContent();
     974                        control.deferred.embedded.resolve(); // This triggers control.ready().
     975                },
     976
     977                /**
     978                 * Set up the control.
     979                 */
     980                ready: function() {
     981                        if ( 'undefined' === typeof this.params.menu_item_id ) {
     982                                throw new Error( 'params.menu_item_id was not defined' );
     983                        }
     984
     985                        this._setupControlToggle();
     986                        this._setupReorderUI();
     987                        this._setupUpdateUI();
     988                        this._setupRemoveUI();
     989                        this._setupLinksUI();
     990                        this._setupTitleUI();
     991                },
     992
     993                /**
     994                 * Show/hide the settings when clicking on the menu item handle.
     995                 */
     996                _setupControlToggle: function() {
     997                        var control = this;
     998
     999                        this.container.find( '.menu-item-handle' ).on( 'click', function( e ) {
     1000                                e.preventDefault();
     1001                                e.stopPropagation();
     1002                                var menuControl = control.getMenuControl();
     1003                                if ( menuControl.isReordering || menuControl.isSorting ) {
     1004                                        return;
     1005                                }
     1006                                control.toggleForm();
     1007                        } );
     1008                },
     1009
     1010                /**
     1011                 * Set up the menu-item-reorder-nav
     1012                 */
     1013                _setupReorderUI: function() {
     1014                        var control = this, template, $reorderNav;
     1015
     1016                        template = wp.template( 'menu-item-reorder-nav' );
     1017
     1018                        // Add the menu item reordering elements to the menu item control.
     1019                        control.container.find( '.item-controls' ).after( template );
     1020
     1021                        // Handle clicks for up/down/left-right on the reorder nav.
     1022                        $reorderNav = control.container.find( '.menu-item-reorder-nav' );
     1023                        $reorderNav.find( '.menus-move-up, .menus-move-down, .menus-move-left, .menus-move-right' ).on( 'click', function() {
     1024                                var moveBtn = $( this );
     1025                                moveBtn.focus();
     1026
     1027                                var isMoveUp = moveBtn.is( '.menus-move-up' ),
     1028                                        isMoveDown = moveBtn.is( '.menus-move-down' ),
     1029                                        isMoveLeft = moveBtn.is( '.menus-move-left' ),
     1030                                        isMoveRight = moveBtn.is( '.menus-move-right' );
     1031
     1032                                if ( isMoveUp ) {
     1033                                        control.moveUp();
     1034                                } else if ( isMoveDown ) {
     1035                                        control.moveDown();
     1036                                } else if ( isMoveLeft ) {
     1037                                        control.moveLeft();
     1038                                } else if ( isMoveRight ) {
     1039                                        control.moveRight();
     1040                                }
     1041
     1042                                moveBtn.focus(); // Re-focus after the container was moved.
     1043                        } );
     1044                },
     1045
     1046                /**
     1047                 * Set up event handlers for menu item updating.
     1048                 */
     1049                _setupUpdateUI: function() {
     1050                        var control = this,
     1051                                settingValue = control.setting();
     1052
     1053                        control.elements = {};
     1054                        control.elements.url = new api.Element( control.container.find( '.edit-menu-item-url' ) );
     1055                        control.elements.title = new api.Element( control.container.find( '.edit-menu-item-title' ) );
     1056                        control.elements.attr_title = new api.Element( control.container.find( '.edit-menu-item-attr-title' ) );
     1057                        control.elements.target = new api.Element( control.container.find( '.edit-menu-item-target' ) );
     1058                        control.elements.classes = new api.Element( control.container.find( '.edit-menu-item-classes' ) );
     1059                        control.elements.xfn = new api.Element( control.container.find( '.edit-menu-item-xfn' ) );
     1060                        control.elements.description = new api.Element( control.container.find( '.edit-menu-item-description' ) );
     1061                        // @todo allow other elements, added by plugins, to be automatically picked up here; allow additional values to be added to setting array.
     1062
     1063                        _.each( control.elements, function( element, property ) {
     1064                                element.bind(function( value ) {
     1065                                        if ( element.element.is( 'input[type=checkbox]' ) ) {
     1066                                                value = ( value ) ? element.element.val() : '';
     1067                                        }
     1068
     1069                                        var settingValue = control.setting();
     1070                                        if ( settingValue && settingValue[ property ] !== value ) {
     1071                                                settingValue = _.clone( settingValue );
     1072                                                settingValue[ property ] = value;
     1073                                                control.setting.set( settingValue );
     1074                                        }
     1075                                });
     1076                                if ( settingValue ) {
     1077                                        element.set( settingValue[ property ] );
     1078                                }
     1079                        });
     1080
     1081                        control.setting.bind(function( to, from ) {
     1082                                var itemId = control.params.menu_item_id,
     1083                                        followingSiblingItemControls = [],
     1084                                        childrenItemControls = [],
     1085                                        menuControl;
     1086
     1087                                if ( false === to ) {
     1088                                        menuControl = api.control( 'nav_menu[' + String( from.nav_menu_term_id ) + ']' );
     1089                                        control.container.remove();
     1090
     1091                                        _.each( menuControl.getMenuItemControls(), function( otherControl ) {
     1092                                                if ( from.menu_item_parent === otherControl.setting().menu_item_parent && otherControl.setting().position > from.position ) {
     1093                                                        followingSiblingItemControls.push( otherControl );
     1094                                                } else if ( otherControl.setting().menu_item_parent === itemId ) {
     1095                                                        childrenItemControls.push( otherControl );
     1096                                                }
     1097                                        });
     1098
     1099                                        // Shift all following siblings by the number of children this item has.
     1100                                        _.each( followingSiblingItemControls, function( followingSiblingItemControl ) {
     1101                                                var value = _.clone( followingSiblingItemControl.setting() );
     1102                                                value.position += childrenItemControls.length;
     1103                                                followingSiblingItemControl.setting.set( value );
     1104                                        });
     1105
     1106                                        // Now move the children up to be the new subsequent siblings.
     1107                                        _.each( childrenItemControls, function( childrenItemControl, i ) {
     1108                                                var value = _.clone( childrenItemControl.setting() );
     1109                                                value.position = from.position + i;
     1110                                                value.menu_item_parent = from.menu_item_parent;
     1111                                                childrenItemControl.setting.set( value );
     1112                                        });
     1113
     1114                                        menuControl.debouncedReflowMenuItems();
     1115                                } else {
     1116                                        // Update the elements' values to match the new setting properties.
     1117                                        _.each( to, function( value, key ) {
     1118                                                if ( control.elements[ key] ) {
     1119                                                        control.elements[ key ].set( to[ key ] );
     1120                                                }
     1121                                        } );
     1122                                        control.container.find( '.menu-item-data-parent-id' ).val( to.menu_item_parent );
     1123
     1124                                        // Handle UI updates when the position or depth (parent) change.
     1125                                        if ( to.position !== from.position || to.menu_item_parent !== from.menu_item_parent ) {
     1126                                                control.getMenuControl().debouncedReflowMenuItems();
     1127                                        }
     1128                                }
     1129                        });
     1130                },
     1131
     1132                /**
     1133                 * Set up event handlers for menu item deletion.
     1134                 */
     1135                _setupRemoveUI: function() {
     1136                        var control = this, $removeBtn;
     1137
     1138                        // Configure delete button.
     1139                        $removeBtn = control.container.find( '.item-delete' );
     1140
     1141                        $removeBtn.on( 'click', function( e ) {
     1142                                // Find an adjacent element to add focus to when this menu item goes away
     1143                                var $adjacentFocusTarget;
     1144                                if ( control.container.next().is( '.customize-control-nav_menu_item' ) ) {
     1145                                        if ( ! $( 'body' ).hasClass( 'adding-menu-items' ) ) {
     1146                                                $adjacentFocusTarget = control.container.next().find( '.item-edit:first' );
     1147                                        } else {
     1148                                                $adjacentFocusTarget = control.container.next().find( '.item-delete:first' );
     1149                                        }
     1150                                } else if ( control.container.prev().is( '.customize-control-nav_menu_item' ) ) {
     1151                                        if ( ! $( 'body' ).hasClass( 'adding-menu-items' ) ) {
     1152                                                $adjacentFocusTarget = control.container.prev().find( '.item-edit:first' );
     1153                                        } else {
     1154                                                $adjacentFocusTarget = control.container.prev().find( '.item-delete:first' );
     1155                                        }
     1156                                } else {
     1157                                        $adjacentFocusTarget = control.container.next( '.customize-control-nav_menu' ).find( '.add-new-menu-item' );
     1158                                }
     1159
     1160                                control.container.slideUp( function() {
     1161                                        control.setting.set( false );
     1162                                        wp.a11y.speak( api.Menus.data.l10n.itemDeleted );
     1163                                        $adjacentFocusTarget.focus(); // keyboard accessibility
     1164                                } );
     1165                        } );
     1166                },
     1167
     1168                _setupLinksUI: function() {
     1169                        var $origBtn;
     1170
     1171                        // Configure original link.
     1172                        $origBtn = this.container.find( 'a.original-link' );
     1173
     1174                        $origBtn.on( 'click', function( e ) {
     1175                                e.preventDefault();
     1176                                api.previewer.previewUrl( e.target.toString() );
     1177                        } );
     1178                },
     1179
     1180                /**
     1181                 * Update item handle title when changed.
     1182                 */
     1183                _setupTitleUI: function() {
     1184                        var control = this;
     1185
     1186                        control.setting.bind( function( item ) {
     1187                                if ( ! item ) {
     1188                                        return;
     1189                                }
     1190
     1191                                var titleEl = control.container.find( '.menu-item-title' );
     1192
     1193                                // Don't update to an empty title.
     1194                                if ( item.title ) {
     1195                                        titleEl
     1196                                                .text( item.title )
     1197                                                .removeClass( 'no-title' );
     1198                                } else {
     1199                                        titleEl
     1200                                                .text( api.Menus.data.l10n.untitled )
     1201                                                .addClass( 'no-title' );
     1202                                }
     1203                        } );
     1204                },
     1205
     1206                /**
     1207                 *
     1208                 * @returns {number}
     1209                 */
     1210                getDepth: function() {
     1211                        var control = this, setting = control.setting(), depth = 0;
     1212                        if ( ! setting ) {
     1213                                return 0;
     1214                        }
     1215                        while ( setting && setting.menu_item_parent ) {
     1216                                depth += 1;
     1217                                control = api.control( 'nav_menu_item[' + setting.menu_item_parent + ']' );
     1218                                if ( ! control ) {
     1219                                        break;
     1220                                }
     1221                                setting = control.setting();
     1222                        }
     1223                        return depth;
     1224                },
     1225
     1226                /**
     1227                 * Amend the control's params with the data necessary for the JS template just in time.
     1228                 */
     1229                renderContent: function() {
     1230                        var control = this,
     1231                                settingValue = control.setting(),
     1232                                containerClasses;
     1233
     1234                        control.params.title = settingValue.title || '';
     1235                        control.params.depth = control.getDepth();
     1236                        control.container.data( 'item-depth', control.params.depth );
     1237                        containerClasses = [
     1238                                'menu-item',
     1239                                'menu-item-depth-' + String( control.params.depth ),
     1240                                'menu-item-' + settingValue.object,
     1241                                'menu-item-edit-inactive'
     1242                        ];
     1243
     1244                        if ( settingValue.invalid ) {
     1245                                containerClasses.push( 'invalid' );
     1246                                control.params.title = api.Menus.data.invalidTitleTpl.replace( '%s', control.params.title );
     1247                        } else if ( 'draft' === settingValue.status ) {
     1248                                containerClasses.push( 'pending' );
     1249                                control.params.title = api.Menus.data.pendingTitleTpl.replace( '%s', control.params.title );
     1250                        }
     1251
     1252                        control.params.el_classes = containerClasses.join( ' ' );
     1253                        control.params.item_type_label = api.Menus.getTypeLabel( settingValue.type, settingValue.object );
     1254                        control.params.item_type = settingValue.type;
     1255                        control.params.url = settingValue.url;
     1256                        control.params.target = settingValue.target;
     1257                        control.params.attr_title = settingValue.attr_title;
     1258                        control.params.classes = _.isArray( settingValue.classes ) ? settingValue.classes.join( ' ' ) : settingValue.classes;
     1259                        control.params.attr_title = settingValue.attr_title;
     1260                        control.params.xfn = settingValue.xfn;
     1261                        control.params.description = settingValue.description;
     1262                        control.params.parent = settingValue.menu_item_parent;
     1263                        control.params.original_title = settingValue.original_title || '';
     1264
     1265                        control.container.addClass( control.params.el_classes );
     1266
     1267                        api.Control.prototype.renderContent.call( control );
     1268                },
     1269
     1270                /***********************************************************************
     1271                 * Begin public API methods
     1272                 **********************************************************************/
     1273
     1274                /**
     1275                 * @return {wp.customize.controlConstructor.nav_menu|null}
     1276                 */
     1277                getMenuControl: function() {
     1278                        var control = this, settingValue = control.setting();
     1279                        if ( settingValue && settingValue.nav_menu_term_id ) {
     1280                                return api.control( 'nav_menu[' + settingValue.nav_menu_term_id + ']' );
     1281                        } else {
     1282                                return null;
     1283                        }
     1284                },
     1285
     1286                /**
     1287                 * Expand the accordion section containing a control
     1288                 */
     1289                expandControlSection: function() {
     1290                        var $section = this.container.closest( '.accordion-section' );
     1291
     1292                        if ( ! $section.hasClass( 'open' ) ) {
     1293                                $section.find( '.accordion-section-title:first' ).trigger( 'click' );
     1294                        }
     1295                },
     1296
     1297                /**
     1298                 * Expand the menu item form control.
     1299                 */
     1300                expandForm: function() {
     1301                        this.toggleForm( true );
     1302                },
     1303
     1304                /**
     1305                 * Collapse the menu item form control.
     1306                 */
     1307                collapseForm: function() {
     1308                        this.toggleForm( false );
     1309                },
     1310
     1311                /**
     1312                 * Expand or collapse the menu item control.
     1313                 *
     1314                 * @param {boolean|undefined} [showOrHide] If not supplied, will be inverse of current visibility
     1315                 */
     1316                toggleForm: function( showOrHide ) {
     1317                        var self = this, $menuitem, $inside, complete;
     1318
     1319                        $menuitem = this.container;
     1320                        $inside = $menuitem.find( '.menu-item-settings:first' );
     1321                        if ( 'undefined' === typeof showOrHide ) {
     1322                                showOrHide = ! $inside.is( ':visible' );
     1323                        }
     1324
     1325                        // Already expanded or collapsed.
     1326                        if ( $inside.is( ':visible' ) === showOrHide ) {
     1327                                return;
     1328                        }
     1329
     1330                        if ( showOrHide ) {
     1331                                // Close all other menu item controls before expanding this one.
     1332                                api.control.each( function( otherControl ) {
     1333                                        if ( self.params.type === otherControl.params.type && self !== otherControl ) {
     1334                                                otherControl.collapseForm();
     1335                                        }
     1336                                } );
     1337
     1338                                complete = function() {
     1339                                        $menuitem
     1340                                                .removeClass( 'menu-item-edit-inactive' )
     1341                                                .addClass( 'menu-item-edit-active' );
     1342                                        self.container.trigger( 'expanded' );
     1343                                };
     1344
     1345                                $inside.slideDown( 'fast', complete );
     1346
     1347                                self.container.trigger( 'expand' );
     1348                        } else {
     1349                                complete = function() {
     1350                                        $menuitem
     1351                                                .addClass( 'menu-item-edit-inactive' )
     1352                                                .removeClass( 'menu-item-edit-active' );
     1353                                        self.container.trigger( 'collapsed' );
     1354                                };
     1355
     1356                                self.container.trigger( 'collapse' );
     1357
     1358                                $inside.slideUp( 'fast', complete );
     1359                        }
     1360                },
     1361
     1362                /**
     1363                 * Expand the containing menu section, expand the form, and focus on
     1364                 * the first input in the control.
     1365                 */
     1366                focus: function() {
     1367                        this.expandControlSection();
     1368                        this.expandForm();
     1369                        this.container.find( '.menu-item-settings :focusable:first' ).focus();
     1370                },
     1371
     1372                /**
     1373                 * Move menu item up one in the menu.
     1374                 */
     1375                moveUp: function() {
     1376                        this._changePosition( -1 );
     1377                        wp.a11y.speak( api.Menus.data.l10n.movedUp );
     1378                },
     1379
     1380                /**
     1381                 * Move menu item up one in the menu.
     1382                 */
     1383                moveDown: function() {
     1384                        this._changePosition( 1 );
     1385                        wp.a11y.speak( api.Menus.data.l10n.movedDown );
     1386                },
     1387                /**
     1388                 * Move menu item and all children up one level of depth.
     1389                 */
     1390                moveLeft: function() {
     1391                        this._changeDepth( -1 );
     1392                        wp.a11y.speak( api.Menus.data.l10n.movedLeft );
     1393                },
     1394
     1395                /**
     1396                 * Move menu item and children one level deeper, as a submenu of the previous item.
     1397                 */
     1398                moveRight: function() {
     1399                        this._changeDepth( 1 );
     1400                        wp.a11y.speak( api.Menus.data.l10n.movedRight );
     1401                },
     1402
     1403                /**
     1404                 * Note that this will trigger a UI update, causing child items to
     1405                 * move as well and cardinal order class names to be updated.
     1406                 *
     1407                 * @private
     1408                 *
     1409                 * @param {Number} offset 1|-1
     1410                 */
     1411                _changePosition: function( offset ) {
     1412                        var control = this,
     1413                                adjacentSetting,
     1414                                settingValue = _.clone( control.setting() ),
     1415                                siblingSettings = [],
     1416                                realPosition;
     1417
     1418                        if ( 1 !== offset && -1 !== offset ) {
     1419                                throw new Error( 'Offset changes by 1 are only supported.' );
     1420                        }
     1421
     1422                        // Skip moving deleted items.
     1423                        if ( ! control.setting() ) {
     1424                                return;
     1425                        }
     1426
     1427                        // Locate the other items under the same parent (siblings).
     1428                        _( control.getMenuControl().getMenuItemControls() ).each(function( otherControl ) {
     1429                                if ( otherControl.setting().menu_item_parent === settingValue.menu_item_parent ) {
     1430                                        siblingSettings.push( otherControl.setting );
     1431                                }
     1432                        });
     1433                        siblingSettings.sort(function( a, b ) {
     1434                                return a().position - b().position;
     1435                        });
     1436
     1437                        realPosition = _.indexOf( siblingSettings, control.setting );
     1438                        if ( -1 === realPosition ) {
     1439                                throw new Error( 'Expected setting to be among siblings.' );
     1440                        }
     1441
     1442                        // Skip doing anything if the item is already at the edge in the desired direction.
     1443                        if ( ( realPosition === 0 && offset < 0 ) || ( realPosition === siblingSettings.length - 1 && offset > 0 ) ) {
     1444                                // @todo Should we allow a menu item to be moved up to break it out of a parent? Adopt with previous or following parent?
     1445                                return;
     1446                        }
     1447
     1448                        // Update any adjacent menu item setting to take on this item's position.
     1449                        adjacentSetting = siblingSettings[ realPosition + offset ];
     1450                        if ( adjacentSetting ) {
     1451                                adjacentSetting.set( $.extend(
     1452                                        _.clone( adjacentSetting() ),
     1453                                        {
     1454                                                position: settingValue.position
     1455                                        }
     1456                                ) );
     1457                        }
     1458
     1459                        settingValue.position += offset;
     1460                        control.setting.set( settingValue );
     1461                },
     1462
     1463                /**
     1464                 * Note that this will trigger a UI update, causing child items to
     1465                 * move as well and cardinal order class names to be updated.
     1466                 *
     1467                 * @private
     1468                 *
     1469                 * @param {Number} offset 1|-1
     1470                 */
     1471                _changeDepth: function( offset ) {
     1472                        if ( 1 !== offset && -1 !== offset ) {
     1473                                throw new Error( 'Offset changes by 1 are only supported.' );
     1474                        }
     1475                        var control = this,
     1476                                settingValue = _.clone( control.setting() ),
     1477                                siblingControls = [],
     1478                                realPosition,
     1479                                siblingControl,
     1480                                parentControl;
     1481
     1482                        // Locate the other items under the same parent (siblings).
     1483                        _( control.getMenuControl().getMenuItemControls() ).each(function( otherControl ) {
     1484                                if ( otherControl.setting().menu_item_parent === settingValue.menu_item_parent ) {
     1485                                        siblingControls.push( otherControl );
     1486                                }
     1487                        });
     1488                        siblingControls.sort(function( a, b ) {
     1489                                return a.setting().position - b.setting().position;
     1490                        });
     1491
     1492                        realPosition = _.indexOf( siblingControls, control );
     1493                        if ( -1 === realPosition ) {
     1494                                throw new Error( 'Expected control to be among siblings.' );
     1495                        }
     1496
     1497                        if ( -1 === offset ) {
     1498                                // Skip moving left an item that is already at the top level.
     1499                                if ( ! settingValue.menu_item_parent ) {
     1500                                        return;
     1501                                }
     1502
     1503                                parentControl = api.control( 'nav_menu_item[' + settingValue.menu_item_parent + ']' );
     1504
     1505                                // Make this control the parent of all the following siblings.
     1506                                _( siblingControls ).chain().slice( realPosition ).each(function( siblingControl, i ) {
     1507                                        siblingControl.setting.set(
     1508                                                $.extend(
     1509                                                        {},
     1510                                                        siblingControl.setting(),
     1511                                                        {
     1512                                                                menu_item_parent: control.params.menu_item_id,
     1513                                                                position: i
     1514                                                        }
     1515                                                )
     1516                                        );
     1517                                });
     1518
     1519                                // Increase the positions of the parent item's subsequent children to make room for this one.
     1520                                _( control.getMenuControl().getMenuItemControls() ).each(function( otherControl ) {
     1521                                        var otherControlSettingValue, isControlToBeShifted;
     1522                                        isControlToBeShifted = (
     1523                                                otherControl.setting().menu_item_parent === parentControl.setting().menu_item_parent &&
     1524                                                otherControl.setting().position > parentControl.setting().position
     1525                                        );
     1526                                        if ( isControlToBeShifted ) {
     1527                                                otherControlSettingValue = _.clone( otherControl.setting() );
     1528                                                otherControl.setting.set(
     1529                                                        $.extend(
     1530                                                                otherControlSettingValue,
     1531                                                                { position: otherControlSettingValue.position + 1 }
     1532                                                        )
     1533                                                );
     1534                                        }
     1535                                });
     1536
     1537                                // Make this control the following sibling of its parent item.
     1538                                settingValue.position = parentControl.setting().position + 1;
     1539                                settingValue.menu_item_parent = parentControl.setting().menu_item_parent;
     1540                                control.setting.set( settingValue );
     1541
     1542                        } else if ( 1 === offset ) {
     1543                                // Skip moving right an item that doesn't have a previous sibling.
     1544                                if ( realPosition === 0 ) {
     1545                                        return;
     1546                                }
     1547
     1548                                // Make the control the last child of the previous sibling.
     1549                                siblingControl = siblingControls[ realPosition - 1 ];
     1550                                settingValue.menu_item_parent = siblingControl.params.menu_item_id;
     1551                                settingValue.position = 0;
     1552                                _( control.getMenuControl().getMenuItemControls() ).each(function( otherControl ) {
     1553                                        if ( otherControl.setting().menu_item_parent === settingValue.menu_item_parent ) {
     1554                                                settingValue.position = Math.max( settingValue.position, otherControl.setting().position );
     1555                                        }
     1556                                });
     1557                                settingValue.position += 1;
     1558                                control.setting.set( settingValue );
     1559                        }
     1560                }
     1561        } );
     1562
     1563        /**
     1564         * wp.customize.Menus.MenuNameControl
     1565         *
     1566         * Customizer control for a nav menu's name.
     1567         *
     1568         * @constructor
     1569         * @augments wp.customize.Control
     1570         */
     1571        api.Menus.MenuNameControl = api.Control.extend({
     1572
     1573                ready: function() {
     1574                        var control = this,
     1575                                settingValue = control.setting();
     1576
     1577                        /*
     1578                         * Since the control is not registered in PHP, we need to prevent the
     1579                         * preview's sending of the activeControls to result in this control
     1580                         * being deactivated.
     1581                         */
     1582                        control.active.validate = function() {
     1583                                return api.section( control.section() ).active();
     1584                        };
     1585
     1586                        control.nameElement = new api.Element( control.container.find( '.menu-name-field' ) );
     1587
     1588                        control.nameElement.bind(function( value ) {
     1589                                var settingValue = control.setting();
     1590                                if ( settingValue && settingValue.name !== value ) {
     1591                                        settingValue = _.clone( settingValue );
     1592                                        settingValue.name = value;
     1593                                        control.setting.set( settingValue );
     1594                                }
     1595                        });
     1596                        if ( settingValue ) {
     1597                                control.nameElement.set( settingValue.name );
     1598                        }
     1599
     1600                        control.setting.bind(function( object ) {
     1601                                if ( object ) {
     1602                                        control.nameElement.set( object.name );
     1603                                }
     1604                        });
     1605                }
     1606
     1607        });
     1608
     1609        /**
     1610         * wp.customize.Menus.MenuControl
     1611         *
     1612         * Customizer control for menus.
     1613         * Note that 'nav_menu' must match the WP_Menu_Customize_Control::$type
     1614         *
     1615         * @constructor
     1616         * @augments wp.customize.Control
     1617         */
     1618        api.Menus.MenuControl = api.Control.extend({
     1619                /**
     1620                 * Set up the control.
     1621                 */
     1622                ready: function() {
     1623                        var control = this,
     1624                                menuId = control.params.menu_id;
     1625
     1626                        if ( 'undefined' === typeof this.params.menu_id ) {
     1627                                throw new Error( 'params.menu_id was not defined' );
     1628                        }
     1629
     1630                        /*
     1631                         * Since the control is not registered in PHP, we need to prevent the
     1632                         * preview's sending of the activeControls to result in this control
     1633                         * being deactivated.
     1634                         */
     1635                        control.active.validate = function() {
     1636                                return api.section( control.section() ).active();
     1637                        };
     1638
     1639                        control.$controlSection = control.container.closest( '.control-section' );
     1640                        control.$sectionContent = control.container.closest( '.accordion-section-content' );
     1641
     1642                        this._setupModel();
     1643
     1644                        api.section( control.section(), function( section ) {
     1645                                section.deferred.initSortables.done(function( menuList ) {
     1646                                        control._setupSortable( menuList );
     1647                                });
     1648                        } );
     1649
     1650                        this._setupAddition();
     1651                        this._setupLocations();
     1652                        this._setupTitle();
     1653
     1654                        // Add menu to Custom Menu widgets.
     1655                        if ( control.setting() ) {
     1656                                api.control.each( function( widgetControl ) {
     1657                                        if ( ! widgetControl.extended( api.controlConstructor.widget_form ) || 'nav_menu' !== widgetControl.params.widget_id_base ) {
     1658                                                return;
     1659                                        }
     1660                                        var select = widgetControl.container.find( 'select' );
     1661                                        if ( select.find( 'option[value=' + String( menuId ) + ']' ).length === 0 ) {
     1662                                                select.append( new Option( control.setting().name, menuId ) );
     1663                                        }
     1664                                } );
     1665                                $( '#available-widgets-list .widget-inside:has(input.id_base[value=nav_menu]) select:first' ).append( new Option( control.setting().name, menuId ) );
     1666                        }
     1667                },
     1668
     1669                /**
     1670                 * Update ordering of menu item controls when the setting is updated.
     1671                 */
     1672                _setupModel: function() {
     1673                        var control = this,
     1674                                menuId = control.params.menu_id;
     1675
     1676                        control.elements = {};
     1677                        control.elements.auto_add = new api.Element( control.container.find( 'input[type=checkbox].auto_add' ) );
     1678
     1679                        control.elements.auto_add.bind(function( auto_add ) {
     1680                                var settingValue = control.setting();
     1681                                if ( settingValue && settingValue.auto_add !== auto_add ) {
     1682                                        settingValue = _.clone( settingValue );
     1683                                        settingValue.auto_add = auto_add;
     1684                                        control.setting.set( settingValue );
     1685                                }
     1686                        });
     1687                        control.elements.auto_add.set( control.setting().auto_add );
     1688                        control.setting.bind(function( object ) {
     1689                                if ( ! object ) {
     1690                                        return;
     1691                                }
     1692                                control.elements.auto_add.set( object.auto_add );
     1693                        });
     1694
     1695                        control.setting.bind( function( to ) {
     1696                                if ( false === to ) {
     1697                                        control._handleDeletion();
     1698                                } else {
     1699                                        // Update names in the Custom Menu widgets.
     1700                                        api.control.each( function( widgetControl ) {
     1701                                                if ( ! widgetControl.extended( api.controlConstructor.widget_form ) || 'nav_menu' !== widgetControl.params.widget_id_base ) {
     1702                                                        return;
     1703                                                }
     1704                                                var select = widgetControl.container.find( 'select' );
     1705                                                select.find( 'option[value=' + String( menuId ) + ']' ).text( to.name );
     1706                                        });
     1707                                        $( '#available-widgets-list .widget-inside:has(input.id_base[value=nav_menu]) select:first option[value=' + String( menuId ) + ']' ).text( to.name );
     1708                                }
     1709                        } );
     1710
     1711                        control.container.find( '.menu-delete' ).on( 'click', function( event ) {
     1712                                event.stopPropagation();
     1713                                event.preventDefault();
     1714                                control.setting.set( false );
     1715                        });
     1716                },
     1717
     1718                /**
     1719                 * Allow items in each menu to be re-ordered, and for the order to be previewed.
     1720                 *
     1721                 * Notice that the UI aspects here are handled by wpNavMenu.initSortables()
     1722                 * which is called in MenuSection.onChangeExpanded()
     1723                 *
     1724                 * @param {object} menuList - The element that has sortable().
     1725                 */
     1726                _setupSortable: function( menuList ) {
     1727                        var control = this;
     1728
     1729                        if ( ! menuList.is( control.$sectionContent ) ) {
     1730                                throw new Error( 'Unexpected menuList.' );
     1731                        }
     1732
     1733                        menuList.on( 'sortstart', function() {
     1734                                control.isSorting = true;
     1735                        });
     1736
     1737                        menuList.on( 'sortstop', function() {
     1738                                setTimeout( function() { // Next tick.
     1739                                        var menuItemContainerIds = control.$sectionContent.sortable( 'toArray' ),
     1740                                                menuItemControls = [],
     1741                                                position = 0,
     1742                                                priority = 10;
     1743
     1744                                        control.isSorting = false;
     1745
     1746                                        _.each( menuItemContainerIds, function( menuItemContainerId ) {
     1747                                                var menuItemId, menuItemControl, matches;
     1748                                                matches = menuItemContainerId.match( /^customize-control-nav_menu_item-(-?\d+)$/, '' );
     1749                                                if ( ! matches ) {
     1750                                                        return;
     1751                                                }
     1752                                                menuItemId = parseInt( matches[1], 10 );
     1753                                                menuItemControl = api.control( 'nav_menu_item[' + String( menuItemId ) + ']' );
     1754                                                if ( menuItemControl ) {
     1755                                                        menuItemControls.push( menuItemControl );
     1756                                                }
     1757                                        } );
     1758
     1759                                        _.each( menuItemControls, function( menuItemControl ) {
     1760                                                if ( false === menuItemControl.setting() ) {
     1761                                                        // Skip deleted items.
     1762                                                        return;
     1763                                                }
     1764                                                var setting = _.clone( menuItemControl.setting() );
     1765                                                position += 1;
     1766                                                priority += 1;
     1767                                                setting.position = position;
     1768                                                menuItemControl.priority( priority );
     1769
     1770                                                // Note that wpNavMenu will be setting this .menu-item-data-parent-id input's value.
     1771                                                setting.menu_item_parent = parseInt( menuItemControl.container.find( '.menu-item-data-parent-id' ).val(), 10 );
     1772                                                if ( ! setting.menu_item_parent ) {
     1773                                                        setting.menu_item_parent = 0;
     1774                                                }
     1775
     1776                                                menuItemControl.setting.set( setting );
     1777                                        });
     1778                                });
     1779                        });
     1780
     1781                        control.isReordering = false;
     1782
     1783                        /**
     1784                         * Keyboard-accessible reordering.
     1785                         */
     1786                        this.container.find( '.reorder-toggle' ).on( 'click', function() {
     1787                                control.toggleReordering( ! control.isReordering );
     1788                        } );
     1789                },
     1790
     1791                /**
     1792                 * Set up UI for adding a new menu item.
     1793                 */
     1794                _setupAddition: function() {
     1795                        var self = this;
     1796
     1797                        this.container.find( '.add-new-menu-item' ).on( 'click', function( event ) {
     1798                                if ( self.$sectionContent.hasClass( 'reordering' ) ) {
     1799                                        return;
     1800                                }
     1801
     1802                                if ( ! $( 'body' ).hasClass( 'adding-menu-items' ) ) {
     1803                                        api.Menus.availableMenuItemsPanel.open( self );
     1804                                } else {
     1805                                        api.Menus.availableMenuItemsPanel.close();
     1806                                        event.stopPropagation();
     1807                                }
     1808                        } );
     1809                },
     1810
     1811                _handleDeletion: function() {
     1812                        var control = this,
     1813                                section,
     1814                                menuId = control.params.menu_id,
     1815                                removeSection;
     1816                        section = api.section( control.section() );
     1817                        removeSection = function() {
     1818                                section.container.remove();
     1819                                api.section.remove( section.id );
     1820                        };
     1821
     1822                        if ( section && section.expanded() ) {
     1823                                section.collapse({
     1824                                        completeCallback: function() {
     1825                                                removeSection();
     1826                                                wp.a11y.speak( api.Menus.data.l10n.menuDeleted );
     1827                                                api.panel( 'menus' ).focus();
     1828                                        }
     1829                                });
     1830                        } else {
     1831                                removeSection();
     1832                        }
     1833
     1834                        // Remove the menu from any Custom Menu widgets.
     1835                        api.control.each(function( widgetControl ) {
     1836                                if ( ! widgetControl.extended( api.controlConstructor.widget_form ) || 'nav_menu' !== widgetControl.params.widget_id_base ) {
     1837                                        return;
     1838                                }
     1839                                var select = widgetControl.container.find( 'select' );
     1840                                if ( select.val() === String( menuId ) ) {
     1841                                        select.prop( 'selectedIndex', 0 ).trigger( 'change' );
     1842                                }
     1843                                select.find( 'option[value=' + String( menuId ) + ']' ).remove();
     1844                        });
     1845                        $( '#available-widgets-list .widget-inside:has(input.id_base[value=nav_menu]) select:first option[value=' + String( menuId ) + ']' ).remove();
     1846                },
     1847
     1848                // Setup theme location checkboxes.
     1849                _setupLocations: function() {
     1850                        var control = this;
     1851
     1852                        control.container.find( '.assigned-menu-location' ).each(function() {
     1853                                var container = $( this ),
     1854                                        checkbox = container.find( 'input[type=checkbox]' ),
     1855                                        element,
     1856                                        updateSelectedMenuLabel,
     1857                                        navMenuLocationSetting = api( 'nav_menu_locations[' + checkbox.data( 'location-id' ) + ']' );
     1858
     1859                                updateSelectedMenuLabel = function( selectedMenuId ) {
     1860                                        var menuSetting = api( 'nav_menu[' + String( selectedMenuId ) + ']' );
     1861                                        if ( ! selectedMenuId || ! menuSetting || ! menuSetting() ) {
     1862                                                container.find( '.theme-location-set' ).hide();
     1863                                        } else {
     1864                                                container.find( '.theme-location-set' ).show().find( 'span' ).text( menuSetting().name );
     1865                                        }
     1866                                };
     1867
     1868                                element = new api.Element( checkbox );
     1869                                element.set( navMenuLocationSetting.get() === control.params.menu_id );
     1870
     1871                                checkbox.on( 'change', function() {
     1872                                        // Note: We can't use element.bind( function( checked ){ ... } ) here because it will trigger a change as well.
     1873                                        navMenuLocationSetting.set( this.checked ? control.params.menu_id : 0 );
     1874                                } );
     1875
     1876                                navMenuLocationSetting.bind(function( selectedMenuId ) {
     1877                                        element.set( selectedMenuId === control.params.menu_id );
     1878                                        updateSelectedMenuLabel( selectedMenuId );
     1879                                });
     1880                                updateSelectedMenuLabel( navMenuLocationSetting.get() );
     1881
     1882                        });
     1883                },
     1884
     1885                /**
     1886                 * Update Section Title as menu name is changed.
     1887                 */
     1888                _setupTitle: function() {
     1889                        var control = this;
     1890
     1891                        control.setting.bind( function( menu ) {
     1892                                if ( ! menu ) {
     1893                                        return;
     1894                                }
     1895
     1896                                // Empty names are not allowed (will not be saved), don't update to one.
     1897                                if ( menu.name ) {
     1898                                        var section = control.container.closest( '.accordion-section' ),
     1899                                                menuId = control.params.menu_id,
     1900                                                controlTitle = section.find( '.accordion-section-title' ),
     1901                                                sectionTitle = section.find( '.customize-section-title h3' ),
     1902                                                location = section.find( '.menu-in-location' ),
     1903                                                action = sectionTitle.find( '.customize-action' );
     1904
     1905                                        // Update the control title
     1906                                        controlTitle.text( menu.name );
     1907                                        if ( location.length ) {
     1908                                                location.appendTo( controlTitle );
     1909                                        }
     1910
     1911                                        // Update the section title
     1912                                        sectionTitle.text( menu.name );
     1913                                        if ( action.length ) {
     1914                                                action.prependTo( sectionTitle );
     1915                                        }
     1916
     1917                                        // Update the nav menu name in location selects.
     1918                                        api.control.each( function( control ) {
     1919                                                if ( /^nav_menu_locations\[/.test( control.id ) ) {
     1920                                                        control.container.find( 'option[value=' + menuId + ']' ).text( menu.name );
     1921                                                }
     1922                                        } );
     1923
     1924                                        // Update the nav menu name in all location checkboxes.
     1925                                        section.find( '.customize-control-checkbox input' ).each( function() {
     1926                                                if ( $( this ).prop( 'checked' ) ) {
     1927                                                        $( '.current-menu-location-name-' + $( this ).data( 'location-id' ) ).text( menu.name );
     1928                                                }
     1929                                        } );
     1930                                }
     1931                        } );
     1932                },
     1933
     1934                /***********************************************************************
     1935                 * Begin public API methods
     1936                 **********************************************************************/
     1937
     1938                /**
     1939                 * Enable/disable the reordering UI
     1940                 *
     1941                 * @param {Boolean} showOrHide to enable/disable reordering
     1942                 */
     1943                toggleReordering: function( showOrHide ) {
     1944                        showOrHide = Boolean( showOrHide );
     1945
     1946                        if ( showOrHide === this.$sectionContent.hasClass( 'reordering' ) ) {
     1947                                return;
     1948                        }
     1949
     1950                        this.isReordering = showOrHide;
     1951                        this.$sectionContent.toggleClass( 'reordering', showOrHide );
     1952                        this.$sectionContent.sortable( this.isReordering ? 'disable' : 'enable' );
     1953
     1954                        if ( showOrHide ) {
     1955                                _( this.getMenuItemControls() ).each( function( formControl ) {
     1956                                        formControl.collapseForm();
     1957                                } );
     1958                        }
     1959                },
     1960
     1961                /**
     1962                 * @return {wp.customize.controlConstructor.nav_menu_item[]}
     1963                 */
     1964                getMenuItemControls: function() {
     1965                        var menuControl = this,
     1966                                menuItemControls = [],
     1967                                menuTermId = menuControl.params.menu_id;
     1968
     1969                        api.control.each(function( control ) {
     1970                                if ( 'nav_menu_item' === control.params.type && control.setting() && menuTermId === control.setting().nav_menu_term_id ) {
     1971                                        menuItemControls.push( control );
     1972                                }
     1973                        });
     1974
     1975                        return menuItemControls;
     1976                },
     1977
     1978                /**
     1979                 * Make sure that each menu item control has the proper depth.
     1980                 */
     1981                reflowMenuItems: function() {
     1982                        var menuControl = this,
     1983                                menuSection = api.section( 'nav_menu[' + String( menuControl.params.menu_id ) + ']' ),
     1984                                menuItemControls = menuControl.getMenuItemControls(),
     1985                                reflowRecursively;
     1986
     1987                        reflowRecursively = function( context ) {
     1988                                var currentMenuItemControls = [],
     1989                                        thisParent = context.currentParent;
     1990                                _.each( context.menuItemControls, function( menuItemControl ) {
     1991                                        if ( thisParent === menuItemControl.setting().menu_item_parent ) {
     1992                                                currentMenuItemControls.push( menuItemControl );
     1993                                                // @todo We could remove this item from menuItemControls now, for efficiency.
     1994                                        }
     1995                                });
     1996                                currentMenuItemControls.sort( function( a, b ) {
     1997                                        return a.setting().position - b.setting().position;
     1998                                });
     1999
     2000                                _.each( currentMenuItemControls, function( menuItemControl ) {
     2001                                        // Update position.
     2002                                        context.currentAbsolutePosition += 1;
     2003                                        menuItemControl.priority.set( context.currentAbsolutePosition ); // This will change the sort order.
     2004
     2005                                        // Update depth.
     2006                                        if ( ! menuItemControl.container.hasClass( 'menu-item-depth-' + String( context.currentDepth ) ) ) {
     2007                                                _.each( menuItemControl.container.prop( 'className' ).match( /menu-item-depth-\d+/g ), function( className ) {
     2008                                                        menuItemControl.container.removeClass( className );
     2009                                                });
     2010                                                menuItemControl.container.addClass( 'menu-item-depth-' + String( context.currentDepth ) );
     2011                                        }
     2012                                        menuItemControl.container.data( 'item-depth', context.currentDepth );
     2013
     2014                                        // Process any children items.
     2015                                        context.currentDepth += 1;
     2016                                        context.currentParent = menuItemControl.params.menu_item_id;
     2017                                        reflowRecursively( context );
     2018                                        context.currentDepth -= 1;
     2019                                        context.currentParent = thisParent;
     2020                                });
     2021
     2022                                // Update class names for reordering controls.
     2023                                if ( currentMenuItemControls.length ) {
     2024                                        _( currentMenuItemControls ).each(function( menuItemControl ) {
     2025                                                menuItemControl.container.removeClass( 'move-up-disabled move-down-disabled move-left-disabled move-right-disabled' );
     2026                                        });
     2027
     2028                                        currentMenuItemControls[0].container
     2029                                                .addClass( 'move-up-disabled' )
     2030                                                .addClass( 'move-right-disabled' )
     2031                                                .toggleClass( 'move-down-disabled', 1 === currentMenuItemControls.length );
     2032                                        currentMenuItemControls[ currentMenuItemControls.length - 1 ].container
     2033                                                .addClass( 'move-down-disabled' )
     2034                                                .toggleClass( 'move-up-disabled', 1 === currentMenuItemControls.length );
     2035                                }
     2036                        };
     2037
     2038                        reflowRecursively( {
     2039                                menuItemControls: menuItemControls,
     2040                                currentParent: 0,
     2041                                currentDepth: 0,
     2042                                currentAbsolutePosition: 0
     2043                        } );
     2044
     2045                        menuSection.container.find( '.menu-item .menu-item-reorder-nav button' ).prop( 'tabIndex', 0 );
     2046                        menuSection.container.find( '.menu-item.move-up-disabled .menus-move-up' ).prop( 'tabIndex', -1 );
     2047                        menuSection.container.find( '.menu-item.move-down-disabled .menus-move-down' ).prop( 'tabIndex', -1 );
     2048                        menuSection.container.find( '.menu-item.move-left-disabled .menus-move-left' ).prop( 'tabIndex', -1 );
     2049                        menuSection.container.find( '.menu-item.move-right-disabled .menus-move-right' ).prop( 'tabIndex', -1 );
     2050
     2051                        menuControl.container.find( '.reorder-toggle' ).toggle( menuItemControls.length > 1 );
     2052                },
     2053
     2054                /**
     2055                 * Note that this function gets debounced so that when a lot of setting
     2056                 * changes are made at once, for instance when moving a menu item that
     2057                 * has child items, this function will only be called once all of the
     2058                 * settings have been updated.
     2059                 */
     2060                debouncedReflowMenuItems: _.debounce( function() {
     2061                        this.reflowMenuItems.apply( this, arguments );
     2062                }, 0 ),
     2063
     2064                /**
     2065                 * Add a new item to this menu.
     2066                 *
     2067                 * @param {object} item - Value for the nav_menu_item setting to be created.
     2068                 * @returns {wp.customize.Menus.controlConstructor.nav_menu_item} The newly-created nav_menu_item control instance.
     2069                 */
     2070                addItemToMenu: function( item ) {
     2071                        var menuControl = this, customizeId, settingArgs, setting, menuItemControl, placeholderId, position = 0, priority = 10;
     2072
     2073                        _.each( menuControl.getMenuItemControls(), function( control ) {
     2074                                if ( false === control.setting() ) {
     2075                                        return;
     2076                                }
     2077                                priority = Math.max( priority, control.priority() );
     2078                                if ( 0 === control.setting().menu_item_parent ) {
     2079                                        position = Math.max( position, control.setting().position );
     2080                                }
     2081                        });
     2082                        position += 1;
     2083                        priority += 1;
     2084
     2085                        item = $.extend(
     2086                                {},
     2087                                api.Menus.data.defaultSettingValues.nav_menu_item,
     2088                                item,
     2089                                {
     2090                                        nav_menu_term_id: menuControl.params.menu_id,
     2091                                        original_title: item.title,
     2092                                        position: position
     2093                                }
     2094                        );
     2095                        delete item.id; // only used by Backbone
     2096
     2097                        placeholderId = api.Menus.generatePlaceholderAutoIncrementId();
     2098                        customizeId = 'nav_menu_item[' + String( placeholderId ) + ']';
     2099                        settingArgs = {
     2100                                type: 'nav_menu_item',
     2101                                transport: 'postMessage',
     2102                                previewer: api.previewer
     2103                        };
     2104                        setting = api.create( customizeId, customizeId, {}, settingArgs );
     2105                        setting.set( item ); // Change from initial empty object to actual item to mark as dirty.
     2106
     2107                        // Add the menu item control.
     2108                        menuItemControl = new api.controlConstructor.nav_menu_item( customizeId, {
     2109                                params: {
     2110                                        type: 'nav_menu_item',
     2111                                        content: '<li id="customize-control-nav_menu_item-' + String( placeholderId ) + '" class="customize-control customize-control-nav_menu_item"></li>',
     2112                                        section: menuControl.id,
     2113                                        priority: priority,
     2114                                        active: true,
     2115                                        settings: {
     2116                                                'default': customizeId
     2117                                        },
     2118                                        menu_item_id: placeholderId
     2119                                },
     2120                                previewer: api.previewer
     2121                        } );
     2122
     2123                        api.control.add( customizeId, menuItemControl );
     2124                        setting.preview();
     2125                        menuControl.debouncedReflowMenuItems();
     2126
     2127                        wp.a11y.speak( api.Menus.data.l10n.itemAdded );
     2128
     2129                        return menuItemControl;
     2130                }
     2131        } );
     2132
     2133        /**
     2134         * wp.customize.Menus.NewMenuControl
     2135         *
     2136         * Customizer control for creating new menus and handling deletion of existing menus.
     2137         * Note that 'new_menu' must match the WP_New_Menu_Customize_Control::$type.
     2138         *
     2139         * @constructor
     2140         * @augments wp.customize.Control
     2141         */
     2142        api.Menus.NewMenuControl = api.Control.extend({
     2143                /**
     2144                 * Set up the control.
     2145                 */
     2146                ready: function() {
     2147                        this._bindHandlers();
     2148                },
     2149
     2150                _bindHandlers: function() {
     2151                        var self = this,
     2152                                name = $( '#customize-control-new_menu_name input' ),
     2153                                submit = $( '#create-new-menu-submit' );
     2154                        name.on( 'keydown', function( event ) {
     2155                                if ( 13 === event.which ) { // Enter.
     2156                                        self.submit();
     2157                                }
     2158                        } );
     2159                        submit.on( 'click', function( event ) {
     2160                                self.submit();
     2161                                event.stopPropagation();
     2162                                event.preventDefault();
     2163                        } );
     2164                },
     2165
     2166                /**
     2167                 * Create the new menu with the name supplied.
     2168                 *
     2169                 * @returns {boolean}
     2170                 */
     2171                submit: function() {
     2172
     2173                        var control = this,
     2174                                container = control.container.closest( '.accordion-section-new-menu' ),
     2175                                nameInput = container.find( '.menu-name-field' ).first(),
     2176                                name = nameInput.val(),
     2177                                menuSection,
     2178                                customizeId,
     2179                                placeholderId = api.Menus.generatePlaceholderAutoIncrementId();
     2180
     2181                        customizeId = 'nav_menu[' + String( placeholderId ) + ']';
     2182
     2183                        // Register the menu control setting.
     2184                        api.create( customizeId, customizeId, {}, {
     2185                                type: 'nav_menu',
     2186                                transport: 'postMessage',
     2187                                previewer: api.previewer
     2188                        } );
     2189                        api( customizeId ).set( $.extend(
     2190                                {},
     2191                                api.Menus.data.defaultSettingValues.nav_menu,
     2192                                {
     2193                                        name: name
     2194                                }
     2195                        ) );
     2196
     2197                        /*
     2198                         * Add the menu section (and its controls).
     2199                         * Note that this will automatically create the required controls
     2200                         * inside via the Section's ready method.
     2201                         */
     2202                        menuSection = new api.Menus.MenuSection( customizeId, {
     2203                                params: {
     2204                                        id: customizeId,
     2205                                        panel: 'menus',
     2206                                        title: name,
     2207                                        customizeAction: api.Menus.data.l10n.customizingMenus,
     2208                                        type: 'menu',
     2209                                        priority: 10,
     2210                                        menu_id: placeholderId
     2211                                }
     2212                        } );
     2213                        api.section.add( customizeId, menuSection );
     2214
     2215                        // Clear name field.
     2216                        nameInput.val( '' );
     2217
     2218                        wp.a11y.speak( api.Menus.data.l10n.menuAdded );
     2219
     2220                        // Focus on the new menu section.
     2221                        api.section( customizeId ).focus(); // @todo should we focus on the new menu's control and open the add-items panel? Thinking user flow...
     2222                }
     2223        });
     2224
     2225        /**
     2226         * Extends wp.customize.controlConstructor with control constructor for
     2227         * menu_location, menu_item, nav_menu, and new_menu.
     2228         */
     2229        $.extend( api.controlConstructor, {
     2230                nav_menu_location: api.Menus.MenuLocationControl,
     2231                nav_menu_item: api.Menus.MenuItemControl,
     2232                nav_menu: api.Menus.MenuControl,
     2233                nav_menu_name: api.Menus.MenuNameControl,
     2234                new_menu: api.Menus.NewMenuControl
     2235        });
     2236
     2237        /**
     2238         * Extends wp.customize.panelConstructor with section constructor for menus.
     2239         */
     2240        $.extend( api.panelConstructor, {
     2241                menus: api.Menus.MenusPanel
     2242        });
     2243
     2244        /**
     2245         * Extends wp.customize.sectionConstructor with section constructor for menu.
     2246         */
     2247        $.extend( api.sectionConstructor, {
     2248                nav_menu: api.Menus.MenuSection,
     2249                new_menu: api.Menus.NewMenuSection
     2250        });
     2251
     2252        /**
     2253         * Init Customizer for menus.
     2254         */
     2255        api.bind( 'ready', function() {
     2256
     2257                // Set up the menu items panel.
     2258                api.Menus.availableMenuItemsPanel = new api.Menus.AvailableMenuItemsPanelView({
     2259                        collection: api.Menus.availableMenuItems
     2260                });
     2261
     2262                api.bind( 'saved', function( data ) {
     2263                        if ( data.nav_menu_updates || data.nav_menu_item_updates ) {
     2264                                api.Menus.applySavedData( data );
     2265                        }
     2266                } );
     2267
     2268                api.previewer.bind( 'refresh', function() {
     2269                        api.previewer.refresh();
     2270                });
     2271        } );
     2272
     2273        /**
     2274         * When customize_save comes back with a success, make sure any inserted
     2275         * nav menus and items are properly re-added with their newly-assigned IDs.
     2276         *
     2277         * @param {object} data
     2278         * @param {array} data.nav_menu_updates
     2279         * @param {array} data.nav_menu_item_updates
     2280         */
     2281        api.Menus.applySavedData = function( data ) {
     2282
     2283                var insertedMenuIdMapping = {};
     2284
     2285                _( data.nav_menu_updates ).each(function( update ) {
     2286                        var oldCustomizeId, newCustomizeId, oldSetting, newSetting, settingValue, oldSection, newSection;
     2287                        if ( 'inserted' === update.status ) {
     2288                                if ( ! update.previous_term_id ) {
     2289                                        throw new Error( 'Expected previous_term_id' );
     2290                                }
     2291                                if ( ! update.term_id ) {
     2292                                        throw new Error( 'Expected term_id' );
     2293                                }
     2294                                oldCustomizeId = 'nav_menu[' + String( update.previous_term_id ) + ']';
     2295                                if ( ! api.has( oldCustomizeId ) ) {
     2296                                        throw new Error( 'Expected setting to exist: ' + oldCustomizeId );
     2297                                }
     2298                                oldSetting = api( oldCustomizeId );
     2299                                if ( ! api.section.has( oldCustomizeId ) ) {
     2300                                        throw new Error( 'Expected control to exist: ' + oldCustomizeId );
     2301                                }
     2302                                oldSection = api.section( oldCustomizeId );
     2303
     2304                                settingValue = oldSetting.get();
     2305                                if ( ! settingValue ) {
     2306                                        throw new Error( 'Did not expect setting to be empty (deleted).' );
     2307                                }
     2308                                settingValue = _.clone( settingValue );
     2309
     2310                                insertedMenuIdMapping[ update.previous_term_id ] = update.term_id;
     2311                                newCustomizeId = 'nav_menu[' + String( update.term_id ) + ']';
     2312                                newSetting = api.create( newCustomizeId, newCustomizeId, settingValue, {
     2313                                        type: 'nav_menu',
     2314                                        transport: 'postMessage',
     2315                                        previewer: api.previewer
     2316                                } );
     2317
     2318                                if ( oldSection.expanded() ) {
     2319                                        oldSection.collapse();
     2320                                }
     2321
     2322                                // Add the menu section.
     2323                                newSection = new api.Menus.MenuSection( newCustomizeId, {
     2324                                        params: {
     2325                                                id: newCustomizeId,
     2326                                                panel: 'menus',
     2327                                                title: settingValue.name,
     2328                                                customizeAction: api.Menus.data.l10n.customizingMenus,
     2329                                                type: 'menu',
     2330                                                priority: oldSection.priority.get(),
     2331                                                active: true,
     2332                                                menu_id: update.term_id
     2333                                        }
     2334                                } );
     2335
     2336                                // Remove old setting and control.
     2337                                oldSection.container.remove();
     2338                                api.section.remove( oldCustomizeId );
     2339
     2340                                // Add new control to take its place.
     2341                                api.section.add( newCustomizeId, newSection );
     2342
     2343                                // Delete the placeholder and preview the new setting.
     2344                                oldSetting.callbacks.disable(); // Prevent setting triggering Customizer dirty state when set.
     2345                                oldSetting.set( false );
     2346                                oldSetting.preview();
     2347                                newSetting.preview();
     2348
     2349                                // Update nav_menu_locations to reference the new ID.
     2350                                api.each( function( setting ) {
     2351                                        var wasSaved = api.state( 'saved' ).get();
     2352                                        if ( /^nav_menu_locations\[/.test( setting.id ) && setting.get() === update.previous_term_id ) {
     2353                                                setting.set( update.term_id );
     2354                                                setting._dirty = false; // Not dirty because this is has also just been done on server in WP_Customize_Nav_Menu_Setting::update().
     2355                                                api.state( 'saved' ).set( wasSaved );
     2356                                                setting.preview();
     2357                                        }
     2358                                } );
     2359
     2360                                if ( oldSection.expanded.get() ) {
     2361                                        // @todo This doesn't seem to be working.
     2362                                        newSection.expand();
     2363                                }
     2364
     2365                                // @todo Update the Custom Menu selects, ensuring the newly-inserted IDs are used for any that have selected a placeholder menu.
     2366                        }
     2367                } );
     2368
     2369                _( data.nav_menu_item_updates ).each(function( update ) {
     2370                        var oldCustomizeId, newCustomizeId, oldSetting, newSetting, settingValue, oldControl, newControl;
     2371                        if ( 'inserted' === update.status ) {
     2372                                if ( ! update.previous_post_id ) {
     2373                                        throw new Error( 'Expected previous_post_id' );
     2374                                }
     2375                                if ( ! update.post_id ) {
     2376                                        throw new Error( 'Expected post_id' );
     2377                                }
     2378                                oldCustomizeId = 'nav_menu_item[' + String( update.previous_post_id ) + ']';
     2379                                if ( ! api.has( oldCustomizeId ) ) {
     2380                                        throw new Error( 'Expected setting to exist: ' + oldCustomizeId );
     2381                                }
     2382                                oldSetting = api( oldCustomizeId );
     2383                                if ( ! api.control.has( oldCustomizeId ) ) {
     2384                                        throw new Error( 'Expected control to exist: ' + oldCustomizeId );
     2385                                }
     2386                                oldControl = api.control( oldCustomizeId );
     2387
     2388                                settingValue = oldSetting.get();
     2389                                if ( ! settingValue ) {
     2390                                        throw new Error( 'Did not expect setting to be empty (deleted).' );
     2391                                }
     2392                                settingValue = _.clone( settingValue );
     2393
     2394                                // If the menu was also inserted, then make sure it uses the new menu ID for nav_menu_term_id.
     2395                                if ( insertedMenuIdMapping[ settingValue.nav_menu_term_id ] ) {
     2396                                        settingValue.nav_menu_term_id = insertedMenuIdMapping[ settingValue.nav_menu_term_id ];
     2397                                }
     2398
     2399                                newCustomizeId = 'nav_menu_item[' + String( update.post_id ) + ']';
     2400                                newSetting = api.create( newCustomizeId, newCustomizeId, settingValue, {
     2401                                        type: 'nav_menu_item',
     2402                                        transport: 'postMessage',
     2403                                        previewer: api.previewer
     2404                                } );
     2405
     2406                                // Add the menu control.
     2407                                newControl = new api.controlConstructor.nav_menu_item( newCustomizeId, {
     2408                                        params: {
     2409                                                type: 'nav_menu_item',
     2410                                                content: '<li id="customize-control-nav_menu_item-' + String( update.post_id ) + '" class="customize-control customize-control-nav_menu_item"></li>',
     2411                                                menu_id: update.post_id,
     2412                                                section: 'nav_menu[' + String( settingValue.nav_menu_term_id ) + ']',
     2413                                                priority: oldControl.priority.get(),
     2414                                                active: true,
     2415                                                settings: {
     2416                                                        'default': newCustomizeId
     2417                                                },
     2418                                                menu_item_id: update.post_id
     2419                                        },
     2420                                        previewer: api.previewer
     2421                                } );
     2422
     2423                                // Remove old setting and control.
     2424                                oldControl.container.remove();
     2425                                api.control.remove( oldCustomizeId );
     2426
     2427                                // Add new control to take its place.
     2428                                api.control.add( newCustomizeId, newControl );
     2429
     2430                                // Delete the placeholder and preview the new setting.
     2431                                oldSetting.callbacks.disable(); // Prevent setting triggering Customizer dirty state when set.
     2432                                oldSetting.set( false );
     2433                                oldSetting.preview();
     2434                                newSetting.preview();
     2435
     2436                                newControl.container.toggleClass( 'menu-item-edit-inactive', oldControl.container.hasClass( 'menu-item-edit-inactive' ) );
     2437                        }
     2438                });
     2439
     2440                // @todo trigger change event for each Custom Menu widget that was modified.
     2441        };
     2442
     2443        /**
     2444         * Focus a menu item control.
     2445         *
     2446         * @param {string} menuItemId
     2447         */
     2448        api.Menus.focusMenuItemControl = function( menuItemId ) {
     2449                var control = api.Menus.getMenuItemControl( menuItemId );
     2450
     2451                if ( control ) {
     2452                        control.focus();
     2453                }
     2454        };
     2455
     2456        /**
     2457         * Get the control for a given menu.
     2458         *
     2459         * @param menuId
     2460         * @return {wp.customize.controlConstructor.menus[]}
     2461         */
     2462        api.Menus.getMenuControl = function( menuId ) {
     2463                return api.control( 'nav_menu[' + menuId + ']' );
     2464        };
     2465
     2466        /**
     2467         * Given a menu item type & object, get the label associated with it.
     2468         *
     2469         * @param {string} type
     2470         * @param {string} object
     2471         * @return {string}
     2472         */
     2473        api.Menus.getTypeLabel = function( type, object ) {
     2474                var label,
     2475                        data = api.Menus.data;
     2476
     2477                if ( 'post_type' === type ) {
     2478                        if ( data.itemTypes.postTypes[ object ] ) {
     2479                                label = data.itemTypes.postTypes[ object ].label;
     2480                        } else {
     2481                                label = data.l10n.postTypeLabel;
     2482                        }
     2483                } else if ( 'taxonomy' === type ) {
     2484                        if ( data.itemTypes.taxonomies[ object ] ) {
     2485                                label = data.itemTypes.taxonomies[ object ].label;
     2486                        } else {
     2487                                label = data.l10n.taxonomyTermLabel;
     2488                        }
     2489                } else {
     2490                        label = data.l10n.custom_label;
     2491                }
     2492
     2493                return label;
     2494        };
     2495
     2496        /**
     2497         * Given a menu item ID, get the control associated with it.
     2498         *
     2499         * @param {string} menuItemId
     2500         * @return {object|null}
     2501         */
     2502        api.Menus.getMenuItemControl = function( menuItemId ) {
     2503                return api.control( menuItemIdToSettingId( menuItemId ) );
     2504        };
     2505
     2506        /**
     2507         * @param {String} menuItemId
     2508         */
     2509        function menuItemIdToSettingId( menuItemId ) {
     2510                return 'nav_menu_item[' + menuItemId + ']';
     2511        }
     2512
     2513})( wp.customize, wp, jQuery );
  • src/wp-includes/class-wp-customize-control.php

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

     
    4949         */
    5050        public $widgets;
    5151
     52        /**
     53         * Methods and properties deailing with managing nav menus in the Customizer.
     54         *
     55         * @var WP_Customize_Nav_Menus
     56         */
     57        public $nav_menus;
     58
    5259        protected $settings   = array();
    5360        protected $containers = array();
    5461        protected $panels     = array();
     
    104111                require_once( ABSPATH . WPINC . '/class-wp-customize-section.php' );
    105112                require_once( ABSPATH . WPINC . '/class-wp-customize-control.php' );
    106113                require_once( ABSPATH . WPINC . '/class-wp-customize-widgets.php' );
     114                require_once( ABSPATH . WPINC . '/class-wp-customize-nav-menus.php' );
    107115
    108116                $this->widgets = new WP_Customize_Widgets( $this );
     117                $this->nav_menus = new WP_Customize_Nav_Menus( $this );
    109118
    110119                add_filter( 'wp_die_handler', array( $this, 'wp_die_handler' ) );
    111120
  • src/wp-includes/class-wp-customize-nav-menus.php

     
     1<?php
     2/**
     3 * WordPress Customize Nav Menus classes
     4 *
     5 * @package WordPress
     6 * @subpackage Customize
     7 * @since 4.3.0
     8 */
     9
     10/**
     11 * Customize Nav Menus class.
     12 *
     13 * Implements menu management in the Customizer.
     14 *
     15 * @since 4.3.0
     16 *
     17 * @see WP_Customize_Manager
     18 */
     19final class WP_Customize_Nav_Menus {
     20
     21        /**
     22         * WP_Customize_Manager instance.
     23         *
     24         * @since 4.3.0
     25         *
     26         * @access public
     27         * @var WP_Customize_Manager
     28         */
     29        public $manager;
     30
     31        /**
     32         * Previewed Menus.
     33         *
     34         * @since 4.3.0
     35         *
     36         * @access public
     37         * @var array
     38         */
     39        public $previewed_menus;
     40
     41        /**
     42         * Constructor.
     43         *
     44         * @since 4.3.0
     45         *
     46         * @access public
     47         * @param object $manager An instance of the WP_Customize_Manager class.
     48         */
     49        public function __construct( $manager ) {
     50                $this->previewed_menus = array();
     51                $this->manager         = $manager;
     52
     53                add_action( 'wp_ajax_load-available-menu-items-customizer', array( $this, 'ajax_load_available_items' ) );
     54                add_action( 'wp_ajax_search-available-menu-items-customizer', array( $this, 'ajax_search_available_items' ) );
     55                add_action( 'customize_controls_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
     56                add_action( 'customize_register', array( $this, 'customize_register' ), 11 ); // Needs to run after core Navigation section is set up.
     57                add_filter( 'customize_dynamic_setting_args', array( $this, 'filter_dynamic_setting_args' ), 10, 2 );
     58                add_filter( 'customize_dynamic_setting_class', array( $this, 'filter_dynamic_setting_class' ), 10, 3 );
     59                add_action( 'customize_controls_print_footer_scripts', array( $this, 'print_templates' ) );
     60                add_action( 'customize_controls_print_footer_scripts', array( $this, 'available_items_template' ) );
     61                add_action( 'customize_preview_init', array( $this, 'customize_preview_init' ) );
     62        }
     63
     64        /**
     65         * Ajax handler for loading available menu items.
     66         *
     67         * @since 4.3.0
     68         */
     69        public function ajax_load_available_items() {
     70                check_ajax_referer( 'customize-menus', 'customize-menus-nonce' );
     71
     72                if ( ! current_user_can( 'edit_theme_options' ) ) {
     73                        wp_send_json_error( array( 'message' => __( 'Error: invalid user capabilities.' ) ) );
     74                }
     75                if ( empty( $_POST['obj_type'] ) || empty( $_POST['type'] ) ) {
     76                        wp_send_json_error( array( 'message' => __( 'Missing obj_type or type param.' ) ) );
     77                }
     78
     79                $obj_type = sanitize_key( $_POST['obj_type'] );
     80                if ( ! in_array( $obj_type, array( 'post_type', 'taxonomy' ) ) ) {
     81                        wp_send_json_error( array( 'message' => __( 'Invalid obj_type param: ' . $obj_type ) ) );
     82                }
     83                $taxonomy_or_post_type = sanitize_key( $_POST['type'] );
     84                $page = isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 0;
     85                $items = array();
     86
     87                if ( 'post_type' === $obj_type ) {
     88                        if ( ! get_post_type_object( $taxonomy_or_post_type ) ) {
     89                                wp_send_json_error( array( 'message' => __( 'Unknown post type.' ) ) );
     90                        }
     91
     92                        if ( 0 === $page && 'page' === $taxonomy_or_post_type ) {
     93                                // Add "Home" link. Treat as a page, but switch to custom on add.
     94                                $items[] = array(
     95                                        'id'         => 'home',
     96                                        'title'      => _x( 'Home', 'nav menu home label' ),
     97                                        'type'       => 'custom',
     98                                        'type_label' => __( 'Custom Link' ),
     99                                        'object'     => '',
     100                                        'url'        => home_url(),
     101                                );
     102                        }
     103
     104                        $posts = get_posts( array(
     105                                'numberposts' => 10,
     106                                'offset'      => 10 * $page,
     107                                'orderby'     => 'date',
     108                                'order'       => 'DESC',
     109                                'post_type'   => $taxonomy_or_post_type,
     110                        ) );
     111                        foreach ( $posts as $post ) {
     112                                $items[] = array(
     113                                        'id'         => "post-{$post->ID}",
     114                                        'title'      => html_entity_decode( get_the_title( $post ), ENT_QUOTES, get_bloginfo( 'charset' ) ),
     115                                        'type'       => 'post_type',
     116                                        'type_label' => get_post_type_object( $post->post_type )->labels->singular_name,
     117                                        'object'     => $post->post_type,
     118                                        'object_id'  => (int) $post->ID,
     119                                );
     120                        }
     121                } else if ( 'taxonomy' === $obj_type ) {
     122                        $terms = get_terms( $taxonomy_or_post_type, array(
     123                                'child_of'     => 0,
     124                                'exclude'      => '',
     125                                'hide_empty'   => false,
     126                                'hierarchical' => 1,
     127                                'include'      => '',
     128                                'number'       => 10,
     129                                'offset'       => 10 * $page,
     130                                'order'        => 'DESC',
     131                                'orderby'      => 'count',
     132                                'pad_counts'   => false,
     133                        ) );
     134                        if ( is_wp_error( $terms ) ) {
     135                                wp_send_json_error( array( 'message' => wp_strip_all_tags( $terms->get_error_message(), true ) ) );
     136                        }
     137
     138                        foreach ( $terms as $term ) {
     139                                $items[] = array(
     140                                        'id'         => "term-{$term->term_id}",
     141                                        'title'      => html_entity_decode( $term->name, ENT_QUOTES, get_bloginfo( 'charset' ) ),
     142                                        'type'       => 'taxonomy',
     143                                        'type_label' => get_taxonomy( $term->taxonomy )->labels->singular_name,
     144                                        'object'     => $term->taxonomy,
     145                                        'object_id'  => $term->term_id,
     146                                );
     147                        }
     148                }
     149
     150                wp_send_json_success( array( 'items' => $items ) );
     151        }
     152
     153        /**
     154         * Ajax handler for searching available menu items.
     155         *
     156         * @since 4.3.0
     157         */
     158        public function ajax_search_available_items() {
     159                check_ajax_referer( 'customize-menus', 'customize-menus-nonce' );
     160
     161                if ( ! current_user_can( 'edit_theme_options' ) ) {
     162                        wp_send_json_error( array( 'message' => __( 'Error: invalid user capabilities.' ) ) );
     163                }
     164                if ( empty( $_POST['search'] ) ) {
     165                        wp_send_json_error( array( 'message' => __( 'Error: missing search parameter.' ) ) );
     166                }
     167
     168                $p = isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 0;
     169                if ( $p < 1 ) {
     170                        $p = 1;
     171                }
     172
     173                $s = sanitize_text_field( wp_unslash( $_POST['search'] ) );
     174                $results = $this->search_available_items_query( array( 'pagenum' => $p, 's' => $s ) );
     175
     176                if ( empty( $results ) ) {
     177                        wp_send_json_error( array( 'message' => __( 'No results found.' ) ) );
     178                } else {
     179                        wp_send_json_success( array( 'items' => $results ) );
     180                }
     181        }
     182
     183        /**
     184         * Performs post queries for available-item searching.
     185         *
     186         * Based on WP_Editor::wp_link_query().
     187         *
     188         * @since 4.3.0
     189         *
     190         * @param array $args Optional. Accepts 'pagenum' and 's' (search) arguments.
     191         * @return array Results.
     192         */
     193        public function search_available_items_query( $args = array() ) {
     194                $results = array();
     195
     196                $post_type_objects = get_post_types( array( 'show_in_nav_menus' => true ), 'objects' );
     197                $query = array(
     198                        'post_type'              => array_keys( $post_type_objects ),
     199                        'suppress_filters'       => true,
     200                        'update_post_term_cache' => false,
     201                        'update_post_meta_cache' => false,
     202                        'post_status'            => 'publish',
     203                        'posts_per_page'         => 20,
     204                );
     205
     206                $args['pagenum'] = isset( $args['pagenum'] ) ? absint( $args['pagenum'] ) : 1;
     207                $query['offset'] = $args['pagenum'] > 1 ? $query['posts_per_page'] * ( $args['pagenum'] - 1 ) : 0;
     208
     209                if ( isset( $args['s'] ) ) {
     210                        $query['s'] = $args['s'];
     211                }
     212
     213                // Query posts.
     214                $get_posts = new WP_Query( $query );
     215
     216                // Check if any posts were found.
     217                if ( $get_posts->post_count ) {
     218                        foreach ( $get_posts->posts as $post ) {
     219                                $results[] = array(
     220                                        'id'         => 'post-' . $post->ID,
     221                                        'type'       => 'post_type',
     222                                        'type_label' => $post_type_objects[ $post->post_type ]->labels->singular_name,
     223                                        'object'     => $post->post_type,
     224                                        'object_id'  => intval( $post->ID ),
     225                                        'title'      => html_entity_decode( get_the_title( $post ), ENT_QUOTES, get_bloginfo( 'charset' ) ),
     226                                );
     227                        }
     228                }
     229
     230                // Query taxonomy terms.
     231                $taxonomies = get_taxonomies( array( 'show_in_nav_menus' => true ), 'names' );
     232                $terms = get_terms( $taxonomies, array(
     233                        'name__like' => $args['s'],
     234                        'number'     => 20,
     235                        'offset'     => 20 * ($args['pagenum'] - 1),
     236                ) );
     237
     238                // Check if any taxonomies were found.
     239                if ( ! empty( $terms ) ) {
     240                        foreach ( $terms as $term ) {
     241                                $results[] = array(
     242                                        'id'         => 'term-' . $term->term_id,
     243                                        'type'       => 'taxonomy',
     244                                        'type_label' => get_taxonomy( $term->taxonomy )->labels->singular_name,
     245                                        'object'     => $term->taxonomy,
     246                                        'object_id'  => intval( $term->term_id ),
     247                                        'title'      => html_entity_decode( $term->name, ENT_QUOTES, get_bloginfo( 'charset' ) ),
     248                                );
     249                        }
     250                }
     251
     252                return $results;
     253        }
     254
     255        /**
     256         * Enqueue scripts and styles for Customizer pane.
     257         *
     258         * @since 4.3.0
     259         */
     260        public function enqueue_scripts() {
     261                wp_enqueue_style( 'customize-nav-menus' );
     262                wp_enqueue_script( 'customize-nav-menus' );
     263
     264                $temp_nav_menu_setting      = new WP_Customize_Nav_Menu_Setting( $this->manager, 'nav_menu[-1]' );
     265                $temp_nav_menu_item_setting = new WP_Customize_Nav_Menu_Item_Setting( $this->manager, 'nav_menu_item[-1]' );
     266
     267                // Pass data to JS.
     268                $settings = array(
     269                        'nonce'                => wp_create_nonce( 'customize-menus' ),
     270                        'allMenus'             => wp_get_nav_menus(),
     271                        'itemTypes'            => $this->available_item_types(),
     272                        'l10n'                 => array(
     273                                'untitled'          => _x( '(no label)', 'Missing menu item navigation label.' ),
     274                                'custom_label'      => _x( 'Custom', 'Custom menu item type label.' ),
     275                                'menuLocation'      => _x( '(Currently set to: %s)', 'Current menu location.' ),
     276                                'deleteWarn'        => __( 'You are about to permanently delete this menu. "Cancel" to stop, "OK" to delete.' ),
     277                                'itemAdded'         => __( 'Menu item added' ),
     278                                'itemDeleted'       => __( 'Menu item deleted' ),
     279                                'menuAdded'         => __( 'Menu created' ),
     280                                'menuDeleted'       => __( 'Menu deleted' ),
     281                                'movedUp'           => __( 'Menu item moved up' ),
     282                                'movedDown'         => __( 'Menu item moved down' ),
     283                                'movedLeft'         => __( 'Menu item moved out of submenu' ),
     284                                'movedRight'        => __( 'Menu item is now a sub-item' ),
     285                                'customizingMenus'  => _x( 'Customizing &#9656; Menus', '&#9656 is the unicode right-pointing triangle' ),
     286                                'invalidTitleTpl'   => __( '%s (Invalid)' ),
     287                                'pendingTitleTpl'   => __( '%s (Pending)' ),
     288                                'taxonomyTermLabel' => __( 'Taxonomy' ),
     289                                'postTypeLabel'     => __( 'Post Type' ),
     290                        ),
     291                        'menuItemTransport'    => 'postMessage',
     292                        'phpIntMax'            => PHP_INT_MAX,
     293                        'defaultSettingValues' => array(
     294                                'nav_menu'      => $temp_nav_menu_setting->default,
     295                                'nav_menu_item' => $temp_nav_menu_item_setting->default,
     296                        ),
     297                );
     298
     299                $data = sprintf( 'var _wpCustomizeNavMenusSettings = %s;', wp_json_encode( $settings ) );
     300                wp_scripts()->add_data( 'customize-nav-menus', 'data', $data );
     301
     302                // This is copied from nav-menus.php, and it has an unfortunate object name of `menus`.
     303                $nav_menus_l10n = array(
     304                        'oneThemeLocationNoMenus' => null,
     305                        'moveUp'       => __( 'Move up one' ),
     306                        'moveDown'     => __( 'Move down one' ),
     307                        'moveToTop'    => __( 'Move to the top' ),
     308                        /* translators: %s: previous item name */
     309                        'moveUnder'    => __( 'Move under %s' ),
     310                        /* translators: %s: previous item name */
     311                        'moveOutFrom'  => __( 'Move out from under %s' ),
     312                        /* translators: %s: previous item name */
     313                        'under'        => __( 'Under %s' ),
     314                        /* translators: %s: previous item name */
     315                        'outFrom'      => __( 'Out from under %s' ),
     316                        /* translators: 1: item name, 2: item position, 3: total number of items */
     317                        'menuFocus'    => __( '%1$s. Menu item %2$d of %3$d.' ),
     318                        /* translators: 1: item name, 2: item position, 3: parent item name */
     319                        'subMenuFocus' => __( '%1$s. Sub item number %2$d under %3$s.' ),
     320                );
     321                wp_localize_script( 'nav-menu', 'menus', $nav_menus_l10n );
     322        }
     323
     324        /**
     325         * Filter a dynamic setting's constructor args.
     326         *
     327         * For a dynamic setting to be registered, this filter must be employed
     328         * to override the default false value with an array of args to pass to
     329         * the WP_Customize_Setting constructor.
     330         *
     331         * @since 4.3.0
     332         *
     333         * @param false|array $setting_args The arguments to the WP_Customize_Setting constructor.
     334         * @param string      $setting_id   ID for dynamic setting, usually coming from `$_POST['customized']`.
     335         * @return array|false
     336         */
     337        public function filter_dynamic_setting_args( $setting_args, $setting_id ) {
     338                if ( preg_match( WP_Customize_Nav_Menu_Setting::ID_PATTERN, $setting_id ) ) {
     339                        $setting_args = array(
     340                                'type' => WP_Customize_Nav_Menu_Setting::TYPE,
     341                        );
     342                } else if ( preg_match( WP_Customize_Nav_Menu_Item_Setting::ID_PATTERN, $setting_id ) ) {
     343                        $setting_args = array(
     344                                'type' => WP_Customize_Nav_Menu_Item_Setting::TYPE,
     345                        );
     346                }
     347                return $setting_args;
     348        }
     349
     350        /**
     351         * Allow non-statically created settings to be constructed with custom WP_Customize_Setting subclass.
     352         *
     353         * @since 4.3.0
     354         *
     355         * @param string $setting_class WP_Customize_Setting or a subclass.
     356         * @param string $setting_id    ID for dynamic setting, usually coming from `$_POST['customized']`.
     357         * @param array  $setting_args  WP_Customize_Setting or a subclass.
     358         * @return string
     359         */
     360        public function filter_dynamic_setting_class( $setting_class, $setting_id, $setting_args ) {
     361                unset( $setting_id );
     362
     363                if ( ! empty( $setting_args['type'] ) && WP_Customize_Nav_Menu_Setting::TYPE === $setting_args['type'] ) {
     364                        $setting_class = 'WP_Customize_Nav_Menu_Setting';
     365                } else if ( ! empty( $setting_args['type'] ) && WP_Customize_Nav_Menu_Item_Setting::TYPE === $setting_args['type'] ) {
     366                        $setting_class = 'WP_Customize_Nav_Menu_Item_Setting';
     367                }
     368                return $setting_class;
     369        }
     370
     371        /**
     372         * Add the customizer settings and controls.
     373         *
     374         * @since 4.3.0
     375         */
     376        public function customize_register() {
     377
     378                // Require JS-rendered control types.
     379                $this->manager->register_panel_type( 'WP_Customize_Menus_Panel' );
     380                $this->manager->register_control_type( 'WP_Customize_Nav_Menu_Control' );
     381                $this->manager->register_control_type( 'WP_Customize_Nav_Menu_Name_Control' );
     382                $this->manager->register_control_type( 'WP_Customize_Nav_Menu_Item_Control' );
     383
     384                // Create a panel for Menus.
     385                $this->manager->add_panel( new WP_Customize_Menus_Panel( $this->manager, 'menus', array(
     386                        'title'       => __( 'Menus' ),
     387                        '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>',
     388                        'priority'    => 100,
     389                        // 'theme_supports' => 'menus|widgets', @todo allow multiple theme supports
     390                ) ) );
     391                $menus = wp_get_nav_menus();
     392
     393                // Menu loactions.
     394                $this->manager->remove_section( 'nav' ); // Remove old core section. @todo core merge remove corresponding code from WP_Customize_Manager::register_controls().
     395                $locations     = get_registered_nav_menus();
     396                $num_locations = count( array_keys( $locations ) );
     397                $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 ) );
     398                $description  .= '</p><p>' . __( 'You can also place menus in widget areas with the Custom Menu widget.' ) . '</p>';
     399
     400                $this->manager->add_section( 'menu_locations', array(
     401                        'title'       => __( 'Menu Locations' ),
     402                        'panel'       => 'menus',
     403                        'priority'    => 5,
     404                        'description' => $description,
     405                ) );
     406
     407                // @todo if ( ! $menus ) : make a "default" menu
     408                if ( $menus ) {
     409                        $choices = array( '0' => __( '&mdash; Select &mdash;' ) );
     410                        foreach ( $menus as $menu ) {
     411                                $choices[ $menu->term_id ] = wp_html_excerpt( $menu->name, 40, '&hellip;' );
     412                        }
     413
     414                        foreach ( $locations as $location => $description ) {
     415                                $setting_id = "nav_menu_locations[{$location}]";
     416
     417                                $setting = $this->manager->get_setting( $setting_id );
     418                                if ( $setting ) {
     419                                        $setting->transport = 'postMessage';
     420                                        remove_filter( "customize_sanitize_{$setting_id}", 'absint' );
     421                                        add_filter( "customize_sanitize_{$setting_id}", array( $this, 'intval_base10' ) );
     422                                } else {
     423                                        $this->manager->add_setting( $setting_id, array(
     424                                                'sanitize_callback' => array( $this, 'intval_base10' ),
     425                                                'theme_supports'    => 'menus',
     426                                                'type'              => 'theme_mod',
     427                                                'transport'         => 'postMessage',
     428                                        ) );
     429                                }
     430
     431                                $this->manager->add_control( new WP_Customize_Nav_Menu_Location_Control( $this->manager, $setting_id, array(
     432                                        'label'       => $description,
     433                                        'location_id' => $location,
     434                                        'section'     => 'menu_locations',
     435                                        'choices'     => $choices,
     436                                ) ) );
     437                        }
     438                }
     439
     440                // Register each menu as a Customizer section, and add each menu item to each menu.
     441                foreach ( $menus as $menu ) {
     442                        $menu_id = $menu->term_id;
     443
     444                        // Create a section for each menu.
     445                        $section_id = 'nav_menu[' . $menu_id . ']';
     446                        $this->manager->add_section( new WP_Customize_Nav_Menu_Section( $this->manager, $section_id, array(
     447                                'title'     => $menu->name,
     448                                'priority'  => 10,
     449                                'panel'     => 'menus',
     450                        ) ) );
     451
     452                        $nav_menu_setting_id = 'nav_menu[' . $menu_id . ']';
     453                        $this->manager->add_setting( new WP_Customize_Nav_Menu_Setting( $this->manager, $nav_menu_setting_id ) );
     454
     455                        // Add the menu contents.
     456                        $menu_items = (array) wp_get_nav_menu_items( $menu_id );
     457
     458                        foreach ( array_values( $menu_items ) as $i => $item ) {
     459
     460                                // Create a setting for each menu item (which doesn't actually manage data, currently).
     461                                $menu_item_setting_id = 'nav_menu_item[' . $item->ID . ']';
     462                                $this->manager->add_setting( new WP_Customize_Nav_Menu_Item_Setting( $this->manager, $menu_item_setting_id ) );
     463
     464                                // Create a control for each menu item.
     465                                $this->manager->add_control( new WP_Customize_Nav_Menu_Item_Control( $this->manager, $menu_item_setting_id, array(
     466                                        'label'    => $item->title,
     467                                        'section'  => $section_id,
     468                                        'priority' => 10 + $i,
     469                                ) ) );
     470                        }
     471
     472                        // Note: other controls inside of this section get added dynamically in JS via the MenuSection.ready() function.
     473                }
     474
     475                // Add the add-new-menu section and controls.
     476                $this->manager->add_section( new WP_Customize_New_Menu_Section( $this->manager, 'add_menu', array(
     477                        'title'    => __( 'Add a Menu' ),
     478                        'panel'    => 'menus',
     479                        'priority' => 999,
     480                ) ) );
     481
     482                $this->manager->add_setting( 'new_menu_name', array(
     483                        'type'      => 'new_menu',
     484                        'default'   => '',
     485                        'transport' => 'postMessage',
     486                ) );
     487
     488                $this->manager->add_control( 'new_menu_name', array(
     489                        'label'       => '',
     490                        'section'     => 'add_menu',
     491                        'type'        => 'text',
     492                        'input_attrs' => array(
     493                                'class'       => 'menu-name-field',
     494                                'placeholder' => __( 'New menu name' ),
     495                        ),
     496                ) );
     497
     498                $this->manager->add_setting( 'create_new_menu', array(
     499                        'type' => 'new_menu',
     500                ) );
     501
     502                $this->manager->add_control( new WP_New_Menu_Customize_Control( $this->manager, 'create_new_menu', array(
     503                        'section' => 'add_menu',
     504                ) ) );
     505        }
     506
     507        /**
     508         * Get the base10 intval.
     509         *
     510         * This is used as a setting's sanitize_callback; we can't use just plain
     511         * intval because the second argument is not what intval() expects.
     512         *
     513         * @since 4.3.0
     514         *
     515         * @param mixed $value Number to convert.
     516         *
     517         * @return int
     518         */
     519        function intval_base10( $value ) {
     520                return intval( $value, 10 );
     521        }
     522
     523        /**
     524         * Return an array of all the available item types.
     525         *
     526         * @since 4.3.0
     527         */
     528        public function available_item_types() {
     529                $items = array(
     530                        'postTypes'  => array(),
     531                        'taxonomies' => array(),
     532                );
     533
     534                $post_types = get_post_types( array( 'show_in_nav_menus' => true ), 'objects' );
     535                foreach ( $post_types as $slug => $post_type ) {
     536                        $items['postTypes'][ $slug ] = array(
     537                                'label' => $post_type->labels->singular_name,
     538                        );
     539                }
     540
     541                $taxonomies = get_taxonomies( array( 'show_in_nav_menus' => true ), 'objects' );
     542                foreach ( $taxonomies as $slug => $taxonomy ) {
     543                        if ( 'post_format' === $taxonomy && ! current_theme_supports( 'post-formats' ) ) {
     544                                continue;
     545                        }
     546                        $items['taxonomies'][ $slug ] = array(
     547                                'label' => $taxonomy->labels->singular_name,
     548                        );
     549                }
     550                return $items;
     551        }
     552
     553        /**
     554         * Print the JavaScript templates used to render Menu Customizer components.
     555         *
     556         * Templates are imported into the JS use wp.template.
     557         *
     558         * @since 4.3.0
     559         */
     560        public function print_templates() {
     561                ?>
     562                <script type="text/html" id="tmpl-available-menu-item">
     563                        <div id="menu-item-tpl-{{ data.id }}" class="menu-item-tpl" data-menu-item-id="{{ data.id }}">
     564                                <dl class="menu-item-bar">
     565                                        <dt class="menu-item-handle">
     566                                                <span class="item-type">{{ data.type_label }}</span>
     567                                                <span class="item-title">{{ data.title || wp.customize.Menus.data.l10n.untitled }}</span>
     568                                                <button type="button" class="not-a-button item-add"><span class="screen-reader-text"><?php _e( 'Add Menu Item' ) ?></span></button>
     569                                        </dt>
     570                                </dl>
     571                        </div>
     572                </script>
     573
     574                <script type="text/html" id="tmpl-available-menu-item-type">
     575                        <div id="available-menu-items-{{ data.type }}" class="accordion-section">
     576                                <h4 class="accordion-section-title">{{ data.type_label }}</h4>
     577                                <div class="accordion-section-content">
     578                                </div>
     579                        </div>
     580                </script>
     581
     582                <script type="text/html" id="tmpl-menu-item-reorder-nav">
     583                        <div class="menu-item-reorder-nav">
     584                                <?php
     585                                printf(
     586                                        '<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>',
     587                                        esc_html__( 'Move up' ),
     588                                        esc_html__( 'Move down' ),
     589                                        esc_html__( 'Move one level up' ),
     590                                        esc_html__( 'Move one level down' )
     591                                );
     592                                ?>
     593                        </div>
     594                </script>
     595        <?php
     596        }
     597
     598        /**
     599         * Print the html template used to render the add-menu-item frame.
     600         *
     601         * @since 4.3.0
     602         */
     603        public function available_items_template() {
     604                ?>
     605                <div id="available-menu-items" class="accordion-container">
     606                        <div class="customize-section-title">
     607                                <button type="button" class="customize-section-back" tabindex="-1">
     608                                        <span class="screen-reader-text"><?php _e( 'Back' ); ?></span>
     609                                </button>
     610                                <h3>
     611                                        <span class="customize-action">
     612                                                <?php
     613                                                        /* translators: &#9656; is the unicode right-pointing triangle, and %s is the section title in the Customizer */
     614                                                        printf( __( 'Customizing &#9656; %s' ), esc_html( $this->manager->get_panel( 'menus' )->title ) );
     615                                                ?>
     616                                        </span>
     617                                        <?php _e( 'Add Menu Items' ); ?>
     618                                </h3>
     619                        </div>
     620                        <div id="available-menu-items-search" class="accordion-section cannot-expand">
     621                                <div class="accordion-section-title">
     622                                        <label class="screen-reader-text" for="menu-items-search"><?php _e( 'Search Menu Items' ); ?></label>
     623                                        <input type="text" id="menu-items-search" placeholder="<?php esc_attr_e( 'Search menu items&hellip;' ) ?>" />
     624                                        <span class="spinner"></span>
     625                                </div>
     626                                <div class="accordion-section-content" data-type="search"></div>
     627                        </div>
     628                        <div id="new-custom-menu-item" class="accordion-section">
     629                                <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>
     630                                <div class="accordion-section-content">
     631                                        <input type="hidden" value="custom" id="custom-menu-item-type" name="menu-item[-1][menu-item-type]" />
     632                                        <p id="menu-item-url-wrap">
     633                                                <label class="howto" for="custom-menu-item-url">
     634                                                        <span><?php _e( 'URL' ); ?></span>
     635                                                        <input id="custom-menu-item-url" name="menu-item[-1][menu-item-url]" type="text" class="code menu-item-textbox" value="http://">
     636                                                </label>
     637                                        </p>
     638                                        <p id="menu-item-name-wrap">
     639                                                <label class="howto" for="custom-menu-item-name">
     640                                                        <span><?php _e( 'Link Text' ); ?></span>
     641                                                        <input id="custom-menu-item-name" name="menu-item[-1][menu-item-title]" type="text" class="regular-text menu-item-textbox">
     642                                                </label>
     643                                        </p>
     644                                        <p class="button-controls">
     645                                                <span class="add-to-menu">
     646                                                        <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">
     647                                                        <span class="spinner"></span>
     648                                                </span>
     649                                        </p>
     650                                </div>
     651                        </div>
     652                        <?php
     653
     654                        // @todo: consider using add_meta_box/do_accordion_section and making screen-optional?
     655                        // Containers for per-post-type item browsing; items added with JS.
     656                        $post_types = get_post_types( array( 'show_in_nav_menus' => true ), 'object' );
     657                        if ( $post_types ) :
     658                                foreach ( $post_types as $type ) :
     659                                        ?>
     660                                        <div id="available-menu-items-<?php echo esc_attr( $type->name ); ?>" class="accordion-section">
     661                                                <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>
     662                                                <div class="accordion-section-content" data-type="<?php echo esc_attr( $type->name ); ?>" data-obj_type="post_type"></div>
     663                                        </div>
     664                                <?php
     665                                endforeach;
     666                        endif;
     667
     668                        $taxonomies = get_taxonomies( array( 'show_in_nav_menus' => true ), 'object' );
     669                        if ( $taxonomies ) :
     670                                foreach ( $taxonomies as $tax ) :
     671                                        ?>
     672                                        <div id="available-menu-items-<?php echo esc_attr( $tax->name ); ?>" class="accordion-section">
     673                                                <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>
     674                                                <div class="accordion-section-content" data-type="<?php echo esc_attr( $tax->name ); ?>" data-obj_type="taxonomy"></div>
     675                                        </div>
     676                                <?php
     677                                endforeach;
     678                        endif;
     679                        ?>
     680                </div><!-- #available-menu-items -->
     681        <?php
     682        }
     683
     684        // Start functionality specific to partial-refresh of menu changes in Customizer preview.
     685        const RENDER_AJAX_ACTION = 'customize_render_menu_partial';
     686        const RENDER_NONCE_POST_KEY = 'render-menu-nonce';
     687        const RENDER_QUERY_VAR = 'wp_customize_menu_render';
     688
     689        /**
     690         * The number of wp_nav_menu() calls which have happened in the preview.
     691         *
     692         * @since 4.3.0
     693         *
     694         * @var int
     695         */
     696        public $preview_nav_menu_instance_number = 0;
     697
     698        /**
     699         * Nav menu args used for each instance.
     700         *
     701         * @since 4.3.0
     702         *
     703         * @var array
     704         */
     705        public $preview_nav_menu_instance_args = array();
     706
     707        /**
     708         * Add hooks for the Customizer preview.
     709         *
     710         * @since 4.3.0
     711         */
     712        function customize_preview_init() {
     713                add_action( 'template_redirect', array( $this, 'render_menu' ) );
     714                add_action( 'wp_enqueue_scripts', array( $this, 'customize_preview_enqueue_deps' ) );
     715
     716                if ( ! isset( $_REQUEST[ self::RENDER_QUERY_VAR ] ) ) {
     717                        add_filter( 'wp_nav_menu_args', array( $this, 'filter_wp_nav_menu_args' ), 1000 );
     718                        add_filter( 'wp_nav_menu', array( $this, 'filter_wp_nav_menu' ), 10, 2 );
     719                }
     720        }
     721
     722        /**
     723         * Keep track of the arguments that are being passed to wp_nav_menu().
     724         *
     725         * @since 4.3.0
     726         *
     727         * @see wp_nav_menu()
     728         *
     729         * @param array $args  An array containing wp_nav_menu() arguments.
     730         * @return array
     731         */
     732        function filter_wp_nav_menu_args( $args ) {
     733                $this->preview_nav_menu_instance_number += 1;
     734                $args['instance_number'] = $this->preview_nav_menu_instance_number;
     735
     736                $can_partial_refresh = (
     737                        $args['echo']
     738                        &&
     739                        is_string( $args['fallback_cb'] )
     740                        &&
     741                        is_string( $args['walker'] )
     742                );
     743                $args['can_partial_refresh'] = $can_partial_refresh;
     744
     745                if ( ! $can_partial_refresh ) {
     746                        unset( $args['fallback_cb'] );
     747                        unset( $args['walker'] );
     748                }
     749
     750                ksort( $args );
     751                $args['args_hash'] = $this->hash_nav_menu_args( $args );
     752
     753                $this->preview_nav_menu_instance_args[ $this->preview_nav_menu_instance_number ] = $args;
     754                return $args;
     755        }
     756
     757        /**
     758         * Prepare wp_nav_menu() calls for partial refresh. Wraps output in container for refreshing.
     759         *
     760         * @since 4.3.0
     761         *
     762         * @see wp_nav_menu()
     763         *
     764         * @param string $nav_menu_content The HTML content for the navigation menu.
     765         * @param object $args             An object containing wp_nav_menu() arguments.
     766         * @return null
     767         */
     768        function filter_wp_nav_menu( $nav_menu_content, $args ) {
     769                if ( ! empty( $args->can_partial_refresh ) && ! empty( $args->