PageRenderTime 39ms CodeModel.GetById 8ms app.highlight 24ms RepoModel.GetById 2ms app.codeStats 0ms

/HotelSpaWP/wp-includes/js/customize-preview-widgets.js

https://bitbucket.org/Trulsh/personal-bootstrap-projects
JavaScript | 673 lines | 424 code | 83 blank | 166 comment | 58 complexity | 436093edec320c9f9da76cee03deb3ac MD5 | raw file
  1/* global _wpWidgetCustomizerPreviewSettings */
  2wp.customize.widgetsPreview = wp.customize.WidgetCustomizerPreview = (function( $, _, wp, api ) {
  3
  4	var self;
  5
  6	self = {
  7		renderedSidebars: {},
  8		renderedWidgets: {},
  9		registeredSidebars: [],
 10		registeredWidgets: {},
 11		widgetSelectors: [],
 12		preview: null,
 13		l10n: {
 14			widgetTooltip: ''
 15		},
 16		selectiveRefreshableWidgets: {}
 17	};
 18
 19	/**
 20	 * Init widgets preview.
 21	 *
 22	 * @since 4.5.0
 23	 */
 24	self.init = function() {
 25		var self = this;
 26
 27		self.preview = api.preview;
 28		if ( ! _.isEmpty( self.selectiveRefreshableWidgets ) ) {
 29			self.addPartials();
 30		}
 31
 32		self.buildWidgetSelectors();
 33		self.highlightControls();
 34
 35		self.preview.bind( 'highlight-widget', self.highlightWidget );
 36
 37		api.preview.bind( 'active', function() {
 38			self.highlightControls();
 39		} );
 40	};
 41
 42	/**
 43	 * Partial representing a widget instance.
 44	 *
 45	 * @class
 46	 * @augments wp.customize.selectiveRefresh.Partial
 47	 * @since 4.5.0
 48	 */
 49	self.WidgetPartial = api.selectiveRefresh.Partial.extend({
 50
 51		/**
 52		 * Constructor.
 53		 *
 54		 * @since 4.5.0
 55		 * @param {string} id - Partial ID.
 56		 * @param {Object} options
 57		 * @param {Object} options.params
 58		 */
 59		initialize: function( id, options ) {
 60			var partial = this, matches;
 61			matches = id.match( /^widget\[(.+)]$/ );
 62			if ( ! matches ) {
 63				throw new Error( 'Illegal id for widget partial.' );
 64			}
 65
 66			partial.widgetId = matches[1];
 67			partial.widgetIdParts = self.parseWidgetId( partial.widgetId );
 68			options = options || {};
 69			options.params = _.extend(
 70				{
 71					settings: [ self.getWidgetSettingId( partial.widgetId ) ],
 72					containerInclusive: true
 73				},
 74				options.params || {}
 75			);
 76
 77			api.selectiveRefresh.Partial.prototype.initialize.call( partial, id, options );
 78		},
 79
 80		/**
 81		 * Refresh widget partial.
 82		 *
 83		 * @returns {Promise}
 84		 */
 85		refresh: function() {
 86			var partial = this, refreshDeferred;
 87			if ( ! self.selectiveRefreshableWidgets[ partial.widgetIdParts.idBase ] ) {
 88				refreshDeferred = $.Deferred();
 89				refreshDeferred.reject();
 90				partial.fallback();
 91				return refreshDeferred.promise();
 92			} else {
 93				return api.selectiveRefresh.Partial.prototype.refresh.call( partial );
 94			}
 95		},
 96
 97		/**
 98		 * Send widget-updated message to parent so spinner will get removed from widget control.
 99		 *
100		 * @inheritdoc
101		 * @param {wp.customize.selectiveRefresh.Placement} placement
102		 */
103		renderContent: function( placement ) {
104			var partial = this;
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 );
108			}
109		}
110	});
111
112	/**
113	 * Partial representing a widget area.
114	 *
115	 * @class
116	 * @augments wp.customize.selectiveRefresh.Partial
117	 * @since 4.5.0
118	 */
119	self.SidebarPartial = api.selectiveRefresh.Partial.extend({
120
121		/**
122		 * Constructor.
123		 *
124		 * @since 4.5.0
125		 * @param {string} id - Partial ID.
126		 * @param {Object} options
127		 * @param {Object} options.params
128		 */
129		initialize: function( id, options ) {
130			var partial = this, matches;
131			matches = id.match( /^sidebar\[(.+)]$/ );
132			if ( ! matches ) {
133				throw new Error( 'Illegal id for sidebar partial.' );
134			}
135			partial.sidebarId = matches[1];
136
137			options = options || {};
138			options.params = _.extend(
139				{
140					settings: [ 'sidebars_widgets[' + partial.sidebarId + ']' ]
141				},
142				options.params || {}
143			);
144
145			api.selectiveRefresh.Partial.prototype.initialize.call( partial, id, options );
146
147			if ( ! partial.params.sidebarArgs ) {
148				throw new Error( 'The sidebarArgs param was not provided.' );
149			}
150			if ( partial.params.settings.length > 1 ) {
151				throw new Error( 'Expected SidebarPartial to only have one associated setting' );
152			}
153		},
154
155		/**
156		 * Set up the partial.
157		 *
158		 * @since 4.5.0
159		 */
160		ready: function() {
161			var sidebarPartial = this;
162
163			// Watch for changes to the sidebar_widgets setting.
164			_.each( sidebarPartial.settings(), function( settingId ) {
165				api( settingId ).bind( _.bind( sidebarPartial.handleSettingChange, sidebarPartial ) );
166			} );
167
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 ) )
173				);
174				if ( isAssignedWidgetPartial ) {
175					api.selectiveRefresh.trigger( 'sidebar-updated', sidebarPartial );
176				}
177			} );
178
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 );
183				if ( ! parsedId ) {
184					return;
185				}
186				widgetId = parsedId.idBase;
187				if ( parsedId.number ) {
188					widgetId += '-' + String( parsedId.number );
189				}
190				if ( -1 !== _.indexOf( sidebarPartial.getWidgetIds(), widgetId ) ) {
191					sidebarPartial.ensureWidgetPlacementContainers( widgetId );
192				}
193			} );
194		},
195
196		/**
197		 * Get the before/after boundary nodes for all instances of this sidebar (usually one).
198		 *
199		 * Note that TreeWalker is not implemented in IE8.
200		 *
201		 * @since 4.5.0
202		 * @returns {Array.<{before: Comment, after: Comment, instanceNumber: number}>}
203		 */
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 ) {
209					var matches;
210					if ( 8 === node.nodeType ) {
211						matches = node.nodeValue.match( regExp );
212						if ( ! matches || matches[2] !== partial.sidebarId ) {
213							return;
214						}
215						if ( _.isUndefined( boundaryNodes[ matches[3] ] ) ) {
216							boundaryNodes[ matches[3] ] = {
217								before: null,
218								after: null,
219								instanceNumber: parseInt( matches[3], 10 )
220							};
221						}
222						if ( 'dynamic_sidebar_before' === matches[1] ) {
223							boundaryNodes[ matches[3] ].before = node;
224						} else {
225							boundaryNodes[ matches[3] ].after = node;
226						}
227					} else if ( 1 === node.nodeType ) {
228						recursiveCommentTraversal( node.childNodes );
229					}
230				} );
231			};
232
233			recursiveCommentTraversal( document.body.childNodes );
234			return _.values( boundaryNodes );
235		},
236
237		/**
238		 * Get the placements for this partial.
239		 *
240		 * @since 4.5.0
241		 * @returns {Array}
242		 */
243		placements: function() {
244			var partial = this;
245			return _.map( partial.findDynamicSidebarBoundaryNodes(), function( boundaryNodes ) {
246				return new api.selectiveRefresh.Placement( {
247					partial: partial,
248					container: null,
249					startNode: boundaryNodes.before,
250					endNode: boundaryNodes.after,
251					context: {
252						instanceNumber: boundaryNodes.instanceNumber
253					}
254				} );
255			} );
256		},
257
258		/**
259		 * Get the list of widget IDs associated with this widget area.
260		 *
261		 * @since 4.5.0
262		 *
263		 * @returns {Array}
264		 */
265		getWidgetIds: function() {
266			var sidebarPartial = this, settingId, widgetIds;
267			settingId = sidebarPartial.settings()[0];
268			if ( ! settingId ) {
269				throw new Error( 'Missing associated setting.' );
270			}
271			if ( ! api.has( settingId ) ) {
272				throw new Error( 'Setting does not exist.' );
273			}
274			widgetIds = api( settingId ).get();
275			if ( ! _.isArray( widgetIds ) ) {
276				throw new Error( 'Expected setting to be array of widget IDs' );
277			}
278			return widgetIds.slice( 0 );
279		},
280
281		/**
282		 * Reflow widgets in the sidebar, ensuring they have the proper position in the DOM.
283		 *
284		 * @since 4.5.0
285		 *
286		 * @return {Array.<wp.customize.selectiveRefresh.Placement>} List of placements that were reflowed.
287		 */
288		reflowWidgets: function() {
289			var sidebarPartial = this, sidebarPlacements, widgetIds, widgetPartials, sortedSidebarContainers = [];
290			widgetIds = sidebarPartial.getWidgetIds();
291			sidebarPlacements = sidebarPartial.placements();
292
293			widgetPartials = {};
294			_.each( widgetIds, function( widgetId ) {
295				var widgetPartial = api.selectiveRefresh.partial( 'widget[' + widgetId + ']' );
296				if ( widgetPartial ) {
297					widgetPartials[ widgetId ] = widgetPartial;
298				}
299			} );
300
301			_.each( sidebarPlacements, function( sidebarPlacement ) {
302				var sidebarWidgets = [], needsSort = false, thisPosition, lastPosition = -1;
303
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 ) {
307
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
314							} );
315							if ( thisPosition < lastPosition ) {
316								needsSort = true;
317							}
318							lastPosition = thisPosition;
319						}
320					} );
321				} );
322
323				if ( needsSort ) {
324					_.each( sidebarWidgets, function( sidebarWidget ) {
325						sidebarPlacement.endNode.parentNode.insertBefore(
326							sidebarWidget.placement.container[0],
327							sidebarPlacement.endNode
328						);
329
330						// @todo Rename partial-placement-moved?
331						api.selectiveRefresh.trigger( 'partial-content-moved', sidebarWidget.placement );
332					} );
333
334					sortedSidebarContainers.push( sidebarPlacement );
335				}
336			} );
337
338			if ( sortedSidebarContainers.length > 0 ) {
339				api.selectiveRefresh.trigger( 'sidebar-updated', sidebarPartial );
340			}
341
342			return sortedSidebarContainers;
343		},
344
345		/**
346		 * Make sure there is a widget instance container in this sidebar for the given widget ID.
347		 *
348		 * @since 4.5.0
349		 *
350		 * @param {string} widgetId
351		 * @returns {wp.customize.selectiveRefresh.Partial} Widget instance partial.
352		 */
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, {
358					params: {}
359				} );
360			}
361
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;
365
366				foundWidgetPlacement = _.find( widgetPartial.placements(), function( widgetPlacement ) {
367					return ( widgetPlacement.context.sidebar_instance_number === sidebarPlacement.context.instanceNumber );
368				} );
369				if ( foundWidgetPlacement ) {
370					return;
371				}
372
373				widgetContainerElement = $(
374					sidebarPartial.params.sidebarArgs.before_widget.replace( /%1\$s/g, widgetId ).replace( /%2\$s/g, 'widget' ) +
375					sidebarPartial.params.sidebarArgs.after_widget
376				);
377
378				// Handle rare case where before_widget and after_widget are empty.
379				if ( ! widgetContainerElement[0] ) {
380					return;
381				}
382
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 );
386
387				/*
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
391				 * multiple times.
392				 */
393				widgetContainerElement.data( 'customize-partial-placement-context', {
394					'sidebar_id': sidebarPartial.sidebarId,
395					'sidebar_instance_number': sidebarPlacement.context.instanceNumber
396				} );
397
398				sidebarPlacement.endNode.parentNode.insertBefore( widgetContainerElement[0], sidebarPlacement.endNode );
399				wasInserted = true;
400			} );
401
402			api.selectiveRefresh.partial.add( widgetPartial.id, widgetPartial );
403
404			if ( wasInserted ) {
405				sidebarPartial.reflowWidgets();
406			}
407
408			return widgetPartial;
409		},
410
411		/**
412		 * Handle change to the sidebars_widgets[] setting.
413		 *
414		 * @since 4.5.0
415		 *
416		 * @param {Array} newWidgetIds New widget ids.
417		 * @param {Array} oldWidgetIds Old widget ids.
418		 */
419		handleSettingChange: function( newWidgetIds, oldWidgetIds ) {
420			var sidebarPartial = this, needsRefresh, widgetsRemoved, widgetsAdded, addedWidgetPartials = [];
421
422			needsRefresh = (
423				( oldWidgetIds.length > 0 && 0 === newWidgetIds.length ) ||
424				( newWidgetIds.length > 0 && 0 === oldWidgetIds.length )
425			);
426			if ( needsRefresh ) {
427				sidebarPartial.fallback();
428				return;
429			}
430
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 ) {
437						var isRemoved = (
438							placement.context.sidebar_id === sidebarPartial.sidebarId ||
439							( placement.context.sidebar_args && placement.context.sidebar_args.id === sidebarPartial.sidebarId )
440						);
441						if ( isRemoved ) {
442							placement.container.remove();
443						}
444					} );
445				}
446			} );
447
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 );
453			} );
454
455			_.each( addedWidgetPartials, function( widgetPartial ) {
456				widgetPartial.refresh();
457			} );
458
459			api.selectiveRefresh.trigger( 'sidebar-updated', sidebarPartial );
460		},
461
462		/**
463		 * Note that the meat is handled in handleSettingChange because it has the context of which widgets were removed.
464		 *
465		 * @since 4.5.0
466		 */
467		refresh: function() {
468			var partial = this, deferred = $.Deferred();
469
470			deferred.fail( function() {
471				partial.fallback();
472			} );
473
474			if ( 0 === partial.placements().length ) {
475				deferred.reject();
476			} else {
477				_.each( partial.reflowWidgets(), function( sidebarPlacement ) {
478					api.selectiveRefresh.trigger( 'partial-content-rendered', sidebarPlacement );
479				} );
480				deferred.resolve();
481			}
482
483			return deferred.promise();
484		}
485	});
486
487	api.selectiveRefresh.partialConstructor.sidebar = self.SidebarPartial;
488	api.selectiveRefresh.partialConstructor.widget = self.WidgetPartial;
489
490	/**
491	 * Add partials for the registered widget areas (sidebars).
492	 *
493	 * @since 4.5.0
494	 */
495	self.addPartials = function() {
496		_.each( self.registeredSidebars, function( registeredSidebar ) {
497			var partial, partialId = 'sidebar[' + registeredSidebar.id + ']';
498			partial = api.selectiveRefresh.partial( partialId );
499			if ( ! partial ) {
500				partial = new self.SidebarPartial( partialId, {
501					params: {
502						sidebarArgs: registeredSidebar
503					}
504				} );
505				api.selectiveRefresh.partial.add( partial.id, partial );
506			}
507		} );
508	};
509
510	/**
511	 * Calculate the selector for the sidebar's widgets based on the registered sidebar's info.
512	 *
513	 * @since 3.9.0
514	 */
515	self.buildWidgetSelectors = function() {
516		var self = this;
517
518		$.each( self.registeredSidebars, function( i, sidebar ) {
519			var widgetTpl = [
520					sidebar.before_widget,
521					sidebar.before_title,
522					sidebar.after_title,
523					sidebar.after_widget
524				].join( '' ),
525				emptyWidget,
526				widgetSelector,
527				widgetClasses;
528
529			emptyWidget = $( widgetTpl );
530			widgetSelector = emptyWidget.prop( 'tagName' ) || '';
531			widgetClasses = emptyWidget.prop( 'className' ) || '';
532
533			// Prevent a rare case when before_widget, before_title, after_title and after_widget is empty.
534			if ( ! widgetClasses ) {
535				return;
536			}
537
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( '.' );
543			}
544			self.widgetSelectors.push( widgetSelector );
545		});
546	};
547
548	/**
549	 * Highlight the widget on widget updates or widget control mouse overs.
550	 *
551	 * @since 3.9.0
552	 * @param  {string} widgetId ID of the widget.
553	 */
554	self.highlightWidget = function( widgetId ) {
555		var $body = $( document.body ),
556			$widget = $( '#' + widgetId );
557
558		$body.find( '.widget-customizer-highlighted-widget' ).removeClass( 'widget-customizer-highlighted-widget' );
559
560		$widget.addClass( 'widget-customizer-highlighted-widget' );
561		setTimeout( function() {
562			$widget.removeClass( 'widget-customizer-highlighted-widget' );
563		}, 500 );
564	};
565
566	/**
567	 * Show a title and highlight widgets on hover. On shift+clicking
568	 * focus the widget control.
569	 *
570	 * @since 3.9.0
571	 */
572	self.highlightControls = function() {
573		var self = this,
574			selector = this.widgetSelectors.join( ',' );
575
576		// Skip adding highlights if not in the customizer preview iframe.
577		if ( ! api.settings.channel ) {
578			return;
579		}
580
581		$( selector ).attr( 'title', this.l10n.widgetTooltip );
582
583		$( document ).on( 'mouseenter', selector, function() {
584			self.preview.send( 'highlight-widget-control', $( this ).prop( 'id' ) );
585		});
586
587		// Open expand the widget control when shift+clicking the widget element
588		$( document ).on( 'click', selector, function( e ) {
589			if ( ! e.shiftKey ) {
590				return;
591			}
592			e.preventDefault();
593
594			self.preview.send( 'focus-widget-control', $( this ).prop( 'id' ) );
595		});
596	};
597
598	/**
599	 * Parse a widget ID.
600	 *
601	 * @since 4.5.0
602	 *
603	 * @param {string} widgetId Widget ID.
604	 * @returns {{idBase: string, number: number|null}}
605	 */
606	self.parseWidgetId = function( widgetId ) {
607		var matches, parsed = {
608			idBase: '',
609			number: null
610		};
611
612		matches = widgetId.match( /^(.+)-(\d+)$/ );
613		if ( matches ) {
614			parsed.idBase = matches[1];
615			parsed.number = parseInt( matches[2], 10 );
616		} else {
617			parsed.idBase = widgetId; // Likely an old single widget.
618		}
619
620		return parsed;
621	};
622
623	/**
624	 * Parse a widget setting ID.
625	 *
626	 * @since 4.5.0
627	 *
628	 * @param {string} settingId Widget setting ID.
629	 * @returns {{idBase: string, number: number|null}|null}
630	 */
631	self.parseWidgetSettingId = function( settingId ) {
632		var matches, parsed = {
633			idBase: '',
634			number: null
635		};
636
637		matches = settingId.match( /^widget_([^\[]+?)(?:\[(\d+)])?$/ );
638		if ( ! matches ) {
639			return null;
640		}
641		parsed.idBase = matches[1];
642		if ( matches[2] ) {
643			parsed.number = parseInt( matches[2], 10 );
644		}
645		return parsed;
646	};
647
648	/**
649	 * Convert a widget ID into a Customizer setting ID.
650	 *
651	 * @since 4.5.0
652	 *
653	 * @param {string} widgetId Widget ID.
654	 * @returns {string} settingId Setting ID.
655	 */
656	self.getWidgetSettingId = function( widgetId ) {
657		var parsed = this.parseWidgetId( widgetId ), settingId;
658
659		settingId = 'widget_' + parsed.idBase;
660		if ( parsed.number ) {
661			settingId += '[' + String( parsed.number ) + ']';
662		}
663
664		return settingId;
665	};
666
667	api.bind( 'preview-ready', function() {
668		$.extend( self, _wpWidgetCustomizerPreviewSettings );
669		self.init();
670	});
671
672	return self;
673})( jQuery, _, wp, wp.customize );