1 /* global _wpWidgetCustomizerPreviewSettings */
2 wp.customize.widgetsPreview = wp.customize.WidgetCustomizerPreview = (function( $, _, wp, api ) {
9 registeredSidebars: [],
10 registeredWidgets: {},
16 selectiveRefreshableWidgets: {}
20 * Init widgets preview.
24 self.init = function() {
27 self.preview = api.preview;
28 if ( ! _.isEmpty( self.selectiveRefreshableWidgets ) ) {
32 self.buildWidgetSelectors();
33 self.highlightControls();
35 self.preview.bind( 'highlight-widget', self.highlightWidget );
37 api.preview.bind( 'active', function() {
38 self.highlightControls();
43 * Partial representing a widget instance.
46 * @augments wp.customize.selectiveRefresh.Partial
49 self.WidgetPartial = api.selectiveRefresh.Partial.extend({
55 * @param {string} id - Partial ID.
56 * @param {Object} options
57 * @param {Object} options.params
59 initialize: function( id, options ) {
60 var partial = this, matches;
61 matches = id.match( /^widget\[(.+)]$/ );
63 throw new Error( 'Illegal id for widget partial.' );
66 partial.widgetId = matches[1];
67 partial.widgetIdParts = self.parseWidgetId( partial.widgetId );
68 options = options || {};
69 options.params = _.extend(
71 settings: [ self.getWidgetSettingId( partial.widgetId ) ],
72 containerInclusive: true
77 api.selectiveRefresh.Partial.prototype.initialize.call( partial, id, options );
81 * Refresh widget partial.
86 var partial = this, refreshDeferred;
87 if ( ! self.selectiveRefreshableWidgets[ partial.widgetIdParts.idBase ] ) {
88 refreshDeferred = $.Deferred();
89 refreshDeferred.reject();
91 return refreshDeferred.promise();
93 return api.selectiveRefresh.Partial.prototype.refresh.call( partial );
98 * Send widget-updated message to parent so spinner will get removed from widget control.
101 * @param {wp.customize.selectiveRefresh.Placement} placement
103 renderContent: function( placement ) {
105 if ( api.selectiveRefresh.Partial.prototype.renderContent.call( partial, placement ) ) {
106 api.preview.send( 'widget-updated', partial.widgetId );
107 api.selectiveRefresh.trigger( 'widget-updated', partial );
113 * Partial representing a widget area.
116 * @augments wp.customize.selectiveRefresh.Partial
119 self.SidebarPartial = api.selectiveRefresh.Partial.extend({
125 * @param {string} id - Partial ID.
126 * @param {Object} options
127 * @param {Object} options.params
129 initialize: function( id, options ) {
130 var partial = this, matches;
131 matches = id.match( /^sidebar\[(.+)]$/ );
133 throw new Error( 'Illegal id for sidebar partial.' );
135 partial.sidebarId = matches[1];
137 options = options || {};
138 options.params = _.extend(
140 settings: [ 'sidebars_widgets[' + partial.sidebarId + ']' ]
145 api.selectiveRefresh.Partial.prototype.initialize.call( partial, id, options );
147 if ( ! partial.params.sidebarArgs ) {
148 throw new Error( 'The sidebarArgs param was not provided.' );
150 if ( partial.params.settings.length > 1 ) {
151 throw new Error( 'Expected SidebarPartial to only have one associated setting' );
156 * Set up the partial.
161 var sidebarPartial = this;
163 // Watch for changes to the sidebar_widgets setting.
164 _.each( sidebarPartial.settings(), function( settingId ) {
165 api( settingId ).bind( _.bind( sidebarPartial.handleSettingChange, sidebarPartial ) );
168 // Trigger an event for this sidebar being updated whenever a widget inside is rendered.
169 api.selectiveRefresh.bind( 'partial-content-rendered', function( placement ) {
170 var isAssignedWidgetPartial = (
171 placement.partial.extended( self.WidgetPartial ) &&
172 ( -1 !== _.indexOf( sidebarPartial.getWidgetIds(), placement.partial.widgetId ) )
174 if ( isAssignedWidgetPartial ) {
175 api.selectiveRefresh.trigger( 'sidebar-updated', sidebarPartial );
179 // Make sure that a widget partial has a container in the DOM prior to a refresh.
180 api.bind( 'change', function( widgetSetting ) {
181 var widgetId, parsedId;
182 parsedId = self.parseWidgetSettingId( widgetSetting.id );
186 widgetId = parsedId.idBase;
187 if ( parsedId.number ) {
188 widgetId += '-' + String( parsedId.number );
190 if ( -1 !== _.indexOf( sidebarPartial.getWidgetIds(), widgetId ) ) {
191 sidebarPartial.ensureWidgetPlacementContainers( widgetId );
197 * Get the before/after boundary nodes for all instances of this sidebar (usually one).
199 * Note that TreeWalker is not implemented in IE8.
202 * @returns {Array.<{before: Comment, after: Comment, instanceNumber: number}>}
204 findDynamicSidebarBoundaryNodes: function() {
205 var partial = this, regExp, boundaryNodes = {}, recursiveCommentTraversal;
206 regExp = /^(dynamic_sidebar_before|dynamic_sidebar_after):(.+):(\d+)$/;
207 recursiveCommentTraversal = function( childNodes ) {
208 _.each( childNodes, function( node ) {
210 if ( 8 === node.nodeType ) {
211 matches = node.nodeValue.match( regExp );
212 if ( ! matches || matches[2] !== partial.sidebarId ) {
215 if ( _.isUndefined( boundaryNodes[ matches[3] ] ) ) {
216 boundaryNodes[ matches[3] ] = {
219 instanceNumber: parseInt( matches[3], 10 )
222 if ( 'dynamic_sidebar_before' === matches[1] ) {
223 boundaryNodes[ matches[3] ].before = node;
225 boundaryNodes[ matches[3] ].after = node;
227 } else if ( 1 === node.nodeType ) {
228 recursiveCommentTraversal( node.childNodes );
233 recursiveCommentTraversal( document.body.childNodes );
234 return _.values( boundaryNodes );
238 * Get the placements for this partial.
243 placements: function() {
245 return _.map( partial.findDynamicSidebarBoundaryNodes(), function( boundaryNodes ) {
246 return new api.selectiveRefresh.Placement( {
249 startNode: boundaryNodes.before,
250 endNode: boundaryNodes.after,
252 instanceNumber: boundaryNodes.instanceNumber
259 * Get the list of widget IDs associated with this widget area.
265 getWidgetIds: function() {
266 var sidebarPartial = this, settingId, widgetIds;
267 settingId = sidebarPartial.settings()[0];
269 throw new Error( 'Missing associated setting.' );
271 if ( ! api.has( settingId ) ) {
272 throw new Error( 'Setting does not exist.' );
274 widgetIds = api( settingId ).get();
275 if ( ! _.isArray( widgetIds ) ) {
276 throw new Error( 'Expected setting to be array of widget IDs' );
278 return widgetIds.slice( 0 );
282 * Reflow widgets in the sidebar, ensuring they have the proper position in the DOM.
286 * @return {Array.<wp.customize.selectiveRefresh.Placement>} List of placements that were reflowed.
288 reflowWidgets: function() {
289 var sidebarPartial = this, sidebarPlacements, widgetIds, widgetPartials, sortedSidebarContainers = [];
290 widgetIds = sidebarPartial.getWidgetIds();
291 sidebarPlacements = sidebarPartial.placements();
294 _.each( widgetIds, function( widgetId ) {
295 var widgetPartial = api.selectiveRefresh.partial( 'widget[' + widgetId + ']' );
296 if ( widgetPartial ) {
297 widgetPartials[ widgetId ] = widgetPartial;
301 _.each( sidebarPlacements, function( sidebarPlacement ) {
302 var sidebarWidgets = [], needsSort = false, thisPosition, lastPosition = -1;
304 // Gather list of widget partial containers in this sidebar, and determine if a sort is needed.
305 _.each( widgetPartials, function( widgetPartial ) {
306 _.each( widgetPartial.placements(), function( widgetPlacement ) {
308 if ( sidebarPlacement.context.instanceNumber === widgetPlacement.context.sidebar_instance_number ) {
309 thisPosition = widgetPlacement.container.index();
310 sidebarWidgets.push( {
311 partial: widgetPartial,
312 placement: widgetPlacement,
313 position: thisPosition
315 if ( thisPosition < lastPosition ) {
318 lastPosition = thisPosition;
324 _.each( sidebarWidgets, function( sidebarWidget ) {
325 sidebarPlacement.endNode.parentNode.insertBefore(
326 sidebarWidget.placement.container[0],
327 sidebarPlacement.endNode
330 // @todo Rename partial-placement-moved?
331 api.selectiveRefresh.trigger( 'partial-content-moved', sidebarWidget.placement );
334 sortedSidebarContainers.push( sidebarPlacement );
338 if ( sortedSidebarContainers.length > 0 ) {
339 api.selectiveRefresh.trigger( 'sidebar-updated', sidebarPartial );
342 return sortedSidebarContainers;
346 * Make sure there is a widget instance container in this sidebar for the given widget ID.
350 * @param {string} widgetId
351 * @returns {wp.customize.selectiveRefresh.Partial} Widget instance partial.
353 ensureWidgetPlacementContainers: function( widgetId ) {
354 var sidebarPartial = this, widgetPartial, wasInserted = false, partialId = 'widget[' + widgetId + ']';
355 widgetPartial = api.selectiveRefresh.partial( partialId );
356 if ( ! widgetPartial ) {
357 widgetPartial = new self.WidgetPartial( partialId, {
362 // Make sure that there is a container element for the widget in the sidebar, if at least a placeholder.
363 _.each( sidebarPartial.placements(), function( sidebarPlacement ) {
364 var foundWidgetPlacement, widgetContainerElement;
366 foundWidgetPlacement = _.find( widgetPartial.placements(), function( widgetPlacement ) {
367 return ( widgetPlacement.context.sidebar_instance_number === sidebarPlacement.context.instanceNumber );
369 if ( foundWidgetPlacement ) {
373 widgetContainerElement = $(
374 sidebarPartial.params.sidebarArgs.before_widget.replace( /%1\$s/g, widgetId ).replace( /%2\$s/g, 'widget' ) +
375 sidebarPartial.params.sidebarArgs.after_widget
378 // Handle rare case where before_widget and after_widget are empty.
379 if ( ! widgetContainerElement[0] ) {
383 widgetContainerElement.attr( 'data-customize-partial-id', widgetPartial.id );
384 widgetContainerElement.attr( 'data-customize-partial-type', 'widget' );
385 widgetContainerElement.attr( 'data-customize-widget-id', widgetId );
388 * Make sure the widget container element has the customize-container context data.
389 * The sidebar_instance_number is used to disambiguate multiple instances of the
390 * same sidebar are rendered onto the template, and so the same widget is embedded
393 widgetContainerElement.data( 'customize-partial-placement-context', {
394 'sidebar_id': sidebarPartial.sidebarId,
395 'sidebar_instance_number': sidebarPlacement.context.instanceNumber
398 sidebarPlacement.endNode.parentNode.insertBefore( widgetContainerElement[0], sidebarPlacement.endNode );
402 api.selectiveRefresh.partial.add( widgetPartial.id, widgetPartial );
405 sidebarPartial.reflowWidgets();
408 return widgetPartial;
412 * Handle change to the sidebars_widgets[] setting.
416 * @param {Array} newWidgetIds New widget ids.
417 * @param {Array} oldWidgetIds Old widget ids.
419 handleSettingChange: function( newWidgetIds, oldWidgetIds ) {
420 var sidebarPartial = this, needsRefresh, widgetsRemoved, widgetsAdded, addedWidgetPartials = [];
423 ( oldWidgetIds.length > 0 && 0 === newWidgetIds.length ) ||
424 ( newWidgetIds.length > 0 && 0 === oldWidgetIds.length )
426 if ( needsRefresh ) {
427 sidebarPartial.fallback();
431 // Handle removal of widgets.
432 widgetsRemoved = _.difference( oldWidgetIds, newWidgetIds );
433 _.each( widgetsRemoved, function( removedWidgetId ) {
434 var widgetPartial = api.selectiveRefresh.partial( 'widget[' + removedWidgetId + ']' );
435 if ( widgetPartial ) {
436 _.each( widgetPartial.placements(), function( placement ) {
438 placement.context.sidebar_id === sidebarPartial.sidebarId ||
439 ( placement.context.sidebar_args && placement.context.sidebar_args.id === sidebarPartial.sidebarId )
442 placement.container.remove();
448 // Handle insertion of widgets.
449 widgetsAdded = _.difference( newWidgetIds, oldWidgetIds );
450 _.each( widgetsAdded, function( addedWidgetId ) {
451 var widgetPartial = sidebarPartial.ensureWidgetPlacementContainers( addedWidgetId );
452 addedWidgetPartials.push( widgetPartial );
455 _.each( addedWidgetPartials, function( widgetPartial ) {
456 widgetPartial.refresh();
459 api.selectiveRefresh.trigger( 'sidebar-updated', sidebarPartial );
463 * Note that the meat is handled in handleSettingChange because it has the context of which widgets were removed.
467 refresh: function() {
468 var partial = this, deferred = $.Deferred();
470 deferred.fail( function() {
474 if ( 0 === partial.placements().length ) {
477 _.each( partial.reflowWidgets(), function( sidebarPlacement ) {
478 api.selectiveRefresh.trigger( 'partial-content-rendered', sidebarPlacement );
483 return deferred.promise();
487 api.selectiveRefresh.partialConstructor.sidebar = self.SidebarPartial;
488 api.selectiveRefresh.partialConstructor.widget = self.WidgetPartial;
491 * Add partials for the registered widget areas (sidebars).
495 self.addPartials = function() {
496 _.each( self.registeredSidebars, function( registeredSidebar ) {
497 var partial, partialId = 'sidebar[' + registeredSidebar.id + ']';
498 partial = api.selectiveRefresh.partial( partialId );
500 partial = new self.SidebarPartial( partialId, {
502 sidebarArgs: registeredSidebar
505 api.selectiveRefresh.partial.add( partial.id, partial );
511 * Calculate the selector for the sidebar's widgets based on the registered sidebar's info.
515 self.buildWidgetSelectors = function() {
518 $.each( self.registeredSidebars, function( i, sidebar ) {
520 sidebar.before_widget,
521 sidebar.before_title,
529 emptyWidget = $( widgetTpl );
530 widgetSelector = emptyWidget.prop( 'tagName' ) || '';
531 widgetClasses = emptyWidget.prop( 'className' ) || '';
533 // Prevent a rare case when before_widget, before_title, after_title and after_widget is empty.
534 if ( ! widgetClasses ) {
538 // Remove class names that incorporate the string formatting placeholders %1$s and %2$s.
539 widgetClasses = widgetClasses.replace( /\S*%[12]\$s\S*/g, '' );
540 widgetClasses = widgetClasses.replace( /^\s+|\s+$/g, '' );
541 if ( widgetClasses ) {
542 widgetSelector += '.' + widgetClasses.split( /\s+/ ).join( '.' );
544 self.widgetSelectors.push( widgetSelector );
549 * Highlight the widget on widget updates or widget control mouse overs.
552 * @param {string} widgetId ID of the widget.
554 self.highlightWidget = function( widgetId ) {
555 var $body = $( document.body ),
556 $widget = $( '#' + widgetId );
558 $body.find( '.widget-customizer-highlighted-widget' ).removeClass( 'widget-customizer-highlighted-widget' );
560 $widget.addClass( 'widget-customizer-highlighted-widget' );
561 setTimeout( function() {
562 $widget.removeClass( 'widget-customizer-highlighted-widget' );
567 * Show a title and highlight widgets on hover. On shift+clicking
568 * focus the widget control.
572 self.highlightControls = function() {
574 selector = this.widgetSelectors.join( ',' );
576 // Skip adding highlights if not in the customizer preview iframe.
577 if ( ! api.settings.channel ) {
581 $( selector ).attr( 'title', this.l10n.widgetTooltip );
583 $( document ).on( 'mouseenter', selector, function() {
584 self.preview.send( 'highlight-widget-control', $( this ).prop( 'id' ) );
587 // Open expand the widget control when shift+clicking the widget element
588 $( document ).on( 'click', selector, function( e ) {
589 if ( ! e.shiftKey ) {
594 self.preview.send( 'focus-widget-control', $( this ).prop( 'id' ) );
603 * @param {string} widgetId Widget ID.
604 * @returns {{idBase: string, number: number|null}}
606 self.parseWidgetId = function( widgetId ) {
607 var matches, parsed = {
612 matches = widgetId.match( /^(.+)-(\d+)$/ );
614 parsed.idBase = matches[1];
615 parsed.number = parseInt( matches[2], 10 );
617 parsed.idBase = widgetId; // Likely an old single widget.
624 * Parse a widget setting ID.
628 * @param {string} settingId Widget setting ID.
629 * @returns {{idBase: string, number: number|null}|null}
631 self.parseWidgetSettingId = function( settingId ) {
632 var matches, parsed = {
637 matches = settingId.match( /^widget_([^\[]+?)(?:\[(\d+)])?$/ );
641 parsed.idBase = matches[1];
643 parsed.number = parseInt( matches[2], 10 );
649 * Convert a widget ID into a Customizer setting ID.
653 * @param {string} widgetId Widget ID.
654 * @returns {string} settingId Setting ID.
656 self.getWidgetSettingId = function( widgetId ) {
657 var parsed = this.parseWidgetId( widgetId ), settingId;
659 settingId = 'widget_' + parsed.idBase;
660 if ( parsed.number ) {
661 settingId += '[' + String( parsed.number ) + ']';
667 api.bind( 'preview-ready', function() {
668 $.extend( self, _wpWidgetCustomizerPreviewSettings );
673 })( jQuery, _, wp, wp.customize );