[ Index ] |
PHP Cross Reference of WordPress |
[Summary view] [Print] [Text view]
1 /** 2 * @output wp-admin/js/customize-widgets.js 3 */ 4 5 /* global _wpCustomizeWidgetsSettings */ 6 (function( wp, $ ){ 7 8 if ( ! wp || ! wp.customize ) { return; } 9 10 // Set up our namespace... 11 var api = wp.customize, 12 l10n; 13 14 /** 15 * @namespace wp.customize.Widgets 16 */ 17 api.Widgets = api.Widgets || {}; 18 api.Widgets.savedWidgetIds = {}; 19 20 // Link settings. 21 api.Widgets.data = _wpCustomizeWidgetsSettings || {}; 22 l10n = api.Widgets.data.l10n; 23 24 /** 25 * wp.customize.Widgets.WidgetModel 26 * 27 * A single widget model. 28 * 29 * @class wp.customize.Widgets.WidgetModel 30 * @augments Backbone.Model 31 */ 32 api.Widgets.WidgetModel = Backbone.Model.extend(/** @lends wp.customize.Widgets.WidgetModel.prototype */{ 33 id: null, 34 temp_id: null, 35 classname: null, 36 control_tpl: null, 37 description: null, 38 is_disabled: null, 39 is_multi: null, 40 multi_number: null, 41 name: null, 42 id_base: null, 43 transport: null, 44 params: [], 45 width: null, 46 height: null, 47 search_matched: true 48 }); 49 50 /** 51 * wp.customize.Widgets.WidgetCollection 52 * 53 * Collection for widget models. 54 * 55 * @class wp.customize.Widgets.WidgetCollection 56 * @augments Backbone.Collection 57 */ 58 api.Widgets.WidgetCollection = Backbone.Collection.extend(/** @lends wp.customize.Widgets.WidgetCollection.prototype */{ 59 model: api.Widgets.WidgetModel, 60 61 // Controls searching on the current widget collection 62 // and triggers an update event. 63 doSearch: function( value ) { 64 65 // Don't do anything if we've already done this search. 66 // Useful because the search handler fires multiple times per keystroke. 67 if ( this.terms === value ) { 68 return; 69 } 70 71 // Updates terms with the value passed. 72 this.terms = value; 73 74 // If we have terms, run a search... 75 if ( this.terms.length > 0 ) { 76 this.search( this.terms ); 77 } 78 79 // If search is blank, set all the widgets as they matched the search to reset the views. 80 if ( this.terms === '' ) { 81 this.each( function ( widget ) { 82 widget.set( 'search_matched', true ); 83 } ); 84 } 85 }, 86 87 // Performs a search within the collection. 88 // @uses RegExp 89 search: function( term ) { 90 var match, haystack; 91 92 // Escape the term string for RegExp meta characters. 93 term = term.replace( /[-\/\\^$*+?.()|[\]{}]/g, '\\$&' ); 94 95 // Consider spaces as word delimiters and match the whole string 96 // so matching terms can be combined. 97 term = term.replace( / /g, ')(?=.*' ); 98 match = new RegExp( '^(?=.*' + term + ').+', 'i' ); 99 100 this.each( function ( data ) { 101 haystack = [ data.get( 'name' ), data.get( 'description' ) ].join( ' ' ); 102 data.set( 'search_matched', match.test( haystack ) ); 103 } ); 104 } 105 }); 106 api.Widgets.availableWidgets = new api.Widgets.WidgetCollection( api.Widgets.data.availableWidgets ); 107 108 /** 109 * wp.customize.Widgets.SidebarModel 110 * 111 * A single sidebar model. 112 * 113 * @class wp.customize.Widgets.SidebarModel 114 * @augments Backbone.Model 115 */ 116 api.Widgets.SidebarModel = Backbone.Model.extend(/** @lends wp.customize.Widgets.SidebarModel.prototype */{ 117 after_title: null, 118 after_widget: null, 119 before_title: null, 120 before_widget: null, 121 'class': null, 122 description: null, 123 id: null, 124 name: null, 125 is_rendered: false 126 }); 127 128 /** 129 * wp.customize.Widgets.SidebarCollection 130 * 131 * Collection for sidebar models. 132 * 133 * @class wp.customize.Widgets.SidebarCollection 134 * @augments Backbone.Collection 135 */ 136 api.Widgets.SidebarCollection = Backbone.Collection.extend(/** @lends wp.customize.Widgets.SidebarCollection.prototype */{ 137 model: api.Widgets.SidebarModel 138 }); 139 api.Widgets.registeredSidebars = new api.Widgets.SidebarCollection( api.Widgets.data.registeredSidebars ); 140 141 api.Widgets.AvailableWidgetsPanelView = wp.Backbone.View.extend(/** @lends wp.customize.Widgets.AvailableWidgetsPanelView.prototype */{ 142 143 el: '#available-widgets', 144 145 events: { 146 'input #widgets-search': 'search', 147 'focus .widget-tpl' : 'focus', 148 'click .widget-tpl' : '_submit', 149 'keypress .widget-tpl' : '_submit', 150 'keydown' : 'keyboardAccessible' 151 }, 152 153 // Cache current selected widget. 154 selected: null, 155 156 // Cache sidebar control which has opened panel. 157 currentSidebarControl: null, 158 $search: null, 159 $clearResults: null, 160 searchMatchesCount: null, 161 162 /** 163 * View class for the available widgets panel. 164 * 165 * @constructs wp.customize.Widgets.AvailableWidgetsPanelView 166 * @augments wp.Backbone.View 167 */ 168 initialize: function() { 169 var self = this; 170 171 this.$search = $( '#widgets-search' ); 172 173 this.$clearResults = this.$el.find( '.clear-results' ); 174 175 _.bindAll( this, 'close' ); 176 177 this.listenTo( this.collection, 'change', this.updateList ); 178 179 this.updateList(); 180 181 // Set the initial search count to the number of available widgets. 182 this.searchMatchesCount = this.collection.length; 183 184 /* 185 * If the available widgets panel is open and the customize controls 186 * are interacted with (i.e. available widgets panel is blurred) then 187 * close the available widgets panel. Also close on back button click. 188 */ 189 $( '#customize-controls, #available-widgets .customize-section-title' ).on( 'click keydown', function( e ) { 190 var isAddNewBtn = $( e.target ).is( '.add-new-widget, .add-new-widget *' ); 191 if ( $( 'body' ).hasClass( 'adding-widget' ) && ! isAddNewBtn ) { 192 self.close(); 193 } 194 } ); 195 196 // Clear the search results and trigger an `input` event to fire a new search. 197 this.$clearResults.on( 'click', function() { 198 self.$search.val( '' ).trigger( 'focus' ).trigger( 'input' ); 199 } ); 200 201 // Close the panel if the URL in the preview changes. 202 api.previewer.bind( 'url', this.close ); 203 }, 204 205 /** 206 * Performs a search and handles selected widget. 207 */ 208 search: _.debounce( function( event ) { 209 var firstVisible; 210 211 this.collection.doSearch( event.target.value ); 212 // Update the search matches count. 213 this.updateSearchMatchesCount(); 214 // Announce how many search results. 215 this.announceSearchMatches(); 216 217 // Remove a widget from being selected if it is no longer visible. 218 if ( this.selected && ! this.selected.is( ':visible' ) ) { 219 this.selected.removeClass( 'selected' ); 220 this.selected = null; 221 } 222 223 // If a widget was selected but the filter value has been cleared out, clear selection. 224 if ( this.selected && ! event.target.value ) { 225 this.selected.removeClass( 'selected' ); 226 this.selected = null; 227 } 228 229 // If a filter has been entered and a widget hasn't been selected, select the first one shown. 230 if ( ! this.selected && event.target.value ) { 231 firstVisible = this.$el.find( '> .widget-tpl:visible:first' ); 232 if ( firstVisible.length ) { 233 this.select( firstVisible ); 234 } 235 } 236 237 // Toggle the clear search results button. 238 if ( '' !== event.target.value ) { 239 this.$clearResults.addClass( 'is-visible' ); 240 } else if ( '' === event.target.value ) { 241 this.$clearResults.removeClass( 'is-visible' ); 242 } 243 244 // Set a CSS class on the search container when there are no search results. 245 if ( ! this.searchMatchesCount ) { 246 this.$el.addClass( 'no-widgets-found' ); 247 } else { 248 this.$el.removeClass( 'no-widgets-found' ); 249 } 250 }, 500 ), 251 252 /** 253 * Updates the count of the available widgets that have the `search_matched` attribute. 254 */ 255 updateSearchMatchesCount: function() { 256 this.searchMatchesCount = this.collection.where({ search_matched: true }).length; 257 }, 258 259 /** 260 * Sends a message to the aria-live region to announce how many search results. 261 */ 262 announceSearchMatches: function() { 263 var message = l10n.widgetsFound.replace( '%d', this.searchMatchesCount ) ; 264 265 if ( ! this.searchMatchesCount ) { 266 message = l10n.noWidgetsFound; 267 } 268 269 wp.a11y.speak( message ); 270 }, 271 272 /** 273 * Changes visibility of available widgets. 274 */ 275 updateList: function() { 276 this.collection.each( function( widget ) { 277 var widgetTpl = $( '#widget-tpl-' + widget.id ); 278 widgetTpl.toggle( widget.get( 'search_matched' ) && ! widget.get( 'is_disabled' ) ); 279 if ( widget.get( 'is_disabled' ) && widgetTpl.is( this.selected ) ) { 280 this.selected = null; 281 } 282 } ); 283 }, 284 285 /** 286 * Highlights a widget. 287 */ 288 select: function( widgetTpl ) { 289 this.selected = $( widgetTpl ); 290 this.selected.siblings( '.widget-tpl' ).removeClass( 'selected' ); 291 this.selected.addClass( 'selected' ); 292 }, 293 294 /** 295 * Highlights a widget on focus. 296 */ 297 focus: function( event ) { 298 this.select( $( event.currentTarget ) ); 299 }, 300 301 /** 302 * Handles submit for keypress and click on widget. 303 */ 304 _submit: function( event ) { 305 // Only proceed with keypress if it is Enter or Spacebar. 306 if ( event.type === 'keypress' && ( event.which !== 13 && event.which !== 32 ) ) { 307 return; 308 } 309 310 this.submit( $( event.currentTarget ) ); 311 }, 312 313 /** 314 * Adds a selected widget to the sidebar. 315 */ 316 submit: function( widgetTpl ) { 317 var widgetId, widget, widgetFormControl; 318 319 if ( ! widgetTpl ) { 320 widgetTpl = this.selected; 321 } 322 323 if ( ! widgetTpl || ! this.currentSidebarControl ) { 324 return; 325 } 326 327 this.select( widgetTpl ); 328 329 widgetId = $( this.selected ).data( 'widget-id' ); 330 widget = this.collection.findWhere( { id: widgetId } ); 331 if ( ! widget ) { 332 return; 333 } 334 335 widgetFormControl = this.currentSidebarControl.addWidget( widget.get( 'id_base' ) ); 336 if ( widgetFormControl ) { 337 widgetFormControl.focus(); 338 } 339 340 this.close(); 341 }, 342 343 /** 344 * Opens the panel. 345 */ 346 open: function( sidebarControl ) { 347 this.currentSidebarControl = sidebarControl; 348 349 // Wide widget controls appear over the preview, and so they need to be collapsed when the panel opens. 350 _( this.currentSidebarControl.getWidgetFormControls() ).each( function( control ) { 351 if ( control.params.is_wide ) { 352 control.collapseForm(); 353 } 354 } ); 355 356 if ( api.section.has( 'publish_settings' ) ) { 357 api.section( 'publish_settings' ).collapse(); 358 } 359 360 $( 'body' ).addClass( 'adding-widget' ); 361 362 this.$el.find( '.selected' ).removeClass( 'selected' ); 363 364 // Reset search. 365 this.collection.doSearch( '' ); 366 367 if ( ! api.settings.browser.mobile ) { 368 this.$search.trigger( 'focus' ); 369 } 370 }, 371 372 /** 373 * Closes the panel. 374 */ 375 close: function( options ) { 376 options = options || {}; 377 378 if ( options.returnFocus && this.currentSidebarControl ) { 379 this.currentSidebarControl.container.find( '.add-new-widget' ).focus(); 380 } 381 382 this.currentSidebarControl = null; 383 this.selected = null; 384 385 $( 'body' ).removeClass( 'adding-widget' ); 386 387 this.$search.val( '' ).trigger( 'input' ); 388 }, 389 390 /** 391 * Adds keyboard accessiblity to the panel. 392 */ 393 keyboardAccessible: function( event ) { 394 var isEnter = ( event.which === 13 ), 395 isEsc = ( event.which === 27 ), 396 isDown = ( event.which === 40 ), 397 isUp = ( event.which === 38 ), 398 isTab = ( event.which === 9 ), 399 isShift = ( event.shiftKey ), 400 selected = null, 401 firstVisible = this.$el.find( '> .widget-tpl:visible:first' ), 402 lastVisible = this.$el.find( '> .widget-tpl:visible:last' ), 403 isSearchFocused = $( event.target ).is( this.$search ), 404 isLastWidgetFocused = $( event.target ).is( '.widget-tpl:visible:last' ); 405 406 if ( isDown || isUp ) { 407 if ( isDown ) { 408 if ( isSearchFocused ) { 409 selected = firstVisible; 410 } else if ( this.selected && this.selected.nextAll( '.widget-tpl:visible' ).length !== 0 ) { 411 selected = this.selected.nextAll( '.widget-tpl:visible:first' ); 412 } 413 } else if ( isUp ) { 414 if ( isSearchFocused ) { 415 selected = lastVisible; 416 } else if ( this.selected && this.selected.prevAll( '.widget-tpl:visible' ).length !== 0 ) { 417 selected = this.selected.prevAll( '.widget-tpl:visible:first' ); 418 } 419 } 420 421 this.select( selected ); 422 423 if ( selected ) { 424 selected.trigger( 'focus' ); 425 } else { 426 this.$search.trigger( 'focus' ); 427 } 428 429 return; 430 } 431 432 // If enter pressed but nothing entered, don't do anything. 433 if ( isEnter && ! this.$search.val() ) { 434 return; 435 } 436 437 if ( isEnter ) { 438 this.submit(); 439 } else if ( isEsc ) { 440 this.close( { returnFocus: true } ); 441 } 442 443 if ( this.currentSidebarControl && isTab && ( isShift && isSearchFocused || ! isShift && isLastWidgetFocused ) ) { 444 this.currentSidebarControl.container.find( '.add-new-widget' ).focus(); 445 event.preventDefault(); 446 } 447 } 448 }); 449 450 /** 451 * Handlers for the widget-synced event, organized by widget ID base. 452 * Other widgets may provide their own update handlers by adding 453 * listeners for the widget-synced event. 454 * 455 * @alias wp.customize.Widgets.formSyncHandlers 456 */ 457 api.Widgets.formSyncHandlers = { 458 459 /** 460 * @param {jQuery.Event} e 461 * @param {jQuery} widget 462 * @param {string} newForm 463 */ 464 rss: function( e, widget, newForm ) { 465 var oldWidgetError = widget.find( '.widget-error:first' ), 466 newWidgetError = $( '<div>' + newForm + '</div>' ).find( '.widget-error:first' ); 467 468 if ( oldWidgetError.length && newWidgetError.length ) { 469 oldWidgetError.replaceWith( newWidgetError ); 470 } else if ( oldWidgetError.length ) { 471 oldWidgetError.remove(); 472 } else if ( newWidgetError.length ) { 473 widget.find( '.widget-content:first' ).prepend( newWidgetError ); 474 } 475 } 476 }; 477 478 api.Widgets.WidgetControl = api.Control.extend(/** @lends wp.customize.Widgets.WidgetControl.prototype */{ 479 defaultExpandedArguments: { 480 duration: 'fast', 481 completeCallback: $.noop 482 }, 483 484 /** 485 * wp.customize.Widgets.WidgetControl 486 * 487 * Customizer control for widgets. 488 * Note that 'widget_form' must match the WP_Widget_Form_Customize_Control::$type 489 * 490 * @since 4.1.0 491 * 492 * @constructs wp.customize.Widgets.WidgetControl 493 * @augments wp.customize.Control 494 */ 495 initialize: function( id, options ) { 496 var control = this; 497 498 control.widgetControlEmbedded = false; 499 control.widgetContentEmbedded = false; 500 control.expanded = new api.Value( false ); 501 control.expandedArgumentsQueue = []; 502 control.expanded.bind( function( expanded ) { 503 var args = control.expandedArgumentsQueue.shift(); 504 args = $.extend( {}, control.defaultExpandedArguments, args ); 505 control.onChangeExpanded( expanded, args ); 506 }); 507 control.altNotice = true; 508 509 api.Control.prototype.initialize.call( control, id, options ); 510 }, 511 512 /** 513 * Set up the control. 514 * 515 * @since 3.9.0 516 */ 517 ready: function() { 518 var control = this; 519 520 /* 521 * Embed a placeholder once the section is expanded. The full widget 522 * form content will be embedded once the control itself is expanded, 523 * and at this point the widget-added event will be triggered. 524 */ 525 if ( ! control.section() ) { 526 control.embedWidgetControl(); 527 } else { 528 api.section( control.section(), function( section ) { 529 var onExpanded = function( isExpanded ) { 530 if ( isExpanded ) { 531 control.embedWidgetControl(); 532 section.expanded.unbind( onExpanded ); 533 } 534 }; 535 if ( section.expanded() ) { 536 onExpanded( true ); 537 } else { 538 section.expanded.bind( onExpanded ); 539 } 540 } ); 541 } 542 }, 543 544 /** 545 * Embed the .widget element inside the li container. 546 * 547 * @since 4.4.0 548 */ 549 embedWidgetControl: function() { 550 var control = this, widgetControl; 551 552 if ( control.widgetControlEmbedded ) { 553 return; 554 } 555 control.widgetControlEmbedded = true; 556 557 widgetControl = $( control.params.widget_control ); 558 control.container.append( widgetControl ); 559 560 control._setupModel(); 561 control._setupWideWidget(); 562 control._setupControlToggle(); 563 564 control._setupWidgetTitle(); 565 control._setupReorderUI(); 566 control._setupHighlightEffects(); 567 control._setupUpdateUI(); 568 control._setupRemoveUI(); 569 }, 570 571 /** 572 * Embed the actual widget form inside of .widget-content and finally trigger the widget-added event. 573 * 574 * @since 4.4.0 575 */ 576 embedWidgetContent: function() { 577 var control = this, widgetContent; 578 579 control.embedWidgetControl(); 580 if ( control.widgetContentEmbedded ) { 581 return; 582 } 583 control.widgetContentEmbedded = true; 584 585 // Update the notification container element now that the widget content has been embedded. 586 control.notifications.container = control.getNotificationsContainerElement(); 587 control.notifications.render(); 588 589 widgetContent = $( control.params.widget_content ); 590 control.container.find( '.widget-content:first' ).append( widgetContent ); 591 592 /* 593 * Trigger widget-added event so that plugins can attach any event 594 * listeners and dynamic UI elements. 595 */ 596 $( document ).trigger( 'widget-added', [ control.container.find( '.widget:first' ) ] ); 597 598 }, 599 600 /** 601 * Handle changes to the setting 602 */ 603 _setupModel: function() { 604 var self = this, rememberSavedWidgetId; 605 606 // Remember saved widgets so we know which to trash (move to inactive widgets sidebar). 607 rememberSavedWidgetId = function() { 608 api.Widgets.savedWidgetIds[self.params.widget_id] = true; 609 }; 610 api.bind( 'ready', rememberSavedWidgetId ); 611 api.bind( 'saved', rememberSavedWidgetId ); 612 613 this._updateCount = 0; 614 this.isWidgetUpdating = false; 615 this.liveUpdateMode = true; 616 617 // Update widget whenever model changes. 618 this.setting.bind( function( to, from ) { 619 if ( ! _( from ).isEqual( to ) && ! self.isWidgetUpdating ) { 620 self.updateWidget( { instance: to } ); 621 } 622 } ); 623 }, 624 625 /** 626 * Add special behaviors for wide widget controls 627 */ 628 _setupWideWidget: function() { 629 var self = this, $widgetInside, $widgetForm, $customizeSidebar, 630 $themeControlsContainer, positionWidget; 631 632 if ( ! this.params.is_wide || $( window ).width() <= 640 /* max-width breakpoint in customize-controls.css */ ) { 633 return; 634 } 635 636 $widgetInside = this.container.find( '.widget-inside' ); 637 $widgetForm = $widgetInside.find( '> .form' ); 638 $customizeSidebar = $( '.wp-full-overlay-sidebar-content:first' ); 639 this.container.addClass( 'wide-widget-control' ); 640 641 this.container.find( '.form:first' ).css( { 642 'max-width': this.params.width, 643 'min-height': this.params.height 644 } ); 645 646 /** 647 * Keep the widget-inside positioned so the top of fixed-positioned 648 * element is at the same top position as the widget-top. When the 649 * widget-top is scrolled out of view, keep the widget-top in view; 650 * likewise, don't allow the widget to drop off the bottom of the window. 651 * If a widget is too tall to fit in the window, don't let the height 652 * exceed the window height so that the contents of the widget control 653 * will become scrollable (overflow:auto). 654 */ 655 positionWidget = function() { 656 var offsetTop = self.container.offset().top, 657 windowHeight = $( window ).height(), 658 formHeight = $widgetForm.outerHeight(), 659 top; 660 $widgetInside.css( 'max-height', windowHeight ); 661 top = Math.max( 662 0, // Prevent top from going off screen. 663 Math.min( 664 Math.max( offsetTop, 0 ), // Distance widget in panel is from top of screen. 665 windowHeight - formHeight // Flush up against bottom of screen. 666 ) 667 ); 668 $widgetInside.css( 'top', top ); 669 }; 670 671 $themeControlsContainer = $( '#customize-theme-controls' ); 672 this.container.on( 'expand', function() { 673 positionWidget(); 674 $customizeSidebar.on( 'scroll', positionWidget ); 675 $( window ).on( 'resize', positionWidget ); 676 $themeControlsContainer.on( 'expanded collapsed', positionWidget ); 677 } ); 678 this.container.on( 'collapsed', function() { 679 $customizeSidebar.off( 'scroll', positionWidget ); 680 $( window ).off( 'resize', positionWidget ); 681 $themeControlsContainer.off( 'expanded collapsed', positionWidget ); 682 } ); 683 684 // Reposition whenever a sidebar's widgets are changed. 685 api.each( function( setting ) { 686 if ( 0 === setting.id.indexOf( 'sidebars_widgets[' ) ) { 687 setting.bind( function() { 688 if ( self.container.hasClass( 'expanded' ) ) { 689 positionWidget(); 690 } 691 } ); 692 } 693 } ); 694 }, 695 696 /** 697 * Show/hide the control when clicking on the form title, when clicking 698 * the close button 699 */ 700 _setupControlToggle: function() { 701 var self = this, $closeBtn; 702 703 this.container.find( '.widget-top' ).on( 'click', function( e ) { 704 e.preventDefault(); 705 var sidebarWidgetsControl = self.getSidebarWidgetsControl(); 706 if ( sidebarWidgetsControl.isReordering ) { 707 return; 708 } 709 self.expanded( ! self.expanded() ); 710 } ); 711 712 $closeBtn = this.container.find( '.widget-control-close' ); 713 $closeBtn.on( 'click', function() { 714 self.collapse(); 715 self.container.find( '.widget-top .widget-action:first' ).focus(); // Keyboard accessibility. 716 } ); 717 }, 718 719 /** 720 * Update the title of the form if a title field is entered 721 */ 722 _setupWidgetTitle: function() { 723 var self = this, updateTitle; 724 725 updateTitle = function() { 726 var title = self.setting().title, 727 inWidgetTitle = self.container.find( '.in-widget-title' ); 728 729 if ( title ) { 730 inWidgetTitle.text( ': ' + title ); 731 } else { 732 inWidgetTitle.text( '' ); 733 } 734 }; 735 this.setting.bind( updateTitle ); 736 updateTitle(); 737 }, 738 739 /** 740 * Set up the widget-reorder-nav 741 */ 742 _setupReorderUI: function() { 743 var self = this, selectSidebarItem, $moveWidgetArea, 744 $reorderNav, updateAvailableSidebars, template; 745 746 /** 747 * select the provided sidebar list item in the move widget area 748 * 749 * @param {jQuery} li 750 */ 751 selectSidebarItem = function( li ) { 752 li.siblings( '.selected' ).removeClass( 'selected' ); 753 li.addClass( 'selected' ); 754 var isSelfSidebar = ( li.data( 'id' ) === self.params.sidebar_id ); 755 self.container.find( '.move-widget-btn' ).prop( 'disabled', isSelfSidebar ); 756 }; 757 758 /** 759 * Add the widget reordering elements to the widget control 760 */ 761 this.container.find( '.widget-title-action' ).after( $( api.Widgets.data.tpl.widgetReorderNav ) ); 762 763 764 template = _.template( api.Widgets.data.tpl.moveWidgetArea ); 765 $moveWidgetArea = $( template( { 766 sidebars: _( api.Widgets.registeredSidebars.toArray() ).pluck( 'attributes' ) 767 } ) 768 ); 769 this.container.find( '.widget-top' ).after( $moveWidgetArea ); 770 771 /** 772 * Update available sidebars when their rendered state changes 773 */ 774 updateAvailableSidebars = function() { 775 var $sidebarItems = $moveWidgetArea.find( 'li' ), selfSidebarItem, 776 renderedSidebarCount = 0; 777 778 selfSidebarItem = $sidebarItems.filter( function(){ 779 return $( this ).data( 'id' ) === self.params.sidebar_id; 780 } ); 781 782 $sidebarItems.each( function() { 783 var li = $( this ), 784 sidebarId, sidebar, sidebarIsRendered; 785 786 sidebarId = li.data( 'id' ); 787 sidebar = api.Widgets.registeredSidebars.get( sidebarId ); 788 sidebarIsRendered = sidebar.get( 'is_rendered' ); 789 790 li.toggle( sidebarIsRendered ); 791 792 if ( sidebarIsRendered ) { 793 renderedSidebarCount += 1; 794 } 795 796 if ( li.hasClass( 'selected' ) && ! sidebarIsRendered ) { 797 selectSidebarItem( selfSidebarItem ); 798 } 799 } ); 800 801 if ( renderedSidebarCount > 1 ) { 802 self.container.find( '.move-widget' ).show(); 803 } else { 804 self.container.find( '.move-widget' ).hide(); 805 } 806 }; 807 808 updateAvailableSidebars(); 809 api.Widgets.registeredSidebars.on( 'change:is_rendered', updateAvailableSidebars ); 810 811 /** 812 * Handle clicks for up/down/move on the reorder nav 813 */ 814 $reorderNav = this.container.find( '.widget-reorder-nav' ); 815 $reorderNav.find( '.move-widget, .move-widget-down, .move-widget-up' ).each( function() { 816 $( this ).prepend( self.container.find( '.widget-title' ).text() + ': ' ); 817 } ).on( 'click keypress', function( event ) { 818 if ( event.type === 'keypress' && ( event.which !== 13 && event.which !== 32 ) ) { 819 return; 820 } 821 $( this ).trigger( 'focus' ); 822 823 if ( $( this ).is( '.move-widget' ) ) { 824 self.toggleWidgetMoveArea(); 825 } else { 826 var isMoveDown = $( this ).is( '.move-widget-down' ), 827 isMoveUp = $( this ).is( '.move-widget-up' ), 828 i = self.getWidgetSidebarPosition(); 829 830 if ( ( isMoveUp && i === 0 ) || ( isMoveDown && i === self.getSidebarWidgetsControl().setting().length - 1 ) ) { 831 return; 832 } 833 834 if ( isMoveUp ) { 835 self.moveUp(); 836 wp.a11y.speak( l10n.widgetMovedUp ); 837 } else { 838 self.moveDown(); 839 wp.a11y.speak( l10n.widgetMovedDown ); 840 } 841 842 $( this ).trigger( 'focus' ); // Re-focus after the container was moved. 843 } 844 } ); 845 846 /** 847 * Handle selecting a sidebar to move to 848 */ 849 this.container.find( '.widget-area-select' ).on( 'click keypress', 'li', function( event ) { 850 if ( event.type === 'keypress' && ( event.which !== 13 && event.which !== 32 ) ) { 851 return; 852 } 853 event.preventDefault(); 854 selectSidebarItem( $( this ) ); 855 } ); 856 857 /** 858 * Move widget to another sidebar 859 */ 860 this.container.find( '.move-widget-btn' ).click( function() { 861 self.getSidebarWidgetsControl().toggleReordering( false ); 862 863 var oldSidebarId = self.params.sidebar_id, 864 newSidebarId = self.container.find( '.widget-area-select li.selected' ).data( 'id' ), 865 oldSidebarWidgetsSetting, newSidebarWidgetsSetting, 866 oldSidebarWidgetIds, newSidebarWidgetIds, i; 867 868 oldSidebarWidgetsSetting = api( 'sidebars_widgets[' + oldSidebarId + ']' ); 869 newSidebarWidgetsSetting = api( 'sidebars_widgets[' + newSidebarId + ']' ); 870 oldSidebarWidgetIds = Array.prototype.slice.call( oldSidebarWidgetsSetting() ); 871 newSidebarWidgetIds = Array.prototype.slice.call( newSidebarWidgetsSetting() ); 872 873 i = self.getWidgetSidebarPosition(); 874 oldSidebarWidgetIds.splice( i, 1 ); 875 newSidebarWidgetIds.push( self.params.widget_id ); 876 877 oldSidebarWidgetsSetting( oldSidebarWidgetIds ); 878 newSidebarWidgetsSetting( newSidebarWidgetIds ); 879 880 self.focus(); 881 } ); 882 }, 883 884 /** 885 * Highlight widgets in preview when interacted with in the Customizer 886 */ 887 _setupHighlightEffects: function() { 888 var self = this; 889 890 // Highlight whenever hovering or clicking over the form. 891 this.container.on( 'mouseenter click', function() { 892 self.setting.previewer.send( 'highlight-widget', self.params.widget_id ); 893 } ); 894 895 // Highlight when the setting is updated. 896 this.setting.bind( function() { 897 self.setting.previewer.send( 'highlight-widget', self.params.widget_id ); 898 } ); 899 }, 900 901 /** 902 * Set up event handlers for widget updating 903 */ 904 _setupUpdateUI: function() { 905 var self = this, $widgetRoot, $widgetContent, 906 $saveBtn, updateWidgetDebounced, formSyncHandler; 907 908 $widgetRoot = this.container.find( '.widget:first' ); 909 $widgetContent = $widgetRoot.find( '.widget-content:first' ); 910 911 // Configure update button. 912 $saveBtn = this.container.find( '.widget-control-save' ); 913 $saveBtn.val( l10n.saveBtnLabel ); 914 $saveBtn.attr( 'title', l10n.saveBtnTooltip ); 915 $saveBtn.removeClass( 'button-primary' ); 916 $saveBtn.on( 'click', function( e ) { 917 e.preventDefault(); 918 self.updateWidget( { disable_form: true } ); // @todo disable_form is unused? 919 } ); 920 921 updateWidgetDebounced = _.debounce( function() { 922 self.updateWidget(); 923 }, 250 ); 924 925 // Trigger widget form update when hitting Enter within an input. 926 $widgetContent.on( 'keydown', 'input', function( e ) { 927 if ( 13 === e.which ) { // Enter. 928 e.preventDefault(); 929 self.updateWidget( { ignoreActiveElement: true } ); 930 } 931 } ); 932 933 // Handle widgets that support live previews. 934 $widgetContent.on( 'change input propertychange', ':input', function( e ) { 935 if ( ! self.liveUpdateMode ) { 936 return; 937 } 938 if ( e.type === 'change' || ( this.checkValidity && this.checkValidity() ) ) { 939 updateWidgetDebounced(); 940 } 941 } ); 942 943 // Remove loading indicators when the setting is saved and the preview updates. 944 this.setting.previewer.channel.bind( 'synced', function() { 945 self.container.removeClass( 'previewer-loading' ); 946 } ); 947 948 api.previewer.bind( 'widget-updated', function( updatedWidgetId ) { 949 if ( updatedWidgetId === self.params.widget_id ) { 950 self.container.removeClass( 'previewer-loading' ); 951 } 952 } ); 953 954 formSyncHandler = api.Widgets.formSyncHandlers[ this.params.widget_id_base ]; 955 if ( formSyncHandler ) { 956 $( document ).on( 'widget-synced', function( e, widget ) { 957 if ( $widgetRoot.is( widget ) ) { 958 formSyncHandler.apply( document, arguments ); 959 } 960 } ); 961 } 962 }, 963 964 /** 965 * Update widget control to indicate whether it is currently rendered. 966 * 967 * Overrides api.Control.toggle() 968 * 969 * @since 4.1.0 970 * 971 * @param {boolean} active 972 * @param {Object} args 973 * @param {function} args.completeCallback 974 */ 975 onChangeActive: function ( active, args ) { 976 // Note: there is a second 'args' parameter being passed, merged on top of this.defaultActiveArguments. 977 this.container.toggleClass( 'widget-rendered', active ); 978 if ( args.completeCallback ) { 979 args.completeCallback(); 980 } 981 }, 982 983 /** 984 * Set up event handlers for widget removal 985 */ 986 _setupRemoveUI: function() { 987 var self = this, $removeBtn, replaceDeleteWithRemove; 988 989 // Configure remove button. 990 $removeBtn = this.container.find( '.widget-control-remove' ); 991 $removeBtn.on( 'click', function() { 992 // Find an adjacent element to add focus to when this widget goes away. 993 var $adjacentFocusTarget; 994 if ( self.container.next().is( '.customize-control-widget_form' ) ) { 995 $adjacentFocusTarget = self.container.next().find( '.widget-action:first' ); 996 } else if ( self.container.prev().is( '.customize-control-widget_form' ) ) { 997 $adjacentFocusTarget = self.container.prev().find( '.widget-action:first' ); 998 } else { 999 $adjacentFocusTarget = self.container.next( '.customize-control-sidebar_widgets' ).find( '.add-new-widget:first' ); 1000 } 1001 1002 self.container.slideUp( function() { 1003 var sidebarsWidgetsControl = api.Widgets.getSidebarWidgetControlContainingWidget( self.params.widget_id ), 1004 sidebarWidgetIds, i; 1005 1006 if ( ! sidebarsWidgetsControl ) { 1007 return; 1008 } 1009 1010 sidebarWidgetIds = sidebarsWidgetsControl.setting().slice(); 1011 i = _.indexOf( sidebarWidgetIds, self.params.widget_id ); 1012 if ( -1 === i ) { 1013 return; 1014 } 1015 1016 sidebarWidgetIds.splice( i, 1 ); 1017 sidebarsWidgetsControl.setting( sidebarWidgetIds ); 1018 1019 $adjacentFocusTarget.focus(); // Keyboard accessibility. 1020 } ); 1021 } ); 1022 1023 replaceDeleteWithRemove = function() { 1024 $removeBtn.text( l10n.removeBtnLabel ); // wp_widget_control() outputs the button as "Delete". 1025 $removeBtn.attr( 'title', l10n.removeBtnTooltip ); 1026 }; 1027 1028 if ( this.params.is_new ) { 1029 api.bind( 'saved', replaceDeleteWithRemove ); 1030 } else { 1031 replaceDeleteWithRemove(); 1032 } 1033 }, 1034 1035 /** 1036 * Find all inputs in a widget container that should be considered when 1037 * comparing the loaded form with the sanitized form, whose fields will 1038 * be aligned to copy the sanitized over. The elements returned by this 1039 * are passed into this._getInputsSignature(), and they are iterated 1040 * over when copying sanitized values over to the form loaded. 1041 * 1042 * @param {jQuery} container element in which to look for inputs 1043 * @return {jQuery} inputs 1044 * @private 1045 */ 1046 _getInputs: function( container ) { 1047 return $( container ).find( ':input[name]' ); 1048 }, 1049 1050 /** 1051 * Iterate over supplied inputs and create a signature string for all of them together. 1052 * This string can be used to compare whether or not the form has all of the same fields. 1053 * 1054 * @param {jQuery} inputs 1055 * @return {string} 1056 * @private 1057 */ 1058 _getInputsSignature: function( inputs ) { 1059 var inputsSignatures = _( inputs ).map( function( input ) { 1060 var $input = $( input ), signatureParts; 1061 1062 if ( $input.is( ':checkbox, :radio' ) ) { 1063 signatureParts = [ $input.attr( 'id' ), $input.attr( 'name' ), $input.prop( 'value' ) ]; 1064 } else { 1065 signatureParts = [ $input.attr( 'id' ), $input.attr( 'name' ) ]; 1066 } 1067 1068 return signatureParts.join( ',' ); 1069 } ); 1070 1071 return inputsSignatures.join( ';' ); 1072 }, 1073 1074 /** 1075 * Get the state for an input depending on its type. 1076 * 1077 * @param {jQuery|Element} input 1078 * @return {string|boolean|Array|*} 1079 * @private 1080 */ 1081 _getInputState: function( input ) { 1082 input = $( input ); 1083 if ( input.is( ':radio, :checkbox' ) ) { 1084 return input.prop( 'checked' ); 1085 } else if ( input.is( 'select[multiple]' ) ) { 1086 return input.find( 'option:selected' ).map( function () { 1087 return $( this ).val(); 1088 } ).get(); 1089 } else { 1090 return input.val(); 1091 } 1092 }, 1093 1094 /** 1095 * Update an input's state based on its type. 1096 * 1097 * @param {jQuery|Element} input 1098 * @param {string|boolean|Array|*} state 1099 * @private 1100 */ 1101 _setInputState: function ( input, state ) { 1102 input = $( input ); 1103 if ( input.is( ':radio, :checkbox' ) ) { 1104 input.prop( 'checked', state ); 1105 } else if ( input.is( 'select[multiple]' ) ) { 1106 if ( ! Array.isArray( state ) ) { 1107 state = []; 1108 } else { 1109 // Make sure all state items are strings since the DOM value is a string. 1110 state = _.map( state, function ( value ) { 1111 return String( value ); 1112 } ); 1113 } 1114 input.find( 'option' ).each( function () { 1115 $( this ).prop( 'selected', -1 !== _.indexOf( state, String( this.value ) ) ); 1116 } ); 1117 } else { 1118 input.val( state ); 1119 } 1120 }, 1121 1122 /*********************************************************************** 1123 * Begin public API methods 1124 **********************************************************************/ 1125 1126 /** 1127 * @return {wp.customize.controlConstructor.sidebar_widgets[]} 1128 */ 1129 getSidebarWidgetsControl: function() { 1130 var settingId, sidebarWidgetsControl; 1131 1132 settingId = 'sidebars_widgets[' + this.params.sidebar_id + ']'; 1133 sidebarWidgetsControl = api.control( settingId ); 1134 1135 if ( ! sidebarWidgetsControl ) { 1136 return; 1137 } 1138 1139 return sidebarWidgetsControl; 1140 }, 1141 1142 /** 1143 * Submit the widget form via Ajax and get back the updated instance, 1144 * along with the new widget control form to render. 1145 * 1146 * @param {Object} [args] 1147 * @param {Object|null} [args.instance=null] When the model changes, the instance is sent here; otherwise, the inputs from the form are used 1148 * @param {Function|null} [args.complete=null] Function which is called when the request finishes. Context is bound to the control. First argument is any error. Following arguments are for success. 1149 * @param {boolean} [args.ignoreActiveElement=false] Whether or not updating a field will be deferred if focus is still on the element. 1150 */ 1151 updateWidget: function( args ) { 1152 var self = this, instanceOverride, completeCallback, $widgetRoot, $widgetContent, 1153 updateNumber, params, data, $inputs, processing, jqxhr, isChanged; 1154 1155 // The updateWidget logic requires that the form fields to be fully present. 1156 self.embedWidgetContent(); 1157 1158 args = $.extend( { 1159 instance: null, 1160 complete: null, 1161 ignoreActiveElement: false 1162 }, args ); 1163 1164 instanceOverride = args.instance; 1165 completeCallback = args.complete; 1166 1167 this._updateCount += 1; 1168 updateNumber = this._updateCount; 1169 1170 $widgetRoot = this.container.find( '.widget:first' ); 1171 $widgetContent = $widgetRoot.find( '.widget-content:first' ); 1172 1173 // Remove a previous error message. 1174 $widgetContent.find( '.widget-error' ).remove(); 1175 1176 this.container.addClass( 'widget-form-loading' ); 1177 this.container.addClass( 'previewer-loading' ); 1178 processing = api.state( 'processing' ); 1179 processing( processing() + 1 ); 1180 1181 if ( ! this.liveUpdateMode ) { 1182 this.container.addClass( 'widget-form-disabled' ); 1183 } 1184 1185 params = {}; 1186 params.action = 'update-widget'; 1187 params.wp_customize = 'on'; 1188 params.nonce = api.settings.nonce['update-widget']; 1189 params.customize_theme = api.settings.theme.stylesheet; 1190 params.customized = wp.customize.previewer.query().customized; 1191 1192 data = $.param( params ); 1193 $inputs = this._getInputs( $widgetContent ); 1194 1195 /* 1196 * Store the value we're submitting in data so that when the response comes back, 1197 * we know if it got sanitized; if there is no difference in the sanitized value, 1198 * then we do not need to touch the UI and mess up the user's ongoing editing. 1199 */ 1200 $inputs.each( function() { 1201 $( this ).data( 'state' + updateNumber, self._getInputState( this ) ); 1202 } ); 1203 1204 if ( instanceOverride ) { 1205 data += '&' + $.param( { 'sanitized_widget_setting': JSON.stringify( instanceOverride ) } ); 1206 } else { 1207 data += '&' + $inputs.serialize(); 1208 } 1209 data += '&' + $widgetContent.find( '~ :input' ).serialize(); 1210 1211 if ( this._previousUpdateRequest ) { 1212 this._previousUpdateRequest.abort(); 1213 } 1214 jqxhr = $.post( wp.ajax.settings.url, data ); 1215 this._previousUpdateRequest = jqxhr; 1216 1217 jqxhr.done( function( r ) { 1218 var message, sanitizedForm, $sanitizedInputs, hasSameInputsInResponse, 1219 isLiveUpdateAborted = false; 1220 1221 // Check if the user is logged out. 1222 if ( '0' === r ) { 1223 api.previewer.preview.iframe.hide(); 1224 api.previewer.login().done( function() { 1225 self.updateWidget( args ); 1226 api.previewer.preview.iframe.show(); 1227 } ); 1228 return; 1229 } 1230 1231 // Check for cheaters. 1232 if ( '-1' === r ) { 1233 api.previewer.cheatin(); 1234 return; 1235 } 1236 1237 if ( r.success ) { 1238 sanitizedForm = $( '<div>' + r.data.form + '</div>' ); 1239 $sanitizedInputs = self._getInputs( sanitizedForm ); 1240 hasSameInputsInResponse = self._getInputsSignature( $inputs ) === self._getInputsSignature( $sanitizedInputs ); 1241 1242 // Restore live update mode if sanitized fields are now aligned with the existing fields. 1243 if ( hasSameInputsInResponse && ! self.liveUpdateMode ) { 1244 self.liveUpdateMode = true; 1245 self.container.removeClass( 'widget-form-disabled' ); 1246 self.container.find( 'input[name="savewidget"]' ).hide(); 1247 } 1248 1249 // Sync sanitized field states to existing fields if they are aligned. 1250 if ( hasSameInputsInResponse && self.liveUpdateMode ) { 1251 $inputs.each( function( i ) { 1252 var $input = $( this ), 1253 $sanitizedInput = $( $sanitizedInputs[i] ), 1254 submittedState, sanitizedState, canUpdateState; 1255 1256 submittedState = $input.data( 'state' + updateNumber ); 1257 sanitizedState = self._getInputState( $sanitizedInput ); 1258 $input.data( 'sanitized', sanitizedState ); 1259 1260 canUpdateState = ( ! _.isEqual( submittedState, sanitizedState ) && ( args.ignoreActiveElement || ! $input.is( document.activeElement ) ) ); 1261 if ( canUpdateState ) { 1262 self._setInputState( $input, sanitizedState ); 1263 } 1264 } ); 1265 1266 $( document ).trigger( 'widget-synced', [ $widgetRoot, r.data.form ] ); 1267 1268 // Otherwise, if sanitized fields are not aligned with existing fields, disable live update mode if enabled. 1269 } else if ( self.liveUpdateMode ) { 1270 self.liveUpdateMode = false; 1271 self.container.find( 'input[name="savewidget"]' ).show(); 1272 isLiveUpdateAborted = true; 1273 1274 // Otherwise, replace existing form with the sanitized form. 1275 } else { 1276 $widgetContent.html( r.data.form ); 1277 1278 self.container.removeClass( 'widget-form-disabled' ); 1279 1280 $( document ).trigger( 'widget-updated', [ $widgetRoot ] ); 1281 } 1282 1283 /** 1284 * If the old instance is identical to the new one, there is nothing new 1285 * needing to be rendered, and so we can preempt the event for the 1286 * preview finishing loading. 1287 */ 1288 isChanged = ! isLiveUpdateAborted && ! _( self.setting() ).isEqual( r.data.instance ); 1289 if ( isChanged ) { 1290 self.isWidgetUpdating = true; // Suppress triggering another updateWidget. 1291 self.setting( r.data.instance ); 1292 self.isWidgetUpdating = false; 1293 } else { 1294 // No change was made, so stop the spinner now instead of when the preview would updates. 1295 self.container.removeClass( 'previewer-loading' ); 1296 } 1297 1298 if ( completeCallback ) { 1299 completeCallback.call( self, null, { noChange: ! isChanged, ajaxFinished: true } ); 1300 } 1301 } else { 1302 // General error message. 1303 message = l10n.error; 1304 1305 if ( r.data && r.data.message ) { 1306 message = r.data.message; 1307 } 1308 1309 if ( completeCallback ) { 1310 completeCallback.call( self, message ); 1311 } else { 1312 $widgetContent.prepend( '<p class="widget-error"><strong>' + message + '</strong></p>' ); 1313 } 1314 } 1315 } ); 1316 1317 jqxhr.fail( function( jqXHR, textStatus ) { 1318 if ( completeCallback ) { 1319 completeCallback.call( self, textStatus ); 1320 } 1321 } ); 1322 1323 jqxhr.always( function() { 1324 self.container.removeClass( 'widget-form-loading' ); 1325 1326 $inputs.each( function() { 1327 $( this ).removeData( 'state' + updateNumber ); 1328 } ); 1329 1330 processing( processing() - 1 ); 1331 } ); 1332 }, 1333 1334 /** 1335 * Expand the accordion section containing a control 1336 */ 1337 expandControlSection: function() { 1338 api.Control.prototype.expand.call( this ); 1339 }, 1340 1341 /** 1342 * @since 4.1.0 1343 * 1344 * @param {Boolean} expanded 1345 * @param {Object} [params] 1346 * @return {Boolean} False if state already applied. 1347 */ 1348 _toggleExpanded: api.Section.prototype._toggleExpanded, 1349 1350 /** 1351 * @since 4.1.0 1352 * 1353 * @param {Object} [params] 1354 * @return {Boolean} False if already expanded. 1355 */ 1356 expand: api.Section.prototype.expand, 1357 1358 /** 1359 * Expand the widget form control 1360 * 1361 * @deprecated 4.1.0 Use this.expand() instead. 1362 */ 1363 expandForm: function() { 1364 this.expand(); 1365 }, 1366 1367 /** 1368 * @since 4.1.0 1369 * 1370 * @param {Object} [params] 1371 * @return {Boolean} False if already collapsed. 1372 */ 1373 collapse: api.Section.prototype.collapse, 1374 1375 /** 1376 * Collapse the widget form control 1377 * 1378 * @deprecated 4.1.0 Use this.collapse() instead. 1379 */ 1380 collapseForm: function() { 1381 this.collapse(); 1382 }, 1383 1384 /** 1385 * Expand or collapse the widget control 1386 * 1387 * @deprecated this is poor naming, and it is better to directly set control.expanded( showOrHide ) 1388 * 1389 * @param {boolean|undefined} [showOrHide] If not supplied, will be inverse of current visibility 1390 */ 1391 toggleForm: function( showOrHide ) { 1392 if ( typeof showOrHide === 'undefined' ) { 1393 showOrHide = ! this.expanded(); 1394 } 1395 this.expanded( showOrHide ); 1396 }, 1397 1398 /** 1399 * Respond to change in the expanded state. 1400 * 1401 * @param {boolean} expanded 1402 * @param {Object} args merged on top of this.defaultActiveArguments 1403 */ 1404 onChangeExpanded: function ( expanded, args ) { 1405 var self = this, $widget, $inside, complete, prevComplete, expandControl, $toggleBtn; 1406 1407 self.embedWidgetControl(); // Make sure the outer form is embedded so that the expanded state can be set in the UI. 1408 if ( expanded ) { 1409 self.embedWidgetContent(); 1410 } 1411 1412 // If the expanded state is unchanged only manipulate container expanded states. 1413 if ( args.unchanged ) { 1414 if ( expanded ) { 1415 api.Control.prototype.expand.call( self, { 1416 completeCallback: args.completeCallback 1417 }); 1418 } 1419 return; 1420 } 1421 1422 $widget = this.container.find( 'div.widget:first' ); 1423 $inside = $widget.find( '.widget-inside:first' ); 1424 $toggleBtn = this.container.find( '.widget-top button.widget-action' ); 1425 1426 expandControl = function() { 1427 1428 // Close all other widget controls before expanding this one. 1429 api.control.each( function( otherControl ) { 1430 if ( self.params.type === otherControl.params.type && self !== otherControl ) { 1431 otherControl.collapse(); 1432 } 1433 } ); 1434 1435 complete = function() { 1436 self.container.removeClass( 'expanding' ); 1437 self.container.addClass( 'expanded' ); 1438 $widget.addClass( 'open' ); 1439 $toggleBtn.attr( 'aria-expanded', 'true' ); 1440 self.container.trigger( 'expanded' ); 1441 }; 1442 if ( args.completeCallback ) { 1443 prevComplete = complete; 1444 complete = function () { 1445 prevComplete(); 1446 args.completeCallback(); 1447 }; 1448 } 1449 1450 if ( self.params.is_wide ) { 1451 $inside.fadeIn( args.duration, complete ); 1452 } else { 1453 $inside.slideDown( args.duration, complete ); 1454 } 1455 1456 self.container.trigger( 'expand' ); 1457 self.container.addClass( 'expanding' ); 1458 }; 1459 1460 if ( $toggleBtn.attr( 'aria-expanded' ) === 'false' ) { 1461 if ( api.section.has( self.section() ) ) { 1462 api.section( self.section() ).expand( { 1463 completeCallback: expandControl 1464 } ); 1465 } else { 1466 expandControl(); 1467 } 1468 } else { 1469 complete = function() { 1470 self.container.removeClass( 'collapsing' ); 1471 self.container.removeClass( 'expanded' ); 1472 $widget.removeClass( 'open' ); 1473 $toggleBtn.attr( 'aria-expanded', 'false' ); 1474 self.container.trigger( 'collapsed' ); 1475 }; 1476 if ( args.completeCallback ) { 1477 prevComplete = complete; 1478 complete = function () { 1479 prevComplete(); 1480 args.completeCallback(); 1481 }; 1482 } 1483 1484 self.container.trigger( 'collapse' ); 1485 self.container.addClass( 'collapsing' ); 1486 1487 if ( self.params.is_wide ) { 1488 $inside.fadeOut( args.duration, complete ); 1489 } else { 1490 $inside.slideUp( args.duration, function() { 1491 $widget.css( { width:'', margin:'' } ); 1492 complete(); 1493 } ); 1494 } 1495 } 1496 }, 1497 1498 /** 1499 * Get the position (index) of the widget in the containing sidebar 1500 * 1501 * @return {number} 1502 */ 1503 getWidgetSidebarPosition: function() { 1504 var sidebarWidgetIds, position; 1505 1506 sidebarWidgetIds = this.getSidebarWidgetsControl().setting(); 1507 position = _.indexOf( sidebarWidgetIds, this.params.widget_id ); 1508 1509 if ( position === -1 ) { 1510 return; 1511 } 1512 1513 return position; 1514 }, 1515 1516 /** 1517 * Move widget up one in the sidebar 1518 */ 1519 moveUp: function() { 1520 this._moveWidgetByOne( -1 ); 1521 }, 1522 1523 /** 1524 * Move widget up one in the sidebar 1525 */ 1526 moveDown: function() { 1527 this._moveWidgetByOne( 1 ); 1528 }, 1529 1530 /** 1531 * @private 1532 * 1533 * @param {number} offset 1|-1 1534 */ 1535 _moveWidgetByOne: function( offset ) { 1536 var i, sidebarWidgetsSetting, sidebarWidgetIds, adjacentWidgetId; 1537 1538 i = this.getWidgetSidebarPosition(); 1539 1540 sidebarWidgetsSetting = this.getSidebarWidgetsControl().setting; 1541 sidebarWidgetIds = Array.prototype.slice.call( sidebarWidgetsSetting() ); // Clone. 1542 adjacentWidgetId = sidebarWidgetIds[i + offset]; 1543 sidebarWidgetIds[i + offset] = this.params.widget_id; 1544 sidebarWidgetIds[i] = adjacentWidgetId; 1545 1546 sidebarWidgetsSetting( sidebarWidgetIds ); 1547 }, 1548 1549 /** 1550 * Toggle visibility of the widget move area 1551 * 1552 * @param {boolean} [showOrHide] 1553 */ 1554 toggleWidgetMoveArea: function( showOrHide ) { 1555 var self = this, $moveWidgetArea; 1556 1557 $moveWidgetArea = this.container.find( '.move-widget-area' ); 1558 1559 if ( typeof showOrHide === 'undefined' ) { 1560 showOrHide = ! $moveWidgetArea.hasClass( 'active' ); 1561 } 1562 1563 if ( showOrHide ) { 1564 // Reset the selected sidebar. 1565 $moveWidgetArea.find( '.selected' ).removeClass( 'selected' ); 1566 1567 $moveWidgetArea.find( 'li' ).filter( function() { 1568 return $( this ).data( 'id' ) === self.params.sidebar_id; 1569 } ).addClass( 'selected' ); 1570 1571 this.container.find( '.move-widget-btn' ).prop( 'disabled', true ); 1572 } 1573 1574 $moveWidgetArea.toggleClass( 'active', showOrHide ); 1575 }, 1576 1577 /** 1578 * Highlight the widget control and section 1579 */ 1580 highlightSectionAndControl: function() { 1581 var $target; 1582 1583 if ( this.container.is( ':hidden' ) ) { 1584 $target = this.container.closest( '.control-section' ); 1585 } else { 1586 $target = this.container; 1587 } 1588 1589 $( '.highlighted' ).removeClass( 'highlighted' ); 1590 $target.addClass( 'highlighted' ); 1591 1592 setTimeout( function() { 1593 $target.removeClass( 'highlighted' ); 1594 }, 500 ); 1595 } 1596 } ); 1597 1598 /** 1599 * wp.customize.Widgets.WidgetsPanel 1600 * 1601 * Customizer panel containing the widget area sections. 1602 * 1603 * @since 4.4.0 1604 * 1605 * @class wp.customize.Widgets.WidgetsPanel 1606 * @augments wp.customize.Panel 1607 */ 1608 api.Widgets.WidgetsPanel = api.Panel.extend(/** @lends wp.customize.Widgets.WigetsPanel.prototype */{ 1609 1610 /** 1611 * Add and manage the display of the no-rendered-areas notice. 1612 * 1613 * @since 4.4.0 1614 */ 1615 ready: function () { 1616 var panel = this; 1617 1618 api.Panel.prototype.ready.call( panel ); 1619 1620 panel.deferred.embedded.done(function() { 1621 var panelMetaContainer, noticeContainer, updateNotice, getActiveSectionCount, shouldShowNotice; 1622 panelMetaContainer = panel.container.find( '.panel-meta' ); 1623 1624 // @todo This should use the Notifications API introduced to panels. See <https://core.trac.wordpress.org/ticket/38794>. 1625 noticeContainer = $( '<div></div>', { 1626 'class': 'no-widget-areas-rendered-notice' 1627 }); 1628 panelMetaContainer.append( noticeContainer ); 1629 1630 /** 1631 * Get the number of active sections in the panel. 1632 * 1633 * @return {number} Number of active sidebar sections. 1634 */ 1635 getActiveSectionCount = function() { 1636 return _.filter( panel.sections(), function( section ) { 1637 return 'sidebar' === section.params.type && section.active(); 1638 } ).length; 1639 }; 1640 1641 /** 1642 * Determine whether or not the notice should be displayed. 1643 * 1644 * @return {boolean} 1645 */ 1646 shouldShowNotice = function() { 1647 var activeSectionCount = getActiveSectionCount(); 1648 if ( 0 === activeSectionCount ) { 1649 return true; 1650 } else { 1651 return activeSectionCount !== api.Widgets.data.registeredSidebars.length; 1652 } 1653 }; 1654 1655 /** 1656 * Update the notice. 1657 * 1658 * @return {void} 1659 */ 1660 updateNotice = function() { 1661 var activeSectionCount = getActiveSectionCount(), someRenderedMessage, nonRenderedAreaCount, registeredAreaCount; 1662 noticeContainer.empty(); 1663 1664 registeredAreaCount = api.Widgets.data.registeredSidebars.length; 1665 if ( activeSectionCount !== registeredAreaCount ) { 1666 1667 if ( 0 !== activeSectionCount ) { 1668 nonRenderedAreaCount = registeredAreaCount - activeSectionCount; 1669 someRenderedMessage = l10n.someAreasShown[ nonRenderedAreaCount ]; 1670 } else { 1671 someRenderedMessage = l10n.noAreasShown; 1672 } 1673 if ( someRenderedMessage ) { 1674 noticeContainer.append( $( '<p></p>', { 1675 text: someRenderedMessage 1676 } ) ); 1677 } 1678 1679 noticeContainer.append( $( '<p></p>', { 1680 text: l10n.navigatePreview 1681 } ) ); 1682 } 1683 }; 1684 updateNotice(); 1685 1686 /* 1687 * Set the initial visibility state for rendered notice. 1688 * Update the visibility of the notice whenever a reflow happens. 1689 */ 1690 noticeContainer.toggle( shouldShowNotice() ); 1691 api.previewer.deferred.active.done( function () { 1692 noticeContainer.toggle( shouldShowNotice() ); 1693 }); 1694 api.bind( 'pane-contents-reflowed', function() { 1695 var duration = ( 'resolved' === api.previewer.deferred.active.state() ) ? 'fast' : 0; 1696 updateNotice(); 1697 if ( shouldShowNotice() ) { 1698 noticeContainer.slideDown( duration ); 1699 } else { 1700 noticeContainer.slideUp( duration ); 1701 } 1702 }); 1703 }); 1704 }, 1705 1706 /** 1707 * Allow an active widgets panel to be contextually active even when it has no active sections (widget areas). 1708 * 1709 * This ensures that the widgets panel appears even when there are no 1710 * sidebars displayed on the URL currently being previewed. 1711 * 1712 * @since 4.4.0 1713 * 1714 * @return {boolean} 1715 */ 1716 isContextuallyActive: function() { 1717 var panel = this; 1718 return panel.active(); 1719 } 1720 }); 1721 1722 /** 1723 * wp.customize.Widgets.SidebarSection 1724 * 1725 * Customizer section representing a widget area widget 1726 * 1727 * @since 4.1.0 1728 * 1729 * @class wp.customize.Widgets.SidebarSection 1730 * @augments wp.customize.Section 1731 */ 1732 api.Widgets.SidebarSection = api.Section.extend(/** @lends wp.customize.Widgets.SidebarSection.prototype */{ 1733 1734 /** 1735 * Sync the section's active state back to the Backbone model's is_rendered attribute 1736 * 1737 * @since 4.1.0 1738 */ 1739 ready: function () { 1740 var section = this, registeredSidebar; 1741 api.Section.prototype.ready.call( this ); 1742 registeredSidebar = api.Widgets.registeredSidebars.get( section.params.sidebarId ); 1743 section.active.bind( function ( active ) { 1744 registeredSidebar.set( 'is_rendered', active ); 1745 }); 1746 registeredSidebar.set( 'is_rendered', section.active() ); 1747 } 1748 }); 1749 1750 /** 1751 * wp.customize.Widgets.SidebarControl 1752 * 1753 * Customizer control for widgets. 1754 * Note that 'sidebar_widgets' must match the WP_Widget_Area_Customize_Control::$type 1755 * 1756 * @since 3.9.0 1757 * 1758 * @class wp.customize.Widgets.SidebarControl 1759 * @augments wp.customize.Control 1760 */ 1761 api.Widgets.SidebarControl = api.Control.extend(/** @lends wp.customize.Widgets.SidebarControl.prototype */{ 1762 1763 /** 1764 * Set up the control 1765 */ 1766 ready: function() { 1767 this.$controlSection = this.container.closest( '.control-section' ); 1768 this.$sectionContent = this.container.closest( '.accordion-section-content' ); 1769 1770 this._setupModel(); 1771 this._setupSortable(); 1772 this._setupAddition(); 1773 this._applyCardinalOrderClassNames(); 1774 }, 1775 1776 /** 1777 * Update ordering of widget control forms when the setting is updated 1778 */ 1779 _setupModel: function() { 1780 var self = this; 1781 1782 this.setting.bind( function( newWidgetIds, oldWidgetIds ) { 1783 var widgetFormControls, removedWidgetIds, priority; 1784 1785 removedWidgetIds = _( oldWidgetIds ).difference( newWidgetIds ); 1786 1787 // Filter out any persistent widget IDs for widgets which have been deactivated. 1788 newWidgetIds = _( newWidgetIds ).filter( function( newWidgetId ) { 1789 var parsedWidgetId = parseWidgetId( newWidgetId ); 1790 1791 return !! api.Widgets.availableWidgets.findWhere( { id_base: parsedWidgetId.id_base } ); 1792 } ); 1793 1794 widgetFormControls = _( newWidgetIds ).map( function( widgetId ) { 1795 var widgetFormControl = api.Widgets.getWidgetFormControlForWidget( widgetId ); 1796 1797 if ( ! widgetFormControl ) { 1798 widgetFormControl = self.addWidget( widgetId ); 1799 } 1800 1801 return widgetFormControl; 1802 } ); 1803 1804 // Sort widget controls to their new positions. 1805 widgetFormControls.sort( function( a, b ) { 1806 var aIndex = _.indexOf( newWidgetIds, a.params.widget_id ), 1807 bIndex = _.indexOf( newWidgetIds, b.params.widget_id ); 1808 return aIndex - bIndex; 1809 }); 1810 1811 priority = 0; 1812 _( widgetFormControls ).each( function ( control ) { 1813 control.priority( priority ); 1814 control.section( self.section() ); 1815 priority += 1; 1816 }); 1817 self.priority( priority ); // Make sure sidebar control remains at end. 1818 1819 // Re-sort widget form controls (including widgets form other sidebars newly moved here). 1820 self._applyCardinalOrderClassNames(); 1821 1822 // If the widget was dragged into the sidebar, make sure the sidebar_id param is updated. 1823 _( widgetFormControls ).each( function( widgetFormControl ) { 1824 widgetFormControl.params.sidebar_id = self.params.sidebar_id; 1825 } ); 1826 1827 // Cleanup after widget removal. 1828 _( removedWidgetIds ).each( function( removedWidgetId ) { 1829 1830 // Using setTimeout so that when moving a widget to another sidebar, 1831 // the other sidebars_widgets settings get a chance to update. 1832 setTimeout( function() { 1833 var removedControl, wasDraggedToAnotherSidebar, inactiveWidgets, removedIdBase, 1834 widget, isPresentInAnotherSidebar = false; 1835 1836 // Check if the widget is in another sidebar. 1837 api.each( function( otherSetting ) { 1838 if ( otherSetting.id === self.setting.id || 0 !== otherSetting.id.indexOf( 'sidebars_widgets[' ) || otherSetting.id === 'sidebars_widgets[wp_inactive_widgets]' ) { 1839 return; 1840 } 1841 1842 var otherSidebarWidgets = otherSetting(), i; 1843 1844 i = _.indexOf( otherSidebarWidgets, removedWidgetId ); 1845 if ( -1 !== i ) { 1846 isPresentInAnotherSidebar = true; 1847 } 1848 } ); 1849 1850 // If the widget is present in another sidebar, abort! 1851 if ( isPresentInAnotherSidebar ) { 1852 return; 1853 } 1854 1855 removedControl = api.Widgets.getWidgetFormControlForWidget( removedWidgetId ); 1856 1857 // Detect if widget control was dragged to another sidebar. 1858 wasDraggedToAnotherSidebar = removedControl && $.contains( document, removedControl.container[0] ) && ! $.contains( self.$sectionContent[0], removedControl.container[0] ); 1859 1860 // Delete any widget form controls for removed widgets. 1861 if ( removedControl && ! wasDraggedToAnotherSidebar ) { 1862 api.control.remove( removedControl.id ); 1863 removedControl.container.remove(); 1864 } 1865 1866 // Move widget to inactive widgets sidebar (move it to Trash) if has been previously saved. 1867 // This prevents the inactive widgets sidebar from overflowing with throwaway widgets. 1868 if ( api.Widgets.savedWidgetIds[removedWidgetId] ) { 1869 inactiveWidgets = api.value( 'sidebars_widgets[wp_inactive_widgets]' )().slice(); 1870 inactiveWidgets.push( removedWidgetId ); 1871 api.value( 'sidebars_widgets[wp_inactive_widgets]' )( _( inactiveWidgets ).unique() ); 1872 } 1873 1874 // Make old single widget available for adding again. 1875 removedIdBase = parseWidgetId( removedWidgetId ).id_base; 1876 widget = api.Widgets.availableWidgets.findWhere( { id_base: removedIdBase } ); 1877 if ( widget && ! widget.get( 'is_multi' ) ) { 1878 widget.set( 'is_disabled', false ); 1879 } 1880 } ); 1881 1882 } ); 1883 } ); 1884 }, 1885 1886 /** 1887 * Allow widgets in sidebar to be re-ordered, and for the order to be previewed 1888 */ 1889 _setupSortable: function() { 1890 var self = this; 1891 1892 this.isReordering = false; 1893 1894 /** 1895 * Update widget order setting when controls are re-ordered 1896 */ 1897 this.$sectionContent.sortable( { 1898 items: '> .customize-control-widget_form', 1899 handle: '.widget-top', 1900 axis: 'y', 1901 tolerance: 'pointer', 1902 connectWith: '.accordion-section-content:has(.customize-control-sidebar_widgets)', 1903 update: function() { 1904 var widgetContainerIds = self.$sectionContent.sortable( 'toArray' ), widgetIds; 1905 1906 widgetIds = $.map( widgetContainerIds, function( widgetContainerId ) { 1907 return $( '#' + widgetContainerId ).find( ':input[name=widget-id]' ).val(); 1908 } ); 1909 1910 self.setting( widgetIds ); 1911 } 1912 } ); 1913 1914 /** 1915 * Expand other Customizer sidebar section when dragging a control widget over it, 1916 * allowing the control to be dropped into another section 1917 */ 1918 this.$controlSection.find( '.accordion-section-title' ).droppable({ 1919 accept: '.customize-control-widget_form', 1920 over: function() { 1921 var section = api.section( self.section.get() ); 1922 section.expand({ 1923 allowMultiple: true, // Prevent the section being dragged from to be collapsed. 1924 completeCallback: function () { 1925 // @todo It is not clear when refreshPositions should be called on which sections, or if it is even needed. 1926 api.section.each( function ( otherSection ) { 1927 if ( otherSection.container.find( '.customize-control-sidebar_widgets' ).length ) { 1928 otherSection.container.find( '.accordion-section-content:first' ).sortable( 'refreshPositions' ); 1929 } 1930 } ); 1931 } 1932 }); 1933 } 1934 }); 1935 1936 /** 1937 * Keyboard-accessible reordering 1938 */ 1939 this.container.find( '.reorder-toggle' ).on( 'click', function() { 1940 self.toggleReordering( ! self.isReordering ); 1941 } ); 1942 }, 1943 1944 /** 1945 * Set up UI for adding a new widget 1946 */ 1947 _setupAddition: function() { 1948 var self = this; 1949 1950 this.container.find( '.add-new-widget' ).on( 'click', function() { 1951 var addNewWidgetBtn = $( this ); 1952 1953 if ( self.$sectionContent.hasClass( 'reordering' ) ) { 1954 return; 1955 } 1956 1957 if ( ! $( 'body' ).hasClass( 'adding-widget' ) ) { 1958 addNewWidgetBtn.attr( 'aria-expanded', 'true' ); 1959 api.Widgets.availableWidgetsPanel.open( self ); 1960 } else { 1961 addNewWidgetBtn.attr( 'aria-expanded', 'false' ); 1962 api.Widgets.availableWidgetsPanel.close(); 1963 } 1964 } ); 1965 }, 1966 1967 /** 1968 * Add classes to the widget_form controls to assist with styling 1969 */ 1970 _applyCardinalOrderClassNames: function() { 1971 var widgetControls = []; 1972 _.each( this.setting(), function ( widgetId ) { 1973 var widgetControl = api.Widgets.getWidgetFormControlForWidget( widgetId ); 1974 if ( widgetControl ) { 1975 widgetControls.push( widgetControl ); 1976 } 1977 }); 1978 1979 if ( 0 === widgetControls.length || ( 1 === api.Widgets.registeredSidebars.length && widgetControls.length <= 1 ) ) { 1980 this.container.find( '.reorder-toggle' ).hide(); 1981 return; 1982 } else { 1983 this.container.find( '.reorder-toggle' ).show(); 1984 } 1985 1986 $( widgetControls ).each( function () { 1987 $( this.container ) 1988 .removeClass( 'first-widget' ) 1989 .removeClass( 'last-widget' ) 1990 .find( '.move-widget-down, .move-widget-up' ).prop( 'tabIndex', 0 ); 1991 }); 1992 1993 _.first( widgetControls ).container 1994 .addClass( 'first-widget' ) 1995 .find( '.move-widget-up' ).prop( 'tabIndex', -1 ); 1996 1997 _.last( widgetControls ).container 1998 .addClass( 'last-widget' ) 1999 .find( '.move-widget-down' ).prop( 'tabIndex', -1 ); 2000 }, 2001 2002 2003 /*********************************************************************** 2004 * Begin public API methods 2005 **********************************************************************/ 2006 2007 /** 2008 * Enable/disable the reordering UI 2009 * 2010 * @param {boolean} showOrHide to enable/disable reordering 2011 * 2012 * @todo We should have a reordering state instead and rename this to onChangeReordering 2013 */ 2014 toggleReordering: function( showOrHide ) { 2015 var addNewWidgetBtn = this.$sectionContent.find( '.add-new-widget' ), 2016 reorderBtn = this.container.find( '.reorder-toggle' ), 2017 widgetsTitle = this.$sectionContent.find( '.widget-title' ); 2018 2019 showOrHide = Boolean( showOrHide ); 2020 2021 if ( showOrHide === this.$sectionContent.hasClass( 'reordering' ) ) { 2022 return; 2023 } 2024 2025 this.isReordering = showOrHide; 2026 this.$sectionContent.toggleClass( 'reordering', showOrHide ); 2027 2028 if ( showOrHide ) { 2029 _( this.getWidgetFormControls() ).each( function( formControl ) { 2030 formControl.collapse(); 2031 } ); 2032 2033 addNewWidgetBtn.attr({ 'tabindex': '-1', 'aria-hidden': 'true' }); 2034 reorderBtn.attr( 'aria-label', l10n.reorderLabelOff ); 2035 wp.a11y.speak( l10n.reorderModeOn ); 2036 // Hide widget titles while reordering: title is already in the reorder controls. 2037 widgetsTitle.attr( 'aria-hidden', 'true' ); 2038 } else { 2039 addNewWidgetBtn.removeAttr( 'tabindex aria-hidden' ); 2040 reorderBtn.attr( 'aria-label', l10n.reorderLabelOn ); 2041 wp.a11y.speak( l10n.reorderModeOff ); 2042 widgetsTitle.attr( 'aria-hidden', 'false' ); 2043 } 2044 }, 2045 2046 /** 2047 * Get the widget_form Customize controls associated with the current sidebar. 2048 * 2049 * @since 3.9.0 2050 * @return {wp.customize.controlConstructor.widget_form[]} 2051 */ 2052 getWidgetFormControls: function() { 2053 var formControls = []; 2054 2055 _( this.setting() ).each( function( widgetId ) { 2056 var settingId = widgetIdToSettingId( widgetId ), 2057 formControl = api.control( settingId ); 2058 if ( formControl ) { 2059 formControls.push( formControl ); 2060 } 2061 } ); 2062 2063 return formControls; 2064 }, 2065 2066 /** 2067 * @param {string} widgetId or an id_base for adding a previously non-existing widget. 2068 * @return {Object|false} widget_form control instance, or false on error. 2069 */ 2070 addWidget: function( widgetId ) { 2071 var self = this, controlHtml, $widget, controlType = 'widget_form', controlContainer, controlConstructor, 2072 parsedWidgetId = parseWidgetId( widgetId ), 2073 widgetNumber = parsedWidgetId.number, 2074 widgetIdBase = parsedWidgetId.id_base, 2075 widget = api.Widgets.availableWidgets.findWhere( {id_base: widgetIdBase} ), 2076 settingId, isExistingWidget, widgetFormControl, sidebarWidgets, settingArgs, setting; 2077 2078 if ( ! widget ) { 2079 return false; 2080 } 2081 2082 if ( widgetNumber && ! widget.get( 'is_multi' ) ) { 2083 return false; 2084 } 2085 2086 // Set up new multi widget. 2087 if ( widget.get( 'is_multi' ) && ! widgetNumber ) { 2088 widget.set( 'multi_number', widget.get( 'multi_number' ) + 1 ); 2089 widgetNumber = widget.get( 'multi_number' ); 2090 } 2091 2092 controlHtml = $( '#widget-tpl-' + widget.get( 'id' ) ).html().trim(); 2093 if ( widget.get( 'is_multi' ) ) { 2094 controlHtml = controlHtml.replace( /<[^<>]+>/g, function( m ) { 2095 return m.replace( /__i__|%i%/g, widgetNumber ); 2096 } ); 2097 } else { 2098 widget.set( 'is_disabled', true ); // Prevent single widget from being added again now. 2099 } 2100 2101 $widget = $( controlHtml ); 2102 2103 controlContainer = $( '<li/>' ) 2104 .addClass( 'customize-control' ) 2105 .addClass( 'customize-control-' + controlType ) 2106 .append( $widget ); 2107 2108 // Remove icon which is visible inside the panel. 2109 controlContainer.find( '> .widget-icon' ).remove(); 2110 2111 if ( widget.get( 'is_multi' ) ) { 2112 controlContainer.find( 'input[name="widget_number"]' ).val( widgetNumber ); 2113 controlContainer.find( 'input[name="multi_number"]' ).val( widgetNumber ); 2114 } 2115 2116 widgetId = controlContainer.find( '[name="widget-id"]' ).val(); 2117 2118 controlContainer.hide(); // To be slid-down below. 2119 2120 settingId = 'widget_' + widget.get( 'id_base' ); 2121 if ( widget.get( 'is_multi' ) ) { 2122 settingId += '[' + widgetNumber + ']'; 2123 } 2124 controlContainer.attr( 'id', 'customize-control-' + settingId.replace( /\]/g, '' ).replace( /\[/g, '-' ) ); 2125 2126 // Only create setting if it doesn't already exist (if we're adding a pre-existing inactive widget). 2127 isExistingWidget = api.has( settingId ); 2128 if ( ! isExistingWidget ) { 2129 settingArgs = { 2130 transport: api.Widgets.data.selectiveRefreshableWidgets[ widget.get( 'id_base' ) ] ? 'postMessage' : 'refresh', 2131 previewer: this.setting.previewer 2132 }; 2133 setting = api.create( settingId, settingId, '', settingArgs ); 2134 setting.set( {} ); // Mark dirty, changing from '' to {}. 2135 } 2136 2137 controlConstructor = api.controlConstructor[controlType]; 2138 widgetFormControl = new controlConstructor( settingId, { 2139 settings: { 2140 'default': settingId 2141 }, 2142 content: controlContainer, 2143 sidebar_id: self.params.sidebar_id, 2144 widget_id: widgetId, 2145 widget_id_base: widget.get( 'id_base' ), 2146 type: controlType, 2147 is_new: ! isExistingWidget, 2148 width: widget.get( 'width' ), 2149 height: widget.get( 'height' ), 2150 is_wide: widget.get( 'is_wide' ) 2151 } ); 2152 api.control.add( widgetFormControl ); 2153 2154 // Make sure widget is removed from the other sidebars. 2155 api.each( function( otherSetting ) { 2156 if ( otherSetting.id === self.setting.id ) { 2157 return; 2158 } 2159 2160 if ( 0 !== otherSetting.id.indexOf( 'sidebars_widgets[' ) ) { 2161 return; 2162 } 2163 2164 var otherSidebarWidgets = otherSetting().slice(), 2165 i = _.indexOf( otherSidebarWidgets, widgetId ); 2166 2167 if ( -1 !== i ) { 2168 otherSidebarWidgets.splice( i ); 2169 otherSetting( otherSidebarWidgets ); 2170 } 2171 } ); 2172 2173 // Add widget to this sidebar. 2174 sidebarWidgets = this.setting().slice(); 2175 if ( -1 === _.indexOf( sidebarWidgets, widgetId ) ) { 2176 sidebarWidgets.push( widgetId ); 2177 this.setting( sidebarWidgets ); 2178 } 2179 2180 controlContainer.slideDown( function() { 2181 if ( isExistingWidget ) { 2182 widgetFormControl.updateWidget( { 2183 instance: widgetFormControl.setting() 2184 } ); 2185 } 2186 } ); 2187 2188 return widgetFormControl; 2189 } 2190 } ); 2191 2192 // Register models for custom panel, section, and control types. 2193 $.extend( api.panelConstructor, { 2194 widgets: api.Widgets.WidgetsPanel 2195 }); 2196 $.extend( api.sectionConstructor, { 2197 sidebar: api.Widgets.SidebarSection 2198 }); 2199 $.extend( api.controlConstructor, { 2200 widget_form: api.Widgets.WidgetControl, 2201 sidebar_widgets: api.Widgets.SidebarControl 2202 }); 2203 2204 /** 2205 * Init Customizer for widgets. 2206 */ 2207 api.bind( 'ready', function() { 2208 // Set up the widgets panel. 2209 api.Widgets.availableWidgetsPanel = new api.Widgets.AvailableWidgetsPanelView({ 2210 collection: api.Widgets.availableWidgets 2211 }); 2212 2213 // Highlight widget control. 2214 api.previewer.bind( 'highlight-widget-control', api.Widgets.highlightWidgetFormControl ); 2215 2216 // Open and focus widget control. 2217 api.previewer.bind( 'focus-widget-control', api.Widgets.focusWidgetFormControl ); 2218 } ); 2219 2220 /** 2221 * Highlight a widget control. 2222 * 2223 * @param {string} widgetId 2224 */ 2225 api.Widgets.highlightWidgetFormControl = function( widgetId ) { 2226 var control = api.Widgets.getWidgetFormControlForWidget( widgetId ); 2227 2228 if ( control ) { 2229 control.highlightSectionAndControl(); 2230 } 2231 }, 2232 2233 /** 2234 * Focus a widget control. 2235 * 2236 * @param {string} widgetId 2237 */ 2238 api.Widgets.focusWidgetFormControl = function( widgetId ) { 2239 var control = api.Widgets.getWidgetFormControlForWidget( widgetId ); 2240 2241 if ( control ) { 2242 control.focus(); 2243 } 2244 }, 2245 2246 /** 2247 * Given a widget control, find the sidebar widgets control that contains it. 2248 * @param {string} widgetId 2249 * @return {Object|null} 2250 */ 2251 api.Widgets.getSidebarWidgetControlContainingWidget = function( widgetId ) { 2252 var foundControl = null; 2253 2254 // @todo This can use widgetIdToSettingId(), then pass into wp.customize.control( x ).getSidebarWidgetsControl(). 2255 api.control.each( function( control ) { 2256 if ( control.params.type === 'sidebar_widgets' && -1 !== _.indexOf( control.setting(), widgetId ) ) { 2257 foundControl = control; 2258 } 2259 } ); 2260 2261 return foundControl; 2262 }; 2263 2264 /** 2265 * Given a widget ID for a widget appearing in the preview, get the widget form control associated with it. 2266 * 2267 * @param {string} widgetId 2268 * @return {Object|null} 2269 */ 2270 api.Widgets.getWidgetFormControlForWidget = function( widgetId ) { 2271 var foundControl = null; 2272 2273 // @todo We can just use widgetIdToSettingId() here. 2274 api.control.each( function( control ) { 2275 if ( control.params.type === 'widget_form' && control.params.widget_id === widgetId ) { 2276 foundControl = control; 2277 } 2278 } ); 2279 2280 return foundControl; 2281 }; 2282 2283 /** 2284 * Initialize Edit Menu button in Nav Menu widget. 2285 */ 2286 $( document ).on( 'widget-added', function( event, widgetContainer ) { 2287 var parsedWidgetId, widgetControl, navMenuSelect, editMenuButton; 2288 parsedWidgetId = parseWidgetId( widgetContainer.find( '> .widget-inside > .form > .widget-id' ).val() ); 2289 if ( 'nav_menu' !== parsedWidgetId.id_base ) { 2290 return; 2291 } 2292 widgetControl = api.control( 'widget_nav_menu[' + String( parsedWidgetId.number ) + ']' ); 2293 if ( ! widgetControl ) { 2294 return; 2295 } 2296 navMenuSelect = widgetContainer.find( 'select[name*="nav_menu"]' ); 2297 editMenuButton = widgetContainer.find( '.edit-selected-nav-menu > button' ); 2298 if ( 0 === navMenuSelect.length || 0 === editMenuButton.length ) { 2299 return; 2300 } 2301 navMenuSelect.on( 'change', function() { 2302 if ( api.section.has( 'nav_menu[' + navMenuSelect.val() + ']' ) ) { 2303 editMenuButton.parent().show(); 2304 } else { 2305 editMenuButton.parent().hide(); 2306 } 2307 }); 2308 editMenuButton.on( 'click', function() { 2309 var section = api.section( 'nav_menu[' + navMenuSelect.val() + ']' ); 2310 if ( section ) { 2311 focusConstructWithBreadcrumb( section, widgetControl ); 2312 } 2313 } ); 2314 } ); 2315 2316 /** 2317 * Focus (expand) one construct and then focus on another construct after the first is collapsed. 2318 * 2319 * This overrides the back button to serve the purpose of breadcrumb navigation. 2320 * 2321 * @param {wp.customize.Section|wp.customize.Panel|wp.customize.Control} focusConstruct - The object to initially focus. 2322 * @param {wp.customize.Section|wp.customize.Panel|wp.customize.Control} returnConstruct - The object to return focus. 2323 */ 2324 function focusConstructWithBreadcrumb( focusConstruct, returnConstruct ) { 2325 focusConstruct.focus(); 2326 function onceCollapsed( isExpanded ) { 2327 if ( ! isExpanded ) { 2328 focusConstruct.expanded.unbind( onceCollapsed ); 2329 returnConstruct.focus(); 2330 } 2331 } 2332 focusConstruct.expanded.bind( onceCollapsed ); 2333 } 2334 2335 /** 2336 * @param {string} widgetId 2337 * @return {Object} 2338 */ 2339 function parseWidgetId( widgetId ) { 2340 var matches, parsed = { 2341 number: null, 2342 id_base: null 2343 }; 2344 2345 matches = widgetId.match( /^(.+)-(\d+)$/ ); 2346 if ( matches ) { 2347 parsed.id_base = matches[1]; 2348 parsed.number = parseInt( matches[2], 10 ); 2349 } else { 2350 // Likely an old single widget. 2351 parsed.id_base = widgetId; 2352 } 2353 2354 return parsed; 2355 } 2356 2357 /** 2358 * @param {string} widgetId 2359 * @return {string} settingId 2360 */ 2361 function widgetIdToSettingId( widgetId ) { 2362 var parsed = parseWidgetId( widgetId ), settingId; 2363 2364 settingId = 'widget_' + parsed.id_base; 2365 if ( parsed.number ) { 2366 settingId += '[' + parsed.number + ']'; 2367 } 2368 2369 return settingId; 2370 } 2371 2372 })( window.wp, jQuery );
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Nov 21 01:00:03 2024 | Cross-referenced by PHPXref 0.7.1 |