media-widgets.js 41 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306
  1. /* eslint consistent-this: [ "error", "control" ] */
  2. wp.mediaWidgets = ( function( $ ) {
  3. 'use strict';
  4. var component = {};
  5. /**
  6. * Widget control (view) constructors, mapping widget id_base to subclass of MediaWidgetControl.
  7. *
  8. * Media widgets register themselves by assigning subclasses of MediaWidgetControl onto this object by widget ID base.
  9. *
  10. * @type {Object.<string, wp.mediaWidgets.MediaWidgetModel>}
  11. */
  12. component.controlConstructors = {};
  13. /**
  14. * Widget model constructors, mapping widget id_base to subclass of MediaWidgetModel.
  15. *
  16. * Media widgets register themselves by assigning subclasses of MediaWidgetControl onto this object by widget ID base.
  17. *
  18. * @type {Object.<string, wp.mediaWidgets.MediaWidgetModel>}
  19. */
  20. component.modelConstructors = {};
  21. /**
  22. * Library which persists the customized display settings across selections.
  23. *
  24. * @class PersistentDisplaySettingsLibrary
  25. * @constructor
  26. */
  27. component.PersistentDisplaySettingsLibrary = wp.media.controller.Library.extend({
  28. /**
  29. * Initialize.
  30. *
  31. * @param {Object} options - Options.
  32. * @returns {void}
  33. */
  34. initialize: function initialize( options ) {
  35. _.bindAll( this, 'handleDisplaySettingChange' );
  36. wp.media.controller.Library.prototype.initialize.call( this, options );
  37. },
  38. /**
  39. * Sync changes to the current display settings back into the current customized.
  40. *
  41. * @param {Backbone.Model} displaySettings - Modified display settings.
  42. * @returns {void}
  43. */
  44. handleDisplaySettingChange: function handleDisplaySettingChange( displaySettings ) {
  45. this.get( 'selectedDisplaySettings' ).set( displaySettings.attributes );
  46. },
  47. /**
  48. * Get the display settings model.
  49. *
  50. * Model returned is updated with the current customized display settings,
  51. * and an event listener is added so that changes made to the settings
  52. * will sync back into the model storing the session's customized display
  53. * settings.
  54. *
  55. * @param {Backbone.Model} model - Display settings model.
  56. * @returns {Backbone.Model} Display settings model.
  57. */
  58. display: function getDisplaySettingsModel( model ) {
  59. var display, selectedDisplaySettings = this.get( 'selectedDisplaySettings' );
  60. display = wp.media.controller.Library.prototype.display.call( this, model );
  61. display.off( 'change', this.handleDisplaySettingChange ); // Prevent duplicated event handlers.
  62. display.set( selectedDisplaySettings.attributes );
  63. if ( 'custom' === selectedDisplaySettings.get( 'link_type' ) ) {
  64. display.linkUrl = selectedDisplaySettings.get( 'link_url' );
  65. }
  66. display.on( 'change', this.handleDisplaySettingChange );
  67. return display;
  68. }
  69. });
  70. /**
  71. * Extended view for managing the embed UI.
  72. *
  73. * @class MediaEmbedView
  74. * @constructor
  75. */
  76. component.MediaEmbedView = wp.media.view.Embed.extend({
  77. /**
  78. * Initialize.
  79. *
  80. * @since 4.9.0
  81. *
  82. * @param {object} options - Options.
  83. * @returns {void}
  84. */
  85. initialize: function( options ) {
  86. var view = this, embedController; // eslint-disable-line consistent-this
  87. wp.media.view.Embed.prototype.initialize.call( view, options );
  88. if ( 'image' !== view.controller.options.mimeType ) {
  89. embedController = view.controller.states.get( 'embed' );
  90. embedController.off( 'scan', embedController.scanImage, embedController );
  91. }
  92. },
  93. /**
  94. * Refresh embed view.
  95. *
  96. * Forked override of {wp.media.view.Embed#refresh()} to suppress irrelevant "link text" field.
  97. *
  98. * @returns {void}
  99. */
  100. refresh: function refresh() {
  101. var Constructor;
  102. if ( 'image' === this.controller.options.mimeType ) {
  103. Constructor = wp.media.view.EmbedImage;
  104. } else {
  105. // This should be eliminated once #40450 lands of when this is merged into core.
  106. Constructor = wp.media.view.EmbedLink.extend({
  107. /**
  108. * Set the disabled state on the Add to Widget button.
  109. *
  110. * @param {boolean} disabled - Disabled.
  111. * @returns {void}
  112. */
  113. setAddToWidgetButtonDisabled: function setAddToWidgetButtonDisabled( disabled ) {
  114. this.views.parent.views.parent.views.get( '.media-frame-toolbar' )[0].$el.find( '.media-button-select' ).prop( 'disabled', disabled );
  115. },
  116. /**
  117. * Set or clear an error notice.
  118. *
  119. * @param {string} notice - Notice.
  120. * @returns {void}
  121. */
  122. setErrorNotice: function setErrorNotice( notice ) {
  123. var embedLinkView = this, noticeContainer; // eslint-disable-line consistent-this
  124. noticeContainer = embedLinkView.views.parent.$el.find( '> .notice:first-child' );
  125. if ( ! notice ) {
  126. if ( noticeContainer.length ) {
  127. noticeContainer.slideUp( 'fast' );
  128. }
  129. } else {
  130. if ( ! noticeContainer.length ) {
  131. noticeContainer = $( '<div class="media-widget-embed-notice notice notice-error notice-alt"></div>' );
  132. noticeContainer.hide();
  133. embedLinkView.views.parent.$el.prepend( noticeContainer );
  134. }
  135. noticeContainer.empty();
  136. noticeContainer.append( $( '<p>', {
  137. html: notice
  138. }));
  139. noticeContainer.slideDown( 'fast' );
  140. }
  141. },
  142. /**
  143. * Update oEmbed.
  144. *
  145. * @since 4.9.0
  146. *
  147. * @returns {void}
  148. */
  149. updateoEmbed: function() {
  150. var embedLinkView = this, url; // eslint-disable-line consistent-this
  151. url = embedLinkView.model.get( 'url' );
  152. // Abort if the URL field was emptied out.
  153. if ( ! url ) {
  154. embedLinkView.setErrorNotice( '' );
  155. embedLinkView.setAddToWidgetButtonDisabled( true );
  156. return;
  157. }
  158. if ( ! url.match( /^(http|https):\/\/.+\// ) ) {
  159. embedLinkView.controller.$el.find( '#embed-url-field' ).addClass( 'invalid' );
  160. embedLinkView.setAddToWidgetButtonDisabled( true );
  161. }
  162. wp.media.view.EmbedLink.prototype.updateoEmbed.call( embedLinkView );
  163. },
  164. /**
  165. * Fetch media.
  166. *
  167. * @returns {void}
  168. */
  169. fetch: function() {
  170. var embedLinkView = this, fetchSuccess, matches, fileExt, urlParser, url, re, youTubeEmbedMatch; // eslint-disable-line consistent-this
  171. url = embedLinkView.model.get( 'url' );
  172. if ( embedLinkView.dfd && 'pending' === embedLinkView.dfd.state() ) {
  173. embedLinkView.dfd.abort();
  174. }
  175. fetchSuccess = function( response ) {
  176. embedLinkView.renderoEmbed({
  177. data: {
  178. body: response
  179. }
  180. });
  181. embedLinkView.controller.$el.find( '#embed-url-field' ).removeClass( 'invalid' );
  182. embedLinkView.setErrorNotice( '' );
  183. embedLinkView.setAddToWidgetButtonDisabled( false );
  184. };
  185. urlParser = document.createElement( 'a' );
  186. urlParser.href = url;
  187. matches = urlParser.pathname.toLowerCase().match( /\.(\w+)$/ );
  188. if ( matches ) {
  189. fileExt = matches[1];
  190. if ( ! wp.media.view.settings.embedMimes[ fileExt ] ) {
  191. embedLinkView.renderFail();
  192. } else if ( 0 !== wp.media.view.settings.embedMimes[ fileExt ].indexOf( embedLinkView.controller.options.mimeType ) ) {
  193. embedLinkView.renderFail();
  194. } else {
  195. fetchSuccess( '<!--success-->' );
  196. }
  197. return;
  198. }
  199. // Support YouTube embed links.
  200. re = /https?:\/\/www\.youtube\.com\/embed\/([^/]+)/;
  201. youTubeEmbedMatch = re.exec( url );
  202. if ( youTubeEmbedMatch ) {
  203. url = 'https://www.youtube.com/watch?v=' + youTubeEmbedMatch[ 1 ];
  204. // silently change url to proper oembed-able version.
  205. embedLinkView.model.attributes.url = url;
  206. }
  207. embedLinkView.dfd = wp.apiRequest({
  208. url: wp.media.view.settings.oEmbedProxyUrl,
  209. data: {
  210. url: url,
  211. maxwidth: embedLinkView.model.get( 'width' ),
  212. maxheight: embedLinkView.model.get( 'height' ),
  213. discover: false
  214. },
  215. type: 'GET',
  216. dataType: 'json',
  217. context: embedLinkView
  218. });
  219. embedLinkView.dfd.done( function( response ) {
  220. if ( embedLinkView.controller.options.mimeType !== response.type ) {
  221. embedLinkView.renderFail();
  222. return;
  223. }
  224. fetchSuccess( response.html );
  225. });
  226. embedLinkView.dfd.fail( _.bind( embedLinkView.renderFail, embedLinkView ) );
  227. },
  228. /**
  229. * Handle render failure.
  230. *
  231. * Overrides the {EmbedLink#renderFail()} method to prevent showing the "Link Text" field.
  232. * The element is getting display:none in the stylesheet, but the underlying method uses
  233. * uses {jQuery.fn.show()} which adds an inline style. This avoids the need for !important.
  234. *
  235. * @returns {void}
  236. */
  237. renderFail: function renderFail() {
  238. var embedLinkView = this; // eslint-disable-line consistent-this
  239. embedLinkView.controller.$el.find( '#embed-url-field' ).addClass( 'invalid' );
  240. embedLinkView.setErrorNotice( embedLinkView.controller.options.invalidEmbedTypeError || 'ERROR' );
  241. embedLinkView.setAddToWidgetButtonDisabled( true );
  242. }
  243. });
  244. }
  245. this.settings( new Constructor({
  246. controller: this.controller,
  247. model: this.model.props,
  248. priority: 40
  249. }));
  250. }
  251. });
  252. /**
  253. * Custom media frame for selecting uploaded media or providing media by URL.
  254. *
  255. * @class MediaFrameSelect
  256. * @constructor
  257. */
  258. component.MediaFrameSelect = wp.media.view.MediaFrame.Post.extend({
  259. /**
  260. * Create the default states.
  261. *
  262. * @returns {void}
  263. */
  264. createStates: function createStates() {
  265. var mime = this.options.mimeType, specificMimes = [];
  266. _.each( wp.media.view.settings.embedMimes, function( embedMime ) {
  267. if ( 0 === embedMime.indexOf( mime ) ) {
  268. specificMimes.push( embedMime );
  269. }
  270. });
  271. if ( specificMimes.length > 0 ) {
  272. mime = specificMimes;
  273. }
  274. this.states.add([
  275. // Main states.
  276. new component.PersistentDisplaySettingsLibrary({
  277. id: 'insert',
  278. title: this.options.title,
  279. selection: this.options.selection,
  280. priority: 20,
  281. toolbar: 'main-insert',
  282. filterable: 'dates',
  283. library: wp.media.query({
  284. type: mime
  285. }),
  286. multiple: false,
  287. editable: true,
  288. selectedDisplaySettings: this.options.selectedDisplaySettings,
  289. displaySettings: _.isUndefined( this.options.showDisplaySettings ) ? true : this.options.showDisplaySettings,
  290. displayUserSettings: false // We use the display settings from the current/default widget instance props.
  291. }),
  292. new wp.media.controller.EditImage({ model: this.options.editImage }),
  293. // Embed states.
  294. new wp.media.controller.Embed({
  295. metadata: this.options.metadata,
  296. type: 'image' === this.options.mimeType ? 'image' : 'link',
  297. invalidEmbedTypeError: this.options.invalidEmbedTypeError
  298. })
  299. ]);
  300. },
  301. /**
  302. * Main insert toolbar.
  303. *
  304. * Forked override of {wp.media.view.MediaFrame.Post#mainInsertToolbar()} to override text.
  305. *
  306. * @param {wp.Backbone.View} view - Toolbar view.
  307. * @this {wp.media.controller.Library}
  308. * @returns {void}
  309. */
  310. mainInsertToolbar: function mainInsertToolbar( view ) {
  311. var controller = this; // eslint-disable-line consistent-this
  312. view.set( 'insert', {
  313. style: 'primary',
  314. priority: 80,
  315. text: controller.options.text, // The whole reason for the fork.
  316. requires: { selection: true },
  317. /**
  318. * Handle click.
  319. *
  320. * @fires wp.media.controller.State#insert()
  321. * @returns {void}
  322. */
  323. click: function onClick() {
  324. var state = controller.state(),
  325. selection = state.get( 'selection' );
  326. controller.close();
  327. state.trigger( 'insert', selection ).reset();
  328. }
  329. });
  330. },
  331. /**
  332. * Main embed toolbar.
  333. *
  334. * Forked override of {wp.media.view.MediaFrame.Post#mainEmbedToolbar()} to override text.
  335. *
  336. * @param {wp.Backbone.View} toolbar - Toolbar view.
  337. * @this {wp.media.controller.Library}
  338. * @returns {void}
  339. */
  340. mainEmbedToolbar: function mainEmbedToolbar( toolbar ) {
  341. toolbar.view = new wp.media.view.Toolbar.Embed({
  342. controller: this,
  343. text: this.options.text,
  344. event: 'insert'
  345. });
  346. },
  347. /**
  348. * Embed content.
  349. *
  350. * Forked override of {wp.media.view.MediaFrame.Post#embedContent()} to suppress irrelevant "link text" field.
  351. *
  352. * @returns {void}
  353. */
  354. embedContent: function embedContent() {
  355. var view = new component.MediaEmbedView({
  356. controller: this,
  357. model: this.state()
  358. }).render();
  359. this.content.set( view );
  360. if ( ! wp.media.isTouchDevice ) {
  361. view.url.focus();
  362. }
  363. }
  364. });
  365. /**
  366. * Media widget control.
  367. *
  368. * @class MediaWidgetControl
  369. * @constructor
  370. * @abstract
  371. */
  372. component.MediaWidgetControl = Backbone.View.extend({
  373. /**
  374. * Translation strings.
  375. *
  376. * The mapping of translation strings is handled by media widget subclasses,
  377. * exported from PHP to JS such as is done in WP_Widget_Media_Image::enqueue_admin_scripts().
  378. *
  379. * @type {Object}
  380. */
  381. l10n: {
  382. add_to_widget: '{{add_to_widget}}',
  383. add_media: '{{add_media}}'
  384. },
  385. /**
  386. * Widget ID base.
  387. *
  388. * This may be defined by the subclass. It may be exported from PHP to JS
  389. * such as is done in WP_Widget_Media_Image::enqueue_admin_scripts(). If not,
  390. * it will attempt to be discovered by looking to see if this control
  391. * instance extends each member of component.controlConstructors, and if
  392. * it does extend one, will use the key as the id_base.
  393. *
  394. * @type {string}
  395. */
  396. id_base: '',
  397. /**
  398. * Mime type.
  399. *
  400. * This must be defined by the subclass. It may be exported from PHP to JS
  401. * such as is done in WP_Widget_Media_Image::enqueue_admin_scripts().
  402. *
  403. * @type {string}
  404. */
  405. mime_type: '',
  406. /**
  407. * View events.
  408. *
  409. * @type {Object}
  410. */
  411. events: {
  412. 'click .notice-missing-attachment a': 'handleMediaLibraryLinkClick',
  413. 'click .select-media': 'selectMedia',
  414. 'click .placeholder': 'selectMedia',
  415. 'click .edit-media': 'editMedia'
  416. },
  417. /**
  418. * Show display settings.
  419. *
  420. * @type {boolean}
  421. */
  422. showDisplaySettings: true,
  423. /**
  424. * Initialize.
  425. *
  426. * @param {Object} options - Options.
  427. * @param {Backbone.Model} options.model - Model.
  428. * @param {jQuery} options.el - Control field container element.
  429. * @param {jQuery} options.syncContainer - Container element where fields are synced for the server.
  430. * @returns {void}
  431. */
  432. initialize: function initialize( options ) {
  433. var control = this;
  434. Backbone.View.prototype.initialize.call( control, options );
  435. if ( ! ( control.model instanceof component.MediaWidgetModel ) ) {
  436. throw new Error( 'Missing options.model' );
  437. }
  438. if ( ! options.el ) {
  439. throw new Error( 'Missing options.el' );
  440. }
  441. if ( ! options.syncContainer ) {
  442. throw new Error( 'Missing options.syncContainer' );
  443. }
  444. control.syncContainer = options.syncContainer;
  445. control.$el.addClass( 'media-widget-control' );
  446. // Allow methods to be passed in with control context preserved.
  447. _.bindAll( control, 'syncModelToInputs', 'render', 'updateSelectedAttachment', 'renderPreview' );
  448. if ( ! control.id_base ) {
  449. _.find( component.controlConstructors, function( Constructor, idBase ) {
  450. if ( control instanceof Constructor ) {
  451. control.id_base = idBase;
  452. return true;
  453. }
  454. return false;
  455. });
  456. if ( ! control.id_base ) {
  457. throw new Error( 'Missing id_base.' );
  458. }
  459. }
  460. // Track attributes needed to renderPreview in it's own model.
  461. control.previewTemplateProps = new Backbone.Model( control.mapModelToPreviewTemplateProps() );
  462. // Re-render the preview when the attachment changes.
  463. control.selectedAttachment = new wp.media.model.Attachment();
  464. control.renderPreview = _.debounce( control.renderPreview );
  465. control.listenTo( control.previewTemplateProps, 'change', control.renderPreview );
  466. // Make sure a copy of the selected attachment is always fetched.
  467. control.model.on( 'change:attachment_id', control.updateSelectedAttachment );
  468. control.model.on( 'change:url', control.updateSelectedAttachment );
  469. control.updateSelectedAttachment();
  470. /*
  471. * Sync the widget instance model attributes onto the hidden inputs that widgets currently use to store the state.
  472. * In the future, when widgets are JS-driven, the underlying widget instance data should be exposed as a model
  473. * from the start, without having to sync with hidden fields. See <https://core.trac.wordpress.org/ticket/33507>.
  474. */
  475. control.listenTo( control.model, 'change', control.syncModelToInputs );
  476. control.listenTo( control.model, 'change', control.syncModelToPreviewProps );
  477. control.listenTo( control.model, 'change', control.render );
  478. // Update the title.
  479. control.$el.on( 'input change', '.title', function updateTitle() {
  480. control.model.set({
  481. title: $.trim( $( this ).val() )
  482. });
  483. });
  484. // Update link_url attribute.
  485. control.$el.on( 'input change', '.link', function updateLinkUrl() {
  486. var linkUrl = $.trim( $( this ).val() ), linkType = 'custom';
  487. if ( control.selectedAttachment.get( 'linkUrl' ) === linkUrl || control.selectedAttachment.get( 'link' ) === linkUrl ) {
  488. linkType = 'post';
  489. } else if ( control.selectedAttachment.get( 'url' ) === linkUrl ) {
  490. linkType = 'file';
  491. }
  492. control.model.set( {
  493. link_url: linkUrl,
  494. link_type: linkType
  495. });
  496. // Update display settings for the next time the user opens to select from the media library.
  497. control.displaySettings.set( {
  498. link: linkType,
  499. linkUrl: linkUrl
  500. });
  501. });
  502. /*
  503. * Copy current display settings from the widget model to serve as basis
  504. * of customized display settings for the current media frame session.
  505. * Changes to display settings will be synced into this model, and
  506. * when a new selection is made, the settings from this will be synced
  507. * into that AttachmentDisplay's model to persist the setting changes.
  508. */
  509. control.displaySettings = new Backbone.Model( _.pick(
  510. control.mapModelToMediaFrameProps(
  511. _.extend( control.model.defaults(), control.model.toJSON() )
  512. ),
  513. _.keys( wp.media.view.settings.defaultProps )
  514. ) );
  515. },
  516. /**
  517. * Update the selected attachment if necessary.
  518. *
  519. * @returns {void}
  520. */
  521. updateSelectedAttachment: function updateSelectedAttachment() {
  522. var control = this, attachment;
  523. if ( 0 === control.model.get( 'attachment_id' ) ) {
  524. control.selectedAttachment.clear();
  525. control.model.set( 'error', false );
  526. } else if ( control.model.get( 'attachment_id' ) !== control.selectedAttachment.get( 'id' ) ) {
  527. attachment = new wp.media.model.Attachment({
  528. id: control.model.get( 'attachment_id' )
  529. });
  530. attachment.fetch()
  531. .done( function done() {
  532. control.model.set( 'error', false );
  533. control.selectedAttachment.set( attachment.toJSON() );
  534. })
  535. .fail( function fail() {
  536. control.model.set( 'error', 'missing_attachment' );
  537. });
  538. }
  539. },
  540. /**
  541. * Sync the model attributes to the hidden inputs, and update previewTemplateProps.
  542. *
  543. * @returns {void}
  544. */
  545. syncModelToPreviewProps: function syncModelToPreviewProps() {
  546. var control = this;
  547. control.previewTemplateProps.set( control.mapModelToPreviewTemplateProps() );
  548. },
  549. /**
  550. * Sync the model attributes to the hidden inputs, and update previewTemplateProps.
  551. *
  552. * @returns {void}
  553. */
  554. syncModelToInputs: function syncModelToInputs() {
  555. var control = this;
  556. control.syncContainer.find( '.media-widget-instance-property' ).each( function() {
  557. var input = $( this ), value, propertyName;
  558. propertyName = input.data( 'property' );
  559. value = control.model.get( propertyName );
  560. if ( _.isUndefined( value ) ) {
  561. return;
  562. }
  563. if ( 'array' === control.model.schema[ propertyName ].type && _.isArray( value ) ) {
  564. value = value.join( ',' );
  565. } else if ( 'boolean' === control.model.schema[ propertyName ].type ) {
  566. value = value ? '1' : ''; // Because in PHP, strval( true ) === '1' && strval( false ) === ''.
  567. } else {
  568. value = String( value );
  569. }
  570. if ( input.val() !== value ) {
  571. input.val( value );
  572. input.trigger( 'change' );
  573. }
  574. });
  575. },
  576. /**
  577. * Get template.
  578. *
  579. * @returns {Function} Template.
  580. */
  581. template: function template() {
  582. var control = this;
  583. if ( ! $( '#tmpl-widget-media-' + control.id_base + '-control' ).length ) {
  584. throw new Error( 'Missing widget control template for ' + control.id_base );
  585. }
  586. return wp.template( 'widget-media-' + control.id_base + '-control' );
  587. },
  588. /**
  589. * Render template.
  590. *
  591. * @returns {void}
  592. */
  593. render: function render() {
  594. var control = this, titleInput;
  595. if ( ! control.templateRendered ) {
  596. control.$el.html( control.template()( control.model.toJSON() ) );
  597. control.renderPreview(); // Hereafter it will re-render when control.selectedAttachment changes.
  598. control.templateRendered = true;
  599. }
  600. titleInput = control.$el.find( '.title' );
  601. if ( ! titleInput.is( document.activeElement ) ) {
  602. titleInput.val( control.model.get( 'title' ) );
  603. }
  604. control.$el.toggleClass( 'selected', control.isSelected() );
  605. },
  606. /**
  607. * Render media preview.
  608. *
  609. * @abstract
  610. * @returns {void}
  611. */
  612. renderPreview: function renderPreview() {
  613. throw new Error( 'renderPreview must be implemented' );
  614. },
  615. /**
  616. * Whether a media item is selected.
  617. *
  618. * @returns {boolean} Whether selected and no error.
  619. */
  620. isSelected: function isSelected() {
  621. var control = this;
  622. if ( control.model.get( 'error' ) ) {
  623. return false;
  624. }
  625. return Boolean( control.model.get( 'attachment_id' ) || control.model.get( 'url' ) );
  626. },
  627. /**
  628. * Handle click on link to Media Library to open modal, such as the link that appears when in the missing attachment error notice.
  629. *
  630. * @param {jQuery.Event} event - Event.
  631. * @returns {void}
  632. */
  633. handleMediaLibraryLinkClick: function handleMediaLibraryLinkClick( event ) {
  634. var control = this;
  635. event.preventDefault();
  636. control.selectMedia();
  637. },
  638. /**
  639. * Open the media select frame to chose an item.
  640. *
  641. * @returns {void}
  642. */
  643. selectMedia: function selectMedia() {
  644. var control = this, selection, mediaFrame, defaultSync, mediaFrameProps, selectionModels = [];
  645. if ( control.isSelected() && 0 !== control.model.get( 'attachment_id' ) ) {
  646. selectionModels.push( control.selectedAttachment );
  647. }
  648. selection = new wp.media.model.Selection( selectionModels, { multiple: false } );
  649. mediaFrameProps = control.mapModelToMediaFrameProps( control.model.toJSON() );
  650. if ( mediaFrameProps.size ) {
  651. control.displaySettings.set( 'size', mediaFrameProps.size );
  652. }
  653. mediaFrame = new component.MediaFrameSelect({
  654. title: control.l10n.add_media,
  655. frame: 'post',
  656. text: control.l10n.add_to_widget,
  657. selection: selection,
  658. mimeType: control.mime_type,
  659. selectedDisplaySettings: control.displaySettings,
  660. showDisplaySettings: control.showDisplaySettings,
  661. metadata: mediaFrameProps,
  662. state: control.isSelected() && 0 === control.model.get( 'attachment_id' ) ? 'embed' : 'insert',
  663. invalidEmbedTypeError: control.l10n.unsupported_file_type
  664. });
  665. wp.media.frame = mediaFrame; // See wp.media().
  666. // Handle selection of a media item.
  667. mediaFrame.on( 'insert', function onInsert() {
  668. var attachment = {}, state = mediaFrame.state();
  669. // Update cached attachment object to avoid having to re-fetch. This also triggers re-rendering of preview.
  670. if ( 'embed' === state.get( 'id' ) ) {
  671. _.extend( attachment, { id: 0 }, state.props.toJSON() );
  672. } else {
  673. _.extend( attachment, state.get( 'selection' ).first().toJSON() );
  674. }
  675. control.selectedAttachment.set( attachment );
  676. control.model.set( 'error', false );
  677. // Update widget instance.
  678. control.model.set( control.getModelPropsFromMediaFrame( mediaFrame ) );
  679. });
  680. // Disable syncing of attachment changes back to server (except for deletions). See <https://core.trac.wordpress.org/ticket/40403>.
  681. defaultSync = wp.media.model.Attachment.prototype.sync;
  682. wp.media.model.Attachment.prototype.sync = function( method ) {
  683. if ( 'delete' === method ) {
  684. return defaultSync.apply( this, arguments );
  685. } else {
  686. return $.Deferred().rejectWith( this ).promise();
  687. }
  688. };
  689. mediaFrame.on( 'close', function onClose() {
  690. wp.media.model.Attachment.prototype.sync = defaultSync;
  691. });
  692. mediaFrame.$el.addClass( 'media-widget' );
  693. mediaFrame.open();
  694. // Clear the selected attachment when it is deleted in the media select frame.
  695. if ( selection ) {
  696. selection.on( 'destroy', function onDestroy( attachment ) {
  697. if ( control.model.get( 'attachment_id' ) === attachment.get( 'id' ) ) {
  698. control.model.set({
  699. attachment_id: 0,
  700. url: ''
  701. });
  702. }
  703. });
  704. }
  705. /*
  706. * Make sure focus is set inside of modal so that hitting Esc will close
  707. * the modal and not inadvertently cause the widget to collapse in the customizer.
  708. */
  709. mediaFrame.$el.find( '.media-frame-menu .media-menu-item.active' ).focus();
  710. },
  711. /**
  712. * Get the instance props from the media selection frame.
  713. *
  714. * @param {wp.media.view.MediaFrame.Select} mediaFrame - Select frame.
  715. * @returns {Object} Props.
  716. */
  717. getModelPropsFromMediaFrame: function getModelPropsFromMediaFrame( mediaFrame ) {
  718. var control = this, state, mediaFrameProps, modelProps;
  719. state = mediaFrame.state();
  720. if ( 'insert' === state.get( 'id' ) ) {
  721. mediaFrameProps = state.get( 'selection' ).first().toJSON();
  722. mediaFrameProps.postUrl = mediaFrameProps.link;
  723. if ( control.showDisplaySettings ) {
  724. _.extend(
  725. mediaFrameProps,
  726. mediaFrame.content.get( '.attachments-browser' ).sidebar.get( 'display' ).model.toJSON()
  727. );
  728. }
  729. if ( mediaFrameProps.sizes && mediaFrameProps.size && mediaFrameProps.sizes[ mediaFrameProps.size ] ) {
  730. mediaFrameProps.url = mediaFrameProps.sizes[ mediaFrameProps.size ].url;
  731. }
  732. } else if ( 'embed' === state.get( 'id' ) ) {
  733. mediaFrameProps = _.extend(
  734. state.props.toJSON(),
  735. { attachment_id: 0 }, // Because some media frames use `attachment_id` not `id`.
  736. control.model.getEmbedResetProps()
  737. );
  738. } else {
  739. throw new Error( 'Unexpected state: ' + state.get( 'id' ) );
  740. }
  741. if ( mediaFrameProps.id ) {
  742. mediaFrameProps.attachment_id = mediaFrameProps.id;
  743. }
  744. modelProps = control.mapMediaToModelProps( mediaFrameProps );
  745. // Clear the extension prop so sources will be reset for video and audio media.
  746. _.each( wp.media.view.settings.embedExts, function( ext ) {
  747. if ( ext in control.model.schema && modelProps.url !== modelProps[ ext ] ) {
  748. modelProps[ ext ] = '';
  749. }
  750. });
  751. return modelProps;
  752. },
  753. /**
  754. * Map media frame props to model props.
  755. *
  756. * @param {Object} mediaFrameProps - Media frame props.
  757. * @returns {Object} Model props.
  758. */
  759. mapMediaToModelProps: function mapMediaToModelProps( mediaFrameProps ) {
  760. var control = this, mediaFramePropToModelPropMap = {}, modelProps = {}, extension;
  761. _.each( control.model.schema, function( fieldSchema, modelProp ) {
  762. // Ignore widget title attribute.
  763. if ( 'title' === modelProp ) {
  764. return;
  765. }
  766. mediaFramePropToModelPropMap[ fieldSchema.media_prop || modelProp ] = modelProp;
  767. });
  768. _.each( mediaFrameProps, function( value, mediaProp ) {
  769. var propName = mediaFramePropToModelPropMap[ mediaProp ] || mediaProp;
  770. if ( control.model.schema[ propName ] ) {
  771. modelProps[ propName ] = value;
  772. }
  773. });
  774. if ( 'custom' === mediaFrameProps.size ) {
  775. modelProps.width = mediaFrameProps.customWidth;
  776. modelProps.height = mediaFrameProps.customHeight;
  777. }
  778. if ( 'post' === mediaFrameProps.link ) {
  779. modelProps.link_url = mediaFrameProps.postUrl || mediaFrameProps.linkUrl;
  780. } else if ( 'file' === mediaFrameProps.link ) {
  781. modelProps.link_url = mediaFrameProps.url;
  782. }
  783. // Because some media frames use `id` instead of `attachment_id`.
  784. if ( ! mediaFrameProps.attachment_id && mediaFrameProps.id ) {
  785. modelProps.attachment_id = mediaFrameProps.id;
  786. }
  787. if ( mediaFrameProps.url ) {
  788. extension = mediaFrameProps.url.replace( /#.*$/, '' ).replace( /\?.*$/, '' ).split( '.' ).pop().toLowerCase();
  789. if ( extension in control.model.schema ) {
  790. modelProps[ extension ] = mediaFrameProps.url;
  791. }
  792. }
  793. // Always omit the titles derived from mediaFrameProps.
  794. return _.omit( modelProps, 'title' );
  795. },
  796. /**
  797. * Map model props to media frame props.
  798. *
  799. * @param {Object} modelProps - Model props.
  800. * @returns {Object} Media frame props.
  801. */
  802. mapModelToMediaFrameProps: function mapModelToMediaFrameProps( modelProps ) {
  803. var control = this, mediaFrameProps = {};
  804. _.each( modelProps, function( value, modelProp ) {
  805. var fieldSchema = control.model.schema[ modelProp ] || {};
  806. mediaFrameProps[ fieldSchema.media_prop || modelProp ] = value;
  807. });
  808. // Some media frames use attachment_id.
  809. mediaFrameProps.attachment_id = mediaFrameProps.id;
  810. if ( 'custom' === mediaFrameProps.size ) {
  811. mediaFrameProps.customWidth = control.model.get( 'width' );
  812. mediaFrameProps.customHeight = control.model.get( 'height' );
  813. }
  814. return mediaFrameProps;
  815. },
  816. /**
  817. * Map model props to previewTemplateProps.
  818. *
  819. * @returns {Object} Preview Template Props.
  820. */
  821. mapModelToPreviewTemplateProps: function mapModelToPreviewTemplateProps() {
  822. var control = this, previewTemplateProps = {};
  823. _.each( control.model.schema, function( value, prop ) {
  824. if ( ! value.hasOwnProperty( 'should_preview_update' ) || value.should_preview_update ) {
  825. previewTemplateProps[ prop ] = control.model.get( prop );
  826. }
  827. });
  828. // Templates need to be aware of the error.
  829. previewTemplateProps.error = control.model.get( 'error' );
  830. return previewTemplateProps;
  831. },
  832. /**
  833. * Open the media frame to modify the selected item.
  834. *
  835. * @abstract
  836. * @returns {void}
  837. */
  838. editMedia: function editMedia() {
  839. throw new Error( 'editMedia not implemented' );
  840. }
  841. });
  842. /**
  843. * Media widget model.
  844. *
  845. * @class MediaWidgetModel
  846. * @constructor
  847. */
  848. component.MediaWidgetModel = Backbone.Model.extend({
  849. /**
  850. * Id attribute.
  851. *
  852. * @type {string}
  853. */
  854. idAttribute: 'widget_id',
  855. /**
  856. * Instance schema.
  857. *
  858. * This adheres to JSON Schema and subclasses should have their schema
  859. * exported from PHP to JS such as is done in WP_Widget_Media_Image::enqueue_admin_scripts().
  860. *
  861. * @type {Object.<string, Object>}
  862. */
  863. schema: {
  864. title: {
  865. type: 'string',
  866. 'default': ''
  867. },
  868. attachment_id: {
  869. type: 'integer',
  870. 'default': 0
  871. },
  872. url: {
  873. type: 'string',
  874. 'default': ''
  875. }
  876. },
  877. /**
  878. * Get default attribute values.
  879. *
  880. * @returns {Object} Mapping of property names to their default values.
  881. */
  882. defaults: function() {
  883. var defaults = {};
  884. _.each( this.schema, function( fieldSchema, field ) {
  885. defaults[ field ] = fieldSchema['default'];
  886. });
  887. return defaults;
  888. },
  889. /**
  890. * Set attribute value(s).
  891. *
  892. * This is a wrapped version of Backbone.Model#set() which allows us to
  893. * cast the attribute values from the hidden inputs' string values into
  894. * the appropriate data types (integers or booleans).
  895. *
  896. * @param {string|Object} key - Attribute name or attribute pairs.
  897. * @param {mixed|Object} [val] - Attribute value or options object.
  898. * @param {Object} [options] - Options when attribute name and value are passed separately.
  899. * @returns {wp.mediaWidgets.MediaWidgetModel} This model.
  900. */
  901. set: function set( key, val, options ) {
  902. var model = this, attrs, opts, castedAttrs; // eslint-disable-line consistent-this
  903. if ( null === key ) {
  904. return model;
  905. }
  906. if ( 'object' === typeof key ) {
  907. attrs = key;
  908. opts = val;
  909. } else {
  910. attrs = {};
  911. attrs[ key ] = val;
  912. opts = options;
  913. }
  914. castedAttrs = {};
  915. _.each( attrs, function( value, name ) {
  916. var type;
  917. if ( ! model.schema[ name ] ) {
  918. castedAttrs[ name ] = value;
  919. return;
  920. }
  921. type = model.schema[ name ].type;
  922. if ( 'array' === type ) {
  923. castedAttrs[ name ] = value;
  924. if ( ! _.isArray( castedAttrs[ name ] ) ) {
  925. castedAttrs[ name ] = castedAttrs[ name ].split( /,/ ); // Good enough for parsing an ID list.
  926. }
  927. if ( model.schema[ name ].items && 'integer' === model.schema[ name ].items.type ) {
  928. castedAttrs[ name ] = _.filter(
  929. _.map( castedAttrs[ name ], function( id ) {
  930. return parseInt( id, 10 );
  931. },
  932. function( id ) {
  933. return 'number' === typeof id;
  934. }
  935. ) );
  936. }
  937. } else if ( 'integer' === type ) {
  938. castedAttrs[ name ] = parseInt( value, 10 );
  939. } else if ( 'boolean' === type ) {
  940. castedAttrs[ name ] = ! ( ! value || '0' === value || 'false' === value );
  941. } else {
  942. castedAttrs[ name ] = value;
  943. }
  944. });
  945. return Backbone.Model.prototype.set.call( this, castedAttrs, opts );
  946. },
  947. /**
  948. * Get props which are merged on top of the model when an embed is chosen (as opposed to an attachment).
  949. *
  950. * @returns {Object} Reset/override props.
  951. */
  952. getEmbedResetProps: function getEmbedResetProps() {
  953. return {
  954. id: 0
  955. };
  956. }
  957. });
  958. /**
  959. * Collection of all widget model instances.
  960. *
  961. * @type {Backbone.Collection}
  962. */
  963. component.modelCollection = new ( Backbone.Collection.extend({
  964. model: component.MediaWidgetModel
  965. }) )();
  966. /**
  967. * Mapping of widget ID to instances of MediaWidgetControl subclasses.
  968. *
  969. * @type {Object.<string, wp.mediaWidgets.MediaWidgetControl>}
  970. */
  971. component.widgetControls = {};
  972. /**
  973. * Handle widget being added or initialized for the first time at the widget-added event.
  974. *
  975. * @param {jQuery.Event} event - Event.
  976. * @param {jQuery} widgetContainer - Widget container element.
  977. * @returns {void}
  978. */
  979. component.handleWidgetAdded = function handleWidgetAdded( event, widgetContainer ) {
  980. var fieldContainer, syncContainer, widgetForm, idBase, ControlConstructor, ModelConstructor, modelAttributes, widgetControl, widgetModel, widgetId, animatedCheckDelay = 50, renderWhenAnimationDone;
  981. widgetForm = widgetContainer.find( '> .widget-inside > .form, > .widget-inside > form' ); // Note: '.form' appears in the customizer, whereas 'form' on the widgets admin screen.
  982. idBase = widgetForm.find( '> .id_base' ).val();
  983. widgetId = widgetForm.find( '> .widget-id' ).val();
  984. // Prevent initializing already-added widgets.
  985. if ( component.widgetControls[ widgetId ] ) {
  986. return;
  987. }
  988. ControlConstructor = component.controlConstructors[ idBase ];
  989. if ( ! ControlConstructor ) {
  990. return;
  991. }
  992. ModelConstructor = component.modelConstructors[ idBase ] || component.MediaWidgetModel;
  993. /*
  994. * Create a container element for the widget control (Backbone.View).
  995. * This is inserted into the DOM immediately before the .widget-content
  996. * element because the contents of this element are essentially "managed"
  997. * by PHP, where each widget update cause the entire element to be emptied
  998. * and replaced with the rendered output of WP_Widget::form() which is
  999. * sent back in Ajax request made to save/update the widget instance.
  1000. * To prevent a "flash of replaced DOM elements and re-initialized JS
  1001. * components", the JS template is rendered outside of the normal form
  1002. * container.
  1003. */
  1004. fieldContainer = $( '<div></div>' );
  1005. syncContainer = widgetContainer.find( '.widget-content:first' );
  1006. syncContainer.before( fieldContainer );
  1007. /*
  1008. * Sync the widget instance model attributes onto the hidden inputs that widgets currently use to store the state.
  1009. * In the future, when widgets are JS-driven, the underlying widget instance data should be exposed as a model
  1010. * from the start, without having to sync with hidden fields. See <https://core.trac.wordpress.org/ticket/33507>.
  1011. */
  1012. modelAttributes = {};
  1013. syncContainer.find( '.media-widget-instance-property' ).each( function() {
  1014. var input = $( this );
  1015. modelAttributes[ input.data( 'property' ) ] = input.val();
  1016. });
  1017. modelAttributes.widget_id = widgetId;
  1018. widgetModel = new ModelConstructor( modelAttributes );
  1019. widgetControl = new ControlConstructor({
  1020. el: fieldContainer,
  1021. syncContainer: syncContainer,
  1022. model: widgetModel
  1023. });
  1024. /*
  1025. * Render the widget once the widget parent's container finishes animating,
  1026. * as the widget-added event fires with a slideDown of the container.
  1027. * This ensures that the container's dimensions are fixed so that ME.js
  1028. * can initialize with the proper dimensions.
  1029. */
  1030. renderWhenAnimationDone = function() {
  1031. if ( ! widgetContainer.hasClass( 'open' ) ) {
  1032. setTimeout( renderWhenAnimationDone, animatedCheckDelay );
  1033. } else {
  1034. widgetControl.render();
  1035. }
  1036. };
  1037. renderWhenAnimationDone();
  1038. /*
  1039. * Note that the model and control currently won't ever get garbage-collected
  1040. * when a widget gets removed/deleted because there is no widget-removed event.
  1041. */
  1042. component.modelCollection.add( [ widgetModel ] );
  1043. component.widgetControls[ widgetModel.get( 'widget_id' ) ] = widgetControl;
  1044. };
  1045. /**
  1046. * Setup widget in accessibility mode.
  1047. *
  1048. * @returns {void}
  1049. */
  1050. component.setupAccessibleMode = function setupAccessibleMode() {
  1051. var widgetForm, widgetId, idBase, widgetControl, ControlConstructor, ModelConstructor, modelAttributes, fieldContainer, syncContainer;
  1052. widgetForm = $( '.editwidget > form' );
  1053. if ( 0 === widgetForm.length ) {
  1054. return;
  1055. }
  1056. idBase = widgetForm.find( '> .widget-control-actions > .id_base' ).val();
  1057. ControlConstructor = component.controlConstructors[ idBase ];
  1058. if ( ! ControlConstructor ) {
  1059. return;
  1060. }
  1061. widgetId = widgetForm.find( '> .widget-control-actions > .widget-id' ).val();
  1062. ModelConstructor = component.modelConstructors[ idBase ] || component.MediaWidgetModel;
  1063. fieldContainer = $( '<div></div>' );
  1064. syncContainer = widgetForm.find( '> .widget-inside' );
  1065. syncContainer.before( fieldContainer );
  1066. modelAttributes = {};
  1067. syncContainer.find( '.media-widget-instance-property' ).each( function() {
  1068. var input = $( this );
  1069. modelAttributes[ input.data( 'property' ) ] = input.val();
  1070. });
  1071. modelAttributes.widget_id = widgetId;
  1072. widgetControl = new ControlConstructor({
  1073. el: fieldContainer,
  1074. syncContainer: syncContainer,
  1075. model: new ModelConstructor( modelAttributes )
  1076. });
  1077. component.modelCollection.add( [ widgetControl.model ] );
  1078. component.widgetControls[ widgetControl.model.get( 'widget_id' ) ] = widgetControl;
  1079. widgetControl.render();
  1080. };
  1081. /**
  1082. * Sync widget instance data sanitized from server back onto widget model.
  1083. *
  1084. * This gets called via the 'widget-updated' event when saving a widget from
  1085. * the widgets admin screen and also via the 'widget-synced' event when making
  1086. * a change to a widget in the customizer.
  1087. *
  1088. * @param {jQuery.Event} event - Event.
  1089. * @param {jQuery} widgetContainer - Widget container element.
  1090. * @returns {void}
  1091. */
  1092. component.handleWidgetUpdated = function handleWidgetUpdated( event, widgetContainer ) {
  1093. var widgetForm, widgetContent, widgetId, widgetControl, attributes = {};
  1094. widgetForm = widgetContainer.find( '> .widget-inside > .form, > .widget-inside > form' );
  1095. widgetId = widgetForm.find( '> .widget-id' ).val();
  1096. widgetControl = component.widgetControls[ widgetId ];
  1097. if ( ! widgetControl ) {
  1098. return;
  1099. }
  1100. // Make sure the server-sanitized values get synced back into the model.
  1101. widgetContent = widgetForm.find( '> .widget-content' );
  1102. widgetContent.find( '.media-widget-instance-property' ).each( function() {
  1103. var property = $( this ).data( 'property' );
  1104. attributes[ property ] = $( this ).val();
  1105. });
  1106. // Suspend syncing model back to inputs when syncing from inputs to model, preventing infinite loop.
  1107. widgetControl.stopListening( widgetControl.model, 'change', widgetControl.syncModelToInputs );
  1108. widgetControl.model.set( attributes );
  1109. widgetControl.listenTo( widgetControl.model, 'change', widgetControl.syncModelToInputs );
  1110. };
  1111. /**
  1112. * Initialize functionality.
  1113. *
  1114. * This function exists to prevent the JS file from having to boot itself.
  1115. * When WordPress enqueues this script, it should have an inline script
  1116. * attached which calls wp.mediaWidgets.init().
  1117. *
  1118. * @returns {void}
  1119. */
  1120. component.init = function init() {
  1121. var $document = $( document );
  1122. $document.on( 'widget-added', component.handleWidgetAdded );
  1123. $document.on( 'widget-synced widget-updated', component.handleWidgetUpdated );
  1124. /*
  1125. * Manually trigger widget-added events for media widgets on the admin
  1126. * screen once they are expanded. The widget-added event is not triggered
  1127. * for each pre-existing widget on the widgets admin screen like it is
  1128. * on the customizer. Likewise, the customizer only triggers widget-added
  1129. * when the widget is expanded to just-in-time construct the widget form
  1130. * when it is actually going to be displayed. So the following implements
  1131. * the same for the widgets admin screen, to invoke the widget-added
  1132. * handler when a pre-existing media widget is expanded.
  1133. */
  1134. $( function initializeExistingWidgetContainers() {
  1135. var widgetContainers;
  1136. if ( 'widgets' !== window.pagenow ) {
  1137. return;
  1138. }
  1139. widgetContainers = $( '.widgets-holder-wrap:not(#available-widgets)' ).find( 'div.widget' );
  1140. widgetContainers.one( 'click.toggle-widget-expanded', function toggleWidgetExpanded() {
  1141. var widgetContainer = $( this );
  1142. component.handleWidgetAdded( new jQuery.Event( 'widget-added' ), widgetContainer );
  1143. });
  1144. // Accessibility mode.
  1145. $( window ).on( 'load', function() {
  1146. component.setupAccessibleMode();
  1147. });
  1148. });
  1149. };
  1150. return component;
  1151. })( jQuery );