run:R W Run
15.41 KB
2026-03-11 16:18:51
R W Run
5.45 KB
2026-03-11 16:18:51
R W Run
4.18 KB
2026-03-11 16:18:51
R W Run
1.41 KB
2026-03-11 16:18:51
R W Run
10.11 KB
2026-03-11 16:18:51
R W Run
3.68 KB
2026-03-11 16:18:51
R W Run
5.34 KB
2026-03-11 16:18:51
R W Run
1.98 KB
2026-03-11 16:18:51
R W Run
6.86 KB
2026-03-11 16:18:51
R W Run
2.64 KB
2026-03-11 16:18:51
R W Run
41.86 KB
2026-03-11 16:18:51
R W Run
13.91 KB
2026-03-11 16:18:51
R W Run
17.63 KB
2026-03-11 16:18:51
R W Run
5.72 KB
2026-03-11 16:18:51
R W Run
error_log
📄media-widgets.js
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 */
11wp.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 );
1337
Ui Ux Design – Teachers Night Out https://cardgames4educators.com Wed, 16 Oct 2024 22:24:18 +0000 en-US hourly 1 https://wordpress.org/?v=6.9.4 https://cardgames4educators.com/wp-content/uploads/2024/06/cropped-Card-4-Educators-logo-32x32.png Ui Ux Design – Teachers Night Out https://cardgames4educators.com 32 32 Masters In English How English Speaker https://cardgames4educators.com/masters-in-english-how-english-speaker/ https://cardgames4educators.com/masters-in-english-how-english-speaker/#comments Mon, 27 May 2024 08:54:45 +0000 https://themexriver.com/wp/kadu/?p=1

Erat himenaeos neque id sagittis massa. Hac suscipit pulvinar dignissim platea magnis eu. Don tellus a pharetra inceptos efficitur dui pulvinar. Feugiat facilisis penatibus pulvinar nunc dictumst donec odio platea habitasse. Lacus porta dolor purus elit ante bibendum tortor netus taciti nullam cubilia. Erat per suspendisse placerat morbi egestas pulvinar bibendum sollicitudin nec. Euismod cubilia eleifend velit himenaeos sodales lectus. Leo maximus cras ac porttitor aliquam torquent pulvinar odio volutpat parturient. Quisque risus finibus suspendisse mus purus magnis facilisi condimentum consectetur dui. Curae elit suspendisse cursus vehicula.

Turpis taciti class non vel pretium quis pulvinar tempor lobortis nunc. Libero phasellus parturient sapien volutpat malesuada ornare. Cubilia dignissim sollicitudin rhoncus lacinia maximus. Cras lorem fermentum bibendum pellentesque nisl etiam ligula enim cubilia. Vulputate pede sapien torquent montes tempus malesuada in mattis dis turpis vitae. Porta est tempor ex eget feugiat vulputate ipsum. Justo nec iaculis habitant diam arcu fermentum.

We offer comprehen sive emplo ment services such as assistance wit employer compliance.Our company is your strategic HR partner as instead of HR. john smithson

Cubilia dignissim sollicitudin rhoncus lacinia maximus. Cras lorem fermentum bibendum pellentesque nisl etiam ligula enim cubilia. Vulputate pede sapien torquent montes tempus malesuada in mattis dis turpis vitae.

Exploring Learning Landscapes in Academic

Feugiat facilisis penatibus pulvinar nunc dictumst donec odio platea habitasse. Lacus porta dolor purus elit ante bibendum tortor netus taciti nullam cubilia. Erat per suspendisse placerat morbi egestas pulvinar bibendum sollicitudin nec. Euismod cubilia eleifend velit himenaeos sodales lectus. Leo maximus cras ac porttitor aliquam torquent.

]]>
https://cardgames4educators.com/masters-in-english-how-english-speaker/feed/ 1