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