balmet.com

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

customize-selective-refresh.js (33332B)


      1 /**
      2  * @output wp-includes/js/customize-selective-refresh.js
      3  */
      4 
      5 /* global jQuery, JSON, _customizePartialRefreshExports, console */
      6 
      7 /** @namespace wp.customize.selectiveRefresh */
      8 wp.customize.selectiveRefresh = ( function( $, api ) {
      9 	'use strict';
     10 	var self, Partial, Placement;
     11 
     12 	self = {
     13 		ready: $.Deferred(),
     14 		editShortcutVisibility: new api.Value(),
     15 		data: {
     16 			partials: {},
     17 			renderQueryVar: '',
     18 			l10n: {
     19 				shiftClickToEdit: ''
     20 			}
     21 		},
     22 		currentRequest: null
     23 	};
     24 
     25 	_.extend( self, api.Events );
     26 
     27 	/**
     28 	 * A Customizer Partial.
     29 	 *
     30 	 * A partial provides a rendering of one or more settings according to a template.
     31 	 *
     32 	 * @memberOf wp.customize.selectiveRefresh
     33 	 *
     34 	 * @see PHP class WP_Customize_Partial.
     35 	 *
     36 	 * @class
     37 	 * @augments wp.customize.Class
     38 	 * @since 4.5.0
     39 	 */
     40 	Partial = self.Partial = api.Class.extend(/** @lends wp.customize.SelectiveRefresh.Partial.prototype */{
     41 
     42 		id: null,
     43 
     44 		/**
     45 		 * Default params.
     46 		 *
     47 		 * @since 4.9.0
     48 		 * @var {object}
     49 		 */
     50 		defaults: {
     51 			selector: null,
     52 			primarySetting: null,
     53 			containerInclusive: false,
     54 			fallbackRefresh: true // Note this needs to be false in a front-end editing context.
     55 		},
     56 
     57 		/**
     58 		 * Constructor.
     59 		 *
     60 		 * @since 4.5.0
     61 		 *
     62 		 * @param {string}  id                      - Unique identifier for the partial instance.
     63 		 * @param {Object}  options                 - Options hash for the partial instance.
     64 		 * @param {string}  options.type            - Type of partial (e.g. nav_menu, widget, etc)
     65 		 * @param {string}  options.selector        - jQuery selector to find the container element in the page.
     66 		 * @param {Array}   options.settings        - The IDs for the settings the partial relates to.
     67 		 * @param {string}  options.primarySetting  - The ID for the primary setting the partial renders.
     68 		 * @param {boolean} options.fallbackRefresh - Whether to refresh the entire preview in case of a partial refresh failure.
     69 		 * @param {Object}  [options.params]        - Deprecated wrapper for the above properties.
     70 		 */
     71 		initialize: function( id, options ) {
     72 			var partial = this;
     73 			options = options || {};
     74 			partial.id = id;
     75 
     76 			partial.params = _.extend(
     77 				{
     78 					settings: []
     79 				},
     80 				partial.defaults,
     81 				options.params || options
     82 			);
     83 
     84 			partial.deferred = {};
     85 			partial.deferred.ready = $.Deferred();
     86 
     87 			partial.deferred.ready.done( function() {
     88 				partial.ready();
     89 			} );
     90 		},
     91 
     92 		/**
     93 		 * Set up the partial.
     94 		 *
     95 		 * @since 4.5.0
     96 		 */
     97 		ready: function() {
     98 			var partial = this;
     99 			_.each( partial.placements(), function( placement ) {
    100 				$( placement.container ).attr( 'title', self.data.l10n.shiftClickToEdit );
    101 				partial.createEditShortcutForPlacement( placement );
    102 			} );
    103 			$( document ).on( 'click', partial.params.selector, function( e ) {
    104 				if ( ! e.shiftKey ) {
    105 					return;
    106 				}
    107 				e.preventDefault();
    108 				_.each( partial.placements(), function( placement ) {
    109 					if ( $( placement.container ).is( e.currentTarget ) ) {
    110 						partial.showControl();
    111 					}
    112 				} );
    113 			} );
    114 		},
    115 
    116 		/**
    117 		 * Create and show the edit shortcut for a given partial placement container.
    118 		 *
    119 		 * @since 4.7.0
    120 		 * @access public
    121 		 *
    122 		 * @param {Placement} placement The placement container element.
    123 		 * @return {void}
    124 		 */
    125 		createEditShortcutForPlacement: function( placement ) {
    126 			var partial = this, $shortcut, $placementContainer, illegalAncestorSelector, illegalContainerSelector;
    127 			if ( ! placement.container ) {
    128 				return;
    129 			}
    130 			$placementContainer = $( placement.container );
    131 			illegalAncestorSelector = 'head';
    132 			illegalContainerSelector = 'area, audio, base, bdi, bdo, br, button, canvas, col, colgroup, command, datalist, embed, head, hr, html, iframe, img, input, keygen, label, link, map, math, menu, meta, noscript, object, optgroup, option, param, progress, rp, rt, ruby, script, select, source, style, svg, table, tbody, textarea, tfoot, thead, title, tr, track, video, wbr';
    133 			if ( ! $placementContainer.length || $placementContainer.is( illegalContainerSelector ) || $placementContainer.closest( illegalAncestorSelector ).length ) {
    134 				return;
    135 			}
    136 			$shortcut = partial.createEditShortcut();
    137 			$shortcut.on( 'click', function( event ) {
    138 				event.preventDefault();
    139 				event.stopPropagation();
    140 				partial.showControl();
    141 			} );
    142 			partial.addEditShortcutToPlacement( placement, $shortcut );
    143 		},
    144 
    145 		/**
    146 		 * Add an edit shortcut to the placement container.
    147 		 *
    148 		 * @since 4.7.0
    149 		 * @access public
    150 		 *
    151 		 * @param {Placement} placement The placement for the partial.
    152 		 * @param {jQuery} $editShortcut The shortcut element as a jQuery object.
    153 		 * @return {void}
    154 		 */
    155 		addEditShortcutToPlacement: function( placement, $editShortcut ) {
    156 			var $placementContainer = $( placement.container );
    157 			$placementContainer.prepend( $editShortcut );
    158 			if ( ! $placementContainer.is( ':visible' ) || 'none' === $placementContainer.css( 'display' ) ) {
    159 				$editShortcut.addClass( 'customize-partial-edit-shortcut-hidden' );
    160 			}
    161 		},
    162 
    163 		/**
    164 		 * Return the unique class name for the edit shortcut button for this partial.
    165 		 *
    166 		 * @since 4.7.0
    167 		 * @access public
    168 		 *
    169 		 * @return {string} Partial ID converted into a class name for use in shortcut.
    170 		 */
    171 		getEditShortcutClassName: function() {
    172 			var partial = this, cleanId;
    173 			cleanId = partial.id.replace( /]/g, '' ).replace( /\[/g, '-' );
    174 			return 'customize-partial-edit-shortcut-' + cleanId;
    175 		},
    176 
    177 		/**
    178 		 * Return the appropriate translated string for the edit shortcut button.
    179 		 *
    180 		 * @since 4.7.0
    181 		 * @access public
    182 		 *
    183 		 * @return {string} Tooltip for edit shortcut.
    184 		 */
    185 		getEditShortcutTitle: function() {
    186 			var partial = this, l10n = self.data.l10n;
    187 			switch ( partial.getType() ) {
    188 				case 'widget':
    189 					return l10n.clickEditWidget;
    190 				case 'blogname':
    191 					return l10n.clickEditTitle;
    192 				case 'blogdescription':
    193 					return l10n.clickEditTitle;
    194 				case 'nav_menu':
    195 					return l10n.clickEditMenu;
    196 				default:
    197 					return l10n.clickEditMisc;
    198 			}
    199 		},
    200 
    201 		/**
    202 		 * Return the type of this partial
    203 		 *
    204 		 * Will use `params.type` if set, but otherwise will try to infer type from settingId.
    205 		 *
    206 		 * @since 4.7.0
    207 		 * @access public
    208 		 *
    209 		 * @return {string} Type of partial derived from type param or the related setting ID.
    210 		 */
    211 		getType: function() {
    212 			var partial = this, settingId;
    213 			settingId = partial.params.primarySetting || _.first( partial.settings() ) || 'unknown';
    214 			if ( partial.params.type ) {
    215 				return partial.params.type;
    216 			}
    217 			if ( settingId.match( /^nav_menu_instance\[/ ) ) {
    218 				return 'nav_menu';
    219 			}
    220 			if ( settingId.match( /^widget_.+\[\d+]$/ ) ) {
    221 				return 'widget';
    222 			}
    223 			return settingId;
    224 		},
    225 
    226 		/**
    227 		 * Create an edit shortcut button for this partial.
    228 		 *
    229 		 * @since 4.7.0
    230 		 * @access public
    231 		 *
    232 		 * @return {jQuery} The edit shortcut button element.
    233 		 */
    234 		createEditShortcut: function() {
    235 			var partial = this, shortcutTitle, $buttonContainer, $button, $image;
    236 			shortcutTitle = partial.getEditShortcutTitle();
    237 			$buttonContainer = $( '<span>', {
    238 				'class': 'customize-partial-edit-shortcut ' + partial.getEditShortcutClassName()
    239 			} );
    240 			$button = $( '<button>', {
    241 				'aria-label': shortcutTitle,
    242 				'title': shortcutTitle,
    243 				'class': 'customize-partial-edit-shortcut-button'
    244 			} );
    245 			$image = $( '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M13.89 3.39l2.71 2.72c.46.46.42 1.24.03 1.64l-8.01 8.02-5.56 1.16 1.16-5.58s7.6-7.63 7.99-8.03c.39-.39 1.22-.39 1.68.07zm-2.73 2.79l-5.59 5.61 1.11 1.11 5.54-5.65zm-2.97 8.23l5.58-5.6-1.07-1.08-5.59 5.6z"/></svg>' );
    246 			$button.append( $image );
    247 			$buttonContainer.append( $button );
    248 			return $buttonContainer;
    249 		},
    250 
    251 		/**
    252 		 * Find all placements for this partial in the document.
    253 		 *
    254 		 * @since 4.5.0
    255 		 *
    256 		 * @return {Array.<Placement>}
    257 		 */
    258 		placements: function() {
    259 			var partial = this, selector;
    260 
    261 			selector = partial.params.selector || '';
    262 			if ( selector ) {
    263 				selector += ', ';
    264 			}
    265 			selector += '[data-customize-partial-id="' + partial.id + '"]'; // @todo Consider injecting customize-partial-id-${id} classnames instead.
    266 
    267 			return $( selector ).map( function() {
    268 				var container = $( this ), context;
    269 
    270 				context = container.data( 'customize-partial-placement-context' );
    271 				if ( _.isString( context ) && '{' === context.substr( 0, 1 ) ) {
    272 					throw new Error( 'context JSON parse error' );
    273 				}
    274 
    275 				return new Placement( {
    276 					partial: partial,
    277 					container: container,
    278 					context: context
    279 				} );
    280 			} ).get();
    281 		},
    282 
    283 		/**
    284 		 * Get list of setting IDs related to this partial.
    285 		 *
    286 		 * @since 4.5.0
    287 		 *
    288 		 * @return {string[]}
    289 		 */
    290 		settings: function() {
    291 			var partial = this;
    292 			if ( partial.params.settings && 0 !== partial.params.settings.length ) {
    293 				return partial.params.settings;
    294 			} else if ( partial.params.primarySetting ) {
    295 				return [ partial.params.primarySetting ];
    296 			} else {
    297 				return [ partial.id ];
    298 			}
    299 		},
    300 
    301 		/**
    302 		 * Return whether the setting is related to the partial.
    303 		 *
    304 		 * @since 4.5.0
    305 		 *
    306 		 * @param {wp.customize.Value|string} setting  ID or object for setting.
    307 		 * @return {boolean} Whether the setting is related to the partial.
    308 		 */
    309 		isRelatedSetting: function( setting /*... newValue, oldValue */ ) {
    310 			var partial = this;
    311 			if ( _.isString( setting ) ) {
    312 				setting = api( setting );
    313 			}
    314 			if ( ! setting ) {
    315 				return false;
    316 			}
    317 			return -1 !== _.indexOf( partial.settings(), setting.id );
    318 		},
    319 
    320 		/**
    321 		 * Show the control to modify this partial's setting(s).
    322 		 *
    323 		 * This may be overridden for inline editing.
    324 		 *
    325 		 * @since 4.5.0
    326 		 */
    327 		showControl: function() {
    328 			var partial = this, settingId = partial.params.primarySetting;
    329 			if ( ! settingId ) {
    330 				settingId = _.first( partial.settings() );
    331 			}
    332 			if ( partial.getType() === 'nav_menu' ) {
    333 				if ( partial.params.navMenuArgs.theme_location ) {
    334 					settingId = 'nav_menu_locations[' + partial.params.navMenuArgs.theme_location + ']';
    335 				} else if ( partial.params.navMenuArgs.menu )   {
    336 					settingId = 'nav_menu[' + String( partial.params.navMenuArgs.menu ) + ']';
    337 				}
    338 			}
    339 			api.preview.send( 'focus-control-for-setting', settingId );
    340 		},
    341 
    342 		/**
    343 		 * Prepare container for selective refresh.
    344 		 *
    345 		 * @since 4.5.0
    346 		 *
    347 		 * @param {Placement} placement
    348 		 */
    349 		preparePlacement: function( placement ) {
    350 			$( placement.container ).addClass( 'customize-partial-refreshing' );
    351 		},
    352 
    353 		/**
    354 		 * Reference to the pending promise returned from self.requestPartial().
    355 		 *
    356 		 * @since 4.5.0
    357 		 * @private
    358 		 */
    359 		_pendingRefreshPromise: null,
    360 
    361 		/**
    362 		 * Request the new partial and render it into the placements.
    363 		 *
    364 		 * @since 4.5.0
    365 		 *
    366 		 * @this {wp.customize.selectiveRefresh.Partial}
    367 		 * @return {jQuery.Promise}
    368 		 */
    369 		refresh: function() {
    370 			var partial = this, refreshPromise;
    371 
    372 			refreshPromise = self.requestPartial( partial );
    373 
    374 			if ( ! partial._pendingRefreshPromise ) {
    375 				_.each( partial.placements(), function( placement ) {
    376 					partial.preparePlacement( placement );
    377 				} );
    378 
    379 				refreshPromise.done( function( placements ) {
    380 					_.each( placements, function( placement ) {
    381 						partial.renderContent( placement );
    382 					} );
    383 				} );
    384 
    385 				refreshPromise.fail( function( data, placements ) {
    386 					partial.fallback( data, placements );
    387 				} );
    388 
    389 				// Allow new request when this one finishes.
    390 				partial._pendingRefreshPromise = refreshPromise;
    391 				refreshPromise.always( function() {
    392 					partial._pendingRefreshPromise = null;
    393 				} );
    394 			}
    395 
    396 			return refreshPromise;
    397 		},
    398 
    399 		/**
    400 		 * Apply the addedContent in the placement to the document.
    401 		 *
    402 		 * Note the placement object will have its container and removedNodes
    403 		 * properties updated.
    404 		 *
    405 		 * @since 4.5.0
    406 		 *
    407 		 * @param {Placement}             placement
    408 		 * @param {Element|jQuery}        [placement.container]  - This param will be empty if there was no element matching the selector.
    409 		 * @param {string|Object|boolean} placement.addedContent - Rendered HTML content, a data object for JS templates to render, or false if no render.
    410 		 * @param {Object}                [placement.context]    - Optional context information about the container.
    411 		 * @return {boolean} Whether the rendering was successful and the fallback was not invoked.
    412 		 */
    413 		renderContent: function( placement ) {
    414 			var partial = this, content, newContainerElement;
    415 			if ( ! placement.container ) {
    416 				partial.fallback( new Error( 'no_container' ), [ placement ] );
    417 				return false;
    418 			}
    419 			placement.container = $( placement.container );
    420 			if ( false === placement.addedContent ) {
    421 				partial.fallback( new Error( 'missing_render' ), [ placement ] );
    422 				return false;
    423 			}
    424 
    425 			// Currently a subclass needs to override renderContent to handle partials returning data object.
    426 			if ( ! _.isString( placement.addedContent ) ) {
    427 				partial.fallback( new Error( 'non_string_content' ), [ placement ] );
    428 				return false;
    429 			}
    430 
    431 			/* jshint ignore:start */
    432 			self.orginalDocumentWrite = document.write;
    433 			document.write = function() {
    434 				throw new Error( self.data.l10n.badDocumentWrite );
    435 			};
    436 			/* jshint ignore:end */
    437 			try {
    438 				content = placement.addedContent;
    439 				if ( wp.emoji && wp.emoji.parse && ! $.contains( document.head, placement.container[0] ) ) {
    440 					content = wp.emoji.parse( content );
    441 				}
    442 
    443 				if ( partial.params.containerInclusive ) {
    444 
    445 					// Note that content may be an empty string, and in this case jQuery will just remove the oldContainer.
    446 					newContainerElement = $( content );
    447 
    448 					// Merge the new context on top of the old context.
    449 					placement.context = _.extend(
    450 						placement.context,
    451 						newContainerElement.data( 'customize-partial-placement-context' ) || {}
    452 					);
    453 					newContainerElement.data( 'customize-partial-placement-context', placement.context );
    454 
    455 					placement.removedNodes = placement.container;
    456 					placement.container = newContainerElement;
    457 					placement.removedNodes.replaceWith( placement.container );
    458 					placement.container.attr( 'title', self.data.l10n.shiftClickToEdit );
    459 				} else {
    460 					placement.removedNodes = document.createDocumentFragment();
    461 					while ( placement.container[0].firstChild ) {
    462 						placement.removedNodes.appendChild( placement.container[0].firstChild );
    463 					}
    464 
    465 					placement.container.html( content );
    466 				}
    467 
    468 				placement.container.removeClass( 'customize-render-content-error' );
    469 			} catch ( error ) {
    470 				if ( 'undefined' !== typeof console && console.error ) {
    471 					console.error( partial.id, error );
    472 				}
    473 				partial.fallback( error, [ placement ] );
    474 			}
    475 			/* jshint ignore:start */
    476 			document.write = self.orginalDocumentWrite;
    477 			self.orginalDocumentWrite = null;
    478 			/* jshint ignore:end */
    479 
    480 			partial.createEditShortcutForPlacement( placement );
    481 			placement.container.removeClass( 'customize-partial-refreshing' );
    482 
    483 			// Prevent placement container from being re-triggered as being rendered among nested partials.
    484 			placement.container.data( 'customize-partial-content-rendered', true );
    485 
    486 			/*
    487 			 * Note that the 'wp_audio_shortcode_library' and 'wp_video_shortcode_library' filters
    488 			 * will determine whether or not wp.mediaelement is loaded and whether it will
    489 			 * initialize audio and video respectively. See also https://core.trac.wordpress.org/ticket/40144
    490 			 */
    491 			if ( wp.mediaelement ) {
    492 				wp.mediaelement.initialize();
    493 			}
    494 
    495 			if ( wp.playlist ) {
    496 				wp.playlist.initialize();
    497 			}
    498 
    499 			/**
    500 			 * Announce when a partial's placement has been rendered so that dynamic elements can be re-built.
    501 			 */
    502 			self.trigger( 'partial-content-rendered', placement );
    503 			return true;
    504 		},
    505 
    506 		/**
    507 		 * Handle fail to render partial.
    508 		 *
    509 		 * The first argument is either the failing jqXHR or an Error object, and the second argument is the array of containers.
    510 		 *
    511 		 * @since 4.5.0
    512 		 */
    513 		fallback: function() {
    514 			var partial = this;
    515 			if ( partial.params.fallbackRefresh ) {
    516 				self.requestFullRefresh();
    517 			}
    518 		}
    519 	} );
    520 
    521 	/**
    522 	 * A Placement for a Partial.
    523 	 *
    524 	 * A partial placement is the actual physical representation of a partial for a given context.
    525 	 * It also may have information in relation to how a placement may have just changed.
    526 	 * The placement is conceptually similar to a DOM Range or MutationRecord.
    527 	 *
    528 	 * @memberOf wp.customize.selectiveRefresh
    529 	 *
    530 	 * @class Placement
    531 	 * @augments wp.customize.Class
    532 	 * @since 4.5.0
    533 	 */
    534 	self.Placement = Placement = api.Class.extend(/** @lends wp.customize.selectiveRefresh.prototype */{
    535 
    536 		/**
    537 		 * The partial with which the container is associated.
    538 		 *
    539 		 * @param {wp.customize.selectiveRefresh.Partial}
    540 		 */
    541 		partial: null,
    542 
    543 		/**
    544 		 * DOM element which contains the placement's contents.
    545 		 *
    546 		 * This will be null if the startNode and endNode do not point to the same
    547 		 * DOM element, such as in the case of a sidebar partial.
    548 		 * This container element itself will be replaced for partials that
    549 		 * have containerInclusive param defined as true.
    550 		 */
    551 		container: null,
    552 
    553 		/**
    554 		 * DOM node for the initial boundary of the placement.
    555 		 *
    556 		 * This will normally be the same as endNode since most placements appear as elements.
    557 		 * This is primarily useful for widget sidebars which do not have intrinsic containers, but
    558 		 * for which an HTML comment is output before to mark the starting position.
    559 		 */
    560 		startNode: null,
    561 
    562 		/**
    563 		 * DOM node for the terminal boundary of the placement.
    564 		 *
    565 		 * This will normally be the same as startNode since most placements appear as elements.
    566 		 * This is primarily useful for widget sidebars which do not have intrinsic containers, but
    567 		 * for which an HTML comment is output before to mark the ending position.
    568 		 */
    569 		endNode: null,
    570 
    571 		/**
    572 		 * Context data.
    573 		 *
    574 		 * This provides information about the placement which is included in the request
    575 		 * in order to render the partial properly.
    576 		 *
    577 		 * @param {object}
    578 		 */
    579 		context: null,
    580 
    581 		/**
    582 		 * The content for the partial when refreshed.
    583 		 *
    584 		 * @param {string}
    585 		 */
    586 		addedContent: null,
    587 
    588 		/**
    589 		 * DOM node(s) removed when the partial is refreshed.
    590 		 *
    591 		 * If the partial is containerInclusive, then the removedNodes will be
    592 		 * the single Element that was the partial's former placement. If the
    593 		 * partial is not containerInclusive, then the removedNodes will be a
    594 		 * documentFragment containing the nodes removed.
    595 		 *
    596 		 * @param {Element|DocumentFragment}
    597 		 */
    598 		removedNodes: null,
    599 
    600 		/**
    601 		 * Constructor.
    602 		 *
    603 		 * @since 4.5.0
    604 		 *
    605 		 * @param {Object}                   args
    606 		 * @param {Partial}                  args.partial
    607 		 * @param {jQuery|Element}           [args.container]
    608 		 * @param {Node}                     [args.startNode]
    609 		 * @param {Node}                     [args.endNode]
    610 		 * @param {Object}                   [args.context]
    611 		 * @param {string}                   [args.addedContent]
    612 		 * @param {jQuery|DocumentFragment}  [args.removedNodes]
    613 		 */
    614 		initialize: function( args ) {
    615 			var placement = this;
    616 
    617 			args = _.extend( {}, args || {} );
    618 			if ( ! args.partial || ! args.partial.extended( Partial ) ) {
    619 				throw new Error( 'Missing partial' );
    620 			}
    621 			args.context = args.context || {};
    622 			if ( args.container ) {
    623 				args.container = $( args.container );
    624 			}
    625 
    626 			_.extend( placement, args );
    627 		}
    628 
    629 	});
    630 
    631 	/**
    632 	 * Mapping of type names to Partial constructor subclasses.
    633 	 *
    634 	 * @since 4.5.0
    635 	 *
    636 	 * @type {Object.<string, wp.customize.selectiveRefresh.Partial>}
    637 	 */
    638 	self.partialConstructor = {};
    639 
    640 	self.partial = new api.Values({ defaultConstructor: Partial });
    641 
    642 	/**
    643 	 * Get the POST vars for a Customizer preview request.
    644 	 *
    645 	 * @since 4.5.0
    646 	 * @see wp.customize.previewer.query()
    647 	 *
    648 	 * @return {Object}
    649 	 */
    650 	self.getCustomizeQuery = function() {
    651 		var dirtyCustomized = {};
    652 		api.each( function( value, key ) {
    653 			if ( value._dirty ) {
    654 				dirtyCustomized[ key ] = value();
    655 			}
    656 		} );
    657 
    658 		return {
    659 			wp_customize: 'on',
    660 			nonce: api.settings.nonce.preview,
    661 			customize_theme: api.settings.theme.stylesheet,
    662 			customized: JSON.stringify( dirtyCustomized ),
    663 			customize_changeset_uuid: api.settings.changeset.uuid
    664 		};
    665 	};
    666 
    667 	/**
    668 	 * Currently-requested partials and their associated deferreds.
    669 	 *
    670 	 * @since 4.5.0
    671 	 * @type {Object<string, { deferred: jQuery.Promise, partial: wp.customize.selectiveRefresh.Partial }>}
    672 	 */
    673 	self._pendingPartialRequests = {};
    674 
    675 	/**
    676 	 * Timeout ID for the current requesr, or null if no request is current.
    677 	 *
    678 	 * @since 4.5.0
    679 	 * @type {number|null}
    680 	 * @private
    681 	 */
    682 	self._debouncedTimeoutId = null;
    683 
    684 	/**
    685 	 * Current jqXHR for the request to the partials.
    686 	 *
    687 	 * @since 4.5.0
    688 	 * @type {jQuery.jqXHR|null}
    689 	 * @private
    690 	 */
    691 	self._currentRequest = null;
    692 
    693 	/**
    694 	 * Request full page refresh.
    695 	 *
    696 	 * When selective refresh is embedded in the context of front-end editing, this request
    697 	 * must fail or else changes will be lost, unless transactions are implemented.
    698 	 *
    699 	 * @since 4.5.0
    700 	 */
    701 	self.requestFullRefresh = function() {
    702 		api.preview.send( 'refresh' );
    703 	};
    704 
    705 	/**
    706 	 * Request a re-rendering of a partial.
    707 	 *
    708 	 * @since 4.5.0
    709 	 *
    710 	 * @param {wp.customize.selectiveRefresh.Partial} partial
    711 	 * @return {jQuery.Promise}
    712 	 */
    713 	self.requestPartial = function( partial ) {
    714 		var partialRequest;
    715 
    716 		if ( self._debouncedTimeoutId ) {
    717 			clearTimeout( self._debouncedTimeoutId );
    718 			self._debouncedTimeoutId = null;
    719 		}
    720 		if ( self._currentRequest ) {
    721 			self._currentRequest.abort();
    722 			self._currentRequest = null;
    723 		}
    724 
    725 		partialRequest = self._pendingPartialRequests[ partial.id ];
    726 		if ( ! partialRequest || 'pending' !== partialRequest.deferred.state() ) {
    727 			partialRequest = {
    728 				deferred: $.Deferred(),
    729 				partial: partial
    730 			};
    731 			self._pendingPartialRequests[ partial.id ] = partialRequest;
    732 		}
    733 
    734 		// Prevent leaking partial into debounced timeout callback.
    735 		partial = null;
    736 
    737 		self._debouncedTimeoutId = setTimeout(
    738 			function() {
    739 				var data, partialPlacementContexts, partialsPlacements, request;
    740 
    741 				self._debouncedTimeoutId = null;
    742 				data = self.getCustomizeQuery();
    743 
    744 				/*
    745 				 * It is key that the containers be fetched exactly at the point of the request being
    746 				 * made, because the containers need to be mapped to responses by array indices.
    747 				 */
    748 				partialsPlacements = {};
    749 
    750 				partialPlacementContexts = {};
    751 
    752 				_.each( self._pendingPartialRequests, function( pending, partialId ) {
    753 					partialsPlacements[ partialId ] = pending.partial.placements();
    754 					if ( ! self.partial.has( partialId ) ) {
    755 						pending.deferred.rejectWith( pending.partial, [ new Error( 'partial_removed' ), partialsPlacements[ partialId ] ] );
    756 					} else {
    757 						/*
    758 						 * Note that this may in fact be an empty array. In that case, it is the responsibility
    759 						 * of the Partial subclass instance to know where to inject the response, or else to
    760 						 * just issue a refresh (default behavior). The data being returned with each container
    761 						 * is the context information that may be needed to render certain partials, such as
    762 						 * the contained sidebar for rendering widgets or what the nav menu args are for a menu.
    763 						 */
    764 						partialPlacementContexts[ partialId ] = _.map( partialsPlacements[ partialId ], function( placement ) {
    765 							return placement.context || {};
    766 						} );
    767 					}
    768 				} );
    769 
    770 				data.partials = JSON.stringify( partialPlacementContexts );
    771 				data[ self.data.renderQueryVar ] = '1';
    772 
    773 				request = self._currentRequest = wp.ajax.send( null, {
    774 					data: data,
    775 					url: api.settings.url.self
    776 				} );
    777 
    778 				request.done( function( data ) {
    779 
    780 					/**
    781 					 * Announce the data returned from a request to render partials.
    782 					 *
    783 					 * The data is filtered on the server via customize_render_partials_response
    784 					 * so plugins can inject data from the server to be utilized
    785 					 * on the client via this event. Plugins may use this filter
    786 					 * to communicate script and style dependencies that need to get
    787 					 * injected into the page to support the rendered partials.
    788 					 * This is similar to the 'saved' event.
    789 					 */
    790 					self.trigger( 'render-partials-response', data );
    791 
    792 					// Relay errors (warnings) captured during rendering and relay to console.
    793 					if ( data.errors && 'undefined' !== typeof console && console.warn ) {
    794 						_.each( data.errors, function( error ) {
    795 							console.warn( error );
    796 						} );
    797 					}
    798 
    799 					/*
    800 					 * Note that data is an array of items that correspond to the array of
    801 					 * containers that were submitted in the request. So we zip up the
    802 					 * array of containers with the array of contents for those containers,
    803 					 * and send them into .
    804 					 */
    805 					_.each( self._pendingPartialRequests, function( pending, partialId ) {
    806 						var placementsContents;
    807 						if ( ! _.isArray( data.contents[ partialId ] ) ) {
    808 							pending.deferred.rejectWith( pending.partial, [ new Error( 'unrecognized_partial' ), partialsPlacements[ partialId ] ] );
    809 						} else {
    810 							placementsContents = _.map( data.contents[ partialId ], function( content, i ) {
    811 								var partialPlacement = partialsPlacements[ partialId ][ i ];
    812 								if ( partialPlacement ) {
    813 									partialPlacement.addedContent = content;
    814 								} else {
    815 									partialPlacement = new Placement( {
    816 										partial: pending.partial,
    817 										addedContent: content
    818 									} );
    819 								}
    820 								return partialPlacement;
    821 							} );
    822 							pending.deferred.resolveWith( pending.partial, [ placementsContents ] );
    823 						}
    824 					} );
    825 					self._pendingPartialRequests = {};
    826 				} );
    827 
    828 				request.fail( function( data, statusText ) {
    829 
    830 					/*
    831 					 * Ignore failures caused by partial.currentRequest.abort()
    832 					 * The pending deferreds will remain in self._pendingPartialRequests
    833 					 * for re-use with the next request.
    834 					 */
    835 					if ( 'abort' === statusText ) {
    836 						return;
    837 					}
    838 
    839 					_.each( self._pendingPartialRequests, function( pending, partialId ) {
    840 						pending.deferred.rejectWith( pending.partial, [ data, partialsPlacements[ partialId ] ] );
    841 					} );
    842 					self._pendingPartialRequests = {};
    843 				} );
    844 			},
    845 			api.settings.timeouts.selectiveRefresh
    846 		);
    847 
    848 		return partialRequest.deferred.promise();
    849 	};
    850 
    851 	/**
    852 	 * Add partials for any nav menu container elements in the document.
    853 	 *
    854 	 * This method may be called multiple times. Containers that already have been
    855 	 * seen will be skipped.
    856 	 *
    857 	 * @since 4.5.0
    858 	 *
    859 	 * @param {jQuery|HTMLElement} [rootElement]
    860 	 * @param {object}             [options]
    861 	 * @param {boolean=true}       [options.triggerRendered]
    862 	 */
    863 	self.addPartials = function( rootElement, options ) {
    864 		var containerElements;
    865 		if ( ! rootElement ) {
    866 			rootElement = document.documentElement;
    867 		}
    868 		rootElement = $( rootElement );
    869 		options = _.extend(
    870 			{
    871 				triggerRendered: true
    872 			},
    873 			options || {}
    874 		);
    875 
    876 		containerElements = rootElement.find( '[data-customize-partial-id]' );
    877 		if ( rootElement.is( '[data-customize-partial-id]' ) ) {
    878 			containerElements = containerElements.add( rootElement );
    879 		}
    880 		containerElements.each( function() {
    881 			var containerElement = $( this ), partial, placement, id, Constructor, partialOptions, containerContext;
    882 			id = containerElement.data( 'customize-partial-id' );
    883 			if ( ! id ) {
    884 				return;
    885 			}
    886 			containerContext = containerElement.data( 'customize-partial-placement-context' ) || {};
    887 
    888 			partial = self.partial( id );
    889 			if ( ! partial ) {
    890 				partialOptions = containerElement.data( 'customize-partial-options' ) || {};
    891 				partialOptions.constructingContainerContext = containerElement.data( 'customize-partial-placement-context' ) || {};
    892 				Constructor = self.partialConstructor[ containerElement.data( 'customize-partial-type' ) ] || self.Partial;
    893 				partial = new Constructor( id, partialOptions );
    894 				self.partial.add( partial );
    895 			}
    896 
    897 			/*
    898 			 * Only trigger renders on (nested) partials that have been not been
    899 			 * handled yet. An example where this would apply is a nav menu
    900 			 * embedded inside of a navigation menu widget. When the widget's title
    901 			 * is updated, the entire widget will re-render and then the event
    902 			 * will be triggered for the nested nav menu to do any initialization.
    903 			 */
    904 			if ( options.triggerRendered && ! containerElement.data( 'customize-partial-content-rendered' ) ) {
    905 
    906 				placement = new Placement( {
    907 					partial: partial,
    908 					context: containerContext,
    909 					container: containerElement
    910 				} );
    911 
    912 				$( placement.container ).attr( 'title', self.data.l10n.shiftClickToEdit );
    913 				partial.createEditShortcutForPlacement( placement );
    914 
    915 				/**
    916 				 * Announce when a partial's nested placement has been re-rendered.
    917 				 */
    918 				self.trigger( 'partial-content-rendered', placement );
    919 			}
    920 			containerElement.data( 'customize-partial-content-rendered', true );
    921 		} );
    922 	};
    923 
    924 	api.bind( 'preview-ready', function() {
    925 		var handleSettingChange, watchSettingChange, unwatchSettingChange;
    926 
    927 		_.extend( self.data, _customizePartialRefreshExports );
    928 
    929 		// Create the partial JS models.
    930 		_.each( self.data.partials, function( data, id ) {
    931 			var Constructor, partial = self.partial( id );
    932 			if ( ! partial ) {
    933 				Constructor = self.partialConstructor[ data.type ] || self.Partial;
    934 				partial = new Constructor(
    935 					id,
    936 					_.extend( { params: data }, data ) // Inclusion of params alias is for back-compat for custom partials that expect to augment this property.
    937 				);
    938 				self.partial.add( partial );
    939 			} else {
    940 				_.extend( partial.params, data );
    941 			}
    942 		} );
    943 
    944 		/**
    945 		 * Handle change to a setting.
    946 		 *
    947 		 * Note this is largely needed because adding a 'change' event handler to wp.customize
    948 		 * will only include the changed setting object as an argument, not including the
    949 		 * new value or the old value.
    950 		 *
    951 		 * @since 4.5.0
    952 		 * @this {wp.customize.Setting}
    953 		 *
    954 		 * @param {*|null} newValue New value, or null if the setting was just removed.
    955 		 * @param {*|null} oldValue Old value, or null if the setting was just added.
    956 		 */
    957 		handleSettingChange = function( newValue, oldValue ) {
    958 			var setting = this;
    959 			self.partial.each( function( partial ) {
    960 				if ( partial.isRelatedSetting( setting, newValue, oldValue ) ) {
    961 					partial.refresh();
    962 				}
    963 			} );
    964 		};
    965 
    966 		/**
    967 		 * Trigger the initial change for the added setting, and watch for changes.
    968 		 *
    969 		 * @since 4.5.0
    970 		 * @this {wp.customize.Values}
    971 		 *
    972 		 * @param {wp.customize.Setting} setting
    973 		 */
    974 		watchSettingChange = function( setting ) {
    975 			handleSettingChange.call( setting, setting(), null );
    976 			setting.bind( handleSettingChange );
    977 		};
    978 
    979 		/**
    980 		 * Trigger the final change for the removed setting, and unwatch for changes.
    981 		 *
    982 		 * @since 4.5.0
    983 		 * @this {wp.customize.Values}
    984 		 *
    985 		 * @param {wp.customize.Setting} setting
    986 		 */
    987 		unwatchSettingChange = function( setting ) {
    988 			handleSettingChange.call( setting, null, setting() );
    989 			setting.unbind( handleSettingChange );
    990 		};
    991 
    992 		api.bind( 'add', watchSettingChange );
    993 		api.bind( 'remove', unwatchSettingChange );
    994 		api.each( function( setting ) {
    995 			setting.bind( handleSettingChange );
    996 		} );
    997 
    998 		// Add (dynamic) initial partials that are declared via data-* attributes.
    999 		self.addPartials( document.documentElement, {
   1000 			triggerRendered: false
   1001 		} );
   1002 
   1003 		// Add new dynamic partials when the document changes.
   1004 		if ( 'undefined' !== typeof MutationObserver ) {
   1005 			self.mutationObserver = new MutationObserver( function( mutations ) {
   1006 				_.each( mutations, function( mutation ) {
   1007 					self.addPartials( $( mutation.target ) );
   1008 				} );
   1009 			} );
   1010 			self.mutationObserver.observe( document.documentElement, {
   1011 				childList: true,
   1012 				subtree: true
   1013 			} );
   1014 		}
   1015 
   1016 		/**
   1017 		 * Handle rendering of partials.
   1018 		 *
   1019 		 * @param {api.selectiveRefresh.Placement} placement
   1020 		 */
   1021 		api.selectiveRefresh.bind( 'partial-content-rendered', function( placement ) {
   1022 			if ( placement.container ) {
   1023 				self.addPartials( placement.container );
   1024 			}
   1025 		} );
   1026 
   1027 		/**
   1028 		 * Handle setting validities in partial refresh response.
   1029 		 *
   1030 		 * @param {object} data Response data.
   1031 		 * @param {object} data.setting_validities Setting validities.
   1032 		 */
   1033 		api.selectiveRefresh.bind( 'render-partials-response', function handleSettingValiditiesResponse( data ) {
   1034 			if ( data.setting_validities ) {
   1035 				api.preview.send( 'selective-refresh-setting-validities', data.setting_validities );
   1036 			}
   1037 		} );
   1038 
   1039 		api.preview.bind( 'edit-shortcut-visibility', function( visibility ) {
   1040 			api.selectiveRefresh.editShortcutVisibility.set( visibility );
   1041 		} );
   1042 		api.selectiveRefresh.editShortcutVisibility.bind( function( visibility ) {
   1043 			var body = $( document.body ), shouldAnimateHide;
   1044 
   1045 			shouldAnimateHide = ( 'hidden' === visibility && body.hasClass( 'customize-partial-edit-shortcuts-shown' ) && ! body.hasClass( 'customize-partial-edit-shortcuts-hidden' ) );
   1046 			body.toggleClass( 'customize-partial-edit-shortcuts-hidden', shouldAnimateHide );
   1047 			body.toggleClass( 'customize-partial-edit-shortcuts-shown', 'visible' === visibility );
   1048 		} );
   1049 
   1050 		api.preview.bind( 'active', function() {
   1051 
   1052 			// Make all partials ready.
   1053 			self.partial.each( function( partial ) {
   1054 				partial.deferred.ready.resolve();
   1055 			} );
   1056 
   1057 			// Make all partials added henceforth as ready upon add.
   1058 			self.partial.bind( 'add', function( partial ) {
   1059 				partial.deferred.ready.resolve();
   1060 			} );
   1061 		} );
   1062 
   1063 	} );
   1064 
   1065 	return self;
   1066 }( jQuery, wp.customize ) );