[ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[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 accessibility 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 'role': 'alert' 1628 }); 1629 panelMetaContainer.append( noticeContainer ); 1630 1631 /** 1632 * Get the number of active sections in the panel. 1633 * 1634 * @return {number} Number of active sidebar sections. 1635 */ 1636 getActiveSectionCount = function() { 1637 return _.filter( panel.sections(), function( section ) { 1638 return 'sidebar' === section.params.type && section.active(); 1639 } ).length; 1640 }; 1641 1642 /** 1643 * Determine whether or not the notice should be displayed. 1644 * 1645 * @return {boolean} 1646 */ 1647 shouldShowNotice = function() { 1648 var activeSectionCount = getActiveSectionCount(); 1649 if ( 0 === activeSectionCount ) { 1650 return true; 1651 } else { 1652 return activeSectionCount !== api.Widgets.data.registeredSidebars.length; 1653 } 1654 }; 1655 1656 /** 1657 * Update the notice. 1658 * 1659 * @return {void} 1660 */ 1661 updateNotice = function() { 1662 var activeSectionCount = getActiveSectionCount(), someRenderedMessage, nonRenderedAreaCount, registeredAreaCount; 1663 noticeContainer.empty(); 1664 1665 registeredAreaCount = api.Widgets.data.registeredSidebars.length; 1666 if ( activeSectionCount !== registeredAreaCount ) { 1667 1668 if ( 0 !== activeSectionCount ) { 1669 nonRenderedAreaCount = registeredAreaCount - activeSectionCount; 1670 someRenderedMessage = l10n.someAreasShown[ nonRenderedAreaCount ]; 1671 } else { 1672 someRenderedMessage = l10n.noAreasShown; 1673 } 1674 if ( someRenderedMessage ) { 1675 noticeContainer.append( $( '<p></p>', { 1676 text: someRenderedMessage 1677 } ) ); 1678 } 1679 1680 noticeContainer.append( $( '<p></p>', { 1681 text: l10n.navigatePreview 1682 } ) ); 1683 } 1684 }; 1685 updateNotice(); 1686 1687 /* 1688 * Set the initial visibility state for rendered notice. 1689 * Update the visibility of the notice whenever a reflow happens. 1690 */ 1691 noticeContainer.toggle( shouldShowNotice() ); 1692 api.previewer.deferred.active.done( function () { 1693 noticeContainer.toggle( shouldShowNotice() ); 1694 }); 1695 api.bind( 'pane-contents-reflowed', function() { 1696 var duration = ( 'resolved' === api.previewer.deferred.active.state() ) ? 'fast' : 0; 1697 updateNotice(); 1698 if ( shouldShowNotice() ) { 1699 noticeContainer.slideDown( duration ); 1700 } else { 1701 noticeContainer.slideUp( duration ); 1702 } 1703 }); 1704 }); 1705 }, 1706 1707 /** 1708 * Allow an active widgets panel to be contextually active even when it has no active sections (widget areas). 1709 * 1710 * This ensures that the widgets panel appears even when there are no 1711 * sidebars displayed on the URL currently being previewed. 1712 * 1713 * @since 4.4.0 1714 * 1715 * @return {boolean} 1716 */ 1717 isContextuallyActive: function() { 1718 var panel = this; 1719 return panel.active(); 1720 } 1721 }); 1722 1723 /** 1724 * wp.customize.Widgets.SidebarSection 1725 * 1726 * Customizer section representing a widget area widget 1727 * 1728 * @since 4.1.0 1729 * 1730 * @class wp.customize.Widgets.SidebarSection 1731 * @augments wp.customize.Section 1732 */ 1733 api.Widgets.SidebarSection = api.Section.extend(/** @lends wp.customize.Widgets.SidebarSection.prototype */{ 1734 1735 /** 1736 * Sync the section's active state back to the Backbone model's is_rendered attribute 1737 * 1738 * @since 4.1.0 1739 */ 1740 ready: function () { 1741 var section = this, registeredSidebar; 1742 api.Section.prototype.ready.call( this ); 1743 registeredSidebar = api.Widgets.registeredSidebars.get( section.params.sidebarId ); 1744 section.active.bind( function ( active ) { 1745 registeredSidebar.set( 'is_rendered', active ); 1746 }); 1747 registeredSidebar.set( 'is_rendered', section.active() ); 1748 } 1749 }); 1750 1751 /** 1752 * wp.customize.Widgets.SidebarControl 1753 * 1754 * Customizer control for widgets. 1755 * Note that 'sidebar_widgets' must match the WP_Widget_Area_Customize_Control::$type 1756 * 1757 * @since 3.9.0 1758 * 1759 * @class wp.customize.Widgets.SidebarControl 1760 * @augments wp.customize.Control 1761 */ 1762 api.Widgets.SidebarControl = api.Control.extend(/** @lends wp.customize.Widgets.SidebarControl.prototype */{ 1763 1764 /** 1765 * Set up the control 1766 */ 1767 ready: function() { 1768 this.$controlSection = this.container.closest( '.control-section' ); 1769 this.$sectionContent = this.container.closest( '.accordion-section-content' ); 1770 1771 this._setupModel(); 1772 this._setupSortable(); 1773 this._setupAddition(); 1774 this._applyCardinalOrderClassNames(); 1775 }, 1776 1777 /** 1778 * Update ordering of widget control forms when the setting is updated 1779 */ 1780 _setupModel: function() { 1781 var self = this; 1782 1783 this.setting.bind( function( newWidgetIds, oldWidgetIds ) { 1784 var widgetFormControls, removedWidgetIds, priority; 1785 1786 removedWidgetIds = _( oldWidgetIds ).difference( newWidgetIds ); 1787 1788 // Filter out any persistent widget IDs for widgets which have been deactivated. 1789 newWidgetIds = _( newWidgetIds ).filter( function( newWidgetId ) { 1790 var parsedWidgetId = parseWidgetId( newWidgetId ); 1791 1792 return !! api.Widgets.availableWidgets.findWhere( { id_base: parsedWidgetId.id_base } ); 1793 } ); 1794 1795 widgetFormControls = _( newWidgetIds ).map( function( widgetId ) { 1796 var widgetFormControl = api.Widgets.getWidgetFormControlForWidget( widgetId ); 1797 1798 if ( ! widgetFormControl ) { 1799 widgetFormControl = self.addWidget( widgetId ); 1800 } 1801 1802 return widgetFormControl; 1803 } ); 1804 1805 // Sort widget controls to their new positions. 1806 widgetFormControls.sort( function( a, b ) { 1807 var aIndex = _.indexOf( newWidgetIds, a.params.widget_id ), 1808 bIndex = _.indexOf( newWidgetIds, b.params.widget_id ); 1809 return aIndex - bIndex; 1810 }); 1811 1812 priority = 0; 1813 _( widgetFormControls ).each( function ( control ) { 1814 control.priority( priority ); 1815 control.section( self.section() ); 1816 priority += 1; 1817 }); 1818 self.priority( priority ); // Make sure sidebar control remains at end. 1819 1820 // Re-sort widget form controls (including widgets form other sidebars newly moved here). 1821 self._applyCardinalOrderClassNames(); 1822 1823 // If the widget was dragged into the sidebar, make sure the sidebar_id param is updated. 1824 _( widgetFormControls ).each( function( widgetFormControl ) { 1825 widgetFormControl.params.sidebar_id = self.params.sidebar_id; 1826 } ); 1827 1828 // Cleanup after widget removal. 1829 _( removedWidgetIds ).each( function( removedWidgetId ) { 1830 1831 // Using setTimeout so that when moving a widget to another sidebar, 1832 // the other sidebars_widgets settings get a chance to update. 1833 setTimeout( function() { 1834 var removedControl, wasDraggedToAnotherSidebar, inactiveWidgets, removedIdBase, 1835 widget, isPresentInAnotherSidebar = false; 1836 1837 // Check if the widget is in another sidebar. 1838 api.each( function( otherSetting ) { 1839 if ( otherSetting.id === self.setting.id || 0 !== otherSetting.id.indexOf( 'sidebars_widgets[' ) || otherSetting.id === 'sidebars_widgets[wp_inactive_widgets]' ) { 1840 return; 1841 } 1842 1843 var otherSidebarWidgets = otherSetting(), i; 1844 1845 i = _.indexOf( otherSidebarWidgets, removedWidgetId ); 1846 if ( -1 !== i ) { 1847 isPresentInAnotherSidebar = true; 1848 } 1849 } ); 1850 1851 // If the widget is present in another sidebar, abort! 1852 if ( isPresentInAnotherSidebar ) { 1853 return; 1854 } 1855 1856 removedControl = api.Widgets.getWidgetFormControlForWidget( removedWidgetId ); 1857 1858 // Detect if widget control was dragged to another sidebar. 1859 wasDraggedToAnotherSidebar = removedControl && $.contains( document, removedControl.container[0] ) && ! $.contains( self.$sectionContent[0], removedControl.container[0] ); 1860 1861 // Delete any widget form controls for removed widgets. 1862 if ( removedControl && ! wasDraggedToAnotherSidebar ) { 1863 api.control.remove( removedControl.id ); 1864 removedControl.container.remove(); 1865 } 1866 1867 // Move widget to inactive widgets sidebar (move it to Trash) if has been previously saved. 1868 // This prevents the inactive widgets sidebar from overflowing with throwaway widgets. 1869 if ( api.Widgets.savedWidgetIds[removedWidgetId] ) { 1870 inactiveWidgets = api.value( 'sidebars_widgets[wp_inactive_widgets]' )().slice(); 1871 inactiveWidgets.push( removedWidgetId ); 1872 api.value( 'sidebars_widgets[wp_inactive_widgets]' )( _( inactiveWidgets ).unique() ); 1873 } 1874 1875 // Make old single widget available for adding again. 1876 removedIdBase = parseWidgetId( removedWidgetId ).id_base; 1877 widget = api.Widgets.availableWidgets.findWhere( { id_base: removedIdBase } ); 1878 if ( widget && ! widget.get( 'is_multi' ) ) { 1879 widget.set( 'is_disabled', false ); 1880 } 1881 } ); 1882 1883 } ); 1884 } ); 1885 }, 1886 1887 /** 1888 * Allow widgets in sidebar to be re-ordered, and for the order to be previewed 1889 */ 1890 _setupSortable: function() { 1891 var self = this; 1892 1893 this.isReordering = false; 1894 1895 /** 1896 * Update widget order setting when controls are re-ordered 1897 */ 1898 this.$sectionContent.sortable( { 1899 items: '> .customize-control-widget_form', 1900 handle: '.widget-top', 1901 axis: 'y', 1902 tolerance: 'pointer', 1903 connectWith: '.accordion-section-content:has(.customize-control-sidebar_widgets)', 1904 update: function() { 1905 var widgetContainerIds = self.$sectionContent.sortable( 'toArray' ), widgetIds; 1906 1907 widgetIds = $.map( widgetContainerIds, function( widgetContainerId ) { 1908 return $( '#' + widgetContainerId ).find( ':input[name=widget-id]' ).val(); 1909 } ); 1910 1911 self.setting( widgetIds ); 1912 } 1913 } ); 1914 1915 /** 1916 * Expand other Customizer sidebar section when dragging a control widget over it, 1917 * allowing the control to be dropped into another section 1918 */ 1919 this.$controlSection.find( '.accordion-section-title' ).droppable({ 1920 accept: '.customize-control-widget_form', 1921 over: function() { 1922 var section = api.section( self.section.get() ); 1923 section.expand({ 1924 allowMultiple: true, // Prevent the section being dragged from to be collapsed. 1925 completeCallback: function () { 1926 // @todo It is not clear when refreshPositions should be called on which sections, or if it is even needed. 1927 api.section.each( function ( otherSection ) { 1928 if ( otherSection.container.find( '.customize-control-sidebar_widgets' ).length ) { 1929 otherSection.container.find( '.accordion-section-content:first' ).sortable( 'refreshPositions' ); 1930 } 1931 } ); 1932 } 1933 }); 1934 } 1935 }); 1936 1937 /** 1938 * Keyboard-accessible reordering 1939 */ 1940 this.container.find( '.reorder-toggle' ).on( 'click', function() { 1941 self.toggleReordering( ! self.isReordering ); 1942 } ); 1943 }, 1944 1945 /** 1946 * Set up UI for adding a new widget 1947 */ 1948 _setupAddition: function() { 1949 var self = this; 1950 1951 this.container.find( '.add-new-widget' ).on( 'click', function() { 1952 var addNewWidgetBtn = $( this ); 1953 1954 if ( self.$sectionContent.hasClass( 'reordering' ) ) { 1955 return; 1956 } 1957 1958 if ( ! $( 'body' ).hasClass( 'adding-widget' ) ) { 1959 addNewWidgetBtn.attr( 'aria-expanded', 'true' ); 1960 api.Widgets.availableWidgetsPanel.open( self ); 1961 } else { 1962 addNewWidgetBtn.attr( 'aria-expanded', 'false' ); 1963 api.Widgets.availableWidgetsPanel.close(); 1964 } 1965 } ); 1966 }, 1967 1968 /** 1969 * Add classes to the widget_form controls to assist with styling 1970 */ 1971 _applyCardinalOrderClassNames: function() { 1972 var widgetControls = []; 1973 _.each( this.setting(), function ( widgetId ) { 1974 var widgetControl = api.Widgets.getWidgetFormControlForWidget( widgetId ); 1975 if ( widgetControl ) { 1976 widgetControls.push( widgetControl ); 1977 } 1978 }); 1979 1980 if ( 0 === widgetControls.length || ( 1 === api.Widgets.registeredSidebars.length && widgetControls.length <= 1 ) ) { 1981 this.container.find( '.reorder-toggle' ).hide(); 1982 return; 1983 } else { 1984 this.container.find( '.reorder-toggle' ).show(); 1985 } 1986 1987 $( widgetControls ).each( function () { 1988 $( this.container ) 1989 .removeClass( 'first-widget' ) 1990 .removeClass( 'last-widget' ) 1991 .find( '.move-widget-down, .move-widget-up' ).prop( 'tabIndex', 0 ); 1992 }); 1993 1994 _.first( widgetControls ).container 1995 .addClass( 'first-widget' ) 1996 .find( '.move-widget-up' ).prop( 'tabIndex', -1 ); 1997 1998 _.last( widgetControls ).container 1999 .addClass( 'last-widget' ) 2000 .find( '.move-widget-down' ).prop( 'tabIndex', -1 ); 2001 }, 2002 2003 2004 /*********************************************************************** 2005 * Begin public API methods 2006 **********************************************************************/ 2007 2008 /** 2009 * Enable/disable the reordering UI 2010 * 2011 * @param {boolean} showOrHide to enable/disable reordering 2012 * 2013 * @todo We should have a reordering state instead and rename this to onChangeReordering 2014 */ 2015 toggleReordering: function( showOrHide ) { 2016 var addNewWidgetBtn = this.$sectionContent.find( '.add-new-widget' ), 2017 reorderBtn = this.container.find( '.reorder-toggle' ), 2018 widgetsTitle = this.$sectionContent.find( '.widget-title' ); 2019 2020 showOrHide = Boolean( showOrHide ); 2021 2022 if ( showOrHide === this.$sectionContent.hasClass( 'reordering' ) ) { 2023 return; 2024 } 2025 2026 this.isReordering = showOrHide; 2027 this.$sectionContent.toggleClass( 'reordering', showOrHide ); 2028 2029 if ( showOrHide ) { 2030 _( this.getWidgetFormControls() ).each( function( formControl ) { 2031 formControl.collapse(); 2032 } ); 2033 2034 addNewWidgetBtn.attr({ 'tabindex': '-1', 'aria-hidden': 'true' }); 2035 reorderBtn.attr( 'aria-label', l10n.reorderLabelOff ); 2036 wp.a11y.speak( l10n.reorderModeOn ); 2037 // Hide widget titles while reordering: title is already in the reorder controls. 2038 widgetsTitle.attr( 'aria-hidden', 'true' ); 2039 } else { 2040 addNewWidgetBtn.removeAttr( 'tabindex aria-hidden' ); 2041 reorderBtn.attr( 'aria-label', l10n.reorderLabelOn ); 2042 wp.a11y.speak( l10n.reorderModeOff ); 2043 widgetsTitle.attr( 'aria-hidden', 'false' ); 2044 } 2045 }, 2046 2047 /** 2048 * Get the widget_form Customize controls associated with the current sidebar. 2049 * 2050 * @since 3.9.0 2051 * @return {wp.customize.controlConstructor.widget_form[]} 2052 */ 2053 getWidgetFormControls: function() { 2054 var formControls = []; 2055 2056 _( this.setting() ).each( function( widgetId ) { 2057 var settingId = widgetIdToSettingId( widgetId ), 2058 formControl = api.control( settingId ); 2059 if ( formControl ) { 2060 formControls.push( formControl ); 2061 } 2062 } ); 2063 2064 return formControls; 2065 }, 2066 2067 /** 2068 * @param {string} widgetId or an id_base for adding a previously non-existing widget. 2069 * @return {Object|false} widget_form control instance, or false on error. 2070 */ 2071 addWidget: function( widgetId ) { 2072 var self = this, controlHtml, $widget, controlType = 'widget_form', controlContainer, controlConstructor, 2073 parsedWidgetId = parseWidgetId( widgetId ), 2074 widgetNumber = parsedWidgetId.number, 2075 widgetIdBase = parsedWidgetId.id_base, 2076 widget = api.Widgets.availableWidgets.findWhere( {id_base: widgetIdBase} ), 2077 settingId, isExistingWidget, widgetFormControl, sidebarWidgets, settingArgs, setting; 2078 2079 if ( ! widget ) { 2080 return false; 2081 } 2082 2083 if ( widgetNumber && ! widget.get( 'is_multi' ) ) { 2084 return false; 2085 } 2086 2087 // Set up new multi widget. 2088 if ( widget.get( 'is_multi' ) && ! widgetNumber ) { 2089 widget.set( 'multi_number', widget.get( 'multi_number' ) + 1 ); 2090 widgetNumber = widget.get( 'multi_number' ); 2091 } 2092 2093 controlHtml = $( '#widget-tpl-' + widget.get( 'id' ) ).html().trim(); 2094 if ( widget.get( 'is_multi' ) ) { 2095 controlHtml = controlHtml.replace( /<[^<>]+>/g, function( m ) { 2096 return m.replace( /__i__|%i%/g, widgetNumber ); 2097 } ); 2098 } else { 2099 widget.set( 'is_disabled', true ); // Prevent single widget from being added again now. 2100 } 2101 2102 $widget = $( controlHtml ); 2103 2104 controlContainer = $( '<li/>' ) 2105 .addClass( 'customize-control' ) 2106 .addClass( 'customize-control-' + controlType ) 2107 .append( $widget ); 2108 2109 // Remove icon which is visible inside the panel. 2110 controlContainer.find( '> .widget-icon' ).remove(); 2111 2112 if ( widget.get( 'is_multi' ) ) { 2113 controlContainer.find( 'input[name="widget_number"]' ).val( widgetNumber ); 2114 controlContainer.find( 'input[name="multi_number"]' ).val( widgetNumber ); 2115 } 2116 2117 widgetId = controlContainer.find( '[name="widget-id"]' ).val(); 2118 2119 controlContainer.hide(); // To be slid-down below. 2120 2121 settingId = 'widget_' + widget.get( 'id_base' ); 2122 if ( widget.get( 'is_multi' ) ) { 2123 settingId += '[' + widgetNumber + ']'; 2124 } 2125 controlContainer.attr( 'id', 'customize-control-' + settingId.replace( /\]/g, '' ).replace( /\[/g, '-' ) ); 2126 2127 // Only create setting if it doesn't already exist (if we're adding a pre-existing inactive widget). 2128 isExistingWidget = api.has( settingId ); 2129 if ( ! isExistingWidget ) { 2130 settingArgs = { 2131 transport: api.Widgets.data.selectiveRefreshableWidgets[ widget.get( 'id_base' ) ] ? 'postMessage' : 'refresh', 2132 previewer: this.setting.previewer 2133 }; 2134 setting = api.create( settingId, settingId, '', settingArgs ); 2135 setting.set( {} ); // Mark dirty, changing from '' to {}. 2136 } 2137 2138 controlConstructor = api.controlConstructor[controlType]; 2139 widgetFormControl = new controlConstructor( settingId, { 2140 settings: { 2141 'default': settingId 2142 }, 2143 content: controlContainer, 2144 sidebar_id: self.params.sidebar_id, 2145 widget_id: widgetId, 2146 widget_id_base: widget.get( 'id_base' ), 2147 type: controlType, 2148 is_new: ! isExistingWidget, 2149 width: widget.get( 'width' ), 2150 height: widget.get( 'height' ), 2151 is_wide: widget.get( 'is_wide' ) 2152 } ); 2153 api.control.add( widgetFormControl ); 2154 2155 // Make sure widget is removed from the other sidebars. 2156 api.each( function( otherSetting ) { 2157 if ( otherSetting.id === self.setting.id ) { 2158 return; 2159 } 2160 2161 if ( 0 !== otherSetting.id.indexOf( 'sidebars_widgets[' ) ) { 2162 return; 2163 } 2164 2165 var otherSidebarWidgets = otherSetting().slice(), 2166 i = _.indexOf( otherSidebarWidgets, widgetId ); 2167 2168 if ( -1 !== i ) { 2169 otherSidebarWidgets.splice( i ); 2170 otherSetting( otherSidebarWidgets ); 2171 } 2172 } ); 2173 2174 // Add widget to this sidebar. 2175 sidebarWidgets = this.setting().slice(); 2176 if ( -1 === _.indexOf( sidebarWidgets, widgetId ) ) { 2177 sidebarWidgets.push( widgetId ); 2178 this.setting( sidebarWidgets ); 2179 } 2180 2181 controlContainer.slideDown( function() { 2182 if ( isExistingWidget ) { 2183 widgetFormControl.updateWidget( { 2184 instance: widgetFormControl.setting() 2185 } ); 2186 } 2187 } ); 2188 2189 return widgetFormControl; 2190 } 2191 } ); 2192 2193 // Register models for custom panel, section, and control types. 2194 $.extend( api.panelConstructor, { 2195 widgets: api.Widgets.WidgetsPanel 2196 }); 2197 $.extend( api.sectionConstructor, { 2198 sidebar: api.Widgets.SidebarSection 2199 }); 2200 $.extend( api.controlConstructor, { 2201 widget_form: api.Widgets.WidgetControl, 2202 sidebar_widgets: api.Widgets.SidebarControl 2203 }); 2204 2205 /** 2206 * Init Customizer for widgets. 2207 */ 2208 api.bind( 'ready', function() { 2209 // Set up the widgets panel. 2210 api.Widgets.availableWidgetsPanel = new api.Widgets.AvailableWidgetsPanelView({ 2211 collection: api.Widgets.availableWidgets 2212 }); 2213 2214 // Highlight widget control. 2215 api.previewer.bind( 'highlight-widget-control', api.Widgets.highlightWidgetFormControl ); 2216 2217 // Open and focus widget control. 2218 api.previewer.bind( 'focus-widget-control', api.Widgets.focusWidgetFormControl ); 2219 } ); 2220 2221 /** 2222 * Highlight a widget control. 2223 * 2224 * @param {string} widgetId 2225 */ 2226 api.Widgets.highlightWidgetFormControl = function( widgetId ) { 2227 var control = api.Widgets.getWidgetFormControlForWidget( widgetId ); 2228 2229 if ( control ) { 2230 control.highlightSectionAndControl(); 2231 } 2232 }, 2233 2234 /** 2235 * Focus a widget control. 2236 * 2237 * @param {string} widgetId 2238 */ 2239 api.Widgets.focusWidgetFormControl = function( widgetId ) { 2240 var control = api.Widgets.getWidgetFormControlForWidget( widgetId ); 2241 2242 if ( control ) { 2243 control.focus(); 2244 } 2245 }, 2246 2247 /** 2248 * Given a widget control, find the sidebar widgets control that contains it. 2249 * @param {string} widgetId 2250 * @return {Object|null} 2251 */ 2252 api.Widgets.getSidebarWidgetControlContainingWidget = function( widgetId ) { 2253 var foundControl = null; 2254 2255 // @todo This can use widgetIdToSettingId(), then pass into wp.customize.control( x ).getSidebarWidgetsControl(). 2256 api.control.each( function( control ) { 2257 if ( control.params.type === 'sidebar_widgets' && -1 !== _.indexOf( control.setting(), widgetId ) ) { 2258 foundControl = control; 2259 } 2260 } ); 2261 2262 return foundControl; 2263 }; 2264 2265 /** 2266 * Given a widget ID for a widget appearing in the preview, get the widget form control associated with it. 2267 * 2268 * @param {string} widgetId 2269 * @return {Object|null} 2270 */ 2271 api.Widgets.getWidgetFormControlForWidget = function( widgetId ) { 2272 var foundControl = null; 2273 2274 // @todo We can just use widgetIdToSettingId() here. 2275 api.control.each( function( control ) { 2276 if ( control.params.type === 'widget_form' && control.params.widget_id === widgetId ) { 2277 foundControl = control; 2278 } 2279 } ); 2280 2281 return foundControl; 2282 }; 2283 2284 /** 2285 * Initialize Edit Menu button in Nav Menu widget. 2286 */ 2287 $( document ).on( 'widget-added', function( event, widgetContainer ) { 2288 var parsedWidgetId, widgetControl, navMenuSelect, editMenuButton; 2289 parsedWidgetId = parseWidgetId( widgetContainer.find( '> .widget-inside > .form > .widget-id' ).val() ); 2290 if ( 'nav_menu' !== parsedWidgetId.id_base ) { 2291 return; 2292 } 2293 widgetControl = api.control( 'widget_nav_menu[' + String( parsedWidgetId.number ) + ']' ); 2294 if ( ! widgetControl ) { 2295 return; 2296 } 2297 navMenuSelect = widgetContainer.find( 'select[name*="nav_menu"]' ); 2298 editMenuButton = widgetContainer.find( '.edit-selected-nav-menu > button' ); 2299 if ( 0 === navMenuSelect.length || 0 === editMenuButton.length ) { 2300 return; 2301 } 2302 navMenuSelect.on( 'change', function() { 2303 if ( api.section.has( 'nav_menu[' + navMenuSelect.val() + ']' ) ) { 2304 editMenuButton.parent().show(); 2305 } else { 2306 editMenuButton.parent().hide(); 2307 } 2308 }); 2309 editMenuButton.on( 'click', function() { 2310 var section = api.section( 'nav_menu[' + navMenuSelect.val() + ']' ); 2311 if ( section ) { 2312 focusConstructWithBreadcrumb( section, widgetControl ); 2313 } 2314 } ); 2315 } ); 2316 2317 /** 2318 * Focus (expand) one construct and then focus on another construct after the first is collapsed. 2319 * 2320 * This overrides the back button to serve the purpose of breadcrumb navigation. 2321 * 2322 * @param {wp.customize.Section|wp.customize.Panel|wp.customize.Control} focusConstruct - The object to initially focus. 2323 * @param {wp.customize.Section|wp.customize.Panel|wp.customize.Control} returnConstruct - The object to return focus. 2324 */ 2325 function focusConstructWithBreadcrumb( focusConstruct, returnConstruct ) { 2326 focusConstruct.focus(); 2327 function onceCollapsed( isExpanded ) { 2328 if ( ! isExpanded ) { 2329 focusConstruct.expanded.unbind( onceCollapsed ); 2330 returnConstruct.focus(); 2331 } 2332 } 2333 focusConstruct.expanded.bind( onceCollapsed ); 2334 } 2335 2336 /** 2337 * @param {string} widgetId 2338 * @return {Object} 2339 */ 2340 function parseWidgetId( widgetId ) { 2341 var matches, parsed = { 2342 number: null, 2343 id_base: null 2344 }; 2345 2346 matches = widgetId.match( /^(.+)-(\d+)$/ ); 2347 if ( matches ) { 2348 parsed.id_base = matches[1]; 2349 parsed.number = parseInt( matches[2], 10 ); 2350 } else { 2351 // Likely an old single widget. 2352 parsed.id_base = widgetId; 2353 } 2354 2355 return parsed; 2356 } 2357 2358 /** 2359 * @param {string} widgetId 2360 * @return {string} settingId 2361 */ 2362 function widgetIdToSettingId( widgetId ) { 2363 var parsed = parseWidgetId( widgetId ), settingId; 2364 2365 settingId = 'widget_' + parsed.id_base; 2366 if ( parsed.number ) { 2367 settingId += '[' + parsed.number + ']'; 2368 } 2369 2370 return settingId; 2371 } 2372 2373 })( window.wp, jQuery );
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated : Sat Nov 23 08:20:01 2024 | Cross-referenced by PHPXref |