media-widgets.js 42 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337
  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. * @return {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. * @return {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. * @return {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. * @return {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. * @return {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. * @return {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. * @return {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. * @return {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. * @return {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. * @return {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. * @return {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. * @return {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. * @return {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. * @return {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. * @return {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. }
  375. });
  376. component.MediaWidgetControl = Backbone.View.extend(/** @lends wp.mediaWidgets.MediaWidgetControl.prototype */{
  377. /**
  378. * Translation strings.
  379. *
  380. * The mapping of translation strings is handled by media widget subclasses,
  381. * exported from PHP to JS such as is done in WP_Widget_Media_Image::enqueue_admin_scripts().
  382. *
  383. * @type {Object}
  384. */
  385. l10n: {
  386. add_to_widget: '{{add_to_widget}}',
  387. add_media: '{{add_media}}'
  388. },
  389. /**
  390. * Widget ID base.
  391. *
  392. * This may be defined by the subclass. It may be exported from PHP to JS
  393. * such as is done in WP_Widget_Media_Image::enqueue_admin_scripts(). If not,
  394. * it will attempt to be discovered by looking to see if this control
  395. * instance extends each member of component.controlConstructors, and if
  396. * it does extend one, will use the key as the id_base.
  397. *
  398. * @type {string}
  399. */
  400. id_base: '',
  401. /**
  402. * Mime type.
  403. *
  404. * This must be defined by the subclass. It may be exported from PHP to JS
  405. * such as is done in WP_Widget_Media_Image::enqueue_admin_scripts().
  406. *
  407. * @type {string}
  408. */
  409. mime_type: '',
  410. /**
  411. * View events.
  412. *
  413. * @type {Object}
  414. */
  415. events: {
  416. 'click .notice-missing-attachment a': 'handleMediaLibraryLinkClick',
  417. 'click .select-media': 'selectMedia',
  418. 'click .placeholder': 'selectMedia',
  419. 'click .edit-media': 'editMedia'
  420. },
  421. /**
  422. * Show display settings.
  423. *
  424. * @type {boolean}
  425. */
  426. showDisplaySettings: true,
  427. /**
  428. * Media Widget Control.
  429. *
  430. * @constructs wp.mediaWidgets.MediaWidgetControl
  431. * @augments Backbone.View
  432. * @abstract
  433. *
  434. * @param {Object} options - Options.
  435. * @param {Backbone.Model} options.model - Model.
  436. * @param {jQuery} options.el - Control field container element.
  437. * @param {jQuery} options.syncContainer - Container element where fields are synced for the server.
  438. *
  439. * @return {void}
  440. */
  441. initialize: function initialize( options ) {
  442. var control = this;
  443. Backbone.View.prototype.initialize.call( control, options );
  444. if ( ! ( control.model instanceof component.MediaWidgetModel ) ) {
  445. throw new Error( 'Missing options.model' );
  446. }
  447. if ( ! options.el ) {
  448. throw new Error( 'Missing options.el' );
  449. }
  450. if ( ! options.syncContainer ) {
  451. throw new Error( 'Missing options.syncContainer' );
  452. }
  453. control.syncContainer = options.syncContainer;
  454. control.$el.addClass( 'media-widget-control' );
  455. // Allow methods to be passed in with control context preserved.
  456. _.bindAll( control, 'syncModelToInputs', 'render', 'updateSelectedAttachment', 'renderPreview' );
  457. if ( ! control.id_base ) {
  458. _.find( component.controlConstructors, function( Constructor, idBase ) {
  459. if ( control instanceof Constructor ) {
  460. control.id_base = idBase;
  461. return true;
  462. }
  463. return false;
  464. });
  465. if ( ! control.id_base ) {
  466. throw new Error( 'Missing id_base.' );
  467. }
  468. }
  469. // Track attributes needed to renderPreview in it's own model.
  470. control.previewTemplateProps = new Backbone.Model( control.mapModelToPreviewTemplateProps() );
  471. // Re-render the preview when the attachment changes.
  472. control.selectedAttachment = new wp.media.model.Attachment();
  473. control.renderPreview = _.debounce( control.renderPreview );
  474. control.listenTo( control.previewTemplateProps, 'change', control.renderPreview );
  475. // Make sure a copy of the selected attachment is always fetched.
  476. control.model.on( 'change:attachment_id', control.updateSelectedAttachment );
  477. control.model.on( 'change:url', control.updateSelectedAttachment );
  478. control.updateSelectedAttachment();
  479. /*
  480. * Sync the widget instance model attributes onto the hidden inputs that widgets currently use to store the state.
  481. * In the future, when widgets are JS-driven, the underlying widget instance data should be exposed as a model
  482. * from the start, without having to sync with hidden fields. See <https://core.trac.wordpress.org/ticket/33507>.
  483. */
  484. control.listenTo( control.model, 'change', control.syncModelToInputs );
  485. control.listenTo( control.model, 'change', control.syncModelToPreviewProps );
  486. control.listenTo( control.model, 'change', control.render );
  487. // Update the title.
  488. control.$el.on( 'input change', '.title', function updateTitle() {
  489. control.model.set({
  490. title: $( this ).val().trim()
  491. });
  492. });
  493. // Update link_url attribute.
  494. control.$el.on( 'input change', '.link', function updateLinkUrl() {
  495. var linkUrl = $( this ).val().trim(), linkType = 'custom';
  496. if ( control.selectedAttachment.get( 'linkUrl' ) === linkUrl || control.selectedAttachment.get( 'link' ) === linkUrl ) {
  497. linkType = 'post';
  498. } else if ( control.selectedAttachment.get( 'url' ) === linkUrl ) {
  499. linkType = 'file';
  500. }
  501. control.model.set( {
  502. link_url: linkUrl,
  503. link_type: linkType
  504. });
  505. // Update display settings for the next time the user opens to select from the media library.
  506. control.displaySettings.set( {
  507. link: linkType,
  508. linkUrl: linkUrl
  509. });
  510. });
  511. /*
  512. * Copy current display settings from the widget model to serve as basis
  513. * of customized display settings for the current media frame session.
  514. * Changes to display settings will be synced into this model, and
  515. * when a new selection is made, the settings from this will be synced
  516. * into that AttachmentDisplay's model to persist the setting changes.
  517. */
  518. control.displaySettings = new Backbone.Model( _.pick(
  519. control.mapModelToMediaFrameProps(
  520. _.extend( control.model.defaults(), control.model.toJSON() )
  521. ),
  522. _.keys( wp.media.view.settings.defaultProps )
  523. ) );
  524. },
  525. /**
  526. * Update the selected attachment if necessary.
  527. *
  528. * @return {void}
  529. */
  530. updateSelectedAttachment: function updateSelectedAttachment() {
  531. var control = this, attachment;
  532. if ( 0 === control.model.get( 'attachment_id' ) ) {
  533. control.selectedAttachment.clear();
  534. control.model.set( 'error', false );
  535. } else if ( control.model.get( 'attachment_id' ) !== control.selectedAttachment.get( 'id' ) ) {
  536. attachment = new wp.media.model.Attachment({
  537. id: control.model.get( 'attachment_id' )
  538. });
  539. attachment.fetch()
  540. .done( function done() {
  541. control.model.set( 'error', false );
  542. control.selectedAttachment.set( attachment.toJSON() );
  543. })
  544. .fail( function fail() {
  545. control.model.set( 'error', 'missing_attachment' );
  546. });
  547. }
  548. },
  549. /**
  550. * Sync the model attributes to the hidden inputs, and update previewTemplateProps.
  551. *
  552. * @return {void}
  553. */
  554. syncModelToPreviewProps: function syncModelToPreviewProps() {
  555. var control = this;
  556. control.previewTemplateProps.set( control.mapModelToPreviewTemplateProps() );
  557. },
  558. /**
  559. * Sync the model attributes to the hidden inputs, and update previewTemplateProps.
  560. *
  561. * @return {void}
  562. */
  563. syncModelToInputs: function syncModelToInputs() {
  564. var control = this;
  565. control.syncContainer.find( '.media-widget-instance-property' ).each( function() {
  566. var input = $( this ), value, propertyName;
  567. propertyName = input.data( 'property' );
  568. value = control.model.get( propertyName );
  569. if ( _.isUndefined( value ) ) {
  570. return;
  571. }
  572. if ( 'array' === control.model.schema[ propertyName ].type && _.isArray( value ) ) {
  573. value = value.join( ',' );
  574. } else if ( 'boolean' === control.model.schema[ propertyName ].type ) {
  575. value = value ? '1' : ''; // Because in PHP, strval( true ) === '1' && strval( false ) === ''.
  576. } else {
  577. value = String( value );
  578. }
  579. if ( input.val() !== value ) {
  580. input.val( value );
  581. input.trigger( 'change' );
  582. }
  583. });
  584. },
  585. /**
  586. * Get template.
  587. *
  588. * @return {Function} Template.
  589. */
  590. template: function template() {
  591. var control = this;
  592. if ( ! $( '#tmpl-widget-media-' + control.id_base + '-control' ).length ) {
  593. throw new Error( 'Missing widget control template for ' + control.id_base );
  594. }
  595. return wp.template( 'widget-media-' + control.id_base + '-control' );
  596. },
  597. /**
  598. * Render template.
  599. *
  600. * @return {void}
  601. */
  602. render: function render() {
  603. var control = this, titleInput;
  604. if ( ! control.templateRendered ) {
  605. control.$el.html( control.template()( control.model.toJSON() ) );
  606. control.renderPreview(); // Hereafter it will re-render when control.selectedAttachment changes.
  607. control.templateRendered = true;
  608. }
  609. titleInput = control.$el.find( '.title' );
  610. if ( ! titleInput.is( document.activeElement ) ) {
  611. titleInput.val( control.model.get( 'title' ) );
  612. }
  613. control.$el.toggleClass( 'selected', control.isSelected() );
  614. },
  615. /**
  616. * Render media preview.
  617. *
  618. * @abstract
  619. * @return {void}
  620. */
  621. renderPreview: function renderPreview() {
  622. throw new Error( 'renderPreview must be implemented' );
  623. },
  624. /**
  625. * Whether a media item is selected.
  626. *
  627. * @return {boolean} Whether selected and no error.
  628. */
  629. isSelected: function isSelected() {
  630. var control = this;
  631. if ( control.model.get( 'error' ) ) {
  632. return false;
  633. }
  634. return Boolean( control.model.get( 'attachment_id' ) || control.model.get( 'url' ) );
  635. },
  636. /**
  637. * Handle click on link to Media Library to open modal, such as the link that appears when in the missing attachment error notice.
  638. *
  639. * @param {jQuery.Event} event - Event.
  640. * @return {void}
  641. */
  642. handleMediaLibraryLinkClick: function handleMediaLibraryLinkClick( event ) {
  643. var control = this;
  644. event.preventDefault();
  645. control.selectMedia();
  646. },
  647. /**
  648. * Open the media select frame to chose an item.
  649. *
  650. * @return {void}
  651. */
  652. selectMedia: function selectMedia() {
  653. var control = this, selection, mediaFrame, defaultSync, mediaFrameProps, selectionModels = [];
  654. if ( control.isSelected() && 0 !== control.model.get( 'attachment_id' ) ) {
  655. selectionModels.push( control.selectedAttachment );
  656. }
  657. selection = new wp.media.model.Selection( selectionModels, { multiple: false } );
  658. mediaFrameProps = control.mapModelToMediaFrameProps( control.model.toJSON() );
  659. if ( mediaFrameProps.size ) {
  660. control.displaySettings.set( 'size', mediaFrameProps.size );
  661. }
  662. mediaFrame = new component.MediaFrameSelect({
  663. title: control.l10n.add_media,
  664. frame: 'post',
  665. text: control.l10n.add_to_widget,
  666. selection: selection,
  667. mimeType: control.mime_type,
  668. selectedDisplaySettings: control.displaySettings,
  669. showDisplaySettings: control.showDisplaySettings,
  670. metadata: mediaFrameProps,
  671. state: control.isSelected() && 0 === control.model.get( 'attachment_id' ) ? 'embed' : 'insert',
  672. invalidEmbedTypeError: control.l10n.unsupported_file_type
  673. });
  674. wp.media.frame = mediaFrame; // See wp.media().
  675. // Handle selection of a media item.
  676. mediaFrame.on( 'insert', function onInsert() {
  677. var attachment = {}, state = mediaFrame.state();
  678. // Update cached attachment object to avoid having to re-fetch. This also triggers re-rendering of preview.
  679. if ( 'embed' === state.get( 'id' ) ) {
  680. _.extend( attachment, { id: 0 }, state.props.toJSON() );
  681. } else {
  682. _.extend( attachment, state.get( 'selection' ).first().toJSON() );
  683. }
  684. control.selectedAttachment.set( attachment );
  685. control.model.set( 'error', false );
  686. // Update widget instance.
  687. control.model.set( control.getModelPropsFromMediaFrame( mediaFrame ) );
  688. });
  689. // Disable syncing of attachment changes back to server (except for deletions). See <https://core.trac.wordpress.org/ticket/40403>.
  690. defaultSync = wp.media.model.Attachment.prototype.sync;
  691. wp.media.model.Attachment.prototype.sync = function( method ) {
  692. if ( 'delete' === method ) {
  693. return defaultSync.apply( this, arguments );
  694. } else {
  695. return $.Deferred().rejectWith( this ).promise();
  696. }
  697. };
  698. mediaFrame.on( 'close', function onClose() {
  699. wp.media.model.Attachment.prototype.sync = defaultSync;
  700. });
  701. mediaFrame.$el.addClass( 'media-widget' );
  702. mediaFrame.open();
  703. // Clear the selected attachment when it is deleted in the media select frame.
  704. if ( selection ) {
  705. selection.on( 'destroy', function onDestroy( attachment ) {
  706. if ( control.model.get( 'attachment_id' ) === attachment.get( 'id' ) ) {
  707. control.model.set({
  708. attachment_id: 0,
  709. url: ''
  710. });
  711. }
  712. });
  713. }
  714. /*
  715. * Make sure focus is set inside of modal so that hitting Esc will close
  716. * the modal and not inadvertently cause the widget to collapse in the customizer.
  717. */
  718. mediaFrame.$el.find( '.media-frame-menu .media-menu-item.active' ).focus();
  719. },
  720. /**
  721. * Get the instance props from the media selection frame.
  722. *
  723. * @param {wp.media.view.MediaFrame.Select} mediaFrame - Select frame.
  724. * @return {Object} Props.
  725. */
  726. getModelPropsFromMediaFrame: function getModelPropsFromMediaFrame( mediaFrame ) {
  727. var control = this, state, mediaFrameProps, modelProps;
  728. state = mediaFrame.state();
  729. if ( 'insert' === state.get( 'id' ) ) {
  730. mediaFrameProps = state.get( 'selection' ).first().toJSON();
  731. mediaFrameProps.postUrl = mediaFrameProps.link;
  732. if ( control.showDisplaySettings ) {
  733. _.extend(
  734. mediaFrameProps,
  735. mediaFrame.content.get( '.attachments-browser' ).sidebar.get( 'display' ).model.toJSON()
  736. );
  737. }
  738. if ( mediaFrameProps.sizes && mediaFrameProps.size && mediaFrameProps.sizes[ mediaFrameProps.size ] ) {
  739. mediaFrameProps.url = mediaFrameProps.sizes[ mediaFrameProps.size ].url;
  740. }
  741. } else if ( 'embed' === state.get( 'id' ) ) {
  742. mediaFrameProps = _.extend(
  743. state.props.toJSON(),
  744. { attachment_id: 0 }, // Because some media frames use `attachment_id` not `id`.
  745. control.model.getEmbedResetProps()
  746. );
  747. } else {
  748. throw new Error( 'Unexpected state: ' + state.get( 'id' ) );
  749. }
  750. if ( mediaFrameProps.id ) {
  751. mediaFrameProps.attachment_id = mediaFrameProps.id;
  752. }
  753. modelProps = control.mapMediaToModelProps( mediaFrameProps );
  754. // Clear the extension prop so sources will be reset for video and audio media.
  755. _.each( wp.media.view.settings.embedExts, function( ext ) {
  756. if ( ext in control.model.schema && modelProps.url !== modelProps[ ext ] ) {
  757. modelProps[ ext ] = '';
  758. }
  759. });
  760. return modelProps;
  761. },
  762. /**
  763. * Map media frame props to model props.
  764. *
  765. * @param {Object} mediaFrameProps - Media frame props.
  766. * @return {Object} Model props.
  767. */
  768. mapMediaToModelProps: function mapMediaToModelProps( mediaFrameProps ) {
  769. var control = this, mediaFramePropToModelPropMap = {}, modelProps = {}, extension;
  770. _.each( control.model.schema, function( fieldSchema, modelProp ) {
  771. // Ignore widget title attribute.
  772. if ( 'title' === modelProp ) {
  773. return;
  774. }
  775. mediaFramePropToModelPropMap[ fieldSchema.media_prop || modelProp ] = modelProp;
  776. });
  777. _.each( mediaFrameProps, function( value, mediaProp ) {
  778. var propName = mediaFramePropToModelPropMap[ mediaProp ] || mediaProp;
  779. if ( control.model.schema[ propName ] ) {
  780. modelProps[ propName ] = value;
  781. }
  782. });
  783. if ( 'custom' === mediaFrameProps.size ) {
  784. modelProps.width = mediaFrameProps.customWidth;
  785. modelProps.height = mediaFrameProps.customHeight;
  786. }
  787. if ( 'post' === mediaFrameProps.link ) {
  788. modelProps.link_url = mediaFrameProps.postUrl || mediaFrameProps.linkUrl;
  789. } else if ( 'file' === mediaFrameProps.link ) {
  790. modelProps.link_url = mediaFrameProps.url;
  791. }
  792. // Because some media frames use `id` instead of `attachment_id`.
  793. if ( ! mediaFrameProps.attachment_id && mediaFrameProps.id ) {
  794. modelProps.attachment_id = mediaFrameProps.id;
  795. }
  796. if ( mediaFrameProps.url ) {
  797. extension = mediaFrameProps.url.replace( /#.*$/, '' ).replace( /\?.*$/, '' ).split( '.' ).pop().toLowerCase();
  798. if ( extension in control.model.schema ) {
  799. modelProps[ extension ] = mediaFrameProps.url;
  800. }
  801. }
  802. // Always omit the titles derived from mediaFrameProps.
  803. return _.omit( modelProps, 'title' );
  804. },
  805. /**
  806. * Map model props to media frame props.
  807. *
  808. * @param {Object} modelProps - Model props.
  809. * @return {Object} Media frame props.
  810. */
  811. mapModelToMediaFrameProps: function mapModelToMediaFrameProps( modelProps ) {
  812. var control = this, mediaFrameProps = {};
  813. _.each( modelProps, function( value, modelProp ) {
  814. var fieldSchema = control.model.schema[ modelProp ] || {};
  815. mediaFrameProps[ fieldSchema.media_prop || modelProp ] = value;
  816. });
  817. // Some media frames use attachment_id.
  818. mediaFrameProps.attachment_id = mediaFrameProps.id;
  819. if ( 'custom' === mediaFrameProps.size ) {
  820. mediaFrameProps.customWidth = control.model.get( 'width' );
  821. mediaFrameProps.customHeight = control.model.get( 'height' );
  822. }
  823. return mediaFrameProps;
  824. },
  825. /**
  826. * Map model props to previewTemplateProps.
  827. *
  828. * @return {Object} Preview Template Props.
  829. */
  830. mapModelToPreviewTemplateProps: function mapModelToPreviewTemplateProps() {
  831. var control = this, previewTemplateProps = {};
  832. _.each( control.model.schema, function( value, prop ) {
  833. if ( ! value.hasOwnProperty( 'should_preview_update' ) || value.should_preview_update ) {
  834. previewTemplateProps[ prop ] = control.model.get( prop );
  835. }
  836. });
  837. // Templates need to be aware of the error.
  838. previewTemplateProps.error = control.model.get( 'error' );
  839. return previewTemplateProps;
  840. },
  841. /**
  842. * Open the media frame to modify the selected item.
  843. *
  844. * @abstract
  845. * @return {void}
  846. */
  847. editMedia: function editMedia() {
  848. throw new Error( 'editMedia not implemented' );
  849. }
  850. });
  851. /**
  852. * Media widget model.
  853. *
  854. * @class wp.mediaWidgets.MediaWidgetModel
  855. * @augments Backbone.Model
  856. */
  857. component.MediaWidgetModel = Backbone.Model.extend(/** @lends wp.mediaWidgets.MediaWidgetModel.prototype */{
  858. /**
  859. * Id attribute.
  860. *
  861. * @type {string}
  862. */
  863. idAttribute: 'widget_id',
  864. /**
  865. * Instance schema.
  866. *
  867. * This adheres to JSON Schema and subclasses should have their schema
  868. * exported from PHP to JS such as is done in WP_Widget_Media_Image::enqueue_admin_scripts().
  869. *
  870. * @type {Object.<string, Object>}
  871. */
  872. schema: {
  873. title: {
  874. type: 'string',
  875. 'default': ''
  876. },
  877. attachment_id: {
  878. type: 'integer',
  879. 'default': 0
  880. },
  881. url: {
  882. type: 'string',
  883. 'default': ''
  884. }
  885. },
  886. /**
  887. * Get default attribute values.
  888. *
  889. * @return {Object} Mapping of property names to their default values.
  890. */
  891. defaults: function() {
  892. var defaults = {};
  893. _.each( this.schema, function( fieldSchema, field ) {
  894. defaults[ field ] = fieldSchema['default'];
  895. });
  896. return defaults;
  897. },
  898. /**
  899. * Set attribute value(s).
  900. *
  901. * This is a wrapped version of Backbone.Model#set() which allows us to
  902. * cast the attribute values from the hidden inputs' string values into
  903. * the appropriate data types (integers or booleans).
  904. *
  905. * @param {string|Object} key - Attribute name or attribute pairs.
  906. * @param {mixed|Object} [val] - Attribute value or options object.
  907. * @param {Object} [options] - Options when attribute name and value are passed separately.
  908. * @return {wp.mediaWidgets.MediaWidgetModel} This model.
  909. */
  910. set: function set( key, val, options ) {
  911. var model = this, attrs, opts, castedAttrs; // eslint-disable-line consistent-this
  912. if ( null === key ) {
  913. return model;
  914. }
  915. if ( 'object' === typeof key ) {
  916. attrs = key;
  917. opts = val;
  918. } else {
  919. attrs = {};
  920. attrs[ key ] = val;
  921. opts = options;
  922. }
  923. castedAttrs = {};
  924. _.each( attrs, function( value, name ) {
  925. var type;
  926. if ( ! model.schema[ name ] ) {
  927. castedAttrs[ name ] = value;
  928. return;
  929. }
  930. type = model.schema[ name ].type;
  931. if ( 'array' === type ) {
  932. castedAttrs[ name ] = value;
  933. if ( ! _.isArray( castedAttrs[ name ] ) ) {
  934. castedAttrs[ name ] = castedAttrs[ name ].split( /,/ ); // Good enough for parsing an ID list.
  935. }
  936. if ( model.schema[ name ].items && 'integer' === model.schema[ name ].items.type ) {
  937. castedAttrs[ name ] = _.filter(
  938. _.map( castedAttrs[ name ], function( id ) {
  939. return parseInt( id, 10 );
  940. },
  941. function( id ) {
  942. return 'number' === typeof id;
  943. }
  944. ) );
  945. }
  946. } else if ( 'integer' === type ) {
  947. castedAttrs[ name ] = parseInt( value, 10 );
  948. } else if ( 'boolean' === type ) {
  949. castedAttrs[ name ] = ! ( ! value || '0' === value || 'false' === value );
  950. } else {
  951. castedAttrs[ name ] = value;
  952. }
  953. });
  954. return Backbone.Model.prototype.set.call( this, castedAttrs, opts );
  955. },
  956. /**
  957. * Get props which are merged on top of the model when an embed is chosen (as opposed to an attachment).
  958. *
  959. * @return {Object} Reset/override props.
  960. */
  961. getEmbedResetProps: function getEmbedResetProps() {
  962. return {
  963. id: 0
  964. };
  965. }
  966. });
  967. /**
  968. * Collection of all widget model instances.
  969. *
  970. * @memberOf wp.mediaWidgets
  971. *
  972. * @type {Backbone.Collection}
  973. */
  974. component.modelCollection = new ( Backbone.Collection.extend( {
  975. model: component.MediaWidgetModel
  976. }) )();
  977. /**
  978. * Mapping of widget ID to instances of MediaWidgetControl subclasses.
  979. *
  980. * @memberOf wp.mediaWidgets
  981. *
  982. * @type {Object.<string, wp.mediaWidgets.MediaWidgetControl>}
  983. */
  984. component.widgetControls = {};
  985. /**
  986. * Handle widget being added or initialized for the first time at the widget-added event.
  987. *
  988. * @memberOf wp.mediaWidgets
  989. *
  990. * @param {jQuery.Event} event - Event.
  991. * @param {jQuery} widgetContainer - Widget container element.
  992. *
  993. * @return {void}
  994. */
  995. component.handleWidgetAdded = function handleWidgetAdded( event, widgetContainer ) {
  996. var fieldContainer, syncContainer, widgetForm, idBase, ControlConstructor, ModelConstructor, modelAttributes, widgetControl, widgetModel, widgetId, animatedCheckDelay = 50, renderWhenAnimationDone;
  997. widgetForm = widgetContainer.find( '> .widget-inside > .form, > .widget-inside > form' ); // Note: '.form' appears in the customizer, whereas 'form' on the widgets admin screen.
  998. idBase = widgetForm.find( '> .id_base' ).val();
  999. widgetId = widgetForm.find( '> .widget-id' ).val();
  1000. // Prevent initializing already-added widgets.
  1001. if ( component.widgetControls[ widgetId ] ) {
  1002. return;
  1003. }
  1004. ControlConstructor = component.controlConstructors[ idBase ];
  1005. if ( ! ControlConstructor ) {
  1006. return;
  1007. }
  1008. ModelConstructor = component.modelConstructors[ idBase ] || component.MediaWidgetModel;
  1009. /*
  1010. * Create a container element for the widget control (Backbone.View).
  1011. * This is inserted into the DOM immediately before the .widget-content
  1012. * element because the contents of this element are essentially "managed"
  1013. * by PHP, where each widget update cause the entire element to be emptied
  1014. * and replaced with the rendered output of WP_Widget::form() which is
  1015. * sent back in Ajax request made to save/update the widget instance.
  1016. * To prevent a "flash of replaced DOM elements and re-initialized JS
  1017. * components", the JS template is rendered outside of the normal form
  1018. * container.
  1019. */
  1020. fieldContainer = $( '<div></div>' );
  1021. syncContainer = widgetContainer.find( '.widget-content:first' );
  1022. syncContainer.before( fieldContainer );
  1023. /*
  1024. * Sync the widget instance model attributes onto the hidden inputs that widgets currently use to store the state.
  1025. * In the future, when widgets are JS-driven, the underlying widget instance data should be exposed as a model
  1026. * from the start, without having to sync with hidden fields. See <https://core.trac.wordpress.org/ticket/33507>.
  1027. */
  1028. modelAttributes = {};
  1029. syncContainer.find( '.media-widget-instance-property' ).each( function() {
  1030. var input = $( this );
  1031. modelAttributes[ input.data( 'property' ) ] = input.val();
  1032. });
  1033. modelAttributes.widget_id = widgetId;
  1034. widgetModel = new ModelConstructor( modelAttributes );
  1035. widgetControl = new ControlConstructor({
  1036. el: fieldContainer,
  1037. syncContainer: syncContainer,
  1038. model: widgetModel
  1039. });
  1040. /*
  1041. * Render the widget once the widget parent's container finishes animating,
  1042. * as the widget-added event fires with a slideDown of the container.
  1043. * This ensures that the container's dimensions are fixed so that ME.js
  1044. * can initialize with the proper dimensions.
  1045. */
  1046. renderWhenAnimationDone = function() {
  1047. if ( ! widgetContainer.hasClass( 'open' ) ) {
  1048. setTimeout( renderWhenAnimationDone, animatedCheckDelay );
  1049. } else {
  1050. widgetControl.render();
  1051. }
  1052. };
  1053. renderWhenAnimationDone();
  1054. /*
  1055. * Note that the model and control currently won't ever get garbage-collected
  1056. * when a widget gets removed/deleted because there is no widget-removed event.
  1057. */
  1058. component.modelCollection.add( [ widgetModel ] );
  1059. component.widgetControls[ widgetModel.get( 'widget_id' ) ] = widgetControl;
  1060. };
  1061. /**
  1062. * Setup widget in accessibility mode.
  1063. *
  1064. * @memberOf wp.mediaWidgets
  1065. *
  1066. * @return {void}
  1067. */
  1068. component.setupAccessibleMode = function setupAccessibleMode() {
  1069. var widgetForm, widgetId, idBase, widgetControl, ControlConstructor, ModelConstructor, modelAttributes, fieldContainer, syncContainer;
  1070. widgetForm = $( '.editwidget > form' );
  1071. if ( 0 === widgetForm.length ) {
  1072. return;
  1073. }
  1074. idBase = widgetForm.find( '.id_base' ).val();
  1075. ControlConstructor = component.controlConstructors[ idBase ];
  1076. if ( ! ControlConstructor ) {
  1077. return;
  1078. }
  1079. widgetId = widgetForm.find( '> .widget-control-actions > .widget-id' ).val();
  1080. ModelConstructor = component.modelConstructors[ idBase ] || component.MediaWidgetModel;
  1081. fieldContainer = $( '<div></div>' );
  1082. syncContainer = widgetForm.find( '> .widget-inside' );
  1083. syncContainer.before( fieldContainer );
  1084. modelAttributes = {};
  1085. syncContainer.find( '.media-widget-instance-property' ).each( function() {
  1086. var input = $( this );
  1087. modelAttributes[ input.data( 'property' ) ] = input.val();
  1088. });
  1089. modelAttributes.widget_id = widgetId;
  1090. widgetControl = new ControlConstructor({
  1091. el: fieldContainer,
  1092. syncContainer: syncContainer,
  1093. model: new ModelConstructor( modelAttributes )
  1094. });
  1095. component.modelCollection.add( [ widgetControl.model ] );
  1096. component.widgetControls[ widgetControl.model.get( 'widget_id' ) ] = widgetControl;
  1097. widgetControl.render();
  1098. };
  1099. /**
  1100. * Sync widget instance data sanitized from server back onto widget model.
  1101. *
  1102. * This gets called via the 'widget-updated' event when saving a widget from
  1103. * the widgets admin screen and also via the 'widget-synced' event when making
  1104. * a change to a widget in the customizer.
  1105. *
  1106. * @memberOf wp.mediaWidgets
  1107. *
  1108. * @param {jQuery.Event} event - Event.
  1109. * @param {jQuery} widgetContainer - Widget container element.
  1110. *
  1111. * @return {void}
  1112. */
  1113. component.handleWidgetUpdated = function handleWidgetUpdated( event, widgetContainer ) {
  1114. var widgetForm, widgetContent, widgetId, widgetControl, attributes = {};
  1115. widgetForm = widgetContainer.find( '> .widget-inside > .form, > .widget-inside > form' );
  1116. widgetId = widgetForm.find( '> .widget-id' ).val();
  1117. widgetControl = component.widgetControls[ widgetId ];
  1118. if ( ! widgetControl ) {
  1119. return;
  1120. }
  1121. // Make sure the server-sanitized values get synced back into the model.
  1122. widgetContent = widgetForm.find( '> .widget-content' );
  1123. widgetContent.find( '.media-widget-instance-property' ).each( function() {
  1124. var property = $( this ).data( 'property' );
  1125. attributes[ property ] = $( this ).val();
  1126. });
  1127. // Suspend syncing model back to inputs when syncing from inputs to model, preventing infinite loop.
  1128. widgetControl.stopListening( widgetControl.model, 'change', widgetControl.syncModelToInputs );
  1129. widgetControl.model.set( attributes );
  1130. widgetControl.listenTo( widgetControl.model, 'change', widgetControl.syncModelToInputs );
  1131. };
  1132. /**
  1133. * Initialize functionality.
  1134. *
  1135. * This function exists to prevent the JS file from having to boot itself.
  1136. * When WordPress enqueues this script, it should have an inline script
  1137. * attached which calls wp.mediaWidgets.init().
  1138. *
  1139. * @memberOf wp.mediaWidgets
  1140. *
  1141. * @return {void}
  1142. */
  1143. component.init = function init() {
  1144. var $document = $( document );
  1145. $document.on( 'widget-added', component.handleWidgetAdded );
  1146. $document.on( 'widget-synced widget-updated', component.handleWidgetUpdated );
  1147. /*
  1148. * Manually trigger widget-added events for media widgets on the admin
  1149. * screen once they are expanded. The widget-added event is not triggered
  1150. * for each pre-existing widget on the widgets admin screen like it is
  1151. * on the customizer. Likewise, the customizer only triggers widget-added
  1152. * when the widget is expanded to just-in-time construct the widget form
  1153. * when it is actually going to be displayed. So the following implements
  1154. * the same for the widgets admin screen, to invoke the widget-added
  1155. * handler when a pre-existing media widget is expanded.
  1156. */
  1157. $( function initializeExistingWidgetContainers() {
  1158. var widgetContainers;
  1159. if ( 'widgets' !== window.pagenow ) {
  1160. return;
  1161. }
  1162. widgetContainers = $( '.widgets-holder-wrap:not(#available-widgets)' ).find( 'div.widget' );
  1163. widgetContainers.one( 'click.toggle-widget-expanded', function toggleWidgetExpanded() {
  1164. var widgetContainer = $( this );
  1165. component.handleWidgetAdded( new jQuery.Event( 'widget-added' ), widgetContainer );
  1166. });
  1167. // Accessibility mode.
  1168. if ( document.readyState === 'complete' ) {
  1169. // Page is fully loaded.
  1170. component.setupAccessibleMode();
  1171. } else {
  1172. // Page is still loading.
  1173. $( window ).on( 'load', function() {
  1174. component.setupAccessibleMode();
  1175. });
  1176. }
  1177. });
  1178. };
  1179. return component;
  1180. })( jQuery );