]> scripts.mit.edu Git - autoinstalls/wordpress.git/blob - wp-includes/js/customize-preview-widgets.js
WordPress 4.7.2-scripts
[autoinstalls/wordpress.git] / wp-includes / js / customize-preview-widgets.js
1 /* global _wpWidgetCustomizerPreviewSettings */
2 wp.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 );