home *** CD-ROM | disk | FTP | other *** search
/ HTML Examples / WP.iso / wordpress / wp-includes / js / customize-preview-nav-menus.js < prev    next >
Encoding:
JavaScript  |  2017-09-08  |  14.6 KB  |  443 lines

  1. /* global _wpCustomizePreviewNavMenusExports */
  2.  
  3. /** @namespace wp.customize.navMenusPreview */
  4. wp.customize.navMenusPreview = wp.customize.MenusCustomizerPreview = ( function( $, _, wp, api ) {
  5.     'use strict';
  6.  
  7.     var self = {
  8.         data: {
  9.             navMenuInstanceArgs: {}
  10.         }
  11.     };
  12.     if ( 'undefined' !== typeof _wpCustomizePreviewNavMenusExports ) {
  13.         _.extend( self.data, _wpCustomizePreviewNavMenusExports );
  14.     }
  15.  
  16.     /**
  17.      * Initialize nav menus preview.
  18.      */
  19.     self.init = function() {
  20.         var self = this, synced = false;
  21.  
  22.         /*
  23.          * Keep track of whether we synced to determine whether or not bindSettingListener
  24.          * should also initially fire the listener. This initial firing needs to wait until
  25.          * after all of the settings have been synced from the pane in order to prevent
  26.          * an infinite selective fallback-refresh. Note that this sync handler will be
  27.          * added after the sync handler in customize-preview.js, so it will be triggered
  28.          * after all of the settings are added.
  29.          */
  30.         api.preview.bind( 'sync', function() {
  31.             synced = true;
  32.         } );
  33.  
  34.         if ( api.selectiveRefresh ) {
  35.             // Listen for changes to settings related to nav menus.
  36.             api.each( function( setting ) {
  37.                 self.bindSettingListener( setting );
  38.             } );
  39.             api.bind( 'add', function( setting ) {
  40.  
  41.                 /*
  42.                  * Handle case where an invalid nav menu item (one for which its associated object has been deleted)
  43.                  * is synced from the controls into the preview. Since invalid nav menu items are filtered out from
  44.                  * being exported to the frontend by the _is_valid_nav_menu_item filter in wp_get_nav_menu_items(),
  45.                  * the customizer controls will have a nav_menu_item setting where the preview will have none, and
  46.                  * this can trigger an infinite fallback refresh when the nav menu item lacks any valid items.
  47.                  */
  48.                 if ( setting.get() && ! setting.get()._invalid ) {
  49.                     self.bindSettingListener( setting, { fire: synced } );
  50.                 }
  51.             } );
  52.             api.bind( 'remove', function( setting ) {
  53.                 self.unbindSettingListener( setting );
  54.             } );
  55.  
  56.             /*
  57.              * Ensure that wp_nav_menu() instances nested inside of other partials
  58.              * will be recognized as being present on the page.
  59.              */
  60.             api.selectiveRefresh.bind( 'render-partials-response', function( response ) {
  61.                 if ( response.nav_menu_instance_args ) {
  62.                     _.extend( self.data.navMenuInstanceArgs, response.nav_menu_instance_args );
  63.                 }
  64.             } );
  65.         }
  66.  
  67.         api.preview.bind( 'active', function() {
  68.             self.highlightControls();
  69.         } );
  70.     };
  71.  
  72.     if ( api.selectiveRefresh ) {
  73.  
  74.         /**
  75.          * Partial representing an invocation of wp_nav_menu().
  76.          *
  77.          * @memberOf wp.customize.navMenusPreview
  78.          * @alias wp.customize.navMenusPreview.NavMenuInstancePartial
  79.          *
  80.          * @class
  81.          * @augments wp.customize.selectiveRefresh.Partial
  82.          * @since 4.5.0
  83.          */
  84.         self.NavMenuInstancePartial = api.selectiveRefresh.Partial.extend(/** @lends wp.customize.navMenusPreview.NavMenuInstancePartial.prototype */{
  85.  
  86.             /**
  87.              * Constructor.
  88.              *
  89.              * @since 4.5.0
  90.              * @param {string} id - Partial ID.
  91.              * @param {Object} options
  92.              * @param {Object} options.params
  93.              * @param {Object} options.params.navMenuArgs
  94.              * @param {string} options.params.navMenuArgs.args_hmac
  95.              * @param {string} [options.params.navMenuArgs.theme_location]
  96.              * @param {number} [options.params.navMenuArgs.menu]
  97.              * @param {object} [options.constructingContainerContext]
  98.              */
  99.             initialize: function( id, options ) {
  100.                 var partial = this, matches, argsHmac;
  101.                 matches = id.match( /^nav_menu_instance\[([0-9a-f]{32})]$/ );
  102.                 if ( ! matches ) {
  103.                     throw new Error( 'Illegal id for nav_menu_instance partial. The key corresponds with the args HMAC.' );
  104.                 }
  105.                 argsHmac = matches[1];
  106.  
  107.                 options = options || {};
  108.                 options.params = _.extend(
  109.                     {
  110.                         selector: '[data-customize-partial-id="' + id + '"]',
  111.                         navMenuArgs: options.constructingContainerContext || {},
  112.                         containerInclusive: true
  113.                     },
  114.                     options.params || {}
  115.                 );
  116.                 api.selectiveRefresh.Partial.prototype.initialize.call( partial, id, options );
  117.  
  118.                 if ( ! _.isObject( partial.params.navMenuArgs ) ) {
  119.                     throw new Error( 'Missing navMenuArgs' );
  120.                 }
  121.                 if ( partial.params.navMenuArgs.args_hmac !== argsHmac ) {
  122.                     throw new Error( 'args_hmac mismatch with id' );
  123.                 }
  124.             },
  125.  
  126.             /**
  127.              * Return whether the setting is related to this partial.
  128.              *
  129.              * @since 4.5.0
  130.              * @param {wp.customize.Value|string} setting  - Object or ID.
  131.              * @param {number|object|false|null}  newValue - New value, or null if the setting was just removed.
  132.              * @param {number|object|false|null}  oldValue - Old value, or null if the setting was just added.
  133.              * @returns {boolean}
  134.              */
  135.             isRelatedSetting: function( setting, newValue, oldValue ) {
  136.                 var partial = this, navMenuLocationSetting, navMenuId, isNavMenuItemSetting, _newValue, _oldValue, urlParser;
  137.                 if ( _.isString( setting ) ) {
  138.                     setting = api( setting );
  139.                 }
  140.  
  141.                 /*
  142.                  * Prevent nav_menu_item changes only containing type_label differences triggering a refresh.
  143.                  * These settings in the preview do not include type_label property, and so if one of these
  144.                  * nav_menu_item settings is dirty, after a refresh the nav menu instance would do a selective
  145.                  * refresh immediately because the setting from the pane would have the type_label whereas
  146.                  * the setting in the preview would not, thus triggering a change event. The following
  147.                  * condition short-circuits this unnecessary selective refresh and also prevents an infinite
  148.                  * loop in the case where a nav_menu_instance partial had done a fallback refresh.
  149.                  * @todo Nav menu item settings should not include a type_label property to begin with.
  150.                  */
  151.                 isNavMenuItemSetting = /^nav_menu_item\[/.test( setting.id );
  152.                 if ( isNavMenuItemSetting && _.isObject( newValue ) && _.isObject( oldValue ) ) {
  153.                     _newValue = _.clone( newValue );
  154.                     _oldValue = _.clone( oldValue );
  155.                     delete _newValue.type_label;
  156.                     delete _oldValue.type_label;
  157.  
  158.                     // Normalize URL scheme when parent frame is HTTPS to prevent selective refresh upon initial page load.
  159.                     if ( 'https' === api.preview.scheme.get() ) {
  160.                         urlParser = document.createElement( 'a' );
  161.                         urlParser.href = _newValue.url;
  162.                         urlParser.protocol = 'https:';
  163.                         _newValue.url = urlParser.href;
  164.                         urlParser.href = _oldValue.url;
  165.                         urlParser.protocol = 'https:';
  166.                         _oldValue.url = urlParser.href;
  167.                     }
  168.  
  169.                     // Prevent original_title differences from causing refreshes if title is present.
  170.                     if ( newValue.title ) {
  171.                         delete _oldValue.original_title;
  172.                         delete _newValue.original_title;
  173.                     }
  174.  
  175.                     if ( _.isEqual( _oldValue, _newValue ) ) {
  176.                         return false;
  177.                     }
  178.                 }
  179.  
  180.                 if ( partial.params.navMenuArgs.theme_location ) {
  181.                     if ( 'nav_menu_locations[' + partial.params.navMenuArgs.theme_location + ']' === setting.id ) {
  182.                         return true;
  183.                     }
  184.                     navMenuLocationSetting = api( 'nav_menu_locations[' + partial.params.navMenuArgs.theme_location + ']' );
  185.                 }
  186.  
  187.                 navMenuId = partial.params.navMenuArgs.menu;
  188.                 if ( ! navMenuId && navMenuLocationSetting ) {
  189.                     navMenuId = navMenuLocationSetting();
  190.                 }
  191.  
  192.                 if ( ! navMenuId ) {
  193.                     return false;
  194.                 }
  195.                 return (
  196.                     ( 'nav_menu[' + navMenuId + ']' === setting.id ) ||
  197.                     ( isNavMenuItemSetting && (
  198.                         ( newValue && newValue.nav_menu_term_id === navMenuId ) ||
  199.                         ( oldValue && oldValue.nav_menu_term_id === navMenuId )
  200.                     ) )
  201.                 );
  202.             },
  203.  
  204.             /**
  205.              * Make sure that partial fallback behavior is invoked if there is no associated menu.
  206.              *
  207.              * @since 4.5.0
  208.              *
  209.              * @returns {Promise}
  210.              */
  211.             refresh: function() {
  212.                 var partial = this, menuId, deferred = $.Deferred();
  213.  
  214.                 // Make sure the fallback behavior is invoked when the partial is no longer associated with a menu.
  215.                 if ( _.isNumber( partial.params.navMenuArgs.menu ) ) {
  216.                     menuId = partial.params.navMenuArgs.menu;
  217.                 } else if ( partial.params.navMenuArgs.theme_location && api.has( 'nav_menu_locations[' + partial.params.navMenuArgs.theme_location + ']' ) ) {
  218.                     menuId = api( 'nav_menu_locations[' + partial.params.navMenuArgs.theme_location + ']' ).get();
  219.                 }
  220.                 if ( ! menuId ) {
  221.                     partial.fallback();
  222.                     deferred.reject();
  223.                     return deferred.promise();
  224.                 }
  225.  
  226.                 return api.selectiveRefresh.Partial.prototype.refresh.call( partial );
  227.             },
  228.  
  229.             /**
  230.              * Render content.
  231.              *
  232.              * @inheritdoc
  233.              * @param {wp.customize.selectiveRefresh.Placement} placement
  234.              */
  235.             renderContent: function( placement ) {
  236.                 var partial = this, previousContainer = placement.container;
  237.  
  238.                 // Do fallback behavior to refresh preview if menu is now empty.
  239.                 if ( '' === placement.addedContent ) {
  240.                     placement.partial.fallback();
  241.                 }
  242.  
  243.                 if ( api.selectiveRefresh.Partial.prototype.renderContent.call( partial, placement ) ) {
  244.  
  245.                     // Trigger deprecated event.
  246.                     $( document ).trigger( 'customize-preview-menu-refreshed', [ {
  247.                         instanceNumber: null, // @deprecated
  248.                         wpNavArgs: placement.context, // @deprecated
  249.                         wpNavMenuArgs: placement.context,
  250.                         oldContainer: previousContainer,
  251.                         newContainer: placement.container
  252.                     } ] );
  253.                 }
  254.             }
  255.         });
  256.  
  257.         api.selectiveRefresh.partialConstructor.nav_menu_instance = self.NavMenuInstancePartial;
  258.  
  259.         /**
  260.          * Request full refresh if there are nav menu instances that lack partials which also match the supplied args.
  261.          *
  262.          * @param {object} navMenuInstanceArgs
  263.          */
  264.         self.handleUnplacedNavMenuInstances = function( navMenuInstanceArgs ) {
  265.             var unplacedNavMenuInstances;
  266.             unplacedNavMenuInstances = _.filter( _.values( self.data.navMenuInstanceArgs ), function( args ) {
  267.                 return ! api.selectiveRefresh.partial.has( 'nav_menu_instance[' + args.args_hmac + ']' );
  268.             } );
  269.             if ( _.findWhere( unplacedNavMenuInstances, navMenuInstanceArgs ) ) {
  270.                 api.selectiveRefresh.requestFullRefresh();
  271.                 return true;
  272.             }
  273.             return false;
  274.         };
  275.  
  276.         /**
  277.          * Add change listener for a nav_menu[], nav_menu_item[], or nav_menu_locations[] setting.
  278.          *
  279.          * @since 4.5.0
  280.          *
  281.          * @param {wp.customize.Value} setting
  282.          * @param {object}             [options]
  283.          * @param {boolean}            options.fire Whether to invoke the callback after binding.
  284.          *                                          This is used when a dynamic setting is added.
  285.          * @return {boolean} Whether the setting was bound.
  286.          */
  287.         self.bindSettingListener = function( setting, options ) {
  288.             var matches;
  289.             options = options || {};
  290.  
  291.             matches = setting.id.match( /^nav_menu\[(-?\d+)]$/ );
  292.             if ( matches ) {
  293.                 setting._navMenuId = parseInt( matches[1], 10 );
  294.                 setting.bind( this.onChangeNavMenuSetting );
  295.                 if ( options.fire ) {
  296.                     this.onChangeNavMenuSetting.call( setting, setting(), false );
  297.                 }
  298.                 return true;
  299.             }
  300.  
  301.             matches = setting.id.match( /^nav_menu_item\[(-?\d+)]$/ );
  302.             if ( matches ) {
  303.                 setting._navMenuItemId = parseInt( matches[1], 10 );
  304.                 setting.bind( this.onChangeNavMenuItemSetting );
  305.                 if ( options.fire ) {
  306.                     this.onChangeNavMenuItemSetting.call( setting, setting(), false );
  307.                 }
  308.                 return true;
  309.             }
  310.  
  311.             matches = setting.id.match( /^nav_menu_locations\[(.+?)]/ );
  312.             if ( matches ) {
  313.                 setting._navMenuThemeLocation = matches[1];
  314.                 setting.bind( this.onChangeNavMenuLocationsSetting );
  315.                 if ( options.fire ) {
  316.                     this.onChangeNavMenuLocationsSetting.call( setting, setting(), false );
  317.                 }
  318.                 return true;
  319.             }
  320.  
  321.             return false;
  322.         };
  323.  
  324.         /**
  325.          * Remove change listeners for nav_menu[], nav_menu_item[], or nav_menu_locations[] setting.
  326.          *
  327.          * @since 4.5.0
  328.          *
  329.          * @param {wp.customize.Value} setting
  330.          */
  331.         self.unbindSettingListener = function( setting ) {
  332.             setting.unbind( this.onChangeNavMenuSetting );
  333.             setting.unbind( this.onChangeNavMenuItemSetting );
  334.             setting.unbind( this.onChangeNavMenuLocationsSetting );
  335.         };
  336.  
  337.         /**
  338.          * Handle change for nav_menu[] setting for nav menu instances lacking partials.
  339.          *
  340.          * @since 4.5.0
  341.          *
  342.          * @this {wp.customize.Value}
  343.          */
  344.         self.onChangeNavMenuSetting = function() {
  345.             var setting = this;
  346.  
  347.             self.handleUnplacedNavMenuInstances( {
  348.                 menu: setting._navMenuId
  349.             } );
  350.  
  351.             // Ensure all nav menu instances with a theme_location assigned to this menu are handled.
  352.             api.each( function( otherSetting ) {
  353.                 if ( ! otherSetting._navMenuThemeLocation ) {
  354.                     return;
  355.                 }
  356.                 if ( setting._navMenuId === otherSetting() ) {
  357.                     self.handleUnplacedNavMenuInstances( {
  358.                         theme_location: otherSetting._navMenuThemeLocation
  359.                     } );
  360.                 }
  361.             } );
  362.         };
  363.  
  364.         /**
  365.          * Handle change for nav_menu_item[] setting for nav menu instances lacking partials.
  366.          *
  367.          * @since 4.5.0
  368.          *
  369.          * @param {object} newItem New value for nav_menu_item[] setting.
  370.          * @param {object} oldItem Old value for nav_menu_item[] setting.
  371.          * @this {wp.customize.Value}
  372.          */
  373.         self.onChangeNavMenuItemSetting = function( newItem, oldItem ) {
  374.             var item = newItem || oldItem, navMenuSetting;
  375.             navMenuSetting = api( 'nav_menu[' + String( item.nav_menu_term_id ) + ']' );
  376.             if ( navMenuSetting ) {
  377.                 self.onChangeNavMenuSetting.call( navMenuSetting );
  378.             }
  379.         };
  380.  
  381.         /**
  382.          * Handle change for nav_menu_locations[] setting for nav menu instances lacking partials.
  383.          *
  384.          * @since 4.5.0
  385.          *
  386.          * @this {wp.customize.Value}
  387.          */
  388.         self.onChangeNavMenuLocationsSetting = function() {
  389.             var setting = this, hasNavMenuInstance;
  390.             self.handleUnplacedNavMenuInstances( {
  391.                 theme_location: setting._navMenuThemeLocation
  392.             } );
  393.  
  394.             // If there are no wp_nav_menu() instances that refer to the theme location, do full refresh.
  395.             hasNavMenuInstance = !! _.findWhere( _.values( self.data.navMenuInstanceArgs ), {
  396.                 theme_location: setting._navMenuThemeLocation
  397.             } );
  398.             if ( ! hasNavMenuInstance ) {
  399.                 api.selectiveRefresh.requestFullRefresh();
  400.             }
  401.         };
  402.     }
  403.  
  404.     /**
  405.      * Connect nav menu items with their corresponding controls in the pane.
  406.      *
  407.      * Setup shift-click on nav menu items which are more granular than the nav menu partial itself.
  408.      * Also this applies even if a nav menu is not partial-refreshable.
  409.      *
  410.      * @since 4.5.0
  411.      */
  412.     self.highlightControls = function() {
  413.         var selector = '.menu-item';
  414.  
  415.         // Skip adding highlights if not in the customizer preview iframe.
  416.         if ( ! api.settings.channel ) {
  417.             return;
  418.         }
  419.  
  420.         // Focus on the menu item control when shift+clicking the menu item.
  421.         $( document ).on( 'click', selector, function( e ) {
  422.             var navMenuItemParts;
  423.             if ( ! e.shiftKey ) {
  424.                 return;
  425.             }
  426.  
  427.             navMenuItemParts = $( this ).attr( 'class' ).match( /(?:^|\s)menu-item-(-?\d+)(?:\s|$)/ );
  428.             if ( navMenuItemParts ) {
  429.                 e.preventDefault();
  430.                 e.stopPropagation(); // Make sure a sub-nav menu item will get focused instead of parent items.
  431.                 api.preview.send( 'focus-nav-menu-item-control', parseInt( navMenuItemParts[1], 10 ) );
  432.             }
  433.         });
  434.     };
  435.  
  436.     api.bind( 'preview-ready', function() {
  437.         self.init();
  438.     } );
  439.  
  440.     return self;
  441.  
  442. }( jQuery, _, wp, wp.customize ) );
  443.