Make WordPress Core

Ticket #32576: 32576.2.diff

File 32576.2.diff, 195.2 KB (added by ocean90, 10 years ago)

Merge styles/scripts

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