You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

773 lines
23 KiB

1 year ago
  1. /**
  2. * @output wp-includes/js/customize-preview-widgets.js
  3. */
  4. /* global _wpWidgetCustomizerPreviewSettings */
  5. /**
  6. * Handles the initialization, refreshing and rendering of widget partials and sidebar widgets.
  7. *
  8. * @since 4.5.0
  9. *
  10. * @namespace wp.customize.widgetsPreview
  11. *
  12. * @param {jQuery} $ The jQuery object.
  13. * @param {Object} _ The utilities library.
  14. * @param {Object} wp Current WordPress environment instance.
  15. * @param {Object} api Information from the API.
  16. *
  17. * @return {Object} Widget-related variables.
  18. */
  19. wp.customize.widgetsPreview = wp.customize.WidgetCustomizerPreview = (function( $, _, wp, api ) {
  20. var self;
  21. self = {
  22. renderedSidebars: {},
  23. renderedWidgets: {},
  24. registeredSidebars: [],
  25. registeredWidgets: {},
  26. widgetSelectors: [],
  27. preview: null,
  28. l10n: {
  29. widgetTooltip: ''
  30. },
  31. selectiveRefreshableWidgets: {}
  32. };
  33. /**
  34. * Initializes the widgets preview.
  35. *
  36. * @since 4.5.0
  37. *
  38. * @memberOf wp.customize.widgetsPreview
  39. *
  40. * @return {void}
  41. */
  42. self.init = function() {
  43. var self = this;
  44. self.preview = api.preview;
  45. if ( ! _.isEmpty( self.selectiveRefreshableWidgets ) ) {
  46. self.addPartials();
  47. }
  48. self.buildWidgetSelectors();
  49. self.highlightControls();
  50. self.preview.bind( 'highlight-widget', self.highlightWidget );
  51. api.preview.bind( 'active', function() {
  52. self.highlightControls();
  53. } );
  54. /*
  55. * Refresh a partial when the controls pane requests it. This is used currently just by the
  56. * Gallery widget so that when an attachment's caption is updated in the media modal,
  57. * the widget in the preview will then be refreshed to show the change. Normally doing this
  58. * would not be necessary because all of the state should be contained inside the changeset,
  59. * as everything done in the Customizer should not make a change to the site unless the
  60. * changeset itself is published. Attachments are a current exception to this rule.
  61. * For a proposal to include attachments in the customized state, see #37887.
  62. */
  63. api.preview.bind( 'refresh-widget-partial', function( widgetId ) {
  64. var partialId = 'widget[' + widgetId + ']';
  65. if ( api.selectiveRefresh.partial.has( partialId ) ) {
  66. api.selectiveRefresh.partial( partialId ).refresh();
  67. } else if ( self.renderedWidgets[ widgetId ] ) {
  68. api.preview.send( 'refresh' ); // Fallback in case theme does not support 'customize-selective-refresh-widgets'.
  69. }
  70. } );
  71. };
  72. self.WidgetPartial = api.selectiveRefresh.Partial.extend(/** @lends wp.customize.widgetsPreview.WidgetPartial.prototype */{
  73. /**
  74. * Represents a partial widget instance.
  75. *
  76. * @since 4.5.0
  77. *
  78. * @constructs
  79. * @augments wp.customize.selectiveRefresh.Partial
  80. *
  81. * @alias wp.customize.widgetsPreview.WidgetPartial
  82. * @memberOf wp.customize.widgetsPreview
  83. *
  84. * @param {string} id The partial's ID.
  85. * @param {Object} options Options used to initialize the partial's
  86. * instance.
  87. * @param {Object} options.params The options parameters.
  88. */
  89. initialize: function( id, options ) {
  90. var partial = this, matches;
  91. matches = id.match( /^widget\[(.+)]$/ );
  92. if ( ! matches ) {
  93. throw new Error( 'Illegal id for widget partial.' );
  94. }
  95. partial.widgetId = matches[1];
  96. partial.widgetIdParts = self.parseWidgetId( partial.widgetId );
  97. options = options || {};
  98. options.params = _.extend(
  99. {
  100. settings: [ self.getWidgetSettingId( partial.widgetId ) ],
  101. containerInclusive: true
  102. },
  103. options.params || {}
  104. );
  105. api.selectiveRefresh.Partial.prototype.initialize.call( partial, id, options );
  106. },
  107. /**
  108. * Refreshes the widget partial.
  109. *
  110. * @since 4.5.0
  111. *
  112. * @return {Promise|void} Either a promise postponing the refresh, or void.
  113. */
  114. refresh: function() {
  115. var partial = this, refreshDeferred;
  116. if ( ! self.selectiveRefreshableWidgets[ partial.widgetIdParts.idBase ] ) {
  117. refreshDeferred = $.Deferred();
  118. refreshDeferred.reject();
  119. partial.fallback();
  120. return refreshDeferred.promise();
  121. } else {
  122. return api.selectiveRefresh.Partial.prototype.refresh.call( partial );
  123. }
  124. },
  125. /**
  126. * Sends the widget-updated message to the parent so the spinner will get
  127. * removed from the widget control.
  128. *
  129. * @inheritDoc
  130. * @param {wp.customize.selectiveRefresh.Placement} placement The placement
  131. * function.
  132. *
  133. * @return {void}
  134. */
  135. renderContent: function( placement ) {
  136. var partial = this;
  137. if ( api.selectiveRefresh.Partial.prototype.renderContent.call( partial, placement ) ) {
  138. api.preview.send( 'widget-updated', partial.widgetId );
  139. api.selectiveRefresh.trigger( 'widget-updated', partial );
  140. }
  141. }
  142. });
  143. self.SidebarPartial = api.selectiveRefresh.Partial.extend(/** @lends wp.customize.widgetsPreview.SidebarPartial.prototype */{
  144. /**
  145. * Represents a partial widget area.
  146. *
  147. * @since 4.5.0
  148. *
  149. * @class
  150. * @augments wp.customize.selectiveRefresh.Partial
  151. *
  152. * @memberOf wp.customize.widgetsPreview
  153. * @alias wp.customize.widgetsPreview.SidebarPartial
  154. *
  155. * @param {string} id The partial's ID.
  156. * @param {Object} options Options used to initialize the partial's instance.
  157. * @param {Object} options.params The options parameters.
  158. */
  159. initialize: function( id, options ) {
  160. var partial = this, matches;
  161. matches = id.match( /^sidebar\[(.+)]$/ );
  162. if ( ! matches ) {
  163. throw new Error( 'Illegal id for sidebar partial.' );
  164. }
  165. partial.sidebarId = matches[1];
  166. options = options || {};
  167. options.params = _.extend(
  168. {
  169. settings: [ 'sidebars_widgets[' + partial.sidebarId + ']' ]
  170. },
  171. options.params || {}
  172. );
  173. api.selectiveRefresh.Partial.prototype.initialize.call( partial, id, options );
  174. if ( ! partial.params.sidebarArgs ) {
  175. throw new Error( 'The sidebarArgs param was not provided.' );
  176. }
  177. if ( partial.params.settings.length > 1 ) {
  178. throw new Error( 'Expected SidebarPartial to only have one associated setting' );
  179. }
  180. },
  181. /**
  182. * Sets up the partial.
  183. *
  184. * @since 4.5.0
  185. *
  186. * @return {void}
  187. */
  188. ready: function() {
  189. var sidebarPartial = this;
  190. // Watch for changes to the sidebar_widgets setting.
  191. _.each( sidebarPartial.settings(), function( settingId ) {
  192. api( settingId ).bind( _.bind( sidebarPartial.handleSettingChange, sidebarPartial ) );
  193. } );
  194. // Trigger an event for this sidebar being updated whenever a widget inside is rendered.
  195. api.selectiveRefresh.bind( 'partial-content-rendered', function( placement ) {
  196. var isAssignedWidgetPartial = (
  197. placement.partial.extended( self.WidgetPartial ) &&
  198. ( -1 !== _.indexOf( sidebarPartial.getWidgetIds(), placement.partial.widgetId ) )
  199. );
  200. if ( isAssignedWidgetPartial ) {
  201. api.selectiveRefresh.trigger( 'sidebar-updated', sidebarPartial );
  202. }
  203. } );
  204. // Make sure that a widget partial has a container in the DOM prior to a refresh.
  205. api.bind( 'change', function( widgetSetting ) {
  206. var widgetId, parsedId;
  207. parsedId = self.parseWidgetSettingId( widgetSetting.id );
  208. if ( ! parsedId ) {
  209. return;
  210. }
  211. widgetId = parsedId.idBase;
  212. if ( parsedId.number ) {
  213. widgetId += '-' + String( parsedId.number );
  214. }
  215. if ( -1 !== _.indexOf( sidebarPartial.getWidgetIds(), widgetId ) ) {
  216. sidebarPartial.ensureWidgetPlacementContainers( widgetId );
  217. }
  218. } );
  219. },
  220. /**
  221. * Gets the before/after boundary nodes for all instances of this sidebar
  222. * (usually one).
  223. *
  224. * Note that TreeWalker is not implemented in IE8.
  225. *
  226. * @since 4.5.0
  227. *
  228. * @return {Array.<{before: Comment, after: Comment, instanceNumber: number}>}
  229. * An array with an object for each sidebar instance, containing the
  230. * node before and after the sidebar instance and its instance number.
  231. */
  232. findDynamicSidebarBoundaryNodes: function() {
  233. var partial = this, regExp, boundaryNodes = {}, recursiveCommentTraversal;
  234. regExp = /^(dynamic_sidebar_before|dynamic_sidebar_after):(.+):(\d+)$/;
  235. recursiveCommentTraversal = function( childNodes ) {
  236. _.each( childNodes, function( node ) {
  237. var matches;
  238. if ( 8 === node.nodeType ) {
  239. matches = node.nodeValue.match( regExp );
  240. if ( ! matches || matches[2] !== partial.sidebarId ) {
  241. return;
  242. }
  243. if ( _.isUndefined( boundaryNodes[ matches[3] ] ) ) {
  244. boundaryNodes[ matches[3] ] = {
  245. before: null,
  246. after: null,
  247. instanceNumber: parseInt( matches[3], 10 )
  248. };
  249. }
  250. if ( 'dynamic_sidebar_before' === matches[1] ) {
  251. boundaryNodes[ matches[3] ].before = node;
  252. } else {
  253. boundaryNodes[ matches[3] ].after = node;
  254. }
  255. } else if ( 1 === node.nodeType ) {
  256. recursiveCommentTraversal( node.childNodes );
  257. }
  258. } );
  259. };
  260. recursiveCommentTraversal( document.body.childNodes );
  261. return _.values( boundaryNodes );
  262. },
  263. /**
  264. * Gets the placements for this partial.
  265. *
  266. * @since 4.5.0
  267. *
  268. * @return {Array} An array containing placement objects for each of the
  269. * dynamic sidebar boundary nodes.
  270. */
  271. placements: function() {
  272. var partial = this;
  273. return _.map( partial.findDynamicSidebarBoundaryNodes(), function( boundaryNodes ) {
  274. return new api.selectiveRefresh.Placement( {
  275. partial: partial,
  276. container: null,
  277. startNode: boundaryNodes.before,
  278. endNode: boundaryNodes.after,
  279. context: {
  280. instanceNumber: boundaryNodes.instanceNumber
  281. }
  282. } );
  283. } );
  284. },
  285. /**
  286. * Get the list of widget IDs associated with this widget area.
  287. *
  288. * @since 4.5.0
  289. *
  290. * @throws {Error} If there's no settingId.
  291. * @throws {Error} If the setting doesn't exist in the API.
  292. * @throws {Error} If the API doesn't pass an array of widget IDs.
  293. *
  294. * @return {Array} A shallow copy of the array containing widget IDs.
  295. */
  296. getWidgetIds: function() {
  297. var sidebarPartial = this, settingId, widgetIds;
  298. settingId = sidebarPartial.settings()[0];
  299. if ( ! settingId ) {
  300. throw new Error( 'Missing associated setting.' );
  301. }
  302. if ( ! api.has( settingId ) ) {
  303. throw new Error( 'Setting does not exist.' );
  304. }
  305. widgetIds = api( settingId ).get();
  306. if ( ! _.isArray( widgetIds ) ) {
  307. throw new Error( 'Expected setting to be array of widget IDs' );
  308. }
  309. return widgetIds.slice( 0 );
  310. },
  311. /**
  312. * Reflows widgets in the sidebar, ensuring they have the proper position in the
  313. * DOM.
  314. *
  315. * @since 4.5.0
  316. *
  317. * @return {Array.<wp.customize.selectiveRefresh.Placement>} List of placements
  318. * that were reflowed.
  319. */
  320. reflowWidgets: function() {
  321. var sidebarPartial = this, sidebarPlacements, widgetIds, widgetPartials, sortedSidebarContainers = [];
  322. widgetIds = sidebarPartial.getWidgetIds();
  323. sidebarPlacements = sidebarPartial.placements();
  324. widgetPartials = {};
  325. _.each( widgetIds, function( widgetId ) {
  326. var widgetPartial = api.selectiveRefresh.partial( 'widget[' + widgetId + ']' );
  327. if ( widgetPartial ) {
  328. widgetPartials[ widgetId ] = widgetPartial;
  329. }
  330. } );
  331. _.each( sidebarPlacements, function( sidebarPlacement ) {
  332. var sidebarWidgets = [], needsSort = false, thisPosition, lastPosition = -1;
  333. // Gather list of widget partial containers in this sidebar, and determine if a sort is needed.
  334. _.each( widgetPartials, function( widgetPartial ) {
  335. _.each( widgetPartial.placements(), function( widgetPlacement ) {
  336. if ( sidebarPlacement.context.instanceNumber === widgetPlacement.context.sidebar_instance_number ) {
  337. thisPosition = widgetPlacement.container.index();
  338. sidebarWidgets.push( {
  339. partial: widgetPartial,
  340. placement: widgetPlacement,
  341. position: thisPosition
  342. } );
  343. if ( thisPosition < lastPosition ) {
  344. needsSort = true;
  345. }
  346. lastPosition = thisPosition;
  347. }
  348. } );
  349. } );
  350. if ( needsSort ) {
  351. _.each( sidebarWidgets, function( sidebarWidget ) {
  352. sidebarPlacement.endNode.parentNode.insertBefore(
  353. sidebarWidget.placement.container[0],
  354. sidebarPlacement.endNode
  355. );
  356. // @todo Rename partial-placement-moved?
  357. api.selectiveRefresh.trigger( 'partial-content-moved', sidebarWidget.placement );
  358. } );
  359. sortedSidebarContainers.push( sidebarPlacement );
  360. }
  361. } );
  362. if ( sortedSidebarContainers.length > 0 ) {
  363. api.selectiveRefresh.trigger( 'sidebar-updated', sidebarPartial );
  364. }
  365. return sortedSidebarContainers;
  366. },
  367. /**
  368. * Makes sure there is a widget instance container in this sidebar for the given
  369. * widget ID.
  370. *
  371. * @since 4.5.0
  372. *
  373. * @param {string} widgetId The widget ID.
  374. *
  375. * @return {wp.customize.selectiveRefresh.Partial} The widget instance partial.
  376. */
  377. ensureWidgetPlacementContainers: function( widgetId ) {
  378. var sidebarPartial = this, widgetPartial, wasInserted = false, partialId = 'widget[' + widgetId + ']';
  379. widgetPartial = api.selectiveRefresh.partial( partialId );
  380. if ( ! widgetPartial ) {
  381. widgetPartial = new self.WidgetPartial( partialId, {
  382. params: {}
  383. } );
  384. }
  385. // Make sure that there is a container element for the widget in the sidebar, if at least a placeholder.
  386. _.each( sidebarPartial.placements(), function( sidebarPlacement ) {
  387. var foundWidgetPlacement, widgetContainerElement;
  388. foundWidgetPlacement = _.find( widgetPartial.placements(), function( widgetPlacement ) {
  389. return ( widgetPlacement.context.sidebar_instance_number === sidebarPlacement.context.instanceNumber );
  390. } );
  391. if ( foundWidgetPlacement ) {
  392. return;
  393. }
  394. widgetContainerElement = $(
  395. sidebarPartial.params.sidebarArgs.before_widget.replace( /%1\$s/g, widgetId ).replace( /%2\$s/g, 'widget' ) +
  396. sidebarPartial.params.sidebarArgs.after_widget
  397. );
  398. // Handle rare case where before_widget and after_widget are empty.
  399. if ( ! widgetContainerElement[0] ) {
  400. return;
  401. }
  402. widgetContainerElement.attr( 'data-customize-partial-id', widgetPartial.id );
  403. widgetContainerElement.attr( 'data-customize-partial-type', 'widget' );
  404. widgetContainerElement.attr( 'data-customize-widget-id', widgetId );
  405. /*
  406. * Make sure the widget container element has the customize-container context data.
  407. * The sidebar_instance_number is used to disambiguate multiple instances of the
  408. * same sidebar are rendered onto the template, and so the same widget is embedded
  409. * multiple times.
  410. */
  411. widgetContainerElement.data( 'customize-partial-placement-context', {
  412. 'sidebar_id': sidebarPartial.sidebarId,
  413. 'sidebar_instance_number': sidebarPlacement.context.instanceNumber
  414. } );
  415. sidebarPlacement.endNode.parentNode.insertBefore( widgetContainerElement[0], sidebarPlacement.endNode );
  416. wasInserted = true;
  417. } );
  418. api.selectiveRefresh.partial.add( widgetPartial );
  419. if ( wasInserted ) {
  420. sidebarPartial.reflowWidgets();
  421. }
  422. return widgetPartial;
  423. },
  424. /**
  425. * Handles changes to the sidebars_widgets[] setting.
  426. *
  427. * @since 4.5.0
  428. *
  429. * @param {Array} newWidgetIds New widget IDs.
  430. * @param {Array} oldWidgetIds Old widget IDs.
  431. *
  432. * @return {void}
  433. */
  434. handleSettingChange: function( newWidgetIds, oldWidgetIds ) {
  435. var sidebarPartial = this, needsRefresh, widgetsRemoved, widgetsAdded, addedWidgetPartials = [];
  436. needsRefresh = (
  437. ( oldWidgetIds.length > 0 && 0 === newWidgetIds.length ) ||
  438. ( newWidgetIds.length > 0 && 0 === oldWidgetIds.length )
  439. );
  440. if ( needsRefresh ) {
  441. sidebarPartial.fallback();
  442. return;
  443. }
  444. // Handle removal of widgets.
  445. widgetsRemoved = _.difference( oldWidgetIds, newWidgetIds );
  446. _.each( widgetsRemoved, function( removedWidgetId ) {
  447. var widgetPartial = api.selectiveRefresh.partial( 'widget[' + removedWidgetId + ']' );
  448. if ( widgetPartial ) {
  449. _.each( widgetPartial.placements(), function( placement ) {
  450. var isRemoved = (
  451. placement.context.sidebar_id === sidebarPartial.sidebarId ||
  452. ( placement.context.sidebar_args && placement.context.sidebar_args.id === sidebarPartial.sidebarId )
  453. );
  454. if ( isRemoved ) {
  455. placement.container.remove();
  456. }
  457. } );
  458. }
  459. delete self.renderedWidgets[ removedWidgetId ];
  460. } );
  461. // Handle insertion of widgets.
  462. widgetsAdded = _.difference( newWidgetIds, oldWidgetIds );
  463. _.each( widgetsAdded, function( addedWidgetId ) {
  464. var widgetPartial = sidebarPartial.ensureWidgetPlacementContainers( addedWidgetId );
  465. addedWidgetPartials.push( widgetPartial );
  466. self.renderedWidgets[ addedWidgetId ] = true;
  467. } );
  468. _.each( addedWidgetPartials, function( widgetPartial ) {
  469. widgetPartial.refresh();
  470. } );
  471. api.selectiveRefresh.trigger( 'sidebar-updated', sidebarPartial );
  472. },
  473. /**
  474. * Refreshes the sidebar partial.
  475. *
  476. * Note that the meat is handled in handleSettingChange because it has the
  477. * context of which widgets were removed.
  478. *
  479. * @since 4.5.0
  480. *
  481. * @return {Promise} A promise postponing the refresh.
  482. */
  483. refresh: function() {
  484. var partial = this, deferred = $.Deferred();
  485. deferred.fail( function() {
  486. partial.fallback();
  487. } );
  488. if ( 0 === partial.placements().length ) {
  489. deferred.reject();
  490. } else {
  491. _.each( partial.reflowWidgets(), function( sidebarPlacement ) {
  492. api.selectiveRefresh.trigger( 'partial-content-rendered', sidebarPlacement );
  493. } );
  494. deferred.resolve();
  495. }
  496. return deferred.promise();
  497. }
  498. });
  499. api.selectiveRefresh.partialConstructor.sidebar = self.SidebarPartial;
  500. api.selectiveRefresh.partialConstructor.widget = self.WidgetPartial;
  501. /**
  502. * Adds partials for the registered widget areas (sidebars).
  503. *
  504. * @since 4.5.0
  505. *
  506. * @return {void}
  507. */
  508. self.addPartials = function() {
  509. _.each( self.registeredSidebars, function( registeredSidebar ) {
  510. var partial, partialId = 'sidebar[' + registeredSidebar.id + ']';
  511. partial = api.selectiveRefresh.partial( partialId );
  512. if ( ! partial ) {
  513. partial = new self.SidebarPartial( partialId, {
  514. params: {
  515. sidebarArgs: registeredSidebar
  516. }
  517. } );
  518. api.selectiveRefresh.partial.add( partial );
  519. }
  520. } );
  521. };
  522. /**
  523. * Calculates the selector for the sidebar's widgets based on the registered
  524. * sidebar's info.
  525. *
  526. * @memberOf wp.customize.widgetsPreview
  527. *
  528. * @since 3.9.0
  529. *
  530. * @return {void}
  531. */
  532. self.buildWidgetSelectors = function() {
  533. var self = this;
  534. $.each( self.registeredSidebars, function( i, sidebar ) {
  535. var widgetTpl = [
  536. sidebar.before_widget,
  537. sidebar.before_title,
  538. sidebar.after_title,
  539. sidebar.after_widget
  540. ].join( '' ),
  541. emptyWidget,
  542. widgetSelector,
  543. widgetClasses;
  544. emptyWidget = $( widgetTpl );
  545. widgetSelector = emptyWidget.prop( 'tagName' ) || '';
  546. widgetClasses = emptyWidget.prop( 'className' ) || '';
  547. // Prevent a rare case when before_widget, before_title, after_title and after_widget is empty.
  548. if ( ! widgetClasses ) {
  549. return;
  550. }
  551. // Remove class names that incorporate the string formatting placeholders %1$s and %2$s.
  552. widgetClasses = widgetClasses.replace( /\S*%[12]\$s\S*/g, '' );
  553. widgetClasses = widgetClasses.replace( /^\s+|\s+$/g, '' );
  554. if ( widgetClasses ) {
  555. widgetSelector += '.' + widgetClasses.split( /\s+/ ).join( '.' );
  556. }
  557. self.widgetSelectors.push( widgetSelector );
  558. });
  559. };
  560. /**
  561. * Highlights the widget on widget updates or widget control mouse overs.
  562. *
  563. * @memberOf wp.customize.widgetsPreview
  564. *
  565. * @since 3.9.0
  566. * @param {string} widgetId ID of the widget.
  567. *
  568. * @return {void}
  569. */
  570. self.highlightWidget = function( widgetId ) {
  571. var $body = $( document.body ),
  572. $widget = $( '#' + widgetId );
  573. $body.find( '.widget-customizer-highlighted-widget' ).removeClass( 'widget-customizer-highlighted-widget' );
  574. $widget.addClass( 'widget-customizer-highlighted-widget' );
  575. setTimeout( function() {
  576. $widget.removeClass( 'widget-customizer-highlighted-widget' );
  577. }, 500 );
  578. };
  579. /**
  580. * Shows a title and highlights widgets on hover. On shift+clicking focuses the
  581. * widget control.
  582. *
  583. * @memberOf wp.customize.widgetsPreview
  584. *
  585. * @since 3.9.0
  586. *
  587. * @return {void}
  588. */
  589. self.highlightControls = function() {
  590. var self = this,
  591. selector = this.widgetSelectors.join( ',' );
  592. // Skip adding highlights if not in the customizer preview iframe.
  593. if ( ! api.settings.channel ) {
  594. return;
  595. }
  596. $( selector ).attr( 'title', this.l10n.widgetTooltip );
  597. // Highlights widget when entering the widget editor.
  598. $( document ).on( 'mouseenter', selector, function() {
  599. self.preview.send( 'highlight-widget-control', $( this ).prop( 'id' ) );
  600. });
  601. // Open expand the widget control when shift+clicking the widget element.
  602. $( document ).on( 'click', selector, function( e ) {
  603. if ( ! e.shiftKey ) {
  604. return;
  605. }
  606. e.preventDefault();
  607. self.preview.send( 'focus-widget-control', $( this ).prop( 'id' ) );
  608. });
  609. };
  610. /**
  611. * Parses a widget ID.
  612. *
  613. * @memberOf wp.customize.widgetsPreview
  614. *
  615. * @since 4.5.0
  616. *
  617. * @param {string} widgetId The widget ID.
  618. *
  619. * @return {{idBase: string, number: number|null}} An object containing the idBase
  620. * and number of the parsed widget ID.
  621. */
  622. self.parseWidgetId = function( widgetId ) {
  623. var matches, parsed = {
  624. idBase: '',
  625. number: null
  626. };
  627. matches = widgetId.match( /^(.+)-(\d+)$/ );
  628. if ( matches ) {
  629. parsed.idBase = matches[1];
  630. parsed.number = parseInt( matches[2], 10 );
  631. } else {
  632. parsed.idBase = widgetId; // Likely an old single widget.
  633. }
  634. return parsed;
  635. };
  636. /**
  637. * Parses a widget setting ID.
  638. *
  639. * @memberOf wp.customize.widgetsPreview
  640. *
  641. * @since 4.5.0
  642. *
  643. * @param {string} settingId Widget setting ID.
  644. *
  645. * @return {{idBase: string, number: number|null}|null} Either an object containing the idBase
  646. * and number of the parsed widget setting ID,
  647. * or null.
  648. */
  649. self.parseWidgetSettingId = function( settingId ) {
  650. var matches, parsed = {
  651. idBase: '',
  652. number: null
  653. };
  654. matches = settingId.match( /^widget_([^\[]+?)(?:\[(\d+)])?$/ );
  655. if ( ! matches ) {
  656. return null;
  657. }
  658. parsed.idBase = matches[1];
  659. if ( matches[2] ) {
  660. parsed.number = parseInt( matches[2], 10 );
  661. }
  662. return parsed;
  663. };
  664. /**
  665. * Converts a widget ID into a Customizer setting ID.
  666. *
  667. * @memberOf wp.customize.widgetsPreview
  668. *
  669. * @since 4.5.0
  670. *
  671. * @param {string} widgetId The widget ID.
  672. *
  673. * @return {string} The setting ID.
  674. */
  675. self.getWidgetSettingId = function( widgetId ) {
  676. var parsed = this.parseWidgetId( widgetId ), settingId;
  677. settingId = 'widget_' + parsed.idBase;
  678. if ( parsed.number ) {
  679. settingId += '[' + String( parsed.number ) + ']';
  680. }
  681. return settingId;
  682. };
  683. api.bind( 'preview-ready', function() {
  684. $.extend( self, _wpWidgetCustomizerPreviewSettings );
  685. self.init();
  686. });
  687. return self;
  688. })( jQuery, _, wp, wp.customize );