media-widgets.js 42 KB

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