[ Index ] |
PHP Cross Reference of WordPress |
[Summary view] [Print] [Text view]
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 ) );
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Nov 21 01:00:03 2024 | Cross-referenced by PHPXref 0.7.1 |