Make WordPress Core

Ticket #32576: 32576.3.diff

File 32576.3.diff, 194.0 KB (added by ocean90, 10 years ago)

Fix JS/CSS

  • 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         * @access public
     1419         * @var string
     1420         */
     1421        public $type = 'menus';
     1422
     1423        /**
     1424         * Render screen options for Menus.
     1425         */
     1426        public function render_screen_options() {
     1427                // Essentially adds the screen options.
     1428                add_filter( 'manage_nav-menus_columns', array( $this, 'wp_nav_menu_manage_columns' ) );
     1429
     1430                // Display screen options.
     1431                $screen = WP_Screen::get( 'nav-menus.php' );
     1432                $screen->render_screen_options();
     1433        }
     1434
     1435        /**
     1436         * Returns the advanced options for the nav menus page.
     1437         *
     1438         * Link title attribute added as it's a relatively advanced concept for new users.
     1439         *
     1440         * @since 4.3.0
     1441         *
     1442         * @return array The advanced menu properties.
     1443         */
     1444        function wp_nav_menu_manage_columns() {
     1445                return array(
     1446                        '_title'      => __( 'Show advanced menu properties' ),
     1447                        'cb'          => '<input type="checkbox" />',
     1448                        'link-target' => __( 'Link Target' ),
     1449                        'attr-title'  => __( 'Title Attribute' ),
     1450                        'css-classes' => __( 'CSS Classes' ),
     1451                        'xfn'         => __( 'Link Relationship (XFN)' ),
     1452                        'description' => __( 'Description' ),
     1453                );
     1454        }
     1455
     1456        /**
     1457         * An Underscore (JS) template for this panel's content (but not its container).
     1458         *
     1459         * Class variables for this panel class are available in the `data` JS object;
     1460         * export custom variables by overriding {@see WP_Customize_Panel::json()}.
     1461         *
     1462         * @see WP_Customize_Panel::print_template()
     1463         *
     1464         * @since 4.3.0
     1465         */
     1466        protected function content_template() {
     1467                ?>
     1468                <li class="panel-meta customize-info accordion-section <# if ( ! data.description ) { #> cannot-expand<# } #>">
     1469                        <button type="button" class="customize-panel-back" tabindex="-1">
     1470                                <span class="screen-reader-text"><?php _e( 'Back' ); ?></span>
     1471                        </button>
     1472                        <div class="accordion-section-title">
     1473                                <span class="preview-notice">
     1474                                        <?php
     1475                                                /* translators: %s is the site/panel title in the Customizer */
     1476                                                printf( __( 'You are customizing %s' ), '<strong class="panel-title">{{ data.title }}</strong>' );
     1477                                        ?>
     1478                                </span>
     1479                                <button type="button" class="customize-screen-options-toggle" aria-expanded="false">
     1480                                        <span class="screen-reader-text"><?php _e( 'Menu Options' ); ?></span>
     1481                                </button>
     1482                                <button type="button" class="customize-help-toggle dashicons dashicons-editor-help" aria-expanded="false">
     1483                                        <span class="screen-reader-text"><?php _e( 'Help' ); ?></span>
     1484                                </button>
     1485                        </div>
     1486                        <# if ( data.description ) { #>
     1487                        <div class="description customize-panel-description">{{{ data.description }}}</div>
     1488                        <# } #>
     1489                        <?php $this->render_screen_options(); ?>
     1490                </li>
     1491                <?php
     1492        }
     1493}
     1494
     1495/**
     1496 * Customize Nav Menu Control Class
     1497 *
     1498 * @since 4.3.0
     1499 */
     1500class WP_Customize_Nav_Menu_Control extends WP_Customize_Control {
     1501
     1502        /**
     1503         * Control type
     1504         *
     1505         * @access public
     1506         * @var string
     1507         */
     1508        public $type = 'nav_menu';
     1509
     1510        /**
     1511         * The nav menu setting
     1512         *
     1513         * @var WP_Customize_Nav_Menu_Setting
     1514         */
     1515        public $setting;
     1516
     1517        /**
     1518         * Don't render the control's content - it uses a JS template instead.
     1519         */
     1520        public function render_content() {}
     1521
     1522        /**
     1523         * JS/Underscore template for the control UI.
     1524         */
     1525        public function content_template() {
     1526                ?>
     1527                <button type="button" class="button-secondary add-new-menu-item">
     1528                        <?php _e( 'Add Items' ); ?>
     1529                </button>
     1530                <button type="button" class="not-a-button reorder-toggle">
     1531                        <span class="reorder"><?php _ex( 'Reorder', 'Reorder menu items in Customizer' ); ?></span>
     1532                        <span class="reorder-done"><?php _ex( 'Done', 'Cancel reordering menu items in Customizer' ); ?></span>
     1533                </button>
     1534                <span class="add-menu-item-loading spinner"></span>
     1535                <span class="menu-delete-item">
     1536                        <button type="button" class="not-a-button menu-delete">
     1537                                <?php _e( 'Delete menu' ); ?> <span class="screen-reader-text">{{ data.menu_name }}</span>
     1538                        </button>
     1539                </span>
     1540                <?php if ( current_theme_supports( 'menus' ) ) : ?>
     1541                <ul class="menu-settings">
     1542                        <li class="customize-control">
     1543                                <span class="customize-control-title"><?php _e( 'Menu locations' ); ?></span>
     1544                        </li>
     1545
     1546                        <?php foreach ( get_registered_nav_menus() as $location => $description ) : ?>
     1547                        <li class="customize-control customize-control-checkbox assigned-menu-location">
     1548                                <label>
     1549                                        <input type="checkbox" data-menu-id="{{ data.menu_id }}" data-location-id="<?php echo esc_attr( $location ); ?>" class="menu-location" /> <?php echo $description; ?>
     1550                                        <span class="theme-location-set"><?php printf( _x( '(Current: %s)', 'Current menu location' ), '<span class="current-menu-location-name-' . esc_attr( $location ) . '"></span>' ); ?></span>
     1551                                </label>
     1552                        </li>
     1553                        <?php endforeach; ?>
     1554
     1555                </ul>
     1556                <?php endif; ?>
     1557                <p>
     1558                        <label>
     1559                                <input type="checkbox" class="auto_add">
     1560                                <?php _e( 'Automatically add new top-level pages to this menu.' ) ?>
     1561                        </label>
     1562                </p>
     1563                <?php
     1564        }
     1565
     1566        /**
     1567         * Return params for this control.
     1568         *
     1569         * @return array
     1570         */
     1571        function json() {
     1572                $exported = parent::json();
     1573                $exported['menu_id'] = $this->setting->term_id;
     1574                return $exported;
     1575        }
     1576}
     1577
     1578/**
     1579 * Customize control to represent the name field for a given menu.
     1580 *
     1581 * @since 4.3.0
     1582 */
     1583class WP_Customize_Nav_Menu_Item_Control extends WP_Customize_Control {
     1584
     1585        /**
     1586         * Control type
     1587         *
     1588         * @access public
     1589         * @var string
     1590         */
     1591        public $type = 'nav_menu_item';
     1592
     1593        /**
     1594         * The nav menu item setting
     1595         *
     1596         * @var WP_Customize_Nav_Menu_Item_Setting
     1597         */
     1598        public $setting;
     1599
     1600        /**
     1601         * Constructor.
     1602         *
     1603         * @uses WP_Customize_Control::__construct()
     1604         *
     1605         * @param WP_Customize_Manager $manager An instance of the WP_Customize_Manager class.
     1606         * @param string               $id      The control ID.
     1607         * @param array                $args    Optional. Overrides class property defaults.
     1608         */
     1609        public function __construct( $manager, $id, $args = array() ) {
     1610                parent::__construct( $manager, $id, $args );
     1611        }
     1612
     1613        /**
     1614         * Don't render the control's content - it's rendered with a JS template.
     1615         */
     1616        public function render_content() {}
     1617
     1618        /**
     1619         * JS/Underscore template for the control UI.
     1620         */
     1621        public function content_template() {
     1622                ?>
     1623                <dl class="menu-item-bar">
     1624                        <dt class="menu-item-handle">
     1625                                <span class="item-type">{{ data.item_type_label }}</span>
     1626                                <span class="item-title">
     1627                                        <span class="spinner"></span>
     1628                                        <span class="menu-item-title">{{ data.title }}</span>
     1629                                </span>
     1630                                <span class="item-controls">
     1631                                        <button type="button" class="not-a-button item-edit"><span class="screen-reader-text"><?php _e( 'Edit Menu Item' ); ?></span></button>
     1632                                        <button type="button" class="not-a-button item-delete submitdelete deletion"><span class="screen-reader-text"><?php _e( 'Remove Menu Item' ); ?></span></button>
     1633                                </span>
     1634                        </dt>
     1635                </dl>
     1636
     1637                <div class="menu-item-settings" id="menu-item-settings-{{ data.menu_item_id }}">
     1638                        <# if ( 'custom' === data.item_type ) { #>
     1639                        <p class="field-url description description-thin">
     1640                                <label for="edit-menu-item-url-{{ data.menu_item_id }}">
     1641                                        <?php _e( 'URL' ); ?><br />
     1642                                        <input class="widefat code edit-menu-item-url" type="text" id="edit-menu-item-url-{{ data.menu_item_id }}" name="menu-item-url" />
     1643                                </label>
     1644                        </p>
     1645                <# } #>
     1646                        <p class="description description-thin">
     1647                                <label for="edit-menu-item-title-{{ data.menu_item_id }}">
     1648                                        <?php _e( 'Navigation Label' ); ?><br />
     1649                                        <input type="text" id="edit-menu-item-title-{{ data.menu_item_id }}" class="widefat edit-menu-item-title" name="menu-item-title" />
     1650                                </label>
     1651                        </p>
     1652                        <p class="field-link-target description description-thin">
     1653                                <label for="edit-menu-item-target-{{ data.menu_item_id }}">
     1654                                        <input type="checkbox" id="edit-menu-item-target-{{ data.menu_item_id }}" class="edit-menu-item-target" value="_blank" name="menu-item-target" />
     1655                                        <?php _e( 'Open link in a new tab' ); ?>
     1656                                </label>
     1657                        </p>
     1658                        <p class="field-attr-title description description-thin">
     1659                                <label for="edit-menu-item-attr-title-{{ data.menu_item_id }}">
     1660                                        <?php _e( 'Title Attribute' ); ?><br />
     1661                                        <input type="text" id="edit-menu-item-attr-title-{{ data.menu_item_id }}" class="widefat edit-menu-item-attr-title" name="menu-item-attr-title" />
     1662                                </label>
     1663                        </p>
     1664                        <p class="field-css-classes description description-thin">
     1665                                <label for="edit-menu-item-classes-{{ data.menu_item_id }}">
     1666                                        <?php _e( 'CSS Classes' ); ?><br />
     1667                                        <input type="text" id="edit-menu-item-classes-{{ data.menu_item_id }}" class="widefat code edit-menu-item-classes" name="menu-item-classes" />
     1668                                </label>
     1669                        </p>
     1670                        <p class="field-xfn description description-thin">
     1671                                <label for="edit-menu-item-xfn-{{ data.menu_item_id }}">
     1672                                        <?php _e( 'Link Relationship (XFN)' ); ?><br />
     1673                                        <input type="text" id="edit-menu-item-xfn-{{ data.menu_item_id }}" class="widefat code edit-menu-item-xfn" name="menu-item-xfn" />
     1674                                </label>
     1675                        </p>
     1676                        <p class="field-description description description-thin">
     1677                                <label for="edit-menu-item-description-{{ data.menu_item_id }}">
     1678                                        <?php _e( 'Description' ); ?><br />
     1679                                        <textarea id="edit-menu-item-description-{{ data.menu_item_id }}" class="widefat edit-menu-item-description" rows="3" cols="20" name="menu-item-description">{{ data.description }}</textarea>
     1680                                        <span class="description"><?php _e( 'The description will be displayed in the menu if the current theme supports it.' ); ?></span>
     1681                                </label>
     1682                        </p>
     1683
     1684                        <div class="menu-item-actions description-thin submitbox">
     1685                                <# if ( 'custom' != data.item_type && '' != data.original_title ) { #>
     1686                                <p class="link-to-original">
     1687                                        <?php printf( __( 'Original: %s' ), '<a class="original-link" href="{{ data.url }}">{{{ data.original_title }}}</a>' ); ?>
     1688                                </p>
     1689                                <# } #>
     1690
     1691                                <button type="button" class="not-a-button item-delete submitdelete deletion"><?php _e( 'Remove' ); ?></button>
     1692                                <span class="spinner"></span>
     1693                        </div>
     1694                        <input type="hidden" name="menu-item-db-id[{{ data.menu_item_id }}]" class="menu-item-data-db-id" value="{{ data.menu_item_id }}" />
     1695                        <input type="hidden" name="menu-item-parent-id[{{ data.menu_item_id }}]" class="menu-item-data-parent-id" value="{{ data.parent }}" />
     1696                </div><!-- .menu-item-settings-->
     1697                <ul class="menu-item-transport"></ul>
     1698                <?php
     1699        }
     1700
     1701        /**
     1702         * Return params for this control.
     1703         *
     1704         * @return array
     1705         */
     1706        function json() {
     1707                $exported = parent::json();
     1708                $exported['menu_item_id'] = $this->setting->post_id;
     1709                return $exported;
     1710        }
     1711}
     1712
     1713/**
     1714 * Customize Menu Location Control Class
     1715 *
     1716 * This custom control is only needed for JS.
     1717 *
     1718 * @since 4.3.0
     1719 */
     1720class WP_Customize_Nav_Menu_Location_Control extends WP_Customize_Control {
     1721
     1722        /**
     1723         * Control type
     1724         *
     1725         * @access public
     1726         * @var string
     1727         */
     1728        public $type = 'nav_menu_location';
     1729
     1730        /**
     1731         * Location ID
     1732         *
     1733         * @access public
     1734         * @var string
     1735         */
     1736        public $location_id = '';
     1737
     1738        /**
     1739         * Refresh the parameters passed to JavaScript via JSON.
     1740         *
     1741         * @uses WP_Customize_Control::to_json()
     1742         */
     1743        public function to_json() {
     1744                parent::to_json();
     1745                $this->json['locationId'] = $this->location_id;
     1746        }
     1747
     1748        /**
     1749         * Render content just like a normal select control.
     1750         */
     1751        public function render_content() {
     1752                if ( empty( $this->choices ) ) {
     1753                        return;
     1754                }
     1755                ?>
     1756                <label>
     1757                        <?php if ( ! empty( $this->label ) ) : ?>
     1758                        <span class="customize-control-title"><?php echo esc_html( $this->label ); ?></span>
     1759                        <?php endif; ?>
     1760
     1761                        <?php if ( ! empty( $this->description ) ) : ?>
     1762                        <span class="description customize-control-description"><?php echo $this->description; ?></span>
     1763                        <?php endif; ?>
     1764
     1765                        <select <?php $this->link(); ?>>
     1766                                <?php
     1767                                foreach ( $this->choices as $value => $label ) :
     1768                                        echo '<option value="' . esc_attr( $value ) . '"' . selected( $this->value(), $value, false ) . '>' . $label . '</option>';
     1769                                endforeach;
     1770                                ?>
     1771                        </select>
     1772                </label>
     1773                <?php
     1774        }
     1775}
     1776
     1777/**
     1778 * Customize control to represent the name field for a given menu.
     1779 *
     1780 * @since 4.3.0
     1781 */
     1782class WP_Customize_Nav_Menu_Name_Control extends WP_Customize_Control {
     1783
     1784        /**
     1785         * Type of control, used by JS.
     1786         *
     1787         * @var string
     1788         */
     1789        public $type = 'nav_menu_name';
     1790
     1791        /**
     1792         * No-op since we're using JS template.
     1793         */
     1794        protected function render_content() {}
     1795
     1796        /**
     1797         * Render the Underscore template for this control.
     1798         */
     1799        protected function content_template() {
     1800                ?>
     1801                <label>
     1802                        <input type="text" class="menu-name-field live-update-section-title" />
     1803                </label>
     1804                <?php
     1805        }
     1806}
     1807
     1808/**
     1809 * Customize control class for new menus.
     1810 *
     1811 * @since 4.3.0
     1812 */
     1813class WP_New_Menu_Customize_Control extends WP_Customize_Control {
     1814
     1815        /**
     1816         * Control type.
     1817         *
     1818         * @access public
     1819         * @var string
     1820         */
     1821        public $type = 'new_menu';
     1822
     1823        /**
     1824         * Render the control's content.
     1825         */
     1826        public function render_content() {
     1827                ?>
     1828                <button type="button" class="button button-primary" id="create-new-menu-submit"><?php _e( 'Create Menu' ); ?></button>
     1829                <span class="spinner"></span>
     1830                <?php
     1831        }
     1832}
  • src/wp-includes/class-wp-customize-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         * @access public
     25         * @var WP_Customize_Manager
     26         */
     27        public $manager;
     28
     29        /**
     30         * Previewed Menus.
     31         *
     32         * @access public
     33         * @var array
     34         */
     35        public $previewed_menus;
     36
     37        /**
     38         * Constructor
     39         *
     40         * @access public
     41         * @param object $manager An instance of the WP_Customize_Manager class.
     42         */
     43        public function __construct( $manager ) {
     44                $this->previewed_menus = array();
     45                $this->manager         = $manager;
     46
     47                add_action( 'wp_ajax_load-available-menu-items-customizer', array( $this, 'ajax_load_available_items' ) );
     48                add_action( 'wp_ajax_search-available-menu-items-customizer', array( $this, 'ajax_search_available_items' ) );
     49                add_action( 'customize_controls_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
     50                add_action( 'customize_register', array( $this, 'customize_register' ), 11 ); // Needs to run after core Navigation section is set up.
     51                add_filter( 'customize_dynamic_setting_args', array( $this, 'filter_dynamic_setting_args' ), 10, 2 );
     52                add_filter( 'customize_dynamic_setting_class', array( $this, 'filter_dynamic_setting_class' ), 10, 3 );
     53                add_action( 'customize_controls_print_footer_scripts', array( $this, 'print_templates' ) );
     54                add_action( 'customize_controls_print_footer_scripts', array( $this, 'available_items_template' ) );
     55                add_action( 'customize_preview_init', array( $this, 'customize_preview_init' ) );
     56        }
     57
     58        /**
     59         * Ajax handler for loading available menu items.
     60         *
     61         * @access public
     62         */
     63        public function ajax_load_available_items() {
     64                check_ajax_referer( 'customize-menus', 'customize-menus-nonce' );
     65
     66                if ( ! current_user_can( 'edit_theme_options' ) ) {
     67                        wp_send_json_error( array( 'message' => __( 'Error: invalid user capabilities.' ) ) );
     68                }
     69                if ( empty( $_POST['obj_type'] ) || empty( $_POST['type'] ) ) {
     70                        wp_send_json_error( array( 'message' => __( 'Missing obj_type or type param.' ) ) );
     71                }
     72
     73                $obj_type = sanitize_key( $_POST['obj_type'] );
     74                if ( ! in_array( $obj_type, array( 'post_type', 'taxonomy' ) ) ) {
     75                        wp_send_json_error( array( 'message' => __( 'Invalid obj_type param: ' . $obj_type ) ) );
     76                }
     77                $taxonomy_or_post_type = sanitize_key( $_POST['type'] );
     78                $page = isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 0;
     79                $items = array();
     80
     81                if ( 'post_type' === $obj_type ) {
     82                        if ( ! get_post_type_object( $taxonomy_or_post_type ) ) {
     83                                wp_send_json_error( array( 'message' => __( 'Unknown post type.' ) ) );
     84                        }
     85
     86                        if ( 0 === $page && 'page' === $taxonomy_or_post_type ) {
     87                                // Add "Home" link. Treat as a page, but switch to custom on add.
     88                                $items[] = array(
     89                                        'id'         => 'home',
     90                                        'title'      => _x( 'Home', 'nav menu home label' ),
     91                                        'type'       => 'custom',
     92                                        'type_label' => __( 'Custom Link' ),
     93                                        'object'     => '',
     94                                        'url'        => home_url(),
     95                                );
     96                        }
     97
     98                        $posts = get_posts( array(
     99                                'numberposts' => 10,
     100                                'offset'      => 10 * $page,
     101                                'orderby'     => 'date',
     102                                'order'       => 'DESC',
     103                                'post_type'   => $taxonomy_or_post_type,
     104                        ) );
     105                        foreach ( $posts as $post ) {
     106                                $items[] = array(
     107                                        'id'         => "post-{$post->ID}",
     108                                        'title'      => html_entity_decode( get_the_title( $post ), ENT_QUOTES, get_bloginfo( 'charset' ) ),
     109                                        'type'       => 'post_type',
     110                                        'type_label' => get_post_type_object( $post->post_type )->labels->singular_name,
     111                                        'object'     => $post->post_type,
     112                                        'object_id'  => (int) $post->ID,
     113                                );
     114                        }
     115                } else if ( 'taxonomy' === $obj_type ) {
     116                        $terms = get_terms( $taxonomy_or_post_type, array(
     117                                'child_of'     => 0,
     118                                'exclude'      => '',
     119                                'hide_empty'   => false,
     120                                'hierarchical' => 1,
     121                                'include'      => '',
     122                                'number'       => 10,
     123                                'offset'       => 10 * $page,
     124                                'order'        => 'DESC',
     125                                'orderby'      => 'count',
     126                                'pad_counts'   => false,
     127                        ) );
     128                        if ( is_wp_error( $terms ) ) {
     129                                wp_send_json_error( array( 'message' => wp_strip_all_tags( $terms->get_error_message(), true ) ) );
     130                        }
     131
     132                        foreach ( $terms as $term ) {
     133                                $items[] = array(
     134                                        'id'         => "term-{$term->term_id}",
     135                                        'title'      => html_entity_decode( $term->name, ENT_QUOTES, get_bloginfo( 'charset' ) ),
     136                                        'type'       => 'taxonomy',
     137                                        'type_label' => get_taxonomy( $term->taxonomy )->labels->singular_name,
     138                                        'object'     => $term->taxonomy,
     139                                        'object_id'  => $term->term_id,
     140                                );
     141                        }
     142                }
     143
     144                wp_send_json_success( array( 'items' => $items ) );
     145        }
     146
     147        /**
     148         * Ajax handler for searching available menu items.
     149         */
     150        public function ajax_search_available_items() {
     151                check_ajax_referer( 'customize-menus', 'customize-menus-nonce' );
     152
     153                if ( ! current_user_can( 'edit_theme_options' ) ) {
     154                        wp_send_json_error( array( 'message' => __( 'Error: invalid user capabilities.' ) ) );
     155                }
     156                if ( empty( $_POST['search'] ) ) {
     157                        wp_send_json_error( array( 'message' => __( 'Error: missing search parameter.' ) ) );
     158                }
     159
     160                $p = isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 0;
     161                if ( $p < 1 ) {
     162                        $p = 1;
     163                }
     164
     165                $s = sanitize_text_field( wp_unslash( $_POST['search'] ) );
     166                $results = $this->search_available_items_query( array( 'pagenum' => $p, 's' => $s ) );
     167
     168                if ( empty( $results ) ) {
     169                        wp_send_json_error( array( 'message' => __( 'No results found.' ) ) );
     170                } else {
     171                        wp_send_json_success( array( 'items' => $results ) );
     172                }
     173        }
     174
     175        /**
     176         * Performs post queries for available-item searching.
     177         *
     178         * Based on WP_Editor::wp_link_query().
     179         *
     180         * @param array $args Optional. Accepts 'pagenum' and 's' (search) arguments.
     181         * @return array Results.
     182         */
     183        public function search_available_items_query( $args = array() ) {
     184                $results = array();
     185
     186                $post_type_objects = get_post_types( array( 'show_in_nav_menus' => true ), 'objects' );
     187                $query = array(
     188                        'post_type'              => array_keys( $post_type_objects ),
     189                        'suppress_filters'       => true,
     190                        'update_post_term_cache' => false,
     191                        'update_post_meta_cache' => false,
     192                        'post_status'            => 'publish',
     193                        'posts_per_page'         => 20,
     194                );
     195
     196                $args['pagenum'] = isset( $args['pagenum'] ) ? absint( $args['pagenum'] ) : 1;
     197                $query['offset'] = $args['pagenum'] > 1 ? $query['posts_per_page'] * ( $args['pagenum'] - 1 ) : 0;
     198
     199                if ( isset( $args['s'] ) ) {
     200                        $query['s'] = $args['s'];
     201                }
     202
     203                // Query posts.
     204                $get_posts = new WP_Query( $query );
     205
     206                // Check if any posts were found.
     207                if ( $get_posts->post_count ) {
     208                        foreach ( $get_posts->posts as $post ) {
     209                                $results[] = array(
     210                                        'id'         => 'post-' . $post->ID,
     211                                        'type'       => 'post_type',
     212                                        'type_label' => $post_type_objects[ $post->post_type ]->labels->singular_name,
     213                                        'object'     => $post->post_type,
     214                                        'object_id'  => intval( $post->ID ),
     215                                        'title'      => html_entity_decode( get_the_title( $post ), ENT_QUOTES, get_bloginfo( 'charset' ) ),
     216                                );
     217                        }
     218                }
     219
     220                // Query taxonomy terms.
     221                $taxonomies = get_taxonomies( array( 'show_in_nav_menus' => true ), 'names' );
     222                $terms = get_terms( $taxonomies, array(
     223                        'name__like' => $args['s'],
     224                        'number'     => 20,
     225                        'offset'     => 20 * ($args['pagenum'] - 1),
     226                ) );
     227
     228                // Check if any taxonomies were found.
     229                if ( ! empty( $terms ) ) {
     230                        foreach ( $terms as $term ) {
     231                                $results[] = array(
     232                                        'id'         => 'term-' . $term->term_id,
     233                                        'type'       => 'taxonomy',
     234                                        'type_label' => get_taxonomy( $term->taxonomy )->labels->singular_name,
     235                                        'object'     => $term->taxonomy,
     236                                        'object_id'  => intval( $term->term_id ),
     237                                        'title'      => html_entity_decode( $term->name, ENT_QUOTES, get_bloginfo( 'charset' ) ),
     238                                );
     239                        }
     240                }
     241
     242                return $results;
     243        }
     244
     245        /**
     246         * Enqueue scripts and styles for Customizer pane.
     247         *
     248         * @since Menu Customizer 0.0
     249         */
     250        public function enqueue_scripts() {
     251                wp_enqueue_style( 'customize-nav-menus' );
     252                wp_enqueue_script( 'customize-nav-menus' );
     253
     254                $temp_nav_menu_setting      = new WP_Customize_Nav_Menu_Setting( $this->manager, 'nav_menu[-1]' );
     255                $temp_nav_menu_item_setting = new WP_Customize_Nav_Menu_Item_Setting( $this->manager, 'nav_menu_item[-1]' );
     256
     257                // Pass data to JS.
     258                $settings = array(
     259                        'nonce'                => wp_create_nonce( 'customize-menus' ),
     260                        'allMenus'             => wp_get_nav_menus(),
     261                        'itemTypes'            => $this->available_item_types(),
     262                        'l10n'                 => array(
     263                                'untitled'          => _x( '(no label)', 'Missing menu item navigation label.' ),
     264                                'custom_label'      => _x( 'Custom', 'Custom menu item type label.' ),
     265                                'menuLocation'      => _x( '(Currently set to: %s)', 'Current menu location.' ),
     266                                'deleteWarn'        => __( 'You are about to permanently delete this menu. "Cancel" to stop, "OK" to delete.' ),
     267                                'itemAdded'         => __( 'Menu item added' ),
     268                                'itemDeleted'       => __( 'Menu item deleted' ),
     269                                'menuAdded'         => __( 'Menu created' ),
     270                                'menuDeleted'       => __( 'Menu deleted' ),
     271                                'movedUp'           => __( 'Menu item moved up' ),
     272                                'movedDown'         => __( 'Menu item moved down' ),
     273                                'movedLeft'         => __( 'Menu item moved out of submenu' ),
     274                                'movedRight'        => __( 'Menu item is now a sub-item' ),
     275                                'customizingMenus'  => _x( 'Customizing &#9656; Menus', '&#9656 is the unicode right-pointing triangle' ),
     276                                'invalidTitleTpl'   => __( '%s (Invalid)' ),
     277                                'pendingTitleTpl'   => __( '%s (Pending)' ),
     278                                'taxonomyTermLabel' => __( 'Taxonomy' ),
     279                                'postTypeLabel'     => __( 'Post Type' ),
     280                        ),
     281                        'menuItemTransport'    => 'postMessage',
     282                        'phpIntMax'            => PHP_INT_MAX,
     283                        'defaultSettingValues' => array(
     284                                'nav_menu'      => $temp_nav_menu_setting->default,
     285                                'nav_menu_item' => $temp_nav_menu_item_setting->default,
     286                        ),
     287                );
     288
     289                $data = sprintf( 'var _wpCustomizeNavMenusSettings = %s;', wp_json_encode( $settings ) );
     290                wp_scripts()->add_data( 'customize-nav-menus', 'data', $data );
     291
     292                // This is copied from nav-menus.php, and it has an unfortunate object name of `menus`.
     293                $nav_menus_l10n = array(
     294                        'oneThemeLocationNoMenus' => null,
     295                        'moveUp'       => __( 'Move up one' ),
     296                        'moveDown'     => __( 'Move down one' ),
     297                        'moveToTop'    => __( 'Move to the top' ),
     298                        /* translators: %s: previous item name */
     299                        'moveUnder'    => __( 'Move under %s' ),
     300                        /* translators: %s: previous item name */
     301                        'moveOutFrom'  => __( 'Move out from under %s' ),
     302                        /* translators: %s: previous item name */
     303                        'under'        => __( 'Under %s' ),
     304                        /* translators: %s: previous item name */
     305                        'outFrom'      => __( 'Out from under %s' ),
     306                        /* translators: 1: item name, 2: item position, 3: total number of items */
     307                        'menuFocus'    => __( '%1$s. Menu item %2$d of %3$d.' ),
     308                        /* translators: 1: item name, 2: item position, 3: parent item name */
     309                        'subMenuFocus' => __( '%1$s. Sub item number %2$d under %3$s.' ),
     310                );
     311                wp_localize_script( 'nav-menu', 'menus', $nav_menus_l10n );
     312        }
     313
     314        /**
     315         * Filter a dynamic setting's constructor args.
     316         *
     317         * For a dynamic setting to be registered, this filter must be employed
     318         * to override the default false value with an array of args to pass to
     319         * the WP_Customize_Setting constructor.
     320         *
     321         * @param false|array $setting_args The arguments to the WP_Customize_Setting constructor.
     322         * @param string      $setting_id   ID for dynamic setting, usually coming from `$_POST['customized']`.
     323         * @return array|false
     324         */
     325        public function filter_dynamic_setting_args( $setting_args, $setting_id ) {
     326                if ( preg_match( WP_Customize_Nav_Menu_Setting::ID_PATTERN, $setting_id ) ) {
     327                        $setting_args = array(
     328                                'type' => WP_Customize_Nav_Menu_Setting::TYPE,
     329                        );
     330                } else if ( preg_match( WP_Customize_Nav_Menu_Item_Setting::ID_PATTERN, $setting_id ) ) {
     331                        $setting_args = array(
     332                                'type' => WP_Customize_Nav_Menu_Item_Setting::TYPE,
     333                        );
     334                }
     335                return $setting_args;
     336        }
     337
     338        /**
     339         * Allow non-statically created settings to be constructed with custom WP_Customize_Setting subclass.
     340         *
     341         * @param string $setting_class WP_Customize_Setting or a subclass.
     342         * @param string $setting_id    ID for dynamic setting, usually coming from `$_POST['customized']`.
     343         * @param array  $setting_args  WP_Customize_Setting or a subclass.
     344         * @return string
     345         */
     346        public function filter_dynamic_setting_class( $setting_class, $setting_id, $setting_args ) {
     347                unset( $setting_id );
     348
     349                if ( ! empty( $setting_args['type'] ) && WP_Customize_Nav_Menu_Setting::TYPE === $setting_args['type'] ) {
     350                        $setting_class = 'WP_Customize_Nav_Menu_Setting';
     351                } else if ( ! empty( $setting_args['type'] ) && WP_Customize_Nav_Menu_Item_Setting::TYPE === $setting_args['type'] ) {
     352                        $setting_class = 'WP_Customize_Nav_Menu_Item_Setting';
     353                }
     354                return $setting_class;
     355        }
     356
     357        /**
     358         * Add the customizer settings and controls.
     359         *
     360         * @since Menu Customizer 0.0
     361         */
     362        public function customize_register() {
     363
     364                // Require JS-rendered control types.
     365                $this->manager->register_panel_type( 'WP_Customize_Menus_Panel' );
     366                $this->manager->register_control_type( 'WP_Customize_Nav_Menu_Control' );
     367                $this->manager->register_control_type( 'WP_Customize_Nav_Menu_Name_Control' );
     368                $this->manager->register_control_type( 'WP_Customize_Nav_Menu_Item_Control' );
     369
     370                // Create a panel for Menus.
     371                $this->manager->add_panel( new WP_Customize_Menus_Panel( $this->manager, 'menus', array(
     372                        'title'       => __( 'Menus' ),
     373                        '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>',
     374                        'priority'    => 100,
     375                        // 'theme_supports' => 'menus|widgets', @todo allow multiple theme supports
     376                ) ) );
     377                $menus = wp_get_nav_menus();
     378
     379                // Menu loactions.
     380                $this->manager->remove_section( 'nav' ); // Remove old core section. @todo core merge remove corresponding code from WP_Customize_Manager::register_controls().
     381                $locations     = get_registered_nav_menus();
     382                $num_locations = count( array_keys( $locations ) );
     383                $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 ) );
     384                $description  .= '</p><p>' . __( 'You can also place menus in widget areas with the Custom Menu widget.' ) . '</p>';
     385
     386                $this->manager->add_section( 'menu_locations', array(
     387                        'title'       => __( 'Menu Locations' ),
     388                        'panel'       => 'menus',
     389                        'priority'    => 5,
     390                        'description' => $description,
     391                ) );
     392
     393                // @todo if ( ! $menus ) : make a "default" menu
     394                if ( $menus ) {
     395                        $choices = array( '0' => __( '&mdash; Select &mdash;' ) );
     396                        foreach ( $menus as $menu ) {
     397                                $choices[ $menu->term_id ] = wp_html_excerpt( $menu->name, 40, '&hellip;' );
     398                        }
     399
     400                        foreach ( $locations as $location => $description ) {
     401                                $setting_id = "nav_menu_locations[{$location}]";
     402
     403                                $setting = $this->manager->get_setting( $setting_id );
     404                                if ( $setting ) {
     405                                        $setting->transport = 'postMessage';
     406                                        remove_filter( "customize_sanitize_{$setting_id}", 'absint' );
     407                                        add_filter( "customize_sanitize_{$setting_id}", array( $this, 'intval_base10' ) );
     408                                } else {
     409                                        $this->manager->add_setting( $setting_id, array(
     410                                                'sanitize_callback' => array( $this, 'intval_base10' ),
     411                                                'theme_supports'    => 'menus',
     412                                                'type'              => 'theme_mod',
     413                                                'transport'         => 'postMessage',
     414                                        ) );
     415                                }
     416
     417                                $this->manager->add_control( new WP_Customize_Nav_Menu_Location_Control( $this->manager, $setting_id, array(
     418                                        'label'       => $description,
     419                                        'location_id' => $location,
     420                                        'section'     => 'menu_locations',
     421                                        'choices'     => $choices,
     422                                ) ) );
     423                        }
     424                }
     425
     426                // Register each menu as a Customizer section, and add each menu item to each menu.
     427                foreach ( $menus as $menu ) {
     428                        $menu_id = $menu->term_id;
     429
     430                        // Create a section for each menu.
     431                        $section_id = 'nav_menu[' . $menu_id . ']';
     432                        $this->manager->add_section( new WP_Customize_Nav_Menu_Section( $this->manager, $section_id, array(
     433                                'title'     => $menu->name,
     434                                'priority'  => 10,
     435                                'panel'     => 'menus',
     436                        ) ) );
     437
     438                        $nav_menu_setting_id = 'nav_menu[' . $menu_id . ']';
     439                        $this->manager->add_setting( new WP_Customize_Nav_Menu_Setting( $this->manager, $nav_menu_setting_id ) );
     440
     441                        // Add the menu contents.
     442                        $menu_items = (array) wp_get_nav_menu_items( $menu_id );
     443
     444                        foreach ( array_values( $menu_items ) as $i => $item ) {
     445
     446                                // Create a setting for each menu item (which doesn't actually manage data, currently).
     447                                $menu_item_setting_id = 'nav_menu_item[' . $item->ID . ']';
     448                                $this->manager->add_setting( new WP_Customize_Nav_Menu_Item_Setting( $this->manager, $menu_item_setting_id ) );
     449
     450                                // Create a control for each menu item.
     451                                $this->manager->add_control( new WP_Customize_Nav_Menu_Item_Control( $this->manager, $menu_item_setting_id, array(
     452                                        'label'    => $item->title,
     453                                        'section'  => $section_id,
     454                                        'priority' => 10 + $i,
     455                                ) ) );
     456                        }
     457
     458                        // Note: other controls inside of this section get added dynamically in JS via the MenuSection.ready() function.
     459                }
     460
     461                // Add the add-new-menu section and controls.
     462                $this->manager->add_section( new WP_Customize_New_Menu_Section( $this->manager, 'add_menu', array(
     463                        'title'    => __( 'Add a Menu' ),
     464                        'panel'    => 'menus',
     465                        'priority' => 999,
     466                ) ) );
     467
     468                $this->manager->add_setting( 'new_menu_name', array(
     469                        'type'      => 'new_menu',
     470                        'default'   => '',
     471                        'transport' => 'postMessage',
     472                ) );
     473
     474                $this->manager->add_control( 'new_menu_name', array(
     475                        'label'       => '',
     476                        'section'     => 'add_menu',
     477                        'type'        => 'text',
     478                        'input_attrs' => array(
     479                                'class'       => 'menu-name-field',
     480                                'placeholder' => __( 'New menu name' ),
     481                        ),
     482                ) );
     483
     484                $this->manager->add_setting( 'create_new_menu', array(
     485                        'type' => 'new_menu',
     486                ) );
     487
     488                $this->manager->add_control( new WP_New_Menu_Customize_Control( $this->manager, 'create_new_menu', array(
     489                        'section' => 'add_menu',
     490                ) ) );
     491        }
     492
     493        /**
     494         * Get the base10 intval.
     495         *
     496         * This is used as a setting's sanitize_callback; we can't use just plain
     497         * intval because the second argument is not what intval() expects.
     498         *
     499         * @param mixed $value Number to convert.
     500         *
     501         * @return int
     502         */
     503        function intval_base10( $value ) {
     504                return intval( $value, 10 );
     505        }
     506
     507        /**
     508         * Return an array of all the available item types.
     509         *
     510         * @since Menu Customizer 0.0
     511         */
     512        public function available_item_types() {
     513                $items = array(
     514                        'postTypes'  => array(),
     515                        'taxonomies' => array(),
     516                );
     517
     518                $post_types = get_post_types( array( 'show_in_nav_menus' => true ), 'objects' );
     519                foreach ( $post_types as $slug => $post_type ) {
     520                        $items['postTypes'][ $slug ] = array(
     521                                'label' => $post_type->labels->singular_name,
     522                        );
     523                }
     524
     525                $taxonomies = get_taxonomies( array( 'show_in_nav_menus' => true ), 'objects' );
     526                foreach ( $taxonomies as $slug => $taxonomy ) {
     527                        if ( 'post_format' === $taxonomy && ! current_theme_supports( 'post-formats' ) ) {
     528                                continue;
     529                        }
     530                        $items['taxonomies'][ $slug ] = array(
     531                                'label' => $taxonomy->labels->singular_name,
     532                        );
     533                }
     534                return $items;
     535        }
     536
     537        /**
     538         * Print the JavaScript templates used to render Menu Customizer components.
     539         *
     540         * Templates are imported into the JS use wp.template.
     541         *
     542         * @since Menu Customizer 0.0
     543         */
     544        public function print_templates() {
     545                ?>
     546                <script type="text/html" id="tmpl-available-menu-item">
     547                        <div id="menu-item-tpl-{{ data.id }}" class="menu-item-tpl" data-menu-item-id="{{ data.id }}">
     548                                <dl class="menu-item-bar">
     549                                        <dt class="menu-item-handle">
     550                                                <span class="item-type">{{ data.type_label }}</span>
     551                                                <span class="item-title">{{ data.title || wp.customize.Menus.data.l10n.untitled }}</span>
     552                                                <button type="button" class="not-a-button item-add"><span class="screen-reader-text"><?php _e( 'Add Menu Item' ) ?></span></button>
     553                                        </dt>
     554                                </dl>
     555                        </div>
     556                </script>
     557
     558                <script type="text/html" id="tmpl-available-menu-item-type">
     559                        <div id="available-menu-items-{{ data.type }}" class="accordion-section">
     560                                <h4 class="accordion-section-title">{{ data.type_label }}</h4>
     561                                <div class="accordion-section-content">
     562                                </div>
     563                        </div>
     564                </script>
     565
     566                <script type="text/html" id="tmpl-menu-item-reorder-nav">
     567                        <div class="menu-item-reorder-nav">
     568                                <?php
     569                                printf(
     570                                        '<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>',
     571                                        esc_html__( 'Move up' ),
     572                                        esc_html__( 'Move down' ),
     573                                        esc_html__( 'Move one level up' ),
     574                                        esc_html__( 'Move one level down' )
     575                                );
     576                                ?>
     577                        </div>
     578                </script>
     579        <?php
     580        }
     581
     582        /**
     583         * Print the html template used to render the add-menu-item frame.
     584         */
     585        public function available_items_template() {
     586                ?>
     587                <div id="available-menu-items" class="accordion-container">
     588                        <div class="customize-section-title">
     589                                <button type="button" class="customize-section-back" tabindex="-1">
     590                                        <span class="screen-reader-text"><?php _e( 'Back' ); ?></span>
     591                                </button>
     592                                <h3>
     593                                        <span class="customize-action">
     594                                                <?php
     595                                                        /* translators: &#9656; is the unicode right-pointing triangle, and %s is the section title in the Customizer */
     596                                                        printf( __( 'Customizing &#9656; %s' ), esc_html( $this->manager->get_panel( 'menus' )->title ) );
     597                                                ?>
     598                                        </span>
     599                                        <?php _e( 'Add Menu Items' ); ?>
     600                                </h3>
     601                        </div>
     602                        <div id="available-menu-items-search" class="accordion-section cannot-expand">
     603                                <div class="accordion-section-title">
     604                                        <label class="screen-reader-text" for="menu-items-search"><?php _e( 'Search Menu Items' ); ?></label>
     605                                        <input type="text" id="menu-items-search" placeholder="<?php esc_attr_e( 'Search menu items&hellip;' ) ?>" />
     606                                        <span class="spinner"></span>
     607                                </div>
     608                                <div class="accordion-section-content" data-type="search"></div>
     609                        </div>
     610                        <div id="new-custom-menu-item" class="accordion-section">
     611                                <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>
     612                                <div class="accordion-section-content">
     613                                        <input type="hidden" value="custom" id="custom-menu-item-type" name="menu-item[-1][menu-item-type]" />
     614                                        <p id="menu-item-url-wrap">
     615                                                <label class="howto" for="custom-menu-item-url">
     616                                                        <span><?php _e( 'URL' ); ?></span>
     617                                                        <input id="custom-menu-item-url" name="menu-item[-1][menu-item-url]" type="text" class="code menu-item-textbox" value="http://">
     618                                                </label>
     619                                        </p>
     620                                        <p id="menu-item-name-wrap">
     621                                                <label class="howto" for="custom-menu-item-name">
     622                                                        <span><?php _e( 'Link Text' ); ?></span>
     623                                                        <input id="custom-menu-item-name" name="menu-item[-1][menu-item-title]" type="text" class="regular-text menu-item-textbox">
     624                                                </label>
     625                                        </p>
     626                                        <p class="button-controls">
     627                                                <span class="add-to-menu">
     628                                                        <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">
     629                                                        <span class="spinner"></span>
     630                                                </span>
     631                                        </p>
     632                                </div>
     633                        </div>
     634                        <?php
     635
     636                        // @todo: consider using add_meta_box/do_accordion_section and making screen-optional?
     637                        // Containers for per-post-type item browsing; items added with JS.
     638                        $post_types = get_post_types( array( 'show_in_nav_menus' => true ), 'object' );
     639                        if ( $post_types ) :
     640                                foreach ( $post_types as $type ) :
     641                                        ?>
     642                                        <div id="available-menu-items-<?php echo esc_attr( $type->name ); ?>" class="accordion-section">
     643                                                <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>
     644                                                <div class="accordion-section-content" data-type="<?php echo esc_attr( $type->name ); ?>" data-obj_type="post_type"></div>
     645                                        </div>
     646                                <?php
     647                                endforeach;
     648                        endif;
     649
     650                        $taxonomies = get_taxonomies( array( 'show_in_nav_menus' => true ), 'object' );
     651                        if ( $taxonomies ) :
     652                                foreach ( $taxonomies as $tax ) :
     653                                        ?>
     654                                        <div id="available-menu-items-<?php echo esc_attr( $tax->name ); ?>" class="accordion-section">
     655                                                <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>
     656                                                <div class="accordion-section-content" data-type="<?php echo esc_attr( $tax->name ); ?>" data-obj_type="taxonomy"></div>
     657                                        </div>
     658                                <?php
     659                                endforeach;
     660                        endif;
     661                        ?>
     662                </div><!-- #available-menu-items -->
     663        <?php
     664        }
     665
     666        // Start functionality specific to partial-refresh of menu changes in Customizer preview.
     667        const RENDER_AJAX_ACTION = 'customize_render_menu_partial';
     668        const RENDER_NONCE_POST_KEY = 'render-menu-nonce';
     669        const RENDER_QUERY_VAR = 'wp_customize_menu_render';
     670
     671        /**
     672         * The number of wp_nav_menu() calls which have happened in the preview.
     673         *
     674         * @var int
     675         */
     676        public $preview_nav_menu_instance_number = 0;
     677
     678        /**
     679         * Nav menu args used for each instance.
     680         *
     681         * @var array[]
     682         */
     683        public $preview_nav_menu_instance_args = array();
     684
     685        /**
     686         * Add hooks for the Customizer preview.
     687         */
     688        function customize_preview_init() {
     689                add_action( 'template_redirect', array( $this, 'render_menu' ) );
     690                add_action( 'wp_enqueue_scripts', array( $this, 'customize_preview_enqueue_deps' ) );
     691
     692                if ( ! isset( $_REQUEST[ self::RENDER_QUERY_VAR ] ) ) {
     693                        add_filter( 'wp_nav_menu_args', array( $this, 'filter_wp_nav_menu_args' ), 1000 );
     694                        add_filter( 'wp_nav_menu', array( $this, 'filter_wp_nav_menu' ), 10, 2 );
     695                }
     696        }
     697
     698        /**
     699         * Keep track of the arguments that are being passed to wp_nav_menu().
     700         *
     701         * @see wp_nav_menu()
     702         *
     703         * @param array $args  An array containing wp_nav_menu() arguments.
     704         * @return array
     705         */
     706        function filter_wp_nav_menu_args( $args ) {
     707                $this->preview_nav_menu_instance_number += 1;
     708                $args['instance_number'] = $this->preview_nav_menu_instance_number;
     709
     710                $can_partial_refresh = (
     711                        $args['echo']
     712                        &&
     713                        is_string( $args['fallback_cb'] )
     714                        &&
     715                        is_string( $args['walker'] )
     716                );
     717                $args['can_partial_refresh'] = $can_partial_refresh;
     718
     719                if ( ! $can_partial_refresh ) {
     720                        unset( $args['fallback_cb'] );
     721                        unset( $args['walker'] );
     722                }
     723
     724                ksort( $args );
     725                $args['args_hash'] = $this->hash_nav_menu_args( $args );
     726
     727                $this->preview_nav_menu_instance_args[ $this->preview_nav_menu_instance_number ] = $args;
     728                return $args;
     729        }
     730
     731        /**
     732         * Prepare wp_nav_menu() calls for partial refresh. Wraps output in container for refreshing.
     733         *
     734         * @see wp_nav_menu()
     735         *
     736         * @param string $nav_menu_content The HTML content for the navigation menu.
     737         * @param object $args             An object containing wp_nav_menu() arguments.
     738         * @return null
     739         */
     740        function filter_wp_nav_menu( $nav_menu_content, $args ) {
     741                if ( ! empty( $args->can_partial_refresh ) && ! empty( $args->instance_number ) ) {
     742                        $nav_menu_content = sprintf(
     743                                '<div id="partial-refresh-menu-container-%1$d" class="partial-refresh-menu-container" data-instance-number="%1$d">%2$s</div>',
     744                                $args->instance_number,
     745                                $nav_menu_content
     746                        );
     747                }
     748                return $nav_menu_content;
     749        }
     750
     751        /**
     752         * Hash (hmac) the arguments with the nonce and secret auth key to ensure they
     753         * are not tampered with when submitted in the Ajax request.
     754         *
     755         * @param array $args The arguments to hash.
     756         * @return string
     757         */
     758        function hash_nav_menu_args( $args ) {
     759                return wp_hash( wp_create_nonce( self::RENDER_AJAX_ACTION ) . serialize( $args ) );
     760        }
     761
     762        /**
     763         * Enqueue scripts for the Customizer preview.
     764         */
     765        function customize_preview_enqueue_deps() {
     766                wp_enqueue_script( 'customize-preview-nav-menus' );
     767                wp_enqueue_style( 'customize-preview' );
     768
     769                add_action( 'wp_print_footer_scripts', array( $this, 'export_preview_data' ) );
     770        }
     771
     772        /**
     773         * Export data from PHP to JS.
     774         */
     775        function export_preview_data() {
     776
     777                // Why not wp_localize_script? Because we're not localizing, and it forces values into strings.
     778                $exports = array(
     779                        'renderQueryVar'        => self::RENDER_QUERY_VAR,
     780                        'renderNonceValue'      => wp_create_nonce( self::RENDER_AJAX_ACTION ),
     781                        'renderNoncePostKey'    => self::RENDER_NONCE_POST_KEY,
     782                        'requestUri'            => '/',
     783                        'theme'                 => array(
     784                                'stylesheet' => $this->manager->get_stylesheet(),
     785                                'active'     => $this->manager->is_theme_active(),
     786                        ),
     787                        'previewCustomizeNonce' => wp_create_nonce( 'preview-customize_' . $this->manager->get_stylesheet() ),
     788                        'navMenuInstanceArgs'   => $this->preview_nav_menu_instance_args,
     789                );
     790
     791                if ( ! empty( $_SERVER['REQUEST_URI'] ) ) {
     792                        $exports['requestUri'] = esc_url_raw( home_url( wp_unslash( $_SERVER['REQUEST_URI'] ) ) );
     793                }
     794
     795                printf( '<script>var _wpCustomizePreviewNavMenusExports = %s;</script>', wp_json_encode( $exports ) );
     796        }
     797
     798        /**
     799         * Render a specific menu via wp_nav_menu() using the supplied arguments.
     800         *
     801         * @see wp_nav_menu()
     802         */
     803        function render_menu() {
     804                if ( empty( $_POST[ self::RENDER_QUERY_VAR ] ) ) {
     805                        return;
     806                }
     807
     808                $this->manager->remove_preview_signature();
     809
     810                if ( ! is_customize_preview() ) {
     811                        wp_send_json_error( 'expected_customize_preview' );
     812                }
     813
     814                if ( empty( $_POST[ self::RENDER_NONCE_POST_KEY ] ) ) {
     815                        wp_send_json_error( 'missing_nonce_param' );
     816                }
     817
     818                if ( ! check_ajax_referer( self::RENDER_AJAX_ACTION, self::RENDER_NONCE_POST_KEY, false ) ) {
     819                        wp_send_json_error( 'nonce_check_fail' );
     820                }
     821
     822                if ( ! current_user_can( 'edit_theme_options' ) ) {
     823                        wp_send_json_error( 'unauthorized' );
     824                }
     825
     826                if ( ! isset( $_POST['wp_nav_menu_args'] ) ) {
     827                        wp_send_json_error( 'missing_param' );
     828                }
     829
     830                if ( ! isset( $_POST['wp_nav_menu_args_hash'] ) ) {
     831                        wp_send_json_error( 'missing_param' );
     832                }
     833
     834                $wp_nav_menu_args_hash = sanitize_text_field( wp_unslash( $_POST['wp_nav_menu_args_hash'] ) );
     835                if ( $this->hash_nav_menu_args( $wp_nav_menu_args ) !== $wp_nav_menu_args_hash ) {
     836                        wp_send_json_error( 'wp_nav_menu_args_hash_mismatch' );
     837                }
     838
     839                $wp_nav_menu_args = json_decode( wp_unslash( $_POST['wp_nav_menu_args'] ), true );
     840
     841                if ( ! is_array( $wp_nav_menu_args ) ) {
     842                        wp_send_json_error( 'wp_nav_menu_args_not_array' );
     843                }
     844
     845                $wp_nav_menu_args['echo'] = false;
     846                wp_send_json_success( wp_nav_menu( $wp_nav_menu_args ) );
     847        }
     848}
  • src/wp-includes/class-wp-customize-section.php

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

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

     
     1.customize-partial-refreshing {
     2        opacity: 0.25;
     3        transition: opacity 0.25s;
     4        cursor: progress;
     5}
  • src/wp-includes/js/customize-preview-nav-menus.js

     
     1/*global jQuery, JSON, _wpCustomizePreviewNavMenusExports, _ */
     2
     3wp.customize.menusPreview = ( function( $, api ) {
     4        'use strict';
     5        var self;
     6
     7        self = {
     8                renderQueryVar: null,
     9                renderNonceValue: null,
     10                renderNoncePostKey: null,
     11                previewCustomizeNonce: null,
     12                previewReady: $.Deferred(),
     13                requestUri: '/',
     14                theme: {
     15                        active: false,
     16                        stylesheet: ''
     17                },
     18                navMenuInstanceArgs: {},
     19                refreshDebounceDelay: 200
     20        };
     21
     22        api.bind( 'preview-ready', function() {
     23                self.previewReady.resolve();
     24        } );
     25        self.previewReady.done( function() {
     26                self.init();
     27        } );
     28
     29        /**
     30         * Bootstrap functionality.
     31         */
     32        self.init = function() {
     33                var self = this;
     34
     35                if ( 'undefined' !== typeof _wpCustomizePreviewNavMenusExports ) {
     36                        $.extend( self, _wpCustomizePreviewNavMenusExports );
     37                }
     38
     39                self.previewReady.done( function() {
     40                        api.each( function( setting, id ) {
     41                                setting.id = id;
     42                                self.bindListener( setting );
     43                        } );
     44
     45                        api.preview.bind( 'setting', function( args ) {
     46                                var id, value, setting;
     47                                args = args.slice();
     48                                id = args.shift();
     49                                value = args.shift();
     50                                if ( ! api.has( id ) ) {
     51                                        // Currently customize-preview.js is not creating settings for dynamically-created settings in the pane; so we have to do it
     52                                        setting = api.create( id, value ); // @todo This should be in core
     53                                        setting.id = id;
     54                                        if ( self.bindListener( setting ) ) {
     55                                                setting.callbacks.fireWith( setting, [ setting(), setting() ] );
     56                                        }
     57                                }
     58                        } );
     59                } );
     60        };
     61
     62        /**
     63         *
     64         * @param {wp.customize.Value} setting
     65         * @returns {boolean} Whether the setting was bound.
     66         */
     67        self.bindListener = function( setting ) {
     68                var matches, themeLocation;
     69
     70                matches = setting.id.match( /^nav_menu\[(-?\d+)]$/ );
     71                if ( matches ) {
     72                        setting.navMenuId = parseInt( matches[1], 10 );
     73                        setting.bind( self.onChangeNavMenuSetting );
     74                        return true;
     75                }
     76
     77                matches = setting.id.match( /^nav_menu_item\[(-?\d+)]$/ );
     78                if ( matches ) {
     79                        setting.navMenuItemId = parseInt( matches[1], 10 );
     80                        setting.bind( self.onChangeNavMenuItemSetting );
     81                        return true;
     82                }
     83
     84                matches = setting.id.match( /^nav_menu_locations\[(.+?)]/ );
     85                if ( matches ) {
     86                        themeLocation = matches[1];
     87                        setting.bind( function() {
     88                                self.refreshMenuLocation( themeLocation );
     89                        } );
     90                        return true;
     91                }
     92
     93                return false;
     94        };
     95
     96        /**
     97         * Handle changing of a nav_menu setting.
     98         *
     99         * @this {wp.customize.Setting}
     100         */
     101        self.onChangeNavMenuSetting = function() {
     102                var setting = this;
     103                if ( ! setting.navMenuId ) {
     104                        throw new Error( 'Expected navMenuId property to be set.' );
     105                }
     106                self.refreshMenu( setting.navMenuId );
     107        };
     108
     109        /**
     110         * Handle changing of a nav_menu_item setting.
     111         *
     112         * @this {wp.customize.Setting}
     113         * @param {object} to
     114         * @param {object} from
     115         */
     116        self.onChangeNavMenuItemSetting = function( to, from ) {
     117                if ( from && from.nav_menu_term_id && ( ! to || from.nav_menu_term_id !== to.nav_menu_term_id ) ) {
     118                        self.refreshMenu( from.nav_menu_term_id );
     119                }
     120                if ( to && to.nav_menu_term_id ) {
     121                        self.refreshMenu( to.nav_menu_term_id );
     122                }
     123        };
     124
     125        /**
     126         * Update a given menu rendered in the preview.
     127         *
     128         * @param {int} menuId
     129         */
     130        self.refreshMenu = function( menuId ) {
     131                var self = this, assignedLocations = [];
     132
     133                api.each(function( setting, id ) {
     134                        var matches = id.match( /^nav_menu_locations\[(.+?)]/ );
     135                        if ( matches && menuId === setting() ) {
     136                                assignedLocations.push( matches[1] );
     137                        }
     138                });
     139
     140                _.each( self.navMenuInstanceArgs, function( navMenuArgs, instanceNumber ) {
     141                        if ( menuId === navMenuArgs.menu || -1 !== _.indexOf( assignedLocations, navMenuArgs.theme_location ) ) {
     142                                self.refreshMenuInstanceDebounced( instanceNumber );
     143                        }
     144                } );
     145        };
     146
     147        self.refreshMenuLocation = function( location ) {
     148                var foundInstance = false;
     149                _.each( self.navMenuInstanceArgs, function( navMenuArgs, instanceNumber ) {
     150                        if ( location === navMenuArgs.theme_location ) {
     151                                self.refreshMenuInstanceDebounced( instanceNumber );
     152                                foundInstance = true;
     153                        }
     154                } );
     155                if ( ! foundInstance ) {
     156                        api.preview.send( 'refresh' );
     157                }
     158        };
     159
     160        /**
     161         * Update a specific instance of a given menu on the page.
     162         *
     163         * @param {int} instanceNumber
     164         */
     165        self.refreshMenuInstance = function( instanceNumber ) {
     166                var self = this, data, customized, container, request, wpNavArgs, instance;
     167
     168                if ( ! self.navMenuInstanceArgs[ instanceNumber ] ) {
     169                        throw new Error( 'unknown_instance_number' );
     170                }
     171                instance = self.navMenuInstanceArgs[ instanceNumber ];
     172
     173                container = $( '#partial-refresh-menu-container-' + String( instanceNumber ) );
     174
     175                if ( ! instance.can_partial_refresh || 0 === container.length ) {
     176                        api.preview.send( 'refresh' );
     177                        return;
     178                }
     179
     180                data = {
     181                        nonce: self.previewCustomizeNonce, // for Customize Preview
     182                        wp_customize: 'on'
     183                };
     184                if ( ! self.theme.active ) {
     185                        data.theme = self.theme.stylesheet;
     186                }
     187                data[ self.renderQueryVar ] = '1';
     188                customized = {};
     189                api.each( function( setting, id ) {
     190                        // @todo We need to limit this to just the menu items that are associated with this menu/location.
     191                        if ( /^(nav_menu|nav_menu_locations)/.test( id ) ) {
     192                                customized[ id ] = setting.get();
     193                        }
     194                } );
     195                data.customized = JSON.stringify( customized );
     196                data[ self.renderNoncePostKey ] = self.renderNonceValue;
     197
     198                wpNavArgs = $.extend( {}, instance );
     199                data.wp_nav_menu_args_hash = wpNavArgs.args_hash;
     200                delete wpNavArgs.args_hash;
     201                data.wp_nav_menu_args = JSON.stringify( wpNavArgs );
     202
     203                container.addClass( 'customize-partial-refreshing' );
     204
     205                request = wp.ajax.send( null, {
     206                        data: data,
     207                        url: self.requestUri
     208                } );
     209                request.done( function( data ) {
     210                        var eventParam;
     211                        container.empty().append( $( data ) );
     212                        eventParam = {
     213                                instanceNumber: instanceNumber,
     214                                wpNavArgs: wpNavArgs
     215                        };
     216                        $( document ).trigger( 'customize-preview-menu-refreshed', [ eventParam ] );
     217                } );
     218                request.fail( function() {
     219                        // @todo provide some indication for why
     220                } );
     221                request.always( function() {
     222                        container.removeClass( 'customize-partial-refreshing' );
     223                } );
     224        };
     225
     226        self.currentRefreshMenuInstanceDebouncedCalls = {};
     227
     228        self.refreshMenuInstanceDebounced = function( instanceNumber ) {
     229                if ( self.currentRefreshMenuInstanceDebouncedCalls[ instanceNumber ] ) {
     230                        clearTimeout( self.currentRefreshMenuInstanceDebouncedCalls[ instanceNumber ] );
     231                }
     232                self.currentRefreshMenuInstanceDebouncedCalls[ instanceNumber ] = setTimeout(
     233                        function() {
     234                                self.refreshMenuInstance( instanceNumber );
     235                        },
     236                        self.refreshDebounceDelay
     237                );
     238        };
     239
     240        return self;
     241
     242}( jQuery, wp.customize ) );
  • src/wp-includes/script-loader.php

     
    406406        $scripts->add( 'customize-widgets', "/wp-admin/js/customize-widgets$suffix.js", array( 'jquery', 'jquery-ui-sortable', 'jquery-ui-droppable', 'wp-backbone', 'customize-controls' ), false, 1 );
    407407        $scripts->add( 'customize-preview-widgets', "/wp-includes/js/customize-preview-widgets$suffix.js", array( 'jquery', 'wp-util', 'customize-preview' ), false, 1 );
    408408
     409        $scripts->add( 'customize-nav-menus', "/wp-admin/js/customize-nav-menus$suffix.js", array( 'jquery', 'wp-backbone', 'customize-controls', 'accordion', 'nav-menu', 'wp-a11y' ), false, 1 );
     410        $scripts->add( 'customize-preview-nav-menus', "/wp-includes/js/customize-preview-nav-menus$suffix.js", array( 'jquery', 'wp-util', 'customize-preview' ), false, 1 );
     411
    409412        $scripts->add( 'accordion', "/wp-admin/js/accordion$suffix.js", array( 'jquery' ), false, 1 );
    410413
    411414        $scripts->add( 'shortcode', "/wp-includes/js/shortcode$suffix.js", array( 'underscore' ), false, 1 );
     
    656659        $suffix = SCRIPT_DEBUG ? '' : '.min';
    657660
    658661        // Admin CSS
    659         $styles->add( 'wp-admin',           "/wp-admin/css/wp-admin$suffix.css", array( 'open-sans', 'dashicons' ) );
    660         $styles->add( 'login',              "/wp-admin/css/login$suffix.css", array( 'buttons', 'open-sans', 'dashicons' ) );
    661         $styles->add( 'install',            "/wp-admin/css/install$suffix.css", array( 'buttons', 'open-sans' ) );
    662         $styles->add( 'wp-color-picker',    "/wp-admin/css/color-picker$suffix.css" );
    663         $styles->add( 'customize-controls', "/wp-admin/css/customize-controls$suffix.css", array( 'wp-admin', 'colors', 'ie', 'imgareaselect' ) );
    664         $styles->add( 'customize-widgets',  "/wp-admin/css/customize-widgets$suffix.css", array( 'wp-admin', 'colors' ) );
    665         $styles->add( 'press-this',         "/wp-admin/css/press-this$suffix.css", array( 'open-sans', 'buttons' ) );
     662        $styles->add( 'wp-admin',            "/wp-admin/css/wp-admin$suffix.css", array( 'open-sans', 'dashicons' ) );
     663        $styles->add( 'login',               "/wp-admin/css/login$suffix.css", array( 'buttons', 'open-sans', 'dashicons' ) );
     664        $styles->add( 'install',             "/wp-admin/css/install$suffix.css", array( 'buttons', 'open-sans' ) );
     665        $styles->add( 'wp-color-picker',     "/wp-admin/css/color-picker$suffix.css" );
     666        $styles->add( 'customize-controls',  "/wp-admin/css/customize-controls$suffix.css", array( 'wp-admin', 'colors', 'ie', 'imgareaselect' ) );
     667        $styles->add( 'customize-widgets',   "/wp-admin/css/customize-widgets$suffix.css", array( 'wp-admin', 'colors' ) );
     668        $styles->add( 'customize-nav-menus', "/wp-admin/css/customize-nav-menus$suffix.css", array( 'wp-admin', 'colors' ) );
     669        $styles->add( 'press-this',          "/wp-admin/css/press-this$suffix.css", array( 'open-sans', 'buttons' ) );
    666670
    667         $styles->add( 'ie',                 "/wp-admin/css/ie$suffix.css" );
     671        $styles->add( 'ie', "/wp-admin/css/ie$suffix.css" );
    668672        $styles->add_data( 'ie', 'conditional', 'lte IE 7' );
    669673
    670674        // Common dependencies
     
    673677        $styles->add( 'open-sans', $open_sans_font_url );
    674678
    675679        // Includes CSS
    676         $styles->add( 'admin-bar',      "/wp-includes/css/admin-bar$suffix.css", array( 'open-sans', 'dashicons' ) );
    677         $styles->add( 'wp-auth-check',  "/wp-includes/css/wp-auth-check$suffix.css", array( 'dashicons' ) );
    678         $styles->add( 'editor-buttons', "/wp-includes/css/editor$suffix.css", array( 'dashicons' ) );
    679         $styles->add( 'media-views',    "/wp-includes/css/media-views$suffix.css", array( 'buttons', 'dashicons', 'wp-mediaelement' ) );
    680         $styles->add( 'wp-pointer',     "/wp-includes/css/wp-pointer$suffix.css", array( 'dashicons' ) );
     680        $styles->add( 'admin-bar',         "/wp-includes/css/admin-bar$suffix.css", array( 'open-sans', 'dashicons' ) );
     681        $styles->add( 'wp-auth-check',     "/wp-includes/css/wp-auth-check$suffix.css", array( 'dashicons' ) );
     682        $styles->add( 'editor-buttons',    "/wp-includes/css/editor$suffix.css", array( 'dashicons' ) );
     683        $styles->add( 'media-views',       "/wp-includes/css/media-views$suffix.css", array( 'buttons', 'dashicons', 'wp-mediaelement' ) );
     684        $styles->add( 'wp-pointer',        "/wp-includes/css/wp-pointer$suffix.css", array( 'dashicons' ) );
     685        $styles->add( 'customize-preview', "/wp-includes/css/customize-preview$suffix.css" );
    681686
    682687        // External libraries and friends
    683688        $styles->add( 'imgareaselect',       '/wp-includes/js/imgareaselect/imgareaselect.css', array(), '0.9.8' );
     
    695700        // RTL CSS
    696701        $rtl_styles = array(
    697702                // wp-admin
    698                 'wp-admin', 'install', 'wp-color-picker', 'customize-controls', 'customize-widgets', 'ie', 'login', 'press-this',
     703                'wp-admin', 'install', 'wp-color-picker', 'customize-controls', 'customize-widgets', 'customize-nav-menus', 'ie', 'login', 'press-this',
    699704                // wp-includes
    700705                'buttons', 'admin-bar', 'wp-auth-check', 'editor-buttons', 'media-views', 'wp-pointer',
    701706                'wp-jquery-ui-dialog',