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