Ticket #32576: 32576.6.diff
File 32576.6.diff, 251.1 KB (added by , 10 years ago) |
---|
-
Gruntfile.js
141 141 }, 142 142 cssmin: { 143 143 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-*'] 145 145 }, 146 146 core: { 147 147 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 616 button.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 781 body.adding-menu-items #available-menu-items { 782 left: 0; 783 } 784 785 body.adding-menu-items .wp-full-overlay-main { 786 left: 300px; 787 } 788 789 body.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 865 li.assigned-to-menu-location .menu-delete-item { 866 display: none; 867 } 868 869 li.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
1402 1402 return $this->manager->widgets->is_widget_rendered( $this->widget_id ); 1403 1403 } 1404 1404 } 1405 1406 /** 1407 * Customize Nav Menus Panel Class 1408 * 1409 * Needed to add screen options. 1410 * 1411 * @since 4.3.0 1412 */ 1413 class 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 */ 1506 class 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 */ 1600 class 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 */ 1750 class 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 */ 1820 class 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 */ 1857 class 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
49 49 */ 50 50 public $widgets; 51 51 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 52 59 protected $settings = array(); 53 60 protected $containers = array(); 54 61 protected $panels = array(); … … 104 111 require_once( ABSPATH . WPINC . '/class-wp-customize-section.php' ); 105 112 require_once( ABSPATH . WPINC . '/class-wp-customize-control.php' ); 106 113 require_once( ABSPATH . WPINC . '/class-wp-customize-widgets.php' ); 114 require_once( ABSPATH . WPINC . '/class-wp-customize-nav-menus.php' ); 107 115 108 116 $this->widgets = new WP_Customize_Widgets( $this ); 117 $this->nav_menus = new WP_Customize_Nav_Menus( $this ); 109 118 110 119 add_filter( 'wp_die_handler', array( $this, 'wp_die_handler' ) ); 111 120 … … 1484 1493 } 1485 1494 } 1486 1495 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( '' => __( '— Select —' ) );1508 foreach ( $menus as $menu ) {1509 $choices[ $menu->term_id ] = wp_html_excerpt( $menu->name, 40, '…' );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 1529 1496 /* Static Front Page */ 1530 1497 // #WP19627 1531 1498 -
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 */ 19 final 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 ▸ Menus', '▸ 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' => __( '— Select —' ) ); 409 foreach ( $menus as $menu ) { 410 $choices[ $menu->term_id ] = wp_html_excerpt( $menu->name, 40, '…' ); 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: ▸ is the unicode right-pointing triangle, and %s is the section title in the Customizer */ 613 printf( __( 'Customizing ▸ %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…' ) ?>" /> 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
501 501 return $this->manager->widgets->is_sidebar_rendered( $this->sidebar_id ); 502 502 } 503 503 } 504 505 /** 506 * Customize Menu Section Class 507 * 508 * Custom section only needed in JS. 509 * 510 * @since 4.3.0 511 */ 512 class 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 */ 546 class 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
630 630 remove_theme_mod( 'background_image_thumb' ); 631 631 } 632 632 } 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 */ 645 class 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 */ 1297 class 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 3 wp.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
406 406 $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 ); 407 407 $scripts->add( 'customize-preview-widgets', "/wp-includes/js/customize-preview-widgets$suffix.js", array( 'jquery', 'wp-util', 'customize-preview' ), false, 1 ); 408 408 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 409 412 $scripts->add( 'accordion', "/wp-admin/js/accordion$suffix.js", array( 'jquery' ), false, 1 ); 410 413 411 414 $scripts->add( 'shortcode', "/wp-includes/js/shortcode$suffix.js", array( 'underscore' ), false, 1 ); … … 656 659 $suffix = SCRIPT_DEBUG ? '' : '.min'; 657 660 658 661 // 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' ) ); 666 670 667 $styles->add( 'ie', 671 $styles->add( 'ie', "/wp-admin/css/ie$suffix.css" ); 668 672 $styles->add_data( 'ie', 'conditional', 'lte IE 7' ); 669 673 670 674 // Common dependencies … … 673 677 $styles->add( 'open-sans', $open_sans_font_url ); 674 678 675 679 // 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" ); 681 686 682 687 // External libraries and friends 683 688 $styles->add( 'imgareaselect', '/wp-includes/js/imgareaselect/imgareaselect.css', array(), '0.9.8' ); … … 695 700 // RTL CSS 696 701 $rtl_styles = array( 697 702 // 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', 699 704 // wp-includes 700 705 'buttons', 'admin-bar', 'wp-auth-check', 'editor-buttons', 'media-views', 'wp-pointer', 701 706 '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 */ 7 class 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 */ 8 class 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 <b>world</b>', $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 */ 8 class 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 ▸ %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 }