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