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