home *** CD-ROM | disk | FTP | other *** search
/ HTML Examples / WP.iso / wordpress / wp-includes / js / customize-selective-refresh.js < prev    next >
Encoding:
JavaScript  |  2018-01-30  |  32.5 KB  |  1,063 lines

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