customize-preview-widgets.js 21 KB

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