Make WordPress Core

Ticket #32576: 32576.6.diff

File 32576.6.diff, 251.1 KB (added by ocean90, 10 years ago)
  • Gruntfile.js

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

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

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

     
    4949         */
    5050        public $widgets;
    5151
     52        /**
     53         * Methods and properties deailing with managing nav menus in the Customizer.
     54         *
     55         * @var WP_Customize_Nav_Menus
     56         */
     57        public $nav_menus;
     58
    5259        protected $settings   = array();
    5360        protected $containers = array();
    5461        protected $panels     = array();
     
    104111                require_once( ABSPATH . WPINC . '/class-wp-customize-section.php' );
    105112                require_once( ABSPATH . WPINC . '/class-wp-customize-control.php' );
    106113                require_once( ABSPATH . WPINC . '/class-wp-customize-widgets.php' );
     114                require_once( ABSPATH . WPINC . '/class-wp-customize-nav-menus.php' );
    107115
    108116                $this->widgets = new WP_Customize_Widgets( $this );
     117                $this->nav_menus = new WP_Customize_Nav_Menus( $this );
    109118
    110119                add_filter( 'wp_die_handler', array( $this, 'wp_die_handler' ) );
    111120
     
    14841493                        }
    14851494                }
    14861495
    1487                 /* Nav Menus */
    1488 
    1489                 $locations      = get_registered_nav_menus();
    1490                 $menus          = wp_get_nav_menus();
    1491                 $num_locations  = count( array_keys( $locations ) );
    1492 
    1493                 if ( 1 == $num_locations ) {
    1494                         $description = __( 'Your theme supports one menu. Select which menu you would like to use.' );
    1495                 } else {
    1496                         $description = sprintf( _n( 'Your theme supports %s menu. Select which menu appears in each location.', 'Your theme supports %s menus. Select which menu appears in each location.', $num_locations ), number_format_i18n( $num_locations ) );
    1497                 }
    1498 
    1499                 $this->add_section( 'nav', array(
    1500                         'title'          => __( 'Navigation' ),
    1501                         'theme_supports' => 'menus',
    1502                         'priority'       => 100,
    1503                         'description'    => $description . "\n\n" . __( 'You can edit your menu content on the Menus screen in the Appearance section.' ),
    1504                 ) );
    1505 
    1506                 if ( $menus ) {
    1507                         $choices = array( '' => __( '&mdash; Select &mdash;' ) );
    1508                         foreach ( $menus as $menu ) {
    1509                                 $choices[ $menu->term_id ] = wp_html_excerpt( $menu->name, 40, '&hellip;' );
    1510                         }
    1511 
    1512                         foreach ( $locations as $location => $description ) {
    1513                                 $menu_setting_id = "nav_menu_locations[{$location}]";
    1514 
    1515                                 $this->add_setting( $menu_setting_id, array(
    1516                                         'sanitize_callback' => 'absint',
    1517                                         'theme_supports'    => 'menus',
    1518                                 ) );
    1519 
    1520                                 $this->add_control( $menu_setting_id, array(
    1521                                         'label'   => $description,
    1522                                         'section' => 'nav',
    1523                                         'type'    => 'select',
    1524                                         'choices' => $choices,
    1525                                 ) );
    1526                         }
    1527                 }
    1528 
    15291496                /* Static Front Page */
    15301497                // #WP19627
    15311498
  • src/wp-includes/class-wp-customize-nav-menus.php

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

     
     1<?php
     2/**
     3 * Tests WP_Customize_Nav_Menu_Item_Setting.
     4 *
     5 * @group customize
     6 */
     7class Test_WP_Customize_Nav_Menu_Item_Setting extends WP_UnitTestCase {
     8
     9        /**
     10         * Instance of WP_Customize_Manager which is reset for each test.
     11         *
     12         * @var WP_Customize_Manager
     13         */
     14        public $wp_customize;
     15
     16        /**
     17         * Set up a test case.
     18         *
     19         * @see WP_UnitTestCase::setup()
     20         */
     21        function setUp() {
     22                parent::setUp();
     23                require_once ABSPATH . WPINC . '/class-wp-customize-manager.php';
     24                wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );
     25
     26                global $wp_customize;
     27                $this->wp_customize = new WP_Customize_Manager();
     28                $wp_customize = $this->wp_customize;
     29        }
     30
     31        /**
     32         * Delete the $wp_customize global when cleaning up scope.
     33         */
     34        function clean_up_global_scope() {
     35                global $wp_customize;
     36                $wp_customize = null;
     37                parent::clean_up_global_scope();
     38        }
     39
     40        /**
     41         * Test constants and statics.
     42         */
     43        function test_constants() {
     44                do_action( 'customize_register', $this->wp_customize );
     45                $this->assertTrue( post_type_exists( WP_Customize_Nav_Menu_Item_Setting::POST_TYPE ) );
     46        }
     47
     48        /**
     49         * Test constructor.
     50         *
     51         * @see WP_Customize_Nav_Menu_Item_Setting::__construct()
     52         */
     53        function test_construct() {
     54                do_action( 'customize_register', $this->wp_customize );
     55
     56                $setting = new WP_Customize_Nav_Menu_Item_Setting( $this->wp_customize, 'nav_menu_item[123]' );
     57                $this->assertEquals( 'nav_menu_item', $setting->type );
     58                $this->assertEquals( 'postMessage', $setting->transport );
     59                $this->assertEquals( 123, $setting->post_id );
     60                $this->assertNull( $setting->previous_post_id );
     61                $this->assertNull( $setting->update_status );
     62                $this->assertNull( $setting->update_error );
     63                $this->assertInternalType( 'array', $setting->default );
     64
     65                $default = array(
     66                        'object_id' => 0,
     67                        'object' => '',
     68                        'menu_item_parent' => 0,
     69                        'position' => 0,
     70                        'type' => 'custom',
     71                        'title' => '',
     72                        'url' => '',
     73                        'target' => '',
     74                        'attr_title' => '',
     75                        'description' => '',
     76                        'classes' => '',
     77                        'xfn' => '',
     78                        'status' => 'publish',
     79                        'original_title' => '',
     80                        'nav_menu_term_id' => 0,
     81                );
     82                $this->assertEquals( $default, $setting->default );
     83
     84                $exception = null;
     85                try {
     86                        $bad_setting = new WP_Customize_Nav_Menu_Item_Setting( $this->wp_customize, 'foo_bar_baz' );
     87                        unset( $bad_setting );
     88                } catch ( Exception $e ) {
     89                        $exception = $e;
     90                }
     91                $this->assertInstanceOf( 'Exception', $exception );
     92        }
     93
     94        /**
     95         * Test empty constructor.
     96         */
     97        function test_construct_empty_menus() {
     98                do_action( 'customize_register', $this->wp_customize );
     99                $_wp_customize = $this->wp_customize;
     100                unset($_wp_customize->nav_menus);
     101
     102                $exception = null;
     103                try {
     104                        $bad_setting = new WP_Customize_Nav_Menu_Item_Setting( $_wp_customize, 'nav_menu_item[123]' );
     105                        unset( $bad_setting );
     106                } catch ( Exception $e ) {
     107                        $exception = $e;
     108                }
     109                $this->assertInstanceOf( 'Exception', $exception );
     110        }
     111
     112        /**
     113         * Test constructor for placeholder (draft) menu.
     114         *
     115         * @see WP_Customize_Nav_Menu_Item_Setting::__construct()
     116         */
     117        function test_construct_placeholder() {
     118                do_action( 'customize_register', $this->wp_customize );
     119                $default = array(
     120                        'title' => 'Lorem',
     121                        'description' => 'ipsum',
     122                        'menu_item_parent' => 123,
     123                );
     124                $setting = new WP_Customize_Nav_Menu_Item_Setting( $this->wp_customize, 'nav_menu_item[-5]', compact( 'default' ) );
     125                $this->assertEquals( -5, $setting->post_id );
     126                $this->assertNull( $setting->previous_post_id );
     127                $this->assertEquals( $default, $setting->default );
     128        }
     129
     130        /**
     131         * Test value method with post.
     132         *
     133         * @see WP_Customize_Nav_Menu_Item_Setting::value()
     134         */
     135        function test_value_type_post_type() {
     136                do_action( 'customize_register', $this->wp_customize );
     137
     138                $post_id = $this->factory->post->create( array( 'post_title' => 'Hello World' ) );
     139
     140                $menu_id = wp_create_nav_menu( 'Menu' );
     141                $item_title = 'Greetings';
     142                $item_id = wp_update_nav_menu_item( $menu_id, 0, array(
     143                        'menu-item-type' => 'post_type',
     144                        'menu-item-object' => 'post',
     145                        'menu-item-object-id' => $post_id,
     146                        'menu-item-title' => $item_title,
     147                        'menu-item-status' => 'publish',
     148                ) );
     149
     150                $post = get_post( $item_id );
     151                $menu_item = wp_setup_nav_menu_item( $post );
     152                $this->assertEquals( $item_title, $menu_item->title );
     153
     154                $setting_id = "nav_menu_item[$item_id]";
     155                $setting = new WP_Customize_Nav_Menu_Item_Setting( $this->wp_customize, $setting_id );
     156
     157                $value = $setting->value();
     158                $this->assertEquals( $menu_item->title, $value['title'] );
     159                $this->assertEquals( $menu_item->type, $value['type'] );
     160                $this->assertEquals( $menu_item->object_id, $value['object_id'] );
     161                $this->assertEquals( $menu_id, $value['nav_menu_term_id'] );
     162                $this->assertEquals( 'Hello World', $value['original_title'] );
     163
     164                $other_menu_id = wp_create_nav_menu( 'Menu2' );
     165                wp_update_nav_menu_item( $other_menu_id, $item_id, array(
     166                        'menu-item-title' => 'Hola',
     167                ) );
     168                $value = $setting->value();
     169                $this->assertEquals( 'Hola', $value['title'] );
     170                $this->assertEquals( $other_menu_id, $value['nav_menu_term_id'] );
     171        }
     172
     173        /**
     174         * Test value method with taxonomy.
     175         *
     176         * @see WP_Customize_Nav_Menu_Item_Setting::value()
     177         */
     178        function test_value_type_taxonomy() {
     179                do_action( 'customize_register', $this->wp_customize );
     180
     181                $tax_id = $this->factory->category->create( array( 'name' => 'Salutations' ) );
     182
     183                $menu_id = wp_create_nav_menu( 'Menu' );
     184                $item_title = 'Greetings';
     185                $item_id = wp_update_nav_menu_item( $menu_id, 0, array(
     186                        'menu-item-type' => 'taxonomy',
     187                        'menu-item-object' => 'category',
     188                        'menu-item-object-id' => $tax_id,
     189                        'menu-item-title' => $item_title,
     190                        'menu-item-status' => 'publish',
     191                ) );
     192
     193                $post = get_post( $item_id );
     194                $menu_item = wp_setup_nav_menu_item( $post );
     195                $this->assertEquals( $item_title, $menu_item->title );
     196
     197                $setting_id = "nav_menu_item[$item_id]";
     198                $setting = new WP_Customize_Nav_Menu_Item_Setting( $this->wp_customize, $setting_id );
     199
     200                $value = $setting->value();
     201                $this->assertEquals( $menu_item->title, $value['title'] );
     202                $this->assertEquals( $menu_item->type, $value['type'] );
     203                $this->assertEquals( $menu_item->object_id, $value['object_id'] );
     204                $this->assertEquals( $menu_id, $value['nav_menu_term_id'] );
     205                $this->assertEquals( 'Salutations', $value['original_title'] );
     206        }
     207
     208        /**
     209         * Test value method returns zero for nav_menu_term_id when previewing a new menu.
     210         *
     211         * @see WP_Customize_Nav_Menu_Item_Setting::value()
     212         */
     213        function test_value_nav_menu_term_id_returns_zero() {
     214                do_action( 'customize_register', $this->wp_customize );
     215
     216                $menu_id = -123;
     217                $post_value = array(
     218                        'name' => 'Secondary',
     219                        'description' => '',
     220                        'parent' => 0,
     221                        'auto_add' => false,
     222                );
     223                $setting_id = "nav_menu[$menu_id]";
     224                $menu = new WP_Customize_Nav_Menu_Setting( $this->wp_customize, $setting_id );
     225
     226                $this->wp_customize->set_post_value( $menu->id, $post_value );
     227                $menu->preview();
     228                $value = $menu->value();
     229                $this->assertEquals( $post_value, $value );
     230
     231                $post_id = $this->factory->post->create( array( 'post_title' => 'Hello World' ) );
     232                $item_id = wp_update_nav_menu_item( $menu_id, 0, array(
     233                        'menu-item-type' => 'post_type',
     234                        'menu-item-object' => 'post',
     235                        'menu-item-object-id' => $post_id,
     236                        'menu-item-title' => 'Hello World',
     237                        'menu-item-status' => 'publish',
     238                ) );
     239
     240                $post = get_post( $item_id );
     241                $menu_item = wp_setup_nav_menu_item( $post );
     242
     243                $setting_id = "nav_menu_item[$item_id]";
     244                $setting = new WP_Customize_Nav_Menu_Item_Setting( $this->wp_customize, $setting_id );
     245                $value = $setting->value();
     246                $this->assertEquals( 0, $value['nav_menu_term_id'] );
     247        }
     248
     249        /**
     250         * Test preview method for updated menu.
     251         *
     252         * @see WP_Customize_Nav_Menu_Item_Setting::preview()
     253         */
     254        function test_preview_updated() {
     255                do_action( 'customize_register', $this->wp_customize );
     256
     257                $first_post_id = $this->factory->post->create( array( 'post_title' => 'Hello World' ) );
     258                $second_post_id = $this->factory->post->create( array( 'post_title' => 'Hola Muno' ) );
     259
     260                $primary_menu_id = wp_create_nav_menu( 'Primary' );
     261                $secondary_menu_id = wp_create_nav_menu( 'Secondary' );
     262                $item_title = 'Greetings';
     263                $item_id = wp_update_nav_menu_item( $primary_menu_id, 0, array(
     264                        'menu-item-type' => 'post_type',
     265                        'menu-item-object' => 'post',
     266                        'menu-item-object-id' => $first_post_id,
     267                        'menu-item-title' => $item_title,
     268                        'menu-item-status' => 'publish',
     269                ) );
     270                $this->assertNotEmpty( wp_get_nav_menu_items( $primary_menu_id, array( 'post_status' => 'publish,draft' ) ) );
     271
     272                $post_value = array(
     273                        'type' => 'post_type',
     274                        'object' => 'post',
     275                        'object_id' => $second_post_id,
     276                        'title' => 'Saludos',
     277                        'status' => 'publish',
     278                        'nav_menu_term_id' => $secondary_menu_id,
     279                );
     280                $setting_id = "nav_menu_item[$item_id]";
     281                $setting = new WP_Customize_Nav_Menu_Item_Setting( $this->wp_customize, $setting_id );
     282                $this->wp_customize->set_post_value( $setting_id, $post_value );
     283                unset( $post_value['nav_menu_term_id'] );
     284                $setting->preview();
     285
     286                // Make sure the menu item appears in the new menu.
     287                $this->assertNotContains( $item_id, wp_list_pluck( wp_get_nav_menu_items( $primary_menu_id ), 'db_id' ) );
     288                $menu_items = wp_get_nav_menu_items( $secondary_menu_id );
     289                $db_ids = wp_list_pluck( $menu_items, 'db_id' );
     290                $this->assertContains( $item_id, $db_ids );
     291                $i = array_search( $item_id, $db_ids );
     292                $updated_item = $menu_items[ $i ];
     293                $post_value['post_status'] = $post_value['status'];
     294                unset( $post_value['status'] );
     295                foreach ( $post_value as $key => $value ) {
     296                        $this->assertEquals( $value, $updated_item->$key, "Key $key mismatch" );
     297                }
     298        }
     299
     300        /**
     301         * Test preview method for inserted menu.
     302         *
     303         * @see WP_Customize_Nav_Menu_Item_Setting::preview()
     304         */
     305        function test_preview_inserted() {
     306                do_action( 'customize_register', $this->wp_customize );
     307
     308                $menu_id = wp_create_nav_menu( 'Primary' );
     309                $post_id = $this->factory->post->create( array( 'post_title' => 'Hello World' ) );
     310                $item_ids = array();
     311                for ( $i = 0; $i < 5; $i += 1 ) {
     312                        $item_id = wp_update_nav_menu_item( $menu_id, 0, array(
     313                                'menu-item-type' => 'post_type',
     314                                'menu-item-object' => 'post',
     315                                'menu-item-object-id' => $post_id,
     316                                'menu-item-title' => "Item $i",
     317                                'menu-item-status' => 'publish',
     318                                'menu-item-position' => $i + 1,
     319                        ) );
     320                        $item_ids[] = $item_id;
     321                }
     322
     323                $post_value = array(
     324                        'type' => 'post_type',
     325                        'object' => 'post',
     326                        'object_id' => $post_id,
     327                        'title' => 'Inserted item',
     328                        'status' => 'publish',
     329                        'nav_menu_term_id' => $menu_id,
     330                        'position' => count( $item_ids ) + 1,
     331                );
     332
     333                $new_item_id = -10;
     334                $setting_id = "nav_menu_item[$new_item_id]";
     335                $setting = new WP_Customize_Nav_Menu_Item_Setting( $this->wp_customize, $setting_id );
     336                $this->wp_customize->set_post_value( $setting_id, $post_value );
     337                unset( $post_value['nav_menu_term_id'] );
     338
     339                $current_items = wp_get_nav_menu_items( $menu_id );
     340                $setting->preview();
     341                $preview_items = wp_get_nav_menu_items( $menu_id );
     342                $this->assertNotEquals( count( $current_items ), count( $preview_items ) );
     343
     344                $last_item = array_pop( $preview_items );
     345                $this->assertEquals( $new_item_id, $last_item->db_id );
     346                $post_value['post_status'] = $post_value['status'];
     347                unset( $post_value['status'] );
     348                $post_value['menu_order'] = $post_value['position'];
     349                unset( $post_value['position'] );
     350                foreach ( $post_value as $key => $value ) {
     351                        $this->assertEquals( $value, $last_item->$key, "Mismatch for $key property." );
     352                }
     353        }
     354
     355        /**
     356         * Test preview method for deleted menu.
     357         *
     358         * @see WP_Customize_Nav_Menu_Item_Setting::preview()
     359         */
     360        function test_preview_deleted() {
     361                do_action( 'customize_register', $this->wp_customize );
     362
     363                $menu_id = wp_create_nav_menu( 'Primary' );
     364                $post_id = $this->factory->post->create( array( 'post_title' => 'Hello World' ) );
     365                $item_ids = array();
     366                for ( $i = 0; $i < 5; $i += 1 ) {
     367                        $item_id = wp_update_nav_menu_item( $menu_id, 0, array(
     368                                'menu-item-type' => 'post_type',
     369                                'menu-item-object' => 'post',
     370                                'menu-item-object-id' => $post_id,
     371                                'menu-item-title' => "Item $i",
     372                                'menu-item-status' => 'publish',
     373                                'menu-item-position' => $i + 1,
     374                        ) );
     375                        $item_ids[] = $item_id;
     376                }
     377
     378                $delete_item_id = $item_ids[2];
     379                $setting_id = "nav_menu_item[$delete_item_id]";
     380                $setting = new WP_Customize_Nav_Menu_Item_Setting( $this->wp_customize, $setting_id );
     381                $this->wp_customize->set_post_value( $setting_id, false );
     382
     383                $current_items = wp_get_nav_menu_items( $menu_id );
     384                $this->assertContains( $delete_item_id, wp_list_pluck( $current_items, 'db_id' ) );
     385                $setting->preview();
     386                $preview_items = wp_get_nav_menu_items( $menu_id );
     387                $this->assertNotEquals( count( $current_items ), count( $preview_items ) );
     388                $this->assertContains( $delete_item_id, wp_list_pluck( $current_items, 'db_id' ) );
     389        }
     390
     391        /**
     392         * Test sanitize method.
     393         *
     394         * @see WP_Customize_Nav_Menu_Item_Setting::sanitize()
     395         */
     396        function test_sanitize() {
     397                do_action( 'customize_register', $this->wp_customize );
     398                $setting = new WP_Customize_Nav_Menu_Item_Setting( $this->wp_customize, 'nav_menu_item[123]' );
     399
     400                $this->assertNull( $setting->sanitize( 'not an array' ) );
     401                $this->assertNull( $setting->sanitize( 123 ) );
     402
     403                $unsanitized = array(
     404                        'object_id' => 'bad',
     405                        'object' => '<b>hello</b>',
     406                        'menu_item_parent' => 'asdasd',
     407                        'position' => -123,
     408                        'type' => 'custom<b>',
     409                        'title' => 'Hi<script>alert(1)</script>',
     410                        'url' => 'javascript:alert(1)',
     411                        'target' => '" onclick="',
     412                        'attr_title' => '<b>evil</b>',
     413                        'description' => '<b>Hello world</b>',
     414                        'classes' => 'hello " inject="',
     415                        'xfn' => 'hello " inject="',
     416                        'status' => 'forbidden',
     417                        'original_title' => 'Hi<script>alert(1)</script>',
     418                        'nav_menu_term_id' => 'heilo',
     419                );
     420
     421                $sanitized = $setting->sanitize( $unsanitized );
     422                $this->assertEqualSets( array_keys( $unsanitized ), array_keys( $sanitized ) );
     423
     424                $this->assertEquals( 0, $sanitized['object_id'] );
     425                $this->assertEquals( 'bhellob', $sanitized['object'] );
     426                $this->assertEquals( 0, $sanitized['menu_item_parent'] );
     427                $this->assertEquals( 0, $sanitized['position'] );
     428                $this->assertEquals( 'customb', $sanitized['type'] );
     429                $this->assertEquals( 'Hi', $sanitized['title'] );
     430                $this->assertEquals( '', $sanitized['url'] );
     431                $this->assertEquals( 'onclick', $sanitized['target'] );
     432                $this->assertEquals( 'evil', $sanitized['attr_title'] );
     433                $this->assertEquals( 'Hello world', $sanitized['description'] );
     434                $this->assertEquals( 'hello  inject', $sanitized['classes'] );
     435                $this->assertEquals( 'hello  inject', $sanitized['xfn'] );
     436                $this->assertEquals( 'publish', $sanitized['status'] );
     437                $this->assertEquals( 'Hi', $sanitized['original_title'] );
     438                $this->assertEquals( 0, $sanitized['nav_menu_term_id'] );
     439        }
     440
     441        /**
     442         * Test protected update() method via the save() method, for updated menu.
     443         *
     444         * @see WP_Customize_Nav_Menu_Item_Setting::update()
     445         */
     446        function test_save_updated() {
     447                do_action( 'customize_register', $this->wp_customize );
     448
     449                $first_post_id = $this->factory->post->create( array( 'post_title' => 'Hello World' ) );
     450                $second_post_id = $this->factory->post->create( array( 'post_title' => 'Hola Muno' ) );
     451
     452                $primary_menu_id = wp_create_nav_menu( 'Primary' );
     453                $secondary_menu_id = wp_create_nav_menu( 'Secondary' );
     454                $item_title = 'Greetings';
     455                $item_id = wp_update_nav_menu_item( $primary_menu_id, 0, array(
     456                        'menu-item-type' => 'post_type',
     457                        'menu-item-object' => 'post',
     458                        'menu-item-object-id' => $first_post_id,
     459                        'menu-item-title' => $item_title,
     460                        'menu-item-status' => 'publish',
     461                ) );
     462                $this->assertNotEmpty( wp_get_nav_menu_items( $primary_menu_id, array( 'post_status' => 'publish,draft' ) ) );
     463
     464                $post_value = array(
     465                        'type' => 'post_type',
     466                        'object' => 'post',
     467                        'object_id' => $second_post_id,
     468                        'title' => 'Saludos',
     469                        'status' => 'publish',
     470                        'nav_menu_term_id' => $secondary_menu_id,
     471                );
     472                $setting_id = "nav_menu_item[$item_id]";
     473                $setting = new WP_Customize_Nav_Menu_Item_Setting( $this->wp_customize, $setting_id );
     474                $this->wp_customize->set_post_value( $setting_id, $post_value );
     475                unset( $post_value['nav_menu_term_id'] );
     476                $setting->save();
     477
     478                // Make sure the menu item appears in the new menu.
     479                $this->assertNotContains( $item_id, wp_list_pluck( wp_get_nav_menu_items( $primary_menu_id ), 'db_id' ) );
     480                $menu_items = wp_get_nav_menu_items( $secondary_menu_id );
     481                $db_ids = wp_list_pluck( $menu_items, 'db_id' );
     482                $this->assertContains( $item_id, $db_ids );
     483                $i = array_search( $item_id, $db_ids );
     484                $updated_item = $menu_items[ $i ];
     485                $post_value['post_status'] = $post_value['status'];
     486                unset( $post_value['status'] );
     487                foreach ( $post_value as $key => $value ) {
     488                        $this->assertEquals( $value, $updated_item->$key, "Key $key mismatch" );
     489                }
     490
     491                // Verify the Ajax responses is being amended.
     492                $save_response = apply_filters( 'customize_save_response', array() );
     493                $this->assertArrayHasKey( 'nav_menu_item_updates', $save_response );
     494                $update_result = array_shift( $save_response['nav_menu_item_updates'] );
     495                $this->assertArrayHasKey( 'post_id', $update_result );
     496                $this->assertArrayHasKey( 'previous_post_id', $update_result );
     497                $this->assertArrayHasKey( 'error', $update_result );
     498                $this->assertArrayHasKey( 'status', $update_result );
     499
     500                $this->assertEquals( $item_id, $update_result['post_id'] );
     501                $this->assertNull( $update_result['previous_post_id'] );
     502                $this->assertNull( $update_result['error'] );
     503                $this->assertEquals( 'updated', $update_result['status'] );
     504        }
     505
     506        /**
     507         * Test protected update() method via the save() method, for inserted menu.
     508         *
     509         * @see WP_Customize_Nav_Menu_Item_Setting::update()
     510         */
     511        function test_save_inserted() {
     512                do_action( 'customize_register', $this->wp_customize );
     513
     514                $menu_id = wp_create_nav_menu( 'Primary' );
     515                $post_id = $this->factory->post->create( array( 'post_title' => 'Hello World' ) );
     516                $item_ids = array();
     517                for ( $i = 0; $i < 5; $i += 1 ) {
     518                        $item_id = wp_update_nav_menu_item( $menu_id, 0, array(
     519                                'menu-item-type' => 'post_type',
     520                                'menu-item-object' => 'post',
     521                                'menu-item-object-id' => $post_id,
     522                                'menu-item-title' => "Item $i",
     523                                'menu-item-status' => 'publish',
     524                                'menu-item-position' => $i + 1,
     525                        ) );
     526                        $item_ids[] = $item_id;
     527                }
     528
     529                $post_value = array(
     530                        'type' => 'post_type',
     531                        'object' => 'post',
     532                        'object_id' => $post_id,
     533                        'title' => 'Inserted item',
     534                        'status' => 'publish',
     535                        'nav_menu_term_id' => $menu_id,
     536                        'position' => count( $item_ids ) + 1,
     537                );
     538
     539                $new_item_id = -10;
     540                $setting_id = "nav_menu_item[$new_item_id]";
     541                $setting = new WP_Customize_Nav_Menu_Item_Setting( $this->wp_customize, $setting_id );
     542                $this->wp_customize->set_post_value( $setting_id, $post_value );
     543                unset( $post_value['nav_menu_term_id'] );
     544
     545                $current_items = wp_get_nav_menu_items( $menu_id );
     546                $setting->save();
     547                $preview_items = wp_get_nav_menu_items( $menu_id );
     548                $this->assertNotEquals( count( $current_items ), count( $preview_items ) );
     549
     550                $last_item = array_pop( $preview_items );
     551                $this->assertEquals( $setting->post_id, $last_item->db_id );
     552                $post_value['post_status'] = $post_value['status'];
     553                unset( $post_value['status'] );
     554                $post_value['menu_order'] = $post_value['position'];
     555                unset( $post_value['position'] );
     556                foreach ( $post_value as $key => $value ) {
     557                        $this->assertEquals( $value, $last_item->$key, "Mismatch for $key property." );
     558                }
     559
     560                // Verify the Ajax responses is being amended.
     561                $save_response = apply_filters( 'customize_save_response', array() );
     562                $this->assertArrayHasKey( 'nav_menu_item_updates', $save_response );
     563                $update_result = array_shift( $save_response['nav_menu_item_updates'] );
     564                $this->assertArrayHasKey( 'post_id', $update_result );
     565                $this->assertArrayHasKey( 'previous_post_id', $update_result );
     566                $this->assertArrayHasKey( 'error', $update_result );
     567                $this->assertArrayHasKey( 'status', $update_result );
     568
     569                $this->assertEquals( $setting->post_id, $update_result['post_id'] );
     570                $this->assertEquals( $new_item_id, $update_result['previous_post_id'] );
     571                $this->assertNull( $update_result['error'] );
     572                $this->assertEquals( 'inserted', $update_result['status'] );
     573        }
     574
     575        /**
     576         * Test protected update() method via the save() method, for deleted menu.
     577         *
     578         * @see WP_Customize_Nav_Menu_Item_Setting::update()
     579         */
     580        function test_save_deleted() {
     581                do_action( 'customize_register', $this->wp_customize );
     582
     583                $menu_id = wp_create_nav_menu( 'Primary' );
     584                $post_id = $this->factory->post->create( array( 'post_title' => 'Hello World' ) );
     585                $item_ids = array();
     586                for ( $i = 0; $i < 5; $i += 1 ) {
     587                        $item_id = wp_update_nav_menu_item( $menu_id, 0, array(
     588                                'menu-item-type' => 'post_type',
     589                                'menu-item-object' => 'post',
     590                                'menu-item-object-id' => $post_id,
     591                                'menu-item-title' => "Item $i",
     592                                'menu-item-status' => 'publish',
     593                                'menu-item-position' => $i + 1,
     594                        ) );
     595                        $item_ids[] = $item_id;
     596                }
     597
     598                $delete_item_id = $item_ids[2];
     599                $setting_id = "nav_menu_item[$delete_item_id]";
     600                $setting = new WP_Customize_Nav_Menu_Item_Setting( $this->wp_customize, $setting_id );
     601                $this->wp_customize->set_post_value( $setting_id, false );
     602
     603                $current_items = wp_get_nav_menu_items( $menu_id );
     604                $this->assertContains( $delete_item_id, wp_list_pluck( $current_items, 'db_id' ) );
     605                $setting->save();
     606                $preview_items = wp_get_nav_menu_items( $menu_id );
     607                $this->assertNotEquals( count( $current_items ), count( $preview_items ) );
     608                $this->assertContains( $delete_item_id, wp_list_pluck( $current_items, 'db_id' ) );
     609
     610                // Verify the Ajax responses is being amended.
     611                $save_response = apply_filters( 'customize_save_response', array() );
     612                $this->assertArrayHasKey( 'nav_menu_item_updates', $save_response );
     613                $update_result = array_shift( $save_response['nav_menu_item_updates'] );
     614                $this->assertArrayHasKey( 'post_id', $update_result );
     615                $this->assertArrayHasKey( 'previous_post_id', $update_result );
     616                $this->assertArrayHasKey( 'error', $update_result );
     617                $this->assertArrayHasKey( 'status', $update_result );
     618
     619                $this->assertEquals( $delete_item_id, $update_result['post_id'] );
     620                $this->assertNull( $update_result['previous_post_id'] );
     621                $this->assertNull( $update_result['error'] );
     622                $this->assertEquals( 'deleted', $update_result['status'] );
     623        }
     624
     625}
  • tests/phpunit/tests/customize/nav-menu-setting.php

     
     1<?php
     2
     3/**
     4 * Tests WP_Customize_Nav_Menu_Setting.
     5 *
     6 * @group customize
     7 */
     8class Test_WP_Customize_Nav_Menu_Setting extends WP_UnitTestCase {
     9
     10        /**
     11         * Instance of WP_Customize_Manager which is reset for each test.
     12         *
     13         * @var WP_Customize_Manager
     14         */
     15        public $wp_customize;
     16
     17        /**
     18         * Set up a test case.
     19         *
     20         * @see WP_UnitTestCase::setup()
     21         */
     22        function setUp() {
     23                parent::setUp();
     24                require_once ABSPATH . WPINC . '/class-wp-customize-manager.php';
     25                wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );
     26
     27                global $wp_customize;
     28                $this->wp_customize = new WP_Customize_Manager();
     29                $wp_customize = $this->wp_customize;
     30        }
     31
     32        /**
     33         * Delete the $wp_customize global when cleaning up scope.
     34         */
     35        function clean_up_global_scope() {
     36                global $wp_customize;
     37                $wp_customize = null;
     38                parent::clean_up_global_scope();
     39        }
     40
     41        /**
     42         * Helper for getting the nav_menu_options option.
     43         *
     44         * @return array
     45         */
     46        function get_nav_menu_items_option() {
     47                return get_option( 'nav_menu_options', array( 'auto_add' => array() ) );
     48        }
     49
     50        /**
     51         * Test constants and statics.
     52         */
     53        function test_constants() {
     54                do_action( 'customize_register', $this->wp_customize );
     55                $this->assertTrue( taxonomy_exists( WP_Customize_Nav_Menu_Setting::TAXONOMY ) );
     56        }
     57
     58        /**
     59         * Test constructor.
     60         *
     61         * @see WP_Customize_Nav_Menu_Setting::__construct()
     62         */
     63        function test_construct() {
     64                do_action( 'customize_register', $this->wp_customize );
     65
     66                $setting = new WP_Customize_Nav_Menu_Setting( $this->wp_customize, 'nav_menu[123]' );
     67                $this->assertEquals( 'nav_menu', $setting->type );
     68                $this->assertEquals( 'postMessage', $setting->transport );
     69                $this->assertEquals( 123, $setting->term_id );
     70                $this->assertNull( $setting->previous_term_id );
     71                $this->assertNull( $setting->update_status );
     72                $this->assertNull( $setting->update_error );
     73                $this->assertInternalType( 'array', $setting->default );
     74                foreach ( array( 'name', 'description', 'parent' ) as $key ) {
     75                        $this->assertArrayHasKey( $key, $setting->default );
     76                }
     77                $this->assertEquals( '', $setting->default['name'] );
     78                $this->assertEquals( '', $setting->default['description'] );
     79                $this->assertEquals( 0, $setting->default['parent'] );
     80
     81                $exception = null;
     82                try {
     83                        $bad_setting = new WP_Customize_Nav_Menu_Setting( $this->wp_customize, 'foo_bar_baz' );
     84                        unset( $bad_setting );
     85                } catch ( Exception $e ) {
     86                        $exception = $e;
     87                }
     88                $this->assertInstanceOf( 'Exception', $exception );
     89        }
     90
     91        /**
     92         * Test empty constructor.
     93         */
     94        function test_construct_empty_menus() {
     95                do_action( 'customize_register', $this->wp_customize );
     96                $_wp_customize = $this->wp_customize;
     97                unset($_wp_customize->nav_menus);
     98
     99                $exception = null;
     100                try {
     101                        $bad_setting = new WP_Customize_Nav_Menu_Setting( $_wp_customize, 'nav_menu_item[123]' );
     102                        unset( $bad_setting );
     103                } catch ( Exception $e ) {
     104                        $exception = $e;
     105                }
     106                $this->assertInstanceOf( 'Exception', $exception );
     107        }
     108
     109        /**
     110         * Test constructor for placeholder (draft) menu.
     111         *
     112         * @see WP_Customize_Nav_Menu_Setting::__construct()
     113         */
     114        function test_construct_placeholder() {
     115                do_action( 'customize_register', $this->wp_customize );
     116                $default = array(
     117                        'name' => 'Lorem',
     118                        'description' => 'ipsum',
     119                        'parent' => 123,
     120                );
     121                $setting = new WP_Customize_Nav_Menu_Setting( $this->wp_customize, 'nav_menu[-5]', compact( 'default' ) );
     122                $this->assertEquals( -5, $setting->term_id );
     123                $this->assertEquals( $default, $setting->default );
     124        }
     125
     126        /**
     127         * Test value method.
     128         *
     129         * @see WP_Customize_Nav_Menu_Setting::value()
     130         */
     131        function test_value() {
     132                do_action( 'customize_register', $this->wp_customize );
     133
     134                $menu_name = 'Test 123';
     135                $parent_menu_id = wp_create_nav_menu( "Parent $menu_name" );
     136                $description = 'Hello my world.';
     137                $menu_id = wp_update_nav_menu_object( 0, array(
     138                        'menu-name' => $menu_name,
     139                        'parent' => $parent_menu_id,
     140                        'description' => $description,
     141                ) );
     142
     143                $setting_id = "nav_menu[$menu_id]";
     144                $setting = new WP_Customize_Nav_Menu_Setting( $this->wp_customize, $setting_id );
     145
     146                $value = $setting->value();
     147                $this->assertInternalType( 'array', $value );
     148                foreach ( array( 'name', 'description', 'parent' ) as $key ) {
     149                        $this->assertArrayHasKey( $key, $value );
     150                }
     151                $this->assertEquals( $menu_name, $value['name'] );
     152                $this->assertEquals( $description, $value['description'] );
     153                $this->assertEquals( $parent_menu_id, $value['parent'] );
     154
     155                $new_menu_name = 'Foo';
     156                wp_update_nav_menu_object( $menu_id, array( 'menu-name' => $new_menu_name ) );
     157                $updated_value = $setting->value();
     158                $this->assertEquals( $new_menu_name, $updated_value['name'] );
     159        }
     160
     161        /**
     162         * Test preview method for updated menu.
     163         *
     164         * @see WP_Customize_Nav_Menu_Setting::preview()
     165         */
     166        function test_preview_updated() {
     167                do_action( 'customize_register', $this->wp_customize );
     168
     169                $menu_id = wp_update_nav_menu_object( 0, array(
     170                        'menu-name' => 'Name 1',
     171                        'description' => 'Description 1',
     172                        'parent' => 0,
     173                ) );
     174                $setting_id = "nav_menu[$menu_id]";
     175                $setting = new WP_Customize_Nav_Menu_Setting( $this->wp_customize, $setting_id );
     176
     177                $nav_menu_options = $this->get_nav_menu_items_option();
     178                $this->assertNotContains( $menu_id, $nav_menu_options['auto_add'] );
     179
     180                $post_value = array(
     181                        'name' => 'Name 2',
     182                        'description' => 'Description 2',
     183                        'parent' => 1,
     184                        'auto_add' => true,
     185                );
     186                $this->wp_customize->set_post_value( $setting_id, $post_value );
     187
     188                $value = $setting->value();
     189                $this->assertEquals( 'Name 1', $value['name'] );
     190                $this->assertEquals( 'Description 1', $value['description'] );
     191                $this->assertEquals( 0, $value['parent'] );
     192
     193                $term = (array) wp_get_nav_menu_object( $menu_id );
     194
     195                $this->assertEqualSets(
     196                        wp_array_slice_assoc( $value, array( 'name', 'description', 'parent' ) ),
     197                        wp_array_slice_assoc( $term, array( 'name', 'description', 'parent' ) )
     198                );
     199
     200                $setting->preview();
     201                $value = $setting->value();
     202                $this->assertEquals( 'Name 2', $value['name'] );
     203                $this->assertEquals( 'Description 2', $value['description'] );
     204                $this->assertEquals( 1, $value['parent'] );
     205                $term = (array) wp_get_nav_menu_object( $menu_id );
     206                $this->assertEqualSets( $value, wp_array_slice_assoc( $term, array_keys( $value ) ) );
     207
     208                $menu_object = wp_get_nav_menu_object( $menu_id );
     209                $this->assertEquals( (object) $term, $menu_object );
     210                $this->assertEquals( $post_value['name'], $menu_object->name );
     211
     212                $nav_menu_options = get_option( 'nav_menu_options', array( 'auto_add' => array() ) );
     213                $this->assertContains( $menu_id, $nav_menu_options['auto_add'] );
     214        }
     215
     216        /**
     217         * Test preview method for inserted menu.
     218         *
     219         * @see WP_Customize_Nav_Menu_Setting::preview()
     220         */
     221        function test_preview_inserted() {
     222                do_action( 'customize_register', $this->wp_customize );
     223
     224                $menu_id = -123;
     225                $post_value = array(
     226                        'name' => 'New Menu Name 1',
     227                        'description' => 'New Menu Description 1',
     228                        'parent' => 0,
     229                        'auto_add' => false,
     230                );
     231                $setting_id = "nav_menu[$menu_id]";
     232                $setting = new WP_Customize_Nav_Menu_Setting( $this->wp_customize, $setting_id );
     233
     234                $this->wp_customize->set_post_value( $setting->id, $post_value );
     235                $setting->preview();
     236                $value = $setting->value();
     237                $this->assertEquals( $post_value, $value );
     238
     239                $term = (array) wp_get_nav_menu_object( $menu_id );
     240                $this->assertNotEmpty( $term );
     241                $this->assertNotInstanceOf( 'WP_Error', $term );
     242                $this->assertEqualSets( $post_value, wp_array_slice_assoc( $term, array_keys( $value ) ) );
     243                $this->assertEquals( $menu_id, $term['term_id'] );
     244                $this->assertEquals( $menu_id, $term['term_taxonomy_id'] );
     245
     246                $menu_object = wp_get_nav_menu_object( $menu_id );
     247                $this->assertEquals( (object) $term, $menu_object );
     248                $this->assertEquals( $post_value['name'], $menu_object->name );
     249
     250                $nav_menu_options = $this->get_nav_menu_items_option();
     251                $this->assertNotContains( $menu_id, $nav_menu_options['auto_add'] );
     252        }
     253
     254        /**
     255         * Test preview method for deleted menu.
     256         *
     257         * @see WP_Customize_Nav_Menu_Setting::preview()
     258         */
     259        function test_preview_deleted() {
     260                do_action( 'customize_register', $this->wp_customize );
     261
     262                $menu_id = wp_update_nav_menu_object( 0, array(
     263                        'menu-name' => 'Name 1',
     264                        'description' => 'Description 1',
     265                        'parent' => 0,
     266                ) );
     267                $setting_id = "nav_menu[$menu_id]";
     268                $setting = new WP_Customize_Nav_Menu_Setting( $this->wp_customize, $setting_id );
     269                $nav_menu_options = $this->get_nav_menu_items_option();
     270                $nav_menu_options['auto_add'][] = $menu_id;
     271                update_option( 'nav_menu_options', $nav_menu_options );
     272
     273                $nav_menu_options = $this->get_nav_menu_items_option();
     274                $this->assertContains( $menu_id, $nav_menu_options['auto_add'] );
     275
     276                $this->wp_customize->set_post_value( $setting_id, false );
     277
     278                $this->assertInternalType( 'array', $setting->value() );
     279                $this->assertInternalType( 'object', wp_get_nav_menu_object( $menu_id ) );
     280                $setting->preview();
     281                $this->assertFalse( $setting->value() );
     282                $this->assertFalse( wp_get_nav_menu_object( $menu_id ) );
     283
     284                $nav_menu_options = $this->get_nav_menu_items_option();
     285                $this->assertNotContains( $menu_id, $nav_menu_options['auto_add'] );
     286        }
     287
     288        /**
     289         * Test sanitize method.
     290         *
     291         * @see WP_Customize_Nav_Menu_Setting::sanitize()
     292         */
     293        function test_sanitize() {
     294                do_action( 'customize_register', $this->wp_customize );
     295                $setting = new WP_Customize_Nav_Menu_Setting( $this->wp_customize, 'nav_menu[123]' );
     296
     297                $this->assertNull( $setting->sanitize( 'not an array' ) );
     298                $this->assertNull( $setting->sanitize( 123 ) );
     299
     300                $value = array(
     301                        'name' => ' Hello <b>world</b> ',
     302                        'description' => "New\nline",
     303                        'parent' => -12,
     304                        'auto_add' => true,
     305                        'extra' => 'ignored',
     306                );
     307                $sanitized = $setting->sanitize( $value );
     308                $this->assertEquals( 'Hello &lt;b&gt;world&lt;/b&gt;', $sanitized['name'] );
     309                $this->assertEquals( 'New line', $sanitized['description'] );
     310                $this->assertEquals( 0, $sanitized['parent'] );
     311                $this->assertEquals( true, $sanitized['auto_add'] );
     312                $this->assertEqualSets( array( 'name', 'description', 'parent', 'auto_add' ), array_keys( $sanitized ) );
     313        }
     314
     315        /**
     316         * Test protected update() method via the save() method, for updated menu.
     317         *
     318         * @see WP_Customize_Nav_Menu_Setting::update()
     319         */
     320        function test_save_updated() {
     321                do_action( 'customize_register', $this->wp_customize );
     322
     323                $menu_id = wp_update_nav_menu_object( 0, array(
     324                        'menu-name' => 'Name 1',
     325                        'description' => 'Description 1',
     326                        'parent' => 0,
     327                ) );
     328                $nav_menu_options = $this->get_nav_menu_items_option();
     329                $nav_menu_options['auto_add'][] = $menu_id;
     330                update_option( 'nav_menu_options', $nav_menu_options );
     331
     332                $setting_id = "nav_menu[$menu_id]";
     333                $setting = new WP_Customize_Nav_Menu_Setting( $this->wp_customize, $setting_id );
     334
     335                $auto_add = false;
     336                $new_value = array(
     337                        'name' => 'Name 2',
     338                        'description' => 'Description 2',
     339                        'parent' => 1,
     340                        'auto_add' => $auto_add,
     341                );
     342
     343                $this->wp_customize->set_post_value( $setting_id, $new_value );
     344                $setting->save();
     345
     346                $menu_object = wp_get_nav_menu_object( $menu_id );
     347                foreach ( array( 'name', 'description', 'parent' ) as $key ) {
     348                        $this->assertEquals( $new_value[ $key ], $menu_object->$key );
     349                }
     350                $this->assertEqualSets(
     351                        wp_array_slice_assoc( $new_value, array( 'name', 'description', 'parent' ) ),
     352                        wp_array_slice_assoc( (array) $menu_object, array( 'name', 'description', 'parent' ) )
     353                );
     354                $this->assertEquals( $new_value, $setting->value() );
     355
     356                $save_response = apply_filters( 'customize_save_response', array() );
     357                $this->assertArrayHasKey( 'nav_menu_updates', $save_response );
     358                $update_result = array_shift( $save_response['nav_menu_updates'] );
     359                $this->assertArrayHasKey( 'term_id', $update_result );
     360                $this->assertArrayHasKey( 'previous_term_id', $update_result );
     361                $this->assertArrayHasKey( 'error', $update_result );
     362                $this->assertArrayHasKey( 'status', $update_result );
     363
     364                $this->assertEquals( $menu_id, $update_result['term_id'] );
     365                $this->assertNull( $update_result['previous_term_id'] );
     366                $this->assertNull( $update_result['error'] );
     367                $this->assertEquals( 'updated', $update_result['status'] );
     368
     369                $nav_menu_options = $this->get_nav_menu_items_option();
     370                $this->assertNotContains( $menu_id, $nav_menu_options['auto_add'] );
     371        }
     372
     373        /**
     374         * Test protected update() method via the save() method, for inserted menu.
     375         *
     376         * @see WP_Customize_Nav_Menu_Setting::update()
     377         */
     378        function test_save_inserted() {
     379                do_action( 'customize_register', $this->wp_customize );
     380
     381                $menu_id = -123;
     382                $post_value = array(
     383                        'name' => 'New Menu Name 1',
     384                        'description' => 'New Menu Description 1',
     385                        'parent' => 0,
     386                        'auto_add' => true,
     387                );
     388                $setting_id = "nav_menu[$menu_id]";
     389                $setting = new WP_Customize_Nav_Menu_Setting( $this->wp_customize, $setting_id );
     390
     391                $this->wp_customize->set_post_value( $setting->id, $post_value );
     392
     393                $this->assertNull( $setting->previous_term_id );
     394                $this->assertLessThan( 0, $setting->term_id );
     395                $setting->save();
     396                $this->assertEquals( $menu_id, $setting->previous_term_id );
     397                $this->assertGreaterThan( 0, $setting->term_id );
     398
     399                $nav_menu_options = $this->get_nav_menu_items_option();
     400                $this->assertContains( $setting->term_id, $nav_menu_options['auto_add'] );
     401
     402                $menu = wp_get_nav_menu_object( $setting->term_id );
     403                unset( $post_value['auto_add'] );
     404                $this->assertEqualSets( $post_value, wp_array_slice_assoc( (array) $menu, array_keys( $post_value ) ) );
     405
     406                $save_response = apply_filters( 'customize_save_response', array() );
     407                $this->assertArrayHasKey( 'nav_menu_updates', $save_response );
     408                $update_result = array_shift( $save_response['nav_menu_updates'] );
     409                $this->assertArrayHasKey( 'term_id', $update_result );
     410                $this->assertArrayHasKey( 'previous_term_id', $update_result );
     411                $this->assertArrayHasKey( 'error', $update_result );
     412                $this->assertArrayHasKey( 'status', $update_result );
     413
     414                $this->assertEquals( $menu->term_id, $update_result['term_id'] );
     415                $this->assertEquals( $menu_id, $update_result['previous_term_id'] );
     416                $this->assertNull( $update_result['error'] );
     417                $this->assertEquals( 'inserted', $update_result['status'] );
     418        }
     419
     420        /**
     421         * Test protected update() method via the save() method, for deleted menu.
     422         *
     423         * @see WP_Customize_Nav_Menu_Setting::update()
     424         */
     425        function test_save_deleted() {
     426                do_action( 'customize_register', $this->wp_customize );
     427
     428                $menu_name = 'Lorem Ipsum';
     429                $menu_id = wp_create_nav_menu( $menu_name );
     430                $setting_id = "nav_menu[$menu_id]";
     431                $setting = new WP_Customize_Nav_Menu_Setting( $this->wp_customize, $setting_id );
     432                $nav_menu_options = $this->get_nav_menu_items_option();
     433                $nav_menu_options['auto_add'][] = $menu_id;
     434                update_option( 'nav_menu_options', $nav_menu_options );
     435
     436                $menu = wp_get_nav_menu_object( $menu_id );
     437                $this->assertEquals( $menu_name, $menu->name );
     438
     439                $this->wp_customize->set_post_value( $setting_id, false );
     440                $setting->save();
     441
     442                $this->assertFalse( wp_get_nav_menu_object( $menu_id ) );
     443
     444                $save_response = apply_filters( 'customize_save_response', array() );
     445                $this->assertArrayHasKey( 'nav_menu_updates', $save_response );
     446                $update_result = array_shift( $save_response['nav_menu_updates'] );
     447                $this->assertArrayHasKey( 'term_id', $update_result );
     448                $this->assertArrayHasKey( 'previous_term_id', $update_result );
     449                $this->assertArrayHasKey( 'error', $update_result );
     450                $this->assertArrayHasKey( 'status', $update_result );
     451
     452                $this->assertEquals( $menu_id, $update_result['term_id'] );
     453                $this->assertNull( $update_result['previous_term_id'] );
     454                $this->assertNull( $update_result['error'] );
     455                $this->assertEquals( 'deleted', $update_result['status'] );
     456
     457                $nav_menu_options = $this->get_nav_menu_items_option();
     458                $this->assertNotContains( $menu_id, $nav_menu_options['auto_add'] );
     459        }
     460
     461}
  • tests/phpunit/tests/customize/nav-menus.php

     
     1<?php
     2
     3/**
     4 * Tests WP_Customize_Nav_Menus.
     5 *
     6 * @group customize
     7 */
     8class Test_WP_Customize_Nav_Menus extends WP_UnitTestCase {
     9
     10        /**
     11         * Instance of WP_Customize_Manager which is reset for each test.
     12         *
     13         * @var WP_Customize_Manager
     14         */
     15        public $wp_customize;
     16
     17        /**
     18         * Set up a test case.
     19         *
     20         * @see WP_UnitTestCase::setup()
     21         */
     22        function setUp() {
     23                parent::setUp();
     24                require_once ABSPATH . WPINC . '/class-wp-customize-manager.php';
     25                wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );
     26                global $wp_customize;
     27                $this->wp_customize = new WP_Customize_Manager();
     28                $wp_customize = $this->wp_customize;
     29        }
     30
     31        /**
     32         * Delete the $wp_customize global when cleaning up scope.
     33         */
     34        function clean_up_global_scope() {
     35                global $wp_customize;
     36                $wp_customize = null;
     37                parent::clean_up_global_scope();
     38        }
     39
     40        /**
     41         * Test constructor.
     42         *
     43         * @see WP_Customize_Nav_Menus::__construct()
     44         */
     45        function test_construct() {
     46                do_action( 'customize_register', $this->wp_customize );
     47                $menus = new WP_Customize_Nav_Menus( $this->wp_customize );
     48                $this->assertInstanceOf( 'WP_Customize_Manager', $menus->manager );
     49        }
     50
     51        /**
     52         * Test the test_load_available_items_ajax method.
     53         *
     54         * @see WP_Customize_Nav_Menus::load_available_items_ajax()
     55         */
     56        function test_load_available_items_ajax() {
     57
     58                $this->markTestIncomplete( 'This test has not been implemented.' );
     59
     60        }
     61
     62        /**
     63         * Test the search_available_items_ajax method.
     64         *
     65         * @see WP_Customize_Nav_Menus::search_available_items_ajax()
     66         */
     67        function test_search_available_items_ajax() {
     68
     69                $this->markTestIncomplete( 'This test has not been implemented.' );
     70
     71        }
     72
     73        /**
     74         * Test the search_available_items_query method.
     75         *
     76         * @see WP_Customize_Nav_Menus::search_available_items_query()
     77         */
     78        function test_search_available_items_query() {
     79                $menus = new WP_Customize_Nav_Menus( $this->wp_customize );
     80
     81                // Create posts
     82                $post_ids = array();
     83                $post_ids[] = $this->factory->post->create( array( 'post_title' => 'Search & Test' ) );
     84                $post_ids[] = $this->factory->post->create( array( 'post_title' => 'Some Other Title' ) );
     85
     86                // Create terms
     87                $term_ids = array();
     88                $term_ids[] = $this->factory->category->create( array( 'name' => 'Dogs Are Cool' ) );
     89                $term_ids[] = $this->factory->category->create( array( 'name' => 'Cats Drool' ) );
     90
     91                // Test empty results
     92                $expected = array();
     93                $results = $menus->search_available_items_query( array( 'pagenum' => 1, 's' => 'This Does NOT Exist' ) );
     94                $this->assertEquals( $expected, $results );
     95
     96                // Test posts
     97                foreach ( $post_ids as $post_id ) {
     98                        $expected = array(
     99                                'id'         => 'post-' . $post_id,
     100                                'type'       => 'post_type',
     101                                'type_label' => get_post_type_object( 'post' )->labels->singular_name,
     102                                'object'     => 'post',
     103                                'object_id'  => intval( $post_id ),
     104                                'title'      => html_entity_decode( get_the_title( $post_id ) ),
     105                        );
     106                        wp_set_object_terms( $post_id, $term_ids, 'category' );
     107                        $search = $post_id === $post_ids[0] ? 'test & search' : 'other title';
     108                        $s = sanitize_text_field( wp_unslash( $search ) );
     109                        $results = $menus->search_available_items_query( array( 'pagenum' => 1, 's' => $s ) );
     110                        $this->assertEquals( $expected, $results[0] );
     111                }
     112
     113                // Test terms
     114                foreach ( $term_ids as $term_id ) {
     115                        $term = get_term_by( 'id', $term_id, 'category' );
     116                        $expected = array(
     117                                'id'         => 'term-' . $term_id,
     118                                'type'       => 'taxonomy',
     119                                'type_label' => get_taxonomy( 'category' )->labels->singular_name,
     120                                'object'     => 'category',
     121                                'object_id'  => intval( $term_id ),
     122                                'title'      => $term->name,
     123                        );
     124                        $s = sanitize_text_field( wp_unslash( $term->name ) );
     125                        $results = $menus->search_available_items_query( array( 'pagenum' => 1, 's' => $s ) );
     126                        $this->assertEquals( $expected, $results[0] );
     127                }
     128        }
     129
     130        /**
     131         * Test the enqueue method.
     132         *
     133         * @see WP_Customize_Nav_Menus::enqueue_scripts()
     134         */
     135        function test_enqueue_scripts() {
     136                do_action( 'customize_register', $this->wp_customize );
     137                $menus = new WP_Customize_Nav_Menus( $this->wp_customize );
     138                $menus->enqueue_scripts();
     139                $this->assertTrue( wp_script_is( 'customize-nav-menus' ) );
     140        }
     141
     142        /**
     143         * Test the filter_dynamic_setting_args method.
     144         *
     145         * @see WP_Customize_Nav_Menus::filter_dynamic_setting_args()
     146         */
     147        function test_filter_dynamic_setting_args() {
     148                $menus = new WP_Customize_Nav_Menus( $this->wp_customize );
     149
     150                $expected = array( 'type' => 'nav_menu_item' );
     151                $results = $menus->filter_dynamic_setting_args( $this->wp_customize, 'nav_menu_item[123]' );
     152                $this->assertEquals( $expected, $results );
     153
     154                $expected = array( 'type' => 'nav_menu' );
     155                $results = $menus->filter_dynamic_setting_args( $this->wp_customize, 'nav_menu[123]' );
     156                $this->assertEquals( $expected, $results );
     157        }
     158
     159        /**
     160         * Test the filter_dynamic_setting_class method.
     161         *
     162         * @see WP_Customize_Nav_Menus::filter_dynamic_setting_class()
     163         */
     164        function test_filter_dynamic_setting_class() {
     165                do_action( 'customize_register', $this->wp_customize );
     166                $menus = new WP_Customize_Nav_Menus( $this->wp_customize );
     167
     168                $expected = 'WP_Customize_Nav_Menu_Item_Setting';
     169                $results = $menus->filter_dynamic_setting_class( 'WP_Customize_Setting', 'nav_menu_item[123]', array( 'type' => 'nav_menu_item' ) );
     170                $this->assertEquals( $expected, $results );
     171
     172                $expected = 'WP_Customize_Nav_Menu_Setting';
     173                $results = $menus->filter_dynamic_setting_class( 'WP_Customize_Setting', 'nav_menu[123]', array( 'type' => 'nav_menu' ) );
     174                $this->assertEquals( $expected, $results );
     175        }
     176
     177        /**
     178         * Test the customize_register method.
     179         *
     180         * @see WP_Customize_Nav_Menus::customize_register()
     181         */
     182        function test_customize_register() {
     183                do_action( 'customize_register', $this->wp_customize );
     184                $menu_id = wp_create_nav_menu( 'Primary' );
     185                $post_id = $this->factory->post->create( array( 'post_title' => 'Hello World' ) );
     186                $item_id = wp_update_nav_menu_item( $menu_id, 0, array(
     187                        'menu-item-type'      => 'post_type',
     188                        'menu-item-object'    => 'post',
     189                        'menu-item-object-id' => $post_id,
     190                        'menu-item-title'     => 'Hello World',
     191                        'menu-item-status'    => 'publish',
     192                ) );
     193                $setting = new WP_Customize_Nav_Menu_Item_Setting( $this->wp_customize, "nav_menu_item[$item_id]" );
     194                do_action( 'customize_register', $this->wp_customize );
     195                $this->assertEquals( 'Primary', $this->wp_customize->get_section( "nav_menu[$menu_id]" )->title );
     196                $this->assertEquals( 'Hello World', $this->wp_customize->get_control( "nav_menu_item[$item_id]" )->label );
     197        }
     198
     199        /**
     200         * Test the intval_base10 method.
     201         *
     202         * @see WP_Customize_Nav_Menus::intval_base10()
     203         */
     204        function test_intval_base10() {
     205
     206                $menus = new WP_Customize_Nav_Menus( $this->wp_customize );
     207
     208                $this->assertEquals( 2, $menus->intval_base10( 2 ) );
     209                $this->assertEquals( 4, $menus->intval_base10( 4.1 ) );
     210                $this->assertEquals( 4, $menus->intval_base10( '4' ) );
     211                $this->assertEquals( 4, $menus->intval_base10( '04' ) );
     212                $this->assertEquals( 42, $menus->intval_base10( +42 ) );
     213                $this->assertEquals( -42, $menus->intval_base10( -42 ) );
     214                $this->assertEquals( 26, $menus->intval_base10( 0x1A ) );
     215                $this->assertEquals( 0, $menus->intval_base10( array() ) );
     216        }
     217
     218        /**
     219         * Test the available_item_types method.
     220         *
     221         * @see WP_Customize_Nav_Menus::available_item_types()
     222         */
     223        function test_available_item_types() {
     224
     225                $menus = new WP_Customize_Nav_Menus( $this->wp_customize );
     226                $expected = array(
     227                        'postTypes' => array(
     228                                'post' => array( 'label' => 'Post' ),
     229                                'page' => array( 'label' => 'Page' ),
     230                        ),
     231                        'taxonomies' => array(
     232                                'category' => array( 'label' => 'Category' ),
     233                                'post_tag' => array( 'label' => 'Tag' ),
     234                        ),
     235                );
     236                if ( current_theme_supports( 'post-formats' ) ) {
     237                        $expected['taxonomies']['post_format'] = array( 'label' => 'Format' );
     238                }
     239                $this->assertEquals( $expected, $menus->available_item_types() );
     240
     241                register_taxonomy( 'wptests_tax', array( 'post' ), array( 'labels' => array( 'name' => 'Foo' ) ) );
     242                $expected = array(
     243                        'postTypes' => array(
     244                                'post' => array( 'label' => 'Post' ),
     245                                'page' => array( 'label' => 'Page' ),
     246                        ),
     247                        'taxonomies' => array(
     248                                'category'    => array( 'label' => 'Category' ),
     249                                'post_tag'    => array( 'label' => 'Tag' ),
     250                                'wptests_tax' => array( 'label' => 'Foo' ),
     251                        ),
     252                );
     253                if ( current_theme_supports( 'post-formats' ) ) {
     254                        $wptests_tax = array_pop( $expected['taxonomies'] );
     255                        $expected['taxonomies']['post_format'] = array( 'label' => 'Format' );
     256                        $expected['taxonomies']['wptests_tax'] = $wptests_tax;
     257                }
     258                $this->assertEquals( $expected, $menus->available_item_types() );
     259
     260        }
     261
     262        /**
     263         * Test the print_templates method.
     264         *
     265         * @see WP_Customize_Nav_Menus::print_templates()
     266         */
     267        function test_print_templates() {
     268                do_action( 'customize_register', $this->wp_customize );
     269                $menus = new WP_Customize_Nav_Menus( $this->wp_customize );
     270
     271                ob_start();
     272                $menus->print_templates();
     273                $template = ob_get_clean();
     274
     275                $expected = sprintf(
     276                        '<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>',
     277                        esc_html( 'Move up' ),
     278                        esc_html( 'Move down' ),
     279                        esc_html( 'Move one level up' ),
     280                        esc_html( 'Move one level down' )
     281                );
     282
     283                $this->assertContains( $expected, $template );
     284        }
     285
     286        /**
     287         * Test the available_items_template method.
     288         *
     289         * @see WP_Customize_Nav_Menus::available_items_template()
     290         */
     291        function test_available_items_template() {
     292                do_action( 'customize_register', $this->wp_customize );
     293                $menus = new WP_Customize_Nav_Menus( $this->wp_customize );
     294
     295                ob_start();
     296                $menus->available_items_template();
     297                $template = ob_get_clean();
     298
     299                $expected = sprintf( 'Customizing &#9656; %s', esc_html( $this->wp_customize->get_panel( 'nav_menus' )->title ) );
     300
     301                $this->assertContains( $expected, $template );
     302
     303                $post_types = get_post_types( array( 'show_in_nav_menus' => true ), 'object' );
     304                if ( $post_types ) {
     305                        foreach ( $post_types as $type ) {
     306                                $this->assertContains( 'available-menu-items-' . esc_attr( $type->name ), $template );
     307                                $this->assertContains( '<h4 class="accordion-section-title">' . esc_html( $type->label ), $template );
     308                                $this->assertContains( 'data-type="' . esc_attr( $type->name ) . '" data-obj_type="post_type"', $template );
     309                        }
     310                }
     311
     312                $taxonomies = get_taxonomies( array( 'show_in_nav_menus' => true ), 'object' );
     313                if ( $taxonomies ) {
     314                        foreach ( $taxonomies as $tax ) {
     315                                $this->assertContains( 'available-menu-items-' . esc_attr( $tax->name ), $template );
     316                                $this->assertContains( '<h4 class="accordion-section-title">' . esc_html( $tax->label ), $template );
     317                                $this->assertContains( 'data-type="' . esc_attr( $tax->name ) . '" data-obj_type="taxonomy"', $template );
     318                        }
     319                }
     320        }
     321
     322        /**
     323         * Test the customize_preview_init method.
     324         *
     325         * @see WP_Customize_Nav_Menus::customize_preview_init()
     326         */
     327        function test_customize_preview_init() {
     328                do_action( 'customize_register', $this->wp_customize );
     329                $menus = new WP_Customize_Nav_Menus( $this->wp_customize );
     330
     331                $menus->customize_preview_init();
     332                $this->assertEquals( 10, has_action( 'template_redirect', array( $menus, 'render_menu' ) ) );
     333                $this->assertEquals( 10, has_action( 'wp_enqueue_scripts', array( $menus, 'customize_preview_enqueue_deps' ) ) );
     334
     335                if ( ! isset( $_REQUEST[ WP_Customize_Nav_Menus::RENDER_QUERY_VAR ] ) ) {
     336                        $this->assertEquals( 1000, has_filter( 'wp_nav_menu_args', array( $menus, 'filter_wp_nav_menu_args' ) ) );
     337                        $this->assertEquals( 10, has_filter( 'wp_nav_menu', array( $menus, 'filter_wp_nav_menu' ) ) );
     338                }
     339        }
     340
     341        /**
     342         * Test the filter_wp_nav_menu_args method.
     343         *
     344         * @see WP_Customize_Nav_Menus::filter_wp_nav_menu_args()
     345         */
     346        function test_filter_wp_nav_menu_args() {
     347                do_action( 'customize_register', $this->wp_customize );
     348                $menus = new WP_Customize_Nav_Menus( $this->wp_customize );
     349
     350                $results = $menus->filter_wp_nav_menu_args( array(
     351                        'echo'            => true,
     352                        'fallback_cb'     => 'wp_page_menu',
     353                        'walker'          => '',
     354                ) );
     355                $this->assertEquals( 1, $results['can_partial_refresh'] );
     356
     357                $expected = array(
     358                        'echo',
     359                        'args_hash',
     360                        'can_partial_refresh',
     361                        'instance_number',
     362                );
     363                $results = $menus->filter_wp_nav_menu_args( array(
     364                        'echo'            => false,
     365                        'fallback_cb'     => 'wp_page_menu',
     366                        'walker'          => new Walker_Nav_Menu(),
     367                ) );
     368                $this->assertEqualSets( $expected, array_keys( $results ) );
     369                $this->assertEquals( 0, $results['can_partial_refresh'] );
     370        }
     371
     372        /**
     373         * Test the filter_wp_nav_menu method.
     374         *
     375         * @see WP_Customize_Nav_Menus::filter_wp_nav_menu()
     376         */
     377        function test_filter_wp_nav_menu() {
     378                do_action( 'customize_register', $this->wp_customize );
     379                $menus = new WP_Customize_Nav_Menus( $this->wp_customize );
     380
     381                $args = $menus->filter_wp_nav_menu_args( array(
     382                        'echo'        => true,
     383                        'fallback_cb' => 'wp_page_menu',
     384                        'walker'      => '',
     385                ) );
     386
     387                ob_start();
     388                wp_nav_menu( $args );
     389                $nav_menu_content = ob_get_clean();
     390
     391                $object_args = json_decode( json_encode( $args ), false );
     392                $result = $menus->filter_wp_nav_menu( $nav_menu_content, $object_args );
     393                $expected = sprintf(
     394                        '<div id="partial-refresh-menu-container-%1$d" class="partial-refresh-menu-container" data-instance-number="%1$d">%2$s</div>',
     395                        $args['instance_number'],
     396                        $nav_menu_content
     397                );
     398                $this->assertEquals( $expected, $result );
     399        }
     400
     401        /**
     402         * Test the customize_preview_enqueue_deps method.
     403         *
     404         * @see WP_Customize_Nav_Menus::customize_preview_enqueue_deps()
     405         */
     406        function test_customize_preview_enqueue_deps() {
     407                do_action( 'customize_register', $this->wp_customize );
     408                $menus = new WP_Customize_Nav_Menus( $this->wp_customize );
     409
     410                $menus->customize_preview_enqueue_deps();
     411
     412                $this->assertTrue( wp_script_is( 'customize-preview-nav-menus' ) );
     413                $this->assertEquals( 10, has_action( 'wp_print_footer_scripts', array( $menus, 'export_preview_data' ) ) );
     414        }
     415
     416        /**
     417         * Test the export_preview_data method.
     418         *
     419         * @see WP_Customize_Nav_Menus::export_preview_data()
     420         */
     421        function test_export_preview_data() {
     422                do_action( 'customize_register', $this->wp_customize );
     423                $menus = new WP_Customize_Nav_Menus( $this->wp_customize );
     424
     425                $request_uri = $_SERVER['REQUEST_URI'];
     426
     427                ob_start();
     428                $_SERVER['REQUEST_URI'] = '/wp-admin';
     429                $menus->export_preview_data();
     430                $data = ob_get_clean();
     431
     432                $_SERVER['REQUEST_URI'] = $request_uri;
     433
     434                $this->assertContains( '_wpCustomizePreviewNavMenusExports', $data );
     435                $this->assertContains( 'renderQueryVar', $data );
     436                $this->assertContains( 'renderNonceValue', $data );
     437                $this->assertContains( 'renderNoncePostKey', $data );
     438                $this->assertContains( 'requestUri', $data );
     439                $this->assertContains( 'theme', $data );
     440                $this->assertContains( 'previewCustomizeNonce', $data );
     441                $this->assertContains( 'navMenuInstanceArgs', $data );
     442                $this->assertContains( 'requestUri', $data );
     443
     444        }
     445
     446        /**
     447         * Test the render_menu method.
     448         *
     449         * @see WP_Customize_Nav_Menus::render_menu()
     450         */
     451        function test_render_menu() {
     452
     453                $this->markTestIncomplete( 'This test has not been implemented.' );
     454        }
     455
     456}