at path:ROOT / wp-admin / js / theme.js
run:R W Run
DIR
2026-03-11 16:18:51
R W Run
2.86 KB
2026-03-11 16:18:51
R W Run
758 By
2026-03-11 16:18:51
R W Run
6.24 KB
2026-03-11 16:18:51
R W Run
2.95 KB
2026-03-11 16:18:51
R W Run
5.66 KB
2026-03-11 16:18:51
R W Run
2.04 KB
2026-03-11 16:18:51
R W Run
11.32 KB
2026-03-11 16:18:51
R W Run
3.01 KB
2026-03-11 16:18:51
R W Run
9.54 KB
2026-03-11 16:18:51
R W Run
3.4 KB
2026-03-11 16:18:51
R W Run
2.85 KB
2026-03-11 16:18:51
R W Run
1.28 KB
2026-03-11 16:18:51
R W Run
61.15 KB
2026-03-11 16:18:51
R W Run
23.12 KB
2026-03-11 16:18:51
R W Run
3.35 KB
2026-03-11 16:18:51
R W Run
1.18 KB
2026-03-11 16:18:51
R W Run
1.98 KB
2026-03-11 16:18:51
R W Run
288.41 KB
2026-03-11 16:18:51
R W Run
109.69 KB
2026-03-11 16:18:51
R W Run
111.46 KB
2026-03-11 16:18:51
R W Run
47.14 KB
2026-03-11 16:18:51
R W Run
70.05 KB
2026-03-11 16:18:51
R W Run
27.41 KB
2026-03-11 16:18:51
R W Run
27.02 KB
2026-03-11 16:18:51
R W Run
8.65 KB
2026-03-11 16:18:51
R W Run
37.12 KB
2026-03-11 16:18:51
R W Run
15.13 KB
2026-03-11 16:18:51
R W Run
41.61 KB
2026-03-11 16:18:51
R W Run
13.14 KB
2026-03-11 16:18:51
R W Run
44 KB
2026-03-11 16:18:51
R W Run
12.78 KB
2026-03-11 16:18:51
R W Run
7.67 KB
2026-03-11 16:18:51
R W Run
5.41 KB
2026-03-11 16:18:51
R W Run
3.65 KB
2026-03-11 16:18:51
R W Run
39.98 KB
2026-03-11 16:18:51
R W Run
15.15 KB
2026-03-11 16:18:51
R W Run
20.17 KB
2026-03-11 16:18:51
R W Run
9.41 KB
2026-03-11 16:18:51
R W Run
7.61 KB
2026-03-11 16:18:51
R W Run
2.93 KB
2026-03-11 16:18:51
R W Run
23.09 KB
2026-03-11 16:18:51
R W Run
890 By
2026-03-11 16:18:51
R W Run
423 By
2026-03-11 16:18:51
R W Run
3.89 KB
2026-03-11 16:18:51
R W Run
1.7 KB
2026-03-11 16:18:51
R W Run
1.27 KB
2026-03-11 16:18:51
R W Run
611 By
2026-03-11 16:18:51
R W Run
3.38 KB
2026-03-11 16:18:51
R W Run
1.13 KB
2026-03-11 16:18:51
R W Run
6.61 KB
2026-03-11 16:18:51
R W Run
2.38 KB
2026-03-11 16:18:51
R W Run
61.15 KB
2026-03-11 16:18:51
R W Run
30.06 KB
2026-03-11 16:18:51
R W Run
4.14 KB
2026-03-11 16:18:51
R W Run
1.1 KB
2026-03-11 16:18:51
R W Run
1.31 KB
2026-03-11 16:18:51
R W Run
847 By
2026-03-11 16:18:51
R W Run
6.92 KB
2026-03-11 16:18:51
R W Run
2.35 KB
2026-03-11 16:18:51
R W Run
38.68 KB
2026-03-11 16:18:51
R W Run
18.4 KB
2026-03-11 16:18:51
R W Run
18.49 KB
2026-03-11 16:18:51
R W Run
6.6 KB
2026-03-11 16:18:51
R W Run
10.67 KB
2026-03-11 16:18:51
R W Run
5.03 KB
2026-03-11 16:18:51
R W Run
33.92 KB
2026-03-11 16:18:51
R W Run
17.97 KB
2026-03-11 16:18:51
R W Run
876 By
2026-03-11 16:18:51
R W Run
620 By
2026-03-11 16:18:51
R W Run
13.15 KB
2026-03-11 16:18:51
R W Run
6.13 KB
2026-03-11 16:18:51
R W Run
6.1 KB
2026-03-11 16:18:51
R W Run
2.2 KB
2026-03-11 16:18:51
R W Run
3.2 KB
2026-03-11 16:18:51
R W Run
1.53 KB
2026-03-11 16:18:51
R W Run
10.88 KB
2026-03-11 16:18:51
R W Run
3 KB
2026-03-11 16:18:51
R W Run
5.64 KB
2026-03-11 16:18:51
R W Run
2.22 KB
2026-03-11 16:18:51
R W Run
5.96 KB
2026-03-11 16:18:51
R W Run
2.41 KB
2026-03-11 16:18:51
R W Run
24.77 KB
2026-03-11 16:18:51
R W Run
11.43 KB
2026-03-11 16:18:51
R W Run
54.94 KB
2026-03-11 16:18:51
R W Run
26.51 KB
2026-03-11 16:18:51
R W Run
109.37 KB
2026-03-11 16:18:51
R W Run
47.31 KB
2026-03-11 16:18:51
R W Run
17.91 KB
2026-03-11 16:18:51
R W Run
7.81 KB
2026-03-11 16:18:51
R W Run
2.25 KB
2026-03-11 16:18:51
R W Run
676 By
2026-03-11 16:18:51
R W Run
22.56 KB
2026-03-11 16:18:51
R W Run
12.31 KB
2026-03-11 16:18:51
R W Run
7.52 KB
2026-03-11 16:18:51
R W Run
1.49 KB
2026-03-11 16:18:51
R W Run
740 By
2026-03-11 16:18:51
R W Run
458 By
2026-03-11 16:18:51
R W Run
error_log
📄theme.js
1/**
2 * @output wp-admin/js/theme.js
3 */
4
5/* global _wpThemeSettings, confirm, tb_position */
6window.wp = window.wp || {};
7
8( function($) {
9
10// Set up our namespace...
11var themes, l10n;
12themes = wp.themes = wp.themes || {};
13
14// Store the theme data and settings for organized and quick access.
15// themes.data.settings, themes.data.themes, themes.data.l10n.
16themes.data = _wpThemeSettings;
17l10n = themes.data.l10n;
18
19// Shortcut for isInstall check.
20themes.isInstall = !! themes.data.settings.isInstall;
21
22// Setup app structure.
23_.extend( themes, { model: {}, view: {}, routes: {}, router: {}, template: wp.template });
24
25themes.Model = Backbone.Model.extend({
26 // Adds attributes to the default data coming through the .org themes api.
27 // Map `id` to `slug` for shared code.
28 initialize: function() {
29 var description;
30
31 if ( this.get( 'slug' ) ) {
32 // If the theme is already installed, set an attribute.
33 if ( _.indexOf( themes.data.installedThemes, this.get( 'slug' ) ) !== -1 ) {
34 this.set({ installed: true });
35 }
36
37 // If the theme is active, set an attribute.
38 if ( themes.data.activeTheme === this.get( 'slug' ) ) {
39 this.set({ active: true });
40 }
41 }
42
43 // Set the attributes.
44 this.set({
45 // `slug` is for installation, `id` is for existing.
46 id: this.get( 'slug' ) || this.get( 'id' )
47 });
48
49 // Map `section.description` to `description`
50 // as the API sometimes returns it differently.
51 if ( this.has( 'sections' ) ) {
52 description = this.get( 'sections' ).description;
53 this.set({ description: description });
54 }
55 }
56});
57
58// Main view controller for themes.php.
59// Unifies and renders all available views.
60themes.view.Appearance = wp.Backbone.View.extend({
61
62 el: '#wpbody-content .wrap .theme-browser',
63
64 window: $( window ),
65 // Pagination instance.
66 page: 0,
67
68 // Sets up a throttler for binding to 'scroll'.
69 initialize: function( options ) {
70 // Scroller checks how far the scroll position is.
71 _.bindAll( this, 'scroller' );
72
73 this.SearchView = options.SearchView ? options.SearchView : themes.view.Search;
74 // Bind to the scroll event and throttle
75 // the results from this.scroller.
76 this.window.on( 'scroll', _.throttle( this.scroller, 300 ) );
77 },
78
79 // Main render control.
80 render: function() {
81 // Setup the main theme view
82 // with the current theme collection.
83 this.view = new themes.view.Themes({
84 collection: this.collection,
85 parent: this
86 });
87
88 // Render search form.
89 this.search();
90
91 this.$el.removeClass( 'search-loading' );
92
93 // Render and append.
94 this.view.render();
95 this.$el.empty().append( this.view.el ).addClass( 'rendered' );
96 },
97
98 // Defines search element container.
99 searchContainer: $( '.search-form' ),
100
101 // Search input and view
102 // for current theme collection.
103 search: function() {
104 var view,
105 self = this;
106
107 // Don't render the search if there is only one theme.
108 if ( themes.data.themes.length === 1 ) {
109 return;
110 }
111
112 view = new this.SearchView({
113 collection: self.collection,
114 parent: this
115 });
116 self.SearchView = view;
117
118 // Render and append after screen title.
119 view.render();
120 this.searchContainer
121 .find( '.search-box' )
122 .append( $.parseHTML( '<label for="wp-filter-search-input">' + l10n.search + '</label>' ) )
123 .append( view.el );
124
125 this.searchContainer.on( 'submit', function( event ) {
126 event.preventDefault();
127 });
128 },
129
130 // Checks when the user gets close to the bottom
131 // of the mage and triggers a theme:scroll event.
132 scroller: function() {
133 var self = this,
134 bottom, threshold;
135
136 bottom = this.window.scrollTop() + self.window.height();
137 threshold = self.$el.offset().top + self.$el.outerHeight( false ) - self.window.height();
138 threshold = Math.round( threshold * 0.9 );
139
140 if ( bottom > threshold ) {
141 this.trigger( 'theme:scroll' );
142 }
143 }
144});
145
146// Set up the Collection for our theme data.
147// @has 'id' 'name' 'screenshot' 'author' 'authorURI' 'version' 'active' ...
148themes.Collection = Backbone.Collection.extend({
149
150 model: themes.Model,
151
152 // Search terms.
153 terms: '',
154
155 // Controls searching on the current theme collection
156 // and triggers an update event.
157 doSearch: function( value ) {
158
159 // Don't do anything if we've already done this search.
160 // Useful because the Search handler fires multiple times per keystroke.
161 if ( this.terms === value ) {
162 return;
163 }
164
165 // Updates terms with the value passed.
166 this.terms = value;
167
168 // If we have terms, run a search...
169 if ( this.terms.length > 0 ) {
170 this.search( this.terms );
171 }
172
173 // If search is blank, show all themes.
174 // Useful for resetting the views when you clean the input.
175 if ( this.terms === '' ) {
176 this.reset( themes.data.themes );
177 $( 'body' ).removeClass( 'no-results' );
178 }
179
180 // Trigger a 'themes:update' event.
181 this.trigger( 'themes:update' );
182 },
183
184 /**
185 * Performs a search within the collection.
186 *
187 * @uses RegExp
188 */
189 search: function( term ) {
190 var match, results, haystack, name, description, author;
191
192 // Start with a full collection.
193 this.reset( themes.data.themes, { silent: true } );
194
195 // Trim the term.
196 term = term.trim();
197
198 // Escape the term string for RegExp meta characters.
199 term = term.replace( /[-\/\\^$*+?.()|[\]{}]/g, '\\$&' );
200
201 // Consider spaces as word delimiters and match the whole string
202 // so matching terms can be combined.
203 term = term.replace( / /g, ')(?=.*' );
204 match = new RegExp( '^(?=.*' + term + ').+', 'i' );
205
206 // Find results.
207 // _.filter() and .test().
208 results = this.filter( function( data ) {
209 name = data.get( 'name' ).replace( /(<([^>]+)>)/ig, '' );
210 description = data.get( 'description' ).replace( /(<([^>]+)>)/ig, '' );
211 author = data.get( 'author' ).replace( /(<([^>]+)>)/ig, '' );
212
213 haystack = _.union( [ name, data.get( 'id' ), description, author, data.get( 'tags' ) ] );
214
215 if ( match.test( data.get( 'author' ) ) && term.length > 2 ) {
216 data.set( 'displayAuthor', true );
217 }
218
219 return match.test( haystack );
220 });
221
222 if ( results.length === 0 ) {
223 this.trigger( 'query:empty' );
224 } else {
225 $( 'body' ).removeClass( 'no-results' );
226 }
227
228 this.reset( results );
229 },
230
231 // Paginates the collection with a helper method
232 // that slices the collection.
233 paginate: function( instance ) {
234 var collection = this;
235 instance = instance || 0;
236
237 // Themes per instance are set at 20.
238 collection = _( collection.rest( 20 * instance ) );
239 collection = _( collection.first( 20 ) );
240
241 return collection;
242 },
243
244 count: false,
245
246 /*
247 * Handles requests for more themes and caches results.
248 *
249 *
250 * When we are missing a cache object we fire an apiCall()
251 * which triggers events of `query:success` or `query:fail`.
252 */
253 query: function( request ) {
254 /**
255 * @static
256 * @type Array
257 */
258 var queries = this.queries,
259 self = this,
260 query, isPaginated, count;
261
262 // Store current query request args
263 // for later use with the event `theme:end`.
264 this.currentQuery.request = request;
265
266 // Search the query cache for matches.
267 query = _.find( queries, function( query ) {
268 return _.isEqual( query.request, request );
269 });
270
271 // If the request matches the stored currentQuery.request
272 // it means we have a paginated request.
273 isPaginated = _.has( request, 'page' );
274
275 // Reset the internal api page counter for non-paginated queries.
276 if ( ! isPaginated ) {
277 this.currentQuery.page = 1;
278 }
279
280 // Otherwise, send a new API call and add it to the cache.
281 if ( ! query && ! isPaginated ) {
282 query = this.apiCall( request ).done( function( data ) {
283
284 // Update the collection with the queried data.
285 if ( data.themes ) {
286 self.reset( data.themes );
287 count = data.info.results;
288 // Store the results and the query request.
289 queries.push( { themes: data.themes, request: request, total: count } );
290 }
291
292 // Trigger a collection refresh event
293 // and a `query:success` event with a `count` argument.
294 self.trigger( 'themes:update' );
295 self.trigger( 'query:success', count );
296
297 if ( data.themes && data.themes.length === 0 ) {
298 self.trigger( 'query:empty' );
299 }
300
301 }).fail( function() {
302 self.trigger( 'query:fail' );
303 });
304 } else {
305 // If it's a paginated request we need to fetch more themes...
306 if ( isPaginated ) {
307 return this.apiCall( request, isPaginated ).done( function( data ) {
308 // Add the new themes to the current collection.
309 // @todo Update counter.
310 self.add( data.themes );
311 self.trigger( 'query:success' );
312
313 // We are done loading themes for now.
314 self.loadingThemes = false;
315
316 }).fail( function() {
317 self.trigger( 'query:fail' );
318 });
319 }
320
321 if ( query.themes.length === 0 ) {
322 self.trigger( 'query:empty' );
323 } else {
324 $( 'body' ).removeClass( 'no-results' );
325 }
326
327 // Only trigger an update event since we already have the themes
328 // on our cached object.
329 if ( _.isNumber( query.total ) ) {
330 this.count = query.total;
331 }
332
333 this.reset( query.themes );
334 if ( ! query.total ) {
335 this.count = this.length;
336 }
337
338 this.trigger( 'themes:update' );
339 this.trigger( 'query:success', this.count );
340 }
341 },
342
343 // Local cache array for API queries.
344 queries: [],
345
346 // Keep track of current query so we can handle pagination.
347 currentQuery: {
348 page: 1,
349 request: {}
350 },
351
352 // Send request to api.wordpress.org/themes.
353 apiCall: function( request, paginated ) {
354 return wp.ajax.send( 'query-themes', {
355 data: {
356 // Request data.
357 request: _.extend({
358 per_page: 100
359 }, request)
360 },
361
362 beforeSend: function() {
363 if ( ! paginated ) {
364 // Spin it.
365 $( 'body' ).addClass( 'loading-content' ).removeClass( 'no-results' );
366 }
367 }
368 });
369 },
370
371 // Static status controller for when we are loading themes.
372 loadingThemes: false
373});
374
375// This is the view that controls each theme item
376// that will be displayed on the screen.
377themes.view.Theme = wp.Backbone.View.extend({
378
379 // Wrap theme data on a div.theme element.
380 className: 'theme',
381
382 // Reflects which theme view we have.
383 // 'grid' (default) or 'detail'.
384 state: 'grid',
385
386 // The HTML template for each element to be rendered.
387 html: themes.template( 'theme' ),
388
389 events: {
390 'click': themes.isInstall ? 'preview': 'expand',
391 'keydown': themes.isInstall ? 'preview': 'expand',
392 'touchend': themes.isInstall ? 'preview': 'expand',
393 'keyup': 'addFocus',
394 'touchmove': 'preventExpand',
395 'click .theme-install': 'installTheme',
396 'click .update-message': 'updateTheme'
397 },
398
399 touchDrag: false,
400
401 initialize: function() {
402 this.model.on( 'change', this.render, this );
403 },
404
405 render: function() {
406 var data = this.model.toJSON();
407
408 // Render themes using the html template.
409 this.$el.html( this.html( data ) ).attr( 'data-slug', data.id );
410
411 // Renders active theme styles.
412 this.activeTheme();
413
414 if ( this.model.get( 'displayAuthor' ) ) {
415 this.$el.addClass( 'display-author' );
416 }
417 },
418
419 // Adds a class to the currently active theme
420 // and to the overlay in detailed view mode.
421 activeTheme: function() {
422 if ( this.model.get( 'active' ) ) {
423 this.$el.addClass( 'active' );
424 }
425 },
426
427 // Add class of focus to the theme we are focused on.
428 addFocus: function() {
429 var $themeToFocus = ( $( ':focus' ).hasClass( 'theme' ) ) ? $( ':focus' ) : $(':focus').parents('.theme');
430
431 $('.theme.focus').removeClass('focus');
432 $themeToFocus.addClass('focus');
433 },
434
435 // Single theme overlay screen.
436 // It's shown when clicking a theme.
437 expand: function( event ) {
438 var self = this;
439
440 event = event || window.event;
441
442 // 'Enter' and 'Space' keys expand the details view when a theme is :focused.
443 if ( event.type === 'keydown' && ( event.which !== 13 && event.which !== 32 ) ) {
444 return;
445 }
446
447 // Bail if the user scrolled on a touch device.
448 if ( this.touchDrag === true ) {
449 return this.touchDrag = false;
450 }
451
452 // Prevent the modal from showing when the user clicks
453 // one of the direct action buttons.
454 if ( $( event.target ).is( '.theme-actions a' ) ) {
455 return;
456 }
457
458 // Prevent the modal from showing when the user clicks one of the direct action buttons.
459 if ( $( event.target ).is( '.theme-actions a, .update-message, .button-link, .notice-dismiss' ) ) {
460 return;
461 }
462
463 // Set focused theme to current element.
464 themes.focusedTheme = this.$el;
465
466 this.trigger( 'theme:expand', self.model.cid );
467 },
468
469 preventExpand: function() {
470 this.touchDrag = true;
471 },
472
473 preview: function( event ) {
474 var self = this,
475 current, preview;
476
477 event = event || window.event;
478
479 // Bail if the user scrolled on a touch device.
480 if ( this.touchDrag === true ) {
481 return this.touchDrag = false;
482 }
483
484 // Allow direct link path to installing a theme.
485 if ( $( event.target ).not( '.install-theme-preview' ).parents( '.theme-actions' ).length ) {
486 return;
487 }
488
489 // 'Enter' and 'Space' keys expand the details view when a theme is :focused.
490 if ( event.type === 'keydown' && ( event.which !== 13 && event.which !== 32 ) ) {
491 return;
492 }
493
494 // Pressing Enter while focused on the buttons shouldn't open the preview.
495 if ( event.type === 'keydown' && event.which !== 13 && $( ':focus' ).hasClass( 'button' ) ) {
496 return;
497 }
498
499 event.preventDefault();
500
501 event = event || window.event;
502
503 // Set focus to current theme.
504 themes.focusedTheme = this.$el;
505
506 // Construct a new Preview view.
507 themes.preview = preview = new themes.view.Preview({
508 model: this.model
509 });
510
511 // Render the view and append it.
512 preview.render();
513 this.setNavButtonsState();
514
515 // Hide previous/next navigation if there is only one theme.
516 if ( this.model.collection.length === 1 ) {
517 preview.$el.addClass( 'no-navigation' );
518 } else {
519 preview.$el.removeClass( 'no-navigation' );
520 }
521
522 // Append preview.
523 $( 'div.wrap' ).append( preview.el );
524
525 // Listen to our preview object
526 // for `theme:next` and `theme:previous` events.
527 this.listenTo( preview, 'theme:next', function() {
528
529 // Keep local track of current theme model.
530 current = self.model;
531
532 // If we have ventured away from current model update the current model position.
533 if ( ! _.isUndefined( self.current ) ) {
534 current = self.current;
535 }
536
537 // Get next theme model.
538 self.current = self.model.collection.at( self.model.collection.indexOf( current ) + 1 );
539
540 // If we have no more themes, bail.
541 if ( _.isUndefined( self.current ) ) {
542 self.options.parent.parent.trigger( 'theme:end' );
543 return self.current = current;
544 }
545
546 preview.model = self.current;
547
548 // Render and append.
549 preview.render();
550 this.setNavButtonsState();
551 $( '.next-theme' ).trigger( 'focus' );
552 })
553 .listenTo( preview, 'theme:previous', function() {
554
555 // Keep track of current theme model.
556 current = self.model;
557
558 // Bail early if we are at the beginning of the collection.
559 if ( self.model.collection.indexOf( self.current ) === 0 ) {
560 return;
561 }
562
563 // If we have ventured away from current model update the current model position.
564 if ( ! _.isUndefined( self.current ) ) {
565 current = self.current;
566 }
567
568 // Get previous theme model.
569 self.current = self.model.collection.at( self.model.collection.indexOf( current ) - 1 );
570
571 // If we have no more themes, bail.
572 if ( _.isUndefined( self.current ) ) {
573 return;
574 }
575
576 preview.model = self.current;
577
578 // Render and append.
579 preview.render();
580 this.setNavButtonsState();
581 $( '.previous-theme' ).trigger( 'focus' );
582 });
583
584 this.listenTo( preview, 'preview:close', function() {
585 self.current = self.model;
586 });
587
588 },
589
590 // Handles .disabled classes for previous/next buttons in theme installer preview.
591 setNavButtonsState: function() {
592 var $themeInstaller = $( '.theme-install-overlay' ),
593 current = _.isUndefined( this.current ) ? this.model : this.current,
594 previousThemeButton = $themeInstaller.find( '.previous-theme' ),
595 nextThemeButton = $themeInstaller.find( '.next-theme' );
596
597 // Disable previous at the zero position.
598 if ( 0 === this.model.collection.indexOf( current ) ) {
599 previousThemeButton
600 .addClass( 'disabled' )
601 .prop( 'disabled', true );
602
603 nextThemeButton.trigger( 'focus' );
604 }
605
606 // Disable next if the next model is undefined.
607 if ( _.isUndefined( this.model.collection.at( this.model.collection.indexOf( current ) + 1 ) ) ) {
608 nextThemeButton
609 .addClass( 'disabled' )
610 .prop( 'disabled', true );
611
612 previousThemeButton.trigger( 'focus' );
613 }
614 },
615
616 installTheme: function( event ) {
617 var _this = this;
618
619 event.preventDefault();
620
621 wp.updates.maybeRequestFilesystemCredentials( event );
622
623 $( document ).on( 'wp-theme-install-success', function( event, response ) {
624 if ( _this.model.get( 'id' ) === response.slug ) {
625 _this.model.set( { 'installed': true } );
626 }
627 if ( response.blockTheme ) {
628 _this.model.set( { 'block_theme': true } );
629 }
630 } );
631
632 wp.updates.installTheme( {
633 slug: $( event.target ).data( 'slug' )
634 } );
635 },
636
637 updateTheme: function( event ) {
638 var _this = this;
639
640 if ( ! this.model.get( 'hasPackage' ) ) {
641 return;
642 }
643
644 event.preventDefault();
645
646 wp.updates.maybeRequestFilesystemCredentials( event );
647
648 $( document ).on( 'wp-theme-update-success', function( event, response ) {
649 _this.model.off( 'change', _this.render, _this );
650 if ( _this.model.get( 'id' ) === response.slug ) {
651 _this.model.set( {
652 hasUpdate: false,
653 version: response.newVersion
654 } );
655 }
656 _this.model.on( 'change', _this.render, _this );
657 } );
658
659 wp.updates.updateTheme( {
660 slug: $( event.target ).parents( 'div.theme' ).first().data( 'slug' )
661 } );
662 }
663});
664
665// Theme Details view.
666// Sets up a modal overlay with the expanded theme data.
667themes.view.Details = wp.Backbone.View.extend({
668
669 // Wrap theme data on a div.theme element.
670 className: 'theme-overlay',
671
672 events: {
673 'click': 'collapse',
674 'click .delete-theme': 'deleteTheme',
675 'click .left': 'previousTheme',
676 'click .right': 'nextTheme',
677 'click #update-theme': 'updateTheme',
678 'click .toggle-auto-update': 'autoupdateState'
679 },
680
681 // The HTML template for the theme overlay.
682 html: themes.template( 'theme-single' ),
683
684 render: function() {
685 var data = this.model.toJSON();
686 this.$el.html( this.html( data ) );
687 // Renders active theme styles.
688 this.activeTheme();
689 // Set up navigation events.
690 this.navigation();
691 // Checks screenshot size.
692 this.screenshotCheck( this.$el );
693 // Contain "tabbing" inside the overlay.
694 this.containFocus( this.$el );
695 },
696
697 // Adds a class to the currently active theme
698 // and to the overlay in detailed view mode.
699 activeTheme: function() {
700 // Check the model has the active property.
701 this.$el.toggleClass( 'active', this.model.get( 'active' ) );
702 },
703
704 // Set initial focus and constrain tabbing within the theme browser modal.
705 containFocus: function( $el ) {
706
707 // Set initial focus on the primary action control.
708 _.delay( function() {
709 $( '.theme-overlay' ).trigger( 'focus' );
710 }, 100 );
711
712 // Constrain tabbing within the modal.
713 $el.on( 'keydown.wp-themes', function( event ) {
714 var $firstFocusable = $el.find( '.theme-header button:not(.disabled)' ).first(),
715 $lastFocusable = $el.find( '.theme-actions a:visible' ).last();
716
717 // Check for the Tab key.
718 if ( 9 === event.which ) {
719 if ( $firstFocusable[0] === event.target && event.shiftKey ) {
720 $lastFocusable.trigger( 'focus' );
721 event.preventDefault();
722 } else if ( $lastFocusable[0] === event.target && ! event.shiftKey ) {
723 $firstFocusable.trigger( 'focus' );
724 event.preventDefault();
725 }
726 }
727 });
728 },
729
730 // Single theme overlay screen.
731 // It's shown when clicking a theme.
732 collapse: function( event ) {
733 var self = this,
734 scroll;
735
736 event = event || window.event;
737
738 // Prevent collapsing detailed view when there is only one theme available.
739 if ( themes.data.themes.length === 1 ) {
740 return;
741 }
742
743 // Detect if the click is inside the overlay and don't close it
744 // unless the target was the div.back button.
745 if ( $( event.target ).is( '.theme-backdrop' ) || $( event.target ).is( '.close' ) || event.keyCode === 27 ) {
746
747 // Add a temporary closing class while overlay fades out.
748 $( 'body' ).addClass( 'closing-overlay' );
749
750 // With a quick fade out animation.
751 this.$el.fadeOut( 130, function() {
752 // Clicking outside the modal box closes the overlay.
753 $( 'body' ).removeClass( 'closing-overlay' );
754 // Handle event cleanup.
755 self.closeOverlay();
756
757 // Get scroll position to avoid jumping to the top.
758 scroll = document.body.scrollTop;
759
760 // Clean the URL structure.
761 themes.router.navigate( themes.router.baseUrl( '' ) );
762
763 // Restore scroll position.
764 document.body.scrollTop = scroll;
765
766 // Return focus to the theme div.
767 if ( themes.focusedTheme ) {
768 themes.focusedTheme.find('.more-details').trigger( 'focus' );
769 }
770 });
771 }
772 },
773
774 // Handles .disabled classes for next/previous buttons.
775 navigation: function() {
776
777 // Disable Left/Right when at the start or end of the collection.
778 if ( this.model.cid === this.model.collection.at(0).cid ) {
779 this.$el.find( '.left' )
780 .addClass( 'disabled' )
781 .prop( 'disabled', true );
782 }
783 if ( this.model.cid === this.model.collection.at( this.model.collection.length - 1 ).cid ) {
784 this.$el.find( '.right' )
785 .addClass( 'disabled' )
786 .prop( 'disabled', true );
787 }
788 },
789
790 // Performs the actions to effectively close
791 // the theme details overlay.
792 closeOverlay: function() {
793 $( 'body' ).removeClass( 'modal-open' );
794 this.remove();
795 this.unbind();
796 this.trigger( 'theme:collapse' );
797 },
798
799 // Set state of the auto-update settings link after it has been changed and saved.
800 autoupdateState: function() {
801 var callback,
802 _this = this;
803
804 // Support concurrent clicks in different Theme Details overlays.
805 callback = function( event, data ) {
806 var autoupdate;
807 if ( _this.model.get( 'id' ) === data.asset ) {
808 autoupdate = _this.model.get( 'autoupdate' );
809 autoupdate.enabled = 'enable' === data.state;
810 _this.model.set( { autoupdate: autoupdate } );
811 $( document ).off( 'wp-auto-update-setting-changed', callback );
812 }
813 };
814
815 // Triggered in updates.js
816 $( document ).on( 'wp-auto-update-setting-changed', callback );
817 },
818
819 updateTheme: function( event ) {
820 var _this = this;
821 event.preventDefault();
822
823 wp.updates.maybeRequestFilesystemCredentials( event );
824
825 $( document ).on( 'wp-theme-update-success', function( event, response ) {
826 if ( _this.model.get( 'id' ) === response.slug ) {
827 _this.model.set( {
828 hasUpdate: false,
829 version: response.newVersion
830 } );
831 }
832 _this.render();
833 } );
834
835 wp.updates.updateTheme( {
836 slug: $( event.target ).data( 'slug' )
837 } );
838 },
839
840 deleteTheme: function( event ) {
841 var _this = this,
842 _collection = _this.model.collection,
843 _themes = themes;
844 event.preventDefault();
845
846 // Confirmation dialog for deleting a theme.
847 if ( ! window.confirm( wp.themes.data.settings.confirmDelete ) ) {
848 return;
849 }
850
851 wp.updates.maybeRequestFilesystemCredentials( event );
852
853 $( document ).one( 'wp-theme-delete-success', function( event, response ) {
854 _this.$el.find( '.close' ).trigger( 'click' );
855 $( '[data-slug="' + response.slug + '"]' ).css( { backgroundColor:'#faafaa' } ).fadeOut( 350, function() {
856 $( this ).remove();
857 _themes.data.themes = _.without( _themes.data.themes, _.findWhere( _themes.data.themes, { id: response.slug } ) );
858
859 $( '.wp-filter-search' ).val( '' );
860 _collection.doSearch( '' );
861 _collection.remove( _this.model );
862 _collection.trigger( 'themes:update' );
863 } );
864 } );
865
866 wp.updates.deleteTheme( {
867 slug: this.model.get( 'id' )
868 } );
869 },
870
871 nextTheme: function() {
872 var self = this;
873 self.trigger( 'theme:next', self.model.cid );
874 return false;
875 },
876
877 previousTheme: function() {
878 var self = this;
879 self.trigger( 'theme:previous', self.model.cid );
880 return false;
881 },
882
883 // Checks if the theme screenshot is the old 300px width version
884 // and adds a corresponding class if it's true.
885 screenshotCheck: function( el ) {
886 var screenshot, image;
887
888 screenshot = el.find( '.screenshot img' );
889 image = new Image();
890 image.src = screenshot.attr( 'src' );
891
892 // Width check.
893 if ( image.width && image.width <= 300 ) {
894 el.addClass( 'small-screenshot' );
895 }
896 }
897});
898
899// Theme Preview view.
900// Sets up a modal overlay with the expanded theme data.
901themes.view.Preview = themes.view.Details.extend({
902
903 className: 'wp-full-overlay expanded',
904 el: '.theme-install-overlay',
905
906 events: {
907 'click .close-full-overlay': 'close',
908 'click .collapse-sidebar': 'collapse',
909 'click .devices button': 'previewDevice',
910 'click .previous-theme': 'previousTheme',
911 'click .next-theme': 'nextTheme',
912 'keyup': 'keyEvent',
913 'click .theme-install': 'installTheme'
914 },
915
916 // The HTML template for the theme preview.
917 html: themes.template( 'theme-preview' ),
918
919 render: function() {
920 var self = this,
921 currentPreviewDevice,
922 data = this.model.toJSON(),
923 $body = $( document.body );
924
925 $body.attr( 'aria-busy', 'true' );
926
927 this.$el.removeClass( 'iframe-ready' ).html( this.html( data ) );
928
929 currentPreviewDevice = this.$el.data( 'current-preview-device' );
930 if ( currentPreviewDevice ) {
931 self.togglePreviewDeviceButtons( currentPreviewDevice );
932 }
933
934 themes.router.navigate( themes.router.baseUrl( themes.router.themePath + this.model.get( 'id' ) ), { replace: false } );
935
936 this.$el.fadeIn( 200, function() {
937 $body.addClass( 'theme-installer-active full-overlay-active' );
938 });
939
940 this.$el.find( 'iframe' ).one( 'load', function() {
941 self.iframeLoaded();
942 });
943 },
944
945 iframeLoaded: function() {
946 this.$el.addClass( 'iframe-ready' );
947 $( document.body ).attr( 'aria-busy', 'false' );
948 },
949
950 close: function() {
951 this.$el.fadeOut( 200, function() {
952 $( 'body' ).removeClass( 'theme-installer-active full-overlay-active' );
953
954 // Return focus to the theme div.
955 if ( themes.focusedTheme ) {
956 themes.focusedTheme.find('.more-details').trigger( 'focus' );
957 }
958 }).removeClass( 'iframe-ready' );
959
960 // Restore the previous browse tab if available.
961 if ( themes.router.selectedTab ) {
962 themes.router.navigate( themes.router.baseUrl( '?browse=' + themes.router.selectedTab ) );
963 themes.router.selectedTab = false;
964 } else {
965 themes.router.navigate( themes.router.baseUrl( '' ) );
966 }
967 this.trigger( 'preview:close' );
968 this.undelegateEvents();
969 this.unbind();
970 return false;
971 },
972
973 collapse: function( event ) {
974 var $button = $( event.currentTarget );
975 if ( 'true' === $button.attr( 'aria-expanded' ) ) {
976 $button.attr({ 'aria-expanded': 'false', 'aria-label': l10n.expandSidebar });
977 } else {
978 $button.attr({ 'aria-expanded': 'true', 'aria-label': l10n.collapseSidebar });
979 }
980
981 this.$el.toggleClass( 'collapsed' ).toggleClass( 'expanded' );
982 return false;
983 },
984
985 previewDevice: function( event ) {
986 var device = $( event.currentTarget ).data( 'device' );
987
988 this.$el
989 .removeClass( 'preview-desktop preview-tablet preview-mobile' )
990 .addClass( 'preview-' + device )
991 .data( 'current-preview-device', device );
992
993 this.togglePreviewDeviceButtons( device );
994 },
995
996 togglePreviewDeviceButtons: function( newDevice ) {
997 var $devices = $( '.wp-full-overlay-footer .devices' );
998
999 $devices.find( 'button' )
1000 .removeClass( 'active' )
1001 .attr( 'aria-pressed', false );
1002
1003 $devices.find( 'button.preview-' + newDevice )
1004 .addClass( 'active' )
1005 .attr( 'aria-pressed', true );
1006 },
1007
1008 keyEvent: function( event ) {
1009 // The escape key closes the preview.
1010 if ( event.keyCode === 27 ) {
1011 this.undelegateEvents();
1012 this.close();
1013 }
1014
1015 // Return if Ctrl + Shift or Shift key pressed
1016 if ( event.shiftKey || ( event.ctrlKey && event.shiftKey ) ) {
1017 return;
1018 }
1019
1020 // The right arrow key, next theme.
1021 if ( event.keyCode === 39 ) {
1022 _.once( this.nextTheme() );
1023 }
1024
1025 // The left arrow key, previous theme.
1026 if ( event.keyCode === 37 ) {
1027 this.previousTheme();
1028 }
1029 },
1030
1031 installTheme: function( event ) {
1032 var _this = this,
1033 $target = $( event.target );
1034 event.preventDefault();
1035
1036 if ( $target.hasClass( 'disabled' ) ) {
1037 return;
1038 }
1039
1040 wp.updates.maybeRequestFilesystemCredentials( event );
1041
1042 $( document ).on( 'wp-theme-install-success', function() {
1043 _this.model.set( { 'installed': true } );
1044 } );
1045
1046 wp.updates.installTheme( {
1047 slug: $target.data( 'slug' )
1048 } );
1049 }
1050});
1051
1052// Controls the rendering of div.themes,
1053// a wrapper that will hold all the theme elements.
1054themes.view.Themes = wp.Backbone.View.extend({
1055
1056 className: 'themes wp-clearfix',
1057 $overlay: $( 'div.theme-overlay' ),
1058
1059 // Number to keep track of scroll position
1060 // while in theme-overlay mode.
1061 index: 0,
1062
1063 // The theme count element.
1064 count: $( '.wrap .theme-count' ),
1065
1066 // The live themes count.
1067 liveThemeCount: 0,
1068
1069 initialize: function( options ) {
1070 var self = this;
1071
1072 // Set up parent.
1073 this.parent = options.parent;
1074
1075 // Set current view to [grid].
1076 this.setView( 'grid' );
1077
1078 // Move the active theme to the beginning of the collection.
1079 self.currentTheme();
1080
1081 // When the collection is updated by user input...
1082 this.listenTo( self.collection, 'themes:update', function() {
1083 self.parent.page = 0;
1084 self.currentTheme();
1085 self.render( this );
1086 } );
1087
1088 // Update theme count to full result set when available.
1089 this.listenTo( self.collection, 'query:success', function( count ) {
1090 if ( _.isNumber( count ) ) {
1091 self.count.text( count );
1092 self.announceSearchResults( count );
1093 } else {
1094 self.count.text( self.collection.length );
1095 self.announceSearchResults( self.collection.length );
1096 }
1097 });
1098
1099 this.listenTo( self.collection, 'query:empty', function() {
1100 $( 'body' ).addClass( 'no-results' );
1101 });
1102
1103 this.listenTo( this.parent, 'theme:scroll', function() {
1104 self.renderThemes( self.parent.page );
1105 });
1106
1107 this.listenTo( this.parent, 'theme:close', function() {
1108 if ( self.overlay ) {
1109 self.overlay.closeOverlay();
1110 }
1111 } );
1112
1113 // Bind keyboard events.
1114 $( 'body' ).on( 'keyup', function( event ) {
1115 if ( ! self.overlay ) {
1116 return;
1117 }
1118
1119 // Bail if the filesystem credentials dialog is shown.
1120 if ( $( '#request-filesystem-credentials-dialog' ).is( ':visible' ) ) {
1121 return;
1122 }
1123
1124 // Return if Ctrl + Shift or Shift key pressed
1125 if ( event.shiftKey || ( event.ctrlKey && event.shiftKey ) ) {
1126 return;
1127 }
1128
1129 // Pressing the right arrow key fires a theme:next event.
1130 if ( event.keyCode === 39 ) {
1131 self.overlay.nextTheme();
1132 }
1133
1134 // Pressing the left arrow key fires a theme:previous event.
1135 if ( event.keyCode === 37 ) {
1136 self.overlay.previousTheme();
1137 }
1138
1139 // Pressing the escape key fires a theme:collapse event.
1140 if ( event.keyCode === 27 ) {
1141 self.overlay.collapse( event );
1142 }
1143 });
1144 },
1145
1146 // Manages rendering of theme pages
1147 // and keeping theme count in sync.
1148 render: function() {
1149 // Clear the DOM, please.
1150 this.$el.empty();
1151
1152 // If the user doesn't have switch capabilities or there is only one theme
1153 // in the collection, render the detailed view of the active theme.
1154 if ( themes.data.themes.length === 1 ) {
1155
1156 // Constructs the view.
1157 this.singleTheme = new themes.view.Details({
1158 model: this.collection.models[0]
1159 });
1160
1161 // Render and apply a 'single-theme' class to our container.
1162 this.singleTheme.render();
1163 this.$el.addClass( 'single-theme' );
1164 this.$el.append( this.singleTheme.el );
1165 }
1166
1167 // Generate the themes using page instance
1168 // while checking the collection has items.
1169 if ( this.options.collection.size() > 0 ) {
1170 this.renderThemes( this.parent.page );
1171 }
1172
1173 // Display a live theme count for the collection.
1174 this.liveThemeCount = this.collection.count ? this.collection.count : this.collection.length;
1175 this.count.text( this.liveThemeCount );
1176
1177 /*
1178 * In the theme installer the themes count is already announced
1179 * because `announceSearchResults` is called on `query:success`.
1180 */
1181 if ( ! themes.isInstall ) {
1182 this.announceSearchResults( this.liveThemeCount );
1183 }
1184 },
1185
1186 // Iterates through each instance of the collection
1187 // and renders each theme module.
1188 renderThemes: function( page ) {
1189 var self = this;
1190
1191 self.instance = self.collection.paginate( page );
1192
1193 // If we have no more themes, bail.
1194 if ( self.instance.size() === 0 ) {
1195 // Fire a no-more-themes event.
1196 this.parent.trigger( 'theme:end' );
1197 return;
1198 }
1199
1200 // Make sure the add-new stays at the end.
1201 if ( ! themes.isInstall && page >= 1 ) {
1202 $( '.add-new-theme' ).remove();
1203 }
1204
1205 // Loop through the themes and setup each theme view.
1206 self.instance.each( function( theme ) {
1207 self.theme = new themes.view.Theme({
1208 model: theme,
1209 parent: self
1210 });
1211
1212 // Render the views...
1213 self.theme.render();
1214 // ...and append them to div.themes.
1215 self.$el.append( self.theme.el );
1216
1217 // Binds to theme:expand to show the modal box
1218 // with the theme details.
1219 self.listenTo( self.theme, 'theme:expand', self.expand, self );
1220 });
1221
1222 // 'Add new theme' element shown at the end of the grid.
1223 if ( ! themes.isInstall && themes.data.settings.canInstall ) {
1224 this.$el.append( '<div class="theme add-new-theme"><a href="' + themes.data.settings.installURI + '"><div class="theme-screenshot"><span aria-hidden="true"></span></div><h2 class="theme-name">' + l10n.addNew + '</h2></a></div>' );
1225 }
1226
1227 this.parent.page++;
1228 },
1229
1230 // Grabs current theme and puts it at the beginning of the collection.
1231 currentTheme: function() {
1232 var self = this,
1233 current;
1234
1235 current = self.collection.findWhere({ active: true });
1236
1237 // Move the active theme to the beginning of the collection.
1238 if ( current ) {
1239 self.collection.remove( current );
1240 self.collection.add( current, { at:0 } );
1241 }
1242 },
1243
1244 // Sets current view.
1245 setView: function( view ) {
1246 return view;
1247 },
1248
1249 // Renders the overlay with the ThemeDetails view.
1250 // Uses the current model data.
1251 expand: function( id ) {
1252 var self = this, $card, $modal;
1253
1254 // Set the current theme model.
1255 this.model = self.collection.get( id );
1256
1257 // Trigger a route update for the current model.
1258 themes.router.navigate( themes.router.baseUrl( themes.router.themePath + this.model.id ) );
1259
1260 // Sets this.view to 'detail'.
1261 this.setView( 'detail' );
1262 $( 'body' ).addClass( 'modal-open' );
1263
1264 // Set up the theme details view.
1265 this.overlay = new themes.view.Details({
1266 model: self.model
1267 });
1268
1269 this.overlay.render();
1270
1271 if ( this.model.get( 'hasUpdate' ) ) {
1272 $card = $( '[data-slug="' + this.model.id + '"]' );
1273 $modal = $( this.overlay.el );
1274
1275 if ( $card.find( '.updating-message' ).length ) {
1276 $modal.find( '.notice-warning h3' ).remove();
1277 $modal.find( '.notice-warning' )
1278 .removeClass( 'notice-large' )
1279 .addClass( 'updating-message' )
1280 .find( 'p' ).text( wp.updates.l10n.updating );
1281 } else if ( $card.find( '.notice-error' ).length ) {
1282 $modal.find( '.notice-warning' ).remove();
1283 }
1284 }
1285
1286 this.$overlay.html( this.overlay.el );
1287
1288 // Bind to theme:next and theme:previous triggered by the arrow keys.
1289 // Keep track of the current model so we can infer an index position.
1290 this.listenTo( this.overlay, 'theme:next', function() {
1291 // Renders the next theme on the overlay.
1292 self.next( [ self.model.cid ] );
1293
1294 })
1295 .listenTo( this.overlay, 'theme:previous', function() {
1296 // Renders the previous theme on the overlay.
1297 self.previous( [ self.model.cid ] );
1298 });
1299 },
1300
1301 /*
1302 * This method renders the next theme on the overlay modal
1303 * based on the current position in the collection.
1304 *
1305 * @params [model cid]
1306 */
1307 next: function( args ) {
1308 var self = this,
1309 model, nextModel;
1310
1311 // Get the current theme.
1312 model = self.collection.get( args[0] );
1313 // Find the next model within the collection.
1314 nextModel = self.collection.at( self.collection.indexOf( model ) + 1 );
1315
1316 // Confidence check which also serves as a boundary test.
1317 if ( nextModel !== undefined ) {
1318
1319 // We have a new theme...
1320 // Close the overlay.
1321 this.overlay.closeOverlay();
1322
1323 // Trigger a route update for the current model.
1324 self.theme.trigger( 'theme:expand', nextModel.cid );
1325
1326 }
1327 },
1328
1329 /*
1330 * This method renders the previous theme on the overlay modal
1331 * based on the current position in the collection.
1332 *
1333 * @params [model cid]
1334 */
1335 previous: function( args ) {
1336 var self = this,
1337 model, previousModel;
1338
1339 // Get the current theme.
1340 model = self.collection.get( args[0] );
1341 // Find the previous model within the collection.
1342 previousModel = self.collection.at( self.collection.indexOf( model ) - 1 );
1343
1344 if ( previousModel !== undefined ) {
1345
1346 // We have a new theme...
1347 // Close the overlay.
1348 this.overlay.closeOverlay();
1349
1350 // Trigger a route update for the current model.
1351 self.theme.trigger( 'theme:expand', previousModel.cid );
1352
1353 }
1354 },
1355
1356 // Dispatch audible search results feedback message.
1357 announceSearchResults: function( count ) {
1358 if ( 0 === count ) {
1359 wp.a11y.speak( l10n.noThemesFound );
1360 } else {
1361 wp.a11y.speak( l10n.themesFound.replace( '%d', count ) );
1362 }
1363 }
1364});
1365
1366// Search input view controller.
1367themes.view.Search = wp.Backbone.View.extend({
1368
1369 tagName: 'input',
1370 className: 'wp-filter-search',
1371 id: 'wp-filter-search-input',
1372 searching: false,
1373
1374 attributes: {
1375 type: 'search',
1376 'aria-describedby': 'live-search-desc'
1377 },
1378
1379 events: {
1380 'input': 'search',
1381 'keyup': 'search',
1382 'blur': 'pushState'
1383 },
1384
1385 initialize: function( options ) {
1386
1387 this.parent = options.parent;
1388
1389 this.listenTo( this.parent, 'theme:close', function() {
1390 this.searching = false;
1391 } );
1392
1393 },
1394
1395 search: function( event ) {
1396 // Clear on escape.
1397 if ( event.type === 'keyup' && event.which === 27 ) {
1398 event.target.value = '';
1399 }
1400
1401 // Since doSearch is debounced, it will only run when user input comes to a rest.
1402 this.doSearch( event );
1403 },
1404
1405 // Runs a search on the theme collection.
1406 doSearch: function( event ) {
1407 var options = {};
1408
1409 this.collection.doSearch( event.target.value.replace( /\+/g, ' ' ) );
1410
1411 // if search is initiated and key is not return.
1412 if ( this.searching && event.which !== 13 ) {
1413 options.replace = true;
1414 } else {
1415 this.searching = true;
1416 }
1417
1418 // Update the URL hash.
1419 if ( event.target.value ) {
1420 themes.router.navigate( themes.router.baseUrl( themes.router.searchPath + event.target.value ), options );
1421 } else {
1422 themes.router.navigate( themes.router.baseUrl( '' ) );
1423 }
1424 },
1425
1426 pushState: function( event ) {
1427 var url = themes.router.baseUrl( '' );
1428
1429 if ( event.target.value ) {
1430 url = themes.router.baseUrl( themes.router.searchPath + encodeURIComponent( event.target.value ) );
1431 }
1432
1433 this.searching = false;
1434 themes.router.navigate( url );
1435
1436 }
1437});
1438
1439/**
1440 * Navigate router.
1441 *
1442 * @since 4.9.0
1443 *
1444 * @param {string} url - URL to navigate to.
1445 * @param {Object} state - State.
1446 * @return {void}
1447 */
1448function navigateRouter( url, state ) {
1449 var router = this;
1450 if ( Backbone.history._hasPushState ) {
1451 Backbone.Router.prototype.navigate.call( router, url, state );
1452 }
1453}
1454
1455// Sets up the routes events for relevant url queries.
1456// Listens to [theme] and [search] params.
1457themes.Router = Backbone.Router.extend({
1458
1459 routes: {
1460 'themes.php?theme=:slug': 'theme',
1461 'themes.php?search=:query': 'search',
1462 'themes.php?s=:query': 'search',
1463 'themes.php': 'themes',
1464 '': 'themes'
1465 },
1466
1467 baseUrl: function( url ) {
1468 return 'themes.php' + url;
1469 },
1470
1471 themePath: '?theme=',
1472 searchPath: '?search=',
1473
1474 search: function( query ) {
1475 $( '.wp-filter-search' ).val( query.replace( /\+/g, ' ' ) );
1476 },
1477
1478 themes: function() {
1479 $( '.wp-filter-search' ).val( '' );
1480 },
1481
1482 navigate: navigateRouter
1483
1484});
1485
1486// Execute and setup the application.
1487themes.Run = {
1488 init: function() {
1489 // Initializes the blog's theme library view.
1490 // Create a new collection with data.
1491 this.themes = new themes.Collection( themes.data.themes );
1492
1493 // Set up the view.
1494 this.view = new themes.view.Appearance({
1495 collection: this.themes
1496 });
1497
1498 this.render();
1499
1500 // Start debouncing user searches after Backbone.history.start().
1501 this.view.SearchView.doSearch = _.debounce( this.view.SearchView.doSearch, 500 );
1502 },
1503
1504 render: function() {
1505
1506 // Render results.
1507 this.view.render();
1508 this.routes();
1509
1510 if ( Backbone.History.started ) {
1511 Backbone.history.stop();
1512 }
1513 Backbone.history.start({
1514 root: themes.data.settings.adminUrl,
1515 pushState: true,
1516 hashChange: false
1517 });
1518 },
1519
1520 routes: function() {
1521 var self = this;
1522 // Bind to our global thx object
1523 // so that the object is available to sub-views.
1524 themes.router = new themes.Router();
1525
1526 // Handles theme details route event.
1527 themes.router.on( 'route:theme', function( slug ) {
1528 self.view.view.expand( slug );
1529 });
1530
1531 themes.router.on( 'route:themes', function() {
1532 self.themes.doSearch( '' );
1533 self.view.trigger( 'theme:close' );
1534 });
1535
1536 // Handles search route event.
1537 themes.router.on( 'route:search', function() {
1538 $( '.wp-filter-search' ).trigger( 'keyup' );
1539 });
1540
1541 this.extraRoutes();
1542 },
1543
1544 extraRoutes: function() {
1545 return false;
1546 }
1547};
1548
1549// Extend the main Search view.
1550themes.view.InstallerSearch = themes.view.Search.extend({
1551
1552 events: {
1553 'input': 'search',
1554 'keyup': 'search'
1555 },
1556
1557 terms: '',
1558
1559 // Handles Ajax request for searching through themes in public repo.
1560 search: function( event ) {
1561
1562 // Tabbing or reverse tabbing into the search input shouldn't trigger a search.
1563 if ( event.type === 'keyup' && ( event.which === 9 || event.which === 16 ) ) {
1564 return;
1565 }
1566
1567 this.collection = this.options.parent.view.collection;
1568
1569 // Clear on escape.
1570 if ( event.type === 'keyup' && event.which === 27 ) {
1571 event.target.value = '';
1572 }
1573
1574 this.doSearch( event.target.value );
1575 },
1576
1577 doSearch: function( value ) {
1578 var request = {};
1579
1580 // Don't do anything if the search terms haven't changed.
1581 if ( this.terms === value ) {
1582 return;
1583 }
1584
1585 // Updates terms with the value passed.
1586 this.terms = value;
1587
1588 request.search = value;
1589
1590 /*
1591 * Intercept an [author] search.
1592 *
1593 * If input value starts with `author:` send a request
1594 * for `author` instead of a regular `search`.
1595 */
1596 if ( value.substring( 0, 7 ) === 'author:' ) {
1597 request.search = '';
1598 request.author = value.slice( 7 );
1599 }
1600
1601 /*
1602 * Intercept a [tag] search.
1603 *
1604 * If input value starts with `tag:` send a request
1605 * for `tag` instead of a regular `search`.
1606 */
1607 if ( value.substring( 0, 4 ) === 'tag:' ) {
1608 request.search = '';
1609 request.tag = [ value.slice( 4 ) ];
1610 }
1611
1612 $( '.filter-links li > a.current' )
1613 .removeClass( 'current' )
1614 .removeAttr( 'aria-current' );
1615
1616 $( 'body' ).removeClass( 'show-filters filters-applied show-favorites-form' );
1617 $( '.drawer-toggle' ).attr( 'aria-expanded', 'false' );
1618
1619 // Get the themes by sending Ajax POST request to api.wordpress.org/themes
1620 // or searching the local cache.
1621 this.collection.query( request );
1622
1623 // Set route.
1624 themes.router.navigate( themes.router.baseUrl( themes.router.searchPath + encodeURIComponent( value ) ), { replace: true } );
1625 }
1626});
1627
1628themes.view.Installer = themes.view.Appearance.extend({
1629
1630 el: '#wpbody-content .wrap',
1631
1632 // Register events for sorting and filters in theme-navigation.
1633 events: {
1634 'click .filter-links li > a': 'onSort',
1635 'click .theme-filter': 'onFilter',
1636 'click .drawer-toggle': 'moreFilters',
1637 'click .filter-drawer .apply-filters': 'applyFilters',
1638 'click .filter-group [type="checkbox"]': 'addFilter',
1639 'click .filter-drawer .clear-filters': 'clearFilters',
1640 'click .edit-filters': 'backToFilters',
1641 'click .favorites-form-submit' : 'saveUsername',
1642 'keyup #wporg-username-input': 'saveUsername'
1643 },
1644
1645 // Initial render method.
1646 render: function() {
1647 var self = this;
1648
1649 this.search();
1650 this.uploader();
1651
1652 this.collection = new themes.Collection();
1653
1654 // Bump `collection.currentQuery.page` and request more themes if we hit the end of the page.
1655 this.listenTo( this, 'theme:end', function() {
1656
1657 // Make sure we are not already loading.
1658 if ( self.collection.loadingThemes ) {
1659 return;
1660 }
1661
1662 // Set loadingThemes to true and bump page instance of currentQuery.
1663 self.collection.loadingThemes = true;
1664 self.collection.currentQuery.page++;
1665
1666 // Use currentQuery.page to build the themes request.
1667 _.extend( self.collection.currentQuery.request, { page: self.collection.currentQuery.page } );
1668 self.collection.query( self.collection.currentQuery.request );
1669 });
1670
1671 this.listenTo( this.collection, 'query:success', function() {
1672 $( 'body' ).removeClass( 'loading-content' );
1673 $( '.theme-browser' ).find( 'div.error' ).remove();
1674 });
1675
1676 this.listenTo( this.collection, 'query:fail', function() {
1677 $( 'body' ).removeClass( 'loading-content' );
1678 $( '.theme-browser' ).find( 'div.error' ).remove();
1679 $( '.theme-browser' ).find( 'div.themes' ).before( '<div class="notice notice-error"><p>' + l10n.error + '</p><p><button class="button try-again">' + l10n.tryAgain + '</button></p></div>' );
1680 $( '.theme-browser .error .try-again' ).on( 'click', function( e ) {
1681 e.preventDefault();
1682 $( 'input.wp-filter-search' ).trigger( 'input' );
1683 } );
1684 });
1685
1686 if ( this.view ) {
1687 this.view.remove();
1688 }
1689
1690 // Sets up the view and passes the section argument.
1691 this.view = new themes.view.Themes({
1692 collection: this.collection,
1693 parent: this
1694 });
1695
1696 // Reset pagination every time the install view handler is run.
1697 this.page = 0;
1698
1699 // Render and append.
1700 this.$el.find( '.themes' ).remove();
1701 this.view.render();
1702 this.$el.find( '.theme-browser' ).append( this.view.el ).addClass( 'rendered' );
1703 },
1704
1705 // Handles all the rendering of the public theme directory.
1706 browse: function( section ) {
1707 // Create a new collection with the proper theme data
1708 // for each section.
1709 if ( 'block-themes' === section ) {
1710 // Get the themes by sending Ajax POST request to api.wordpress.org/themes
1711 // or searching the local cache.
1712 this.collection.query( { tag: 'full-site-editing' } );
1713 } else {
1714 this.collection.query( { browse: section } );
1715 }
1716 },
1717
1718 // Sorting navigation.
1719 onSort: function( event ) {
1720 var $el = $( event.target ),
1721 sort = $el.data( 'sort' );
1722
1723 event.preventDefault();
1724
1725 $( 'body' ).removeClass( 'filters-applied show-filters' );
1726 $( '.drawer-toggle' ).attr( 'aria-expanded', 'false' );
1727
1728 // Bail if this is already active.
1729 if ( $el.hasClass( this.activeClass ) ) {
1730 return;
1731 }
1732
1733 this.sort( sort );
1734
1735 // Trigger a router.navigate update.
1736 themes.router.navigate( themes.router.baseUrl( themes.router.browsePath + sort ) );
1737 },
1738
1739 sort: function( sort ) {
1740 this.clearSearch();
1741
1742 // Track sorting so we can restore the correct tab when closing preview.
1743 themes.router.selectedTab = sort;
1744
1745 $( '.filter-links li > a, .theme-filter' )
1746 .removeClass( this.activeClass )
1747 .removeAttr( 'aria-current' );
1748
1749 $( '[data-sort="' + sort + '"]' )
1750 .addClass( this.activeClass )
1751 .attr( 'aria-current', 'page' );
1752
1753 if ( 'favorites' === sort ) {
1754 $( 'body' ).addClass( 'show-favorites-form' );
1755 } else {
1756 $( 'body' ).removeClass( 'show-favorites-form' );
1757 }
1758
1759 this.browse( sort );
1760 },
1761
1762 // Filters and Tags.
1763 onFilter: function( event ) {
1764 var request,
1765 $el = $( event.target ),
1766 filter = $el.data( 'filter' );
1767
1768 // Bail if this is already active.
1769 if ( $el.hasClass( this.activeClass ) ) {
1770 return;
1771 }
1772
1773 $( '.filter-links li > a, .theme-section' )
1774 .removeClass( this.activeClass )
1775 .removeAttr( 'aria-current' );
1776 $el
1777 .addClass( this.activeClass )
1778 .attr( 'aria-current', 'page' );
1779
1780 if ( ! filter ) {
1781 return;
1782 }
1783
1784 // Construct the filter request
1785 // using the default values.
1786 filter = _.union( [ filter, this.filtersChecked() ] );
1787 request = { tag: [ filter ] };
1788
1789 // Get the themes by sending Ajax POST request to api.wordpress.org/themes
1790 // or searching the local cache.
1791 this.collection.query( request );
1792 },
1793
1794 // Clicking on a checkbox to add another filter to the request.
1795 addFilter: function() {
1796 this.filtersChecked();
1797 },
1798
1799 // Applying filters triggers a tag request.
1800 applyFilters: function( event ) {
1801 var name,
1802 tags = this.filtersChecked(),
1803 request = { tag: tags },
1804 filteringBy = $( '.filtered-by .tags' );
1805
1806 if ( event ) {
1807 event.preventDefault();
1808 }
1809
1810 if ( ! tags ) {
1811 wp.a11y.speak( l10n.selectFeatureFilter );
1812 return;
1813 }
1814
1815 $( 'body' ).addClass( 'filters-applied' );
1816 $( '.filter-links li > a.current' )
1817 .removeClass( 'current' )
1818 .removeAttr( 'aria-current' );
1819
1820 filteringBy.empty();
1821
1822 _.each( tags, function( tag ) {
1823 name = $( 'label[for="filter-id-' + tag + '"]' ).text();
1824 filteringBy.append( '<span class="tag">' + name + '</span>' );
1825 });
1826
1827 // Get the themes by sending Ajax POST request to api.wordpress.org/themes
1828 // or searching the local cache.
1829 this.collection.query( request );
1830 },
1831
1832 // Save the user's WordPress.org username and get his favorite themes.
1833 saveUsername: function ( event ) {
1834 var username = $( '#wporg-username-input' ).val(),
1835 nonce = $( '#wporg-username-nonce' ).val(),
1836 request = { browse: 'favorites', user: username },
1837 that = this;
1838
1839 if ( event ) {
1840 event.preventDefault();
1841 }
1842
1843 // Save username on enter.
1844 if ( event.type === 'keyup' && event.which !== 13 ) {
1845 return;
1846 }
1847
1848 return wp.ajax.send( 'save-wporg-username', {
1849 data: {
1850 _wpnonce: nonce,
1851 username: username
1852 },
1853 success: function () {
1854 // Get the themes by sending Ajax POST request to api.wordpress.org/themes
1855 // or searching the local cache.
1856 that.collection.query( request );
1857 }
1858 } );
1859 },
1860
1861 /**
1862 * Get the checked filters.
1863 *
1864 * @return {Array} of tags or false
1865 */
1866 filtersChecked: function() {
1867 var items = $( '.filter-group' ).find( ':checkbox' ),
1868 tags = [];
1869
1870 _.each( items.filter( ':checked' ), function( item ) {
1871 tags.push( $( item ).prop( 'value' ) );
1872 });
1873
1874 // When no filters are checked, restore initial state and return.
1875 if ( tags.length === 0 ) {
1876 $( '.filter-drawer .apply-filters' ).find( 'span' ).text( '' );
1877 $( '.filter-drawer .clear-filters' ).hide();
1878 $( 'body' ).removeClass( 'filters-applied' );
1879 return false;
1880 }
1881
1882 $( '.filter-drawer .apply-filters' ).find( 'span' ).text( tags.length );
1883 $( '.filter-drawer .clear-filters' ).css( 'display', 'inline-block' );
1884
1885 return tags;
1886 },
1887
1888 activeClass: 'current',
1889
1890 /**
1891 * When users press the "Upload Theme" button, show the upload form in place.
1892 */
1893 uploader: function() {
1894 var uploadViewToggle = $( '.upload-view-toggle' ),
1895 $body = $( document.body );
1896
1897 uploadViewToggle.on( 'click', function() {
1898 // Toggle the upload view.
1899 $body.toggleClass( 'show-upload-view' );
1900 // Toggle the `aria-expanded` button attribute.
1901 uploadViewToggle.attr( 'aria-expanded', $body.hasClass( 'show-upload-view' ) );
1902 });
1903 },
1904
1905 // Toggle the full filters navigation.
1906 moreFilters: function( event ) {
1907 var $body = $( 'body' ),
1908 $toggleButton = $( '.drawer-toggle' );
1909
1910 event.preventDefault();
1911
1912 if ( $body.hasClass( 'filters-applied' ) ) {
1913 return this.backToFilters();
1914 }
1915
1916 this.clearSearch();
1917
1918 themes.router.navigate( themes.router.baseUrl( '' ) );
1919 // Toggle the feature filters view.
1920 $body.toggleClass( 'show-filters' );
1921 // Toggle the `aria-expanded` button attribute.
1922 $toggleButton.attr( 'aria-expanded', $body.hasClass( 'show-filters' ) );
1923 },
1924
1925 /**
1926 * Clears all the checked filters.
1927 *
1928 * @uses filtersChecked()
1929 */
1930 clearFilters: function( event ) {
1931 var items = $( '.filter-group' ).find( ':checkbox' ),
1932 self = this;
1933
1934 event.preventDefault();
1935
1936 _.each( items.filter( ':checked' ), function( item ) {
1937 $( item ).prop( 'checked', false );
1938 return self.filtersChecked();
1939 });
1940 },
1941
1942 backToFilters: function( event ) {
1943 if ( event ) {
1944 event.preventDefault();
1945 }
1946
1947 $( 'body' ).removeClass( 'filters-applied' );
1948 },
1949
1950 clearSearch: function() {
1951 $( '#wp-filter-search-input').val( '' );
1952 }
1953});
1954
1955themes.InstallerRouter = Backbone.Router.extend({
1956 routes: {
1957 'theme-install.php?theme=:slug': 'preview',
1958 'theme-install.php?browse=:sort': 'sort',
1959 'theme-install.php?search=:query': 'search',
1960 'theme-install.php': 'sort'
1961 },
1962
1963 baseUrl: function( url ) {
1964 return 'theme-install.php' + url;
1965 },
1966
1967 themePath: '?theme=',
1968 browsePath: '?browse=',
1969 searchPath: '?search=',
1970
1971 search: function( query ) {
1972 $( '.wp-filter-search' ).val( query.replace( /\+/g, ' ' ) );
1973 },
1974
1975 navigate: navigateRouter
1976});
1977
1978
1979themes.RunInstaller = {
1980
1981 init: function() {
1982 // Set up the view.
1983 // Passes the default 'section' as an option.
1984 this.view = new themes.view.Installer({
1985 section: 'popular',
1986 SearchView: themes.view.InstallerSearch
1987 });
1988
1989 // Render results.
1990 this.render();
1991
1992 // Start debouncing user searches after Backbone.history.start().
1993 this.view.SearchView.doSearch = _.debounce( this.view.SearchView.doSearch, 500 );
1994 },
1995
1996 render: function() {
1997
1998 // Render results.
1999 this.view.render();
2000 this.routes();
2001
2002 if ( Backbone.History.started ) {
2003 Backbone.history.stop();
2004 }
2005 Backbone.history.start({
2006 root: themes.data.settings.adminUrl,
2007 pushState: true,
2008 hashChange: false
2009 });
2010 },
2011
2012 routes: function() {
2013 var self = this,
2014 request = {};
2015
2016 // Bind to our global `wp.themes` object
2017 // so that the router is available to sub-views.
2018 themes.router = new themes.InstallerRouter();
2019
2020 // Handles `theme` route event.
2021 // Queries the API for the passed theme slug.
2022 themes.router.on( 'route:preview', function( slug ) {
2023
2024 // Remove existing handlers.
2025 if ( themes.preview ) {
2026 themes.preview.undelegateEvents();
2027 themes.preview.unbind();
2028 }
2029
2030 // If the theme preview is active, set the current theme.
2031 if ( self.view.view.theme && self.view.view.theme.preview ) {
2032 self.view.view.theme.model = self.view.collection.findWhere( { 'slug': slug } );
2033 self.view.view.theme.preview();
2034 } else {
2035
2036 // Select the theme by slug.
2037 request.theme = slug;
2038 self.view.collection.query( request );
2039 self.view.collection.trigger( 'update' );
2040
2041 // Open the theme preview.
2042 self.view.collection.once( 'query:success', function() {
2043 $( 'div[data-slug="' + slug + '"]' ).trigger( 'click' );
2044 });
2045
2046 }
2047 });
2048
2049 /*
2050 * Handles sorting / browsing routes.
2051 * Also handles the root URL triggering a sort request
2052 * for `popular`, the default view.
2053 */
2054 themes.router.on( 'route:sort', function( sort ) {
2055 if ( ! sort ) {
2056 sort = 'popular';
2057 themes.router.navigate( themes.router.baseUrl( '?browse=popular' ), { replace: true } );
2058 }
2059 self.view.sort( sort );
2060
2061 // Close the preview if open.
2062 if ( themes.preview ) {
2063 themes.preview.close();
2064 }
2065 });
2066
2067 // The `search` route event. The router populates the input field.
2068 themes.router.on( 'route:search', function() {
2069 $( '.wp-filter-search' ).trigger( 'focus' ).trigger( 'keyup' );
2070 });
2071
2072 this.extraRoutes();
2073 },
2074
2075 extraRoutes: function() {
2076 return false;
2077 }
2078};
2079
2080// Ready...
2081$( function() {
2082 if ( themes.isInstall ) {
2083 themes.RunInstaller.init();
2084 } else {
2085 themes.Run.init();
2086 }
2087
2088 // Update the return param just in time.
2089 $( document.body ).on( 'click', '.load-customize', function() {
2090 var link = $( this ), urlParser = document.createElement( 'a' );
2091 urlParser.href = link.prop( 'href' );
2092 urlParser.search = $.param( _.extend(
2093 wp.customize.utils.parseQueryString( urlParser.search.substr( 1 ) ),
2094 {
2095 'return': window.location.href
2096 }
2097 ) );
2098 link.prop( 'href', urlParser.href );
2099 });
2100
2101 $( '.broken-themes .delete-theme' ).on( 'click', function() {
2102 return confirm( _wpThemeSettings.settings.confirmDelete );
2103 });
2104});
2105
2106})( jQuery );
2107
2108// Align theme browser thickbox.
2109jQuery( function($) {
2110 window.tb_position = function() {
2111 var tbWindow = $('#TB_window'),
2112 width = $(window).width(),
2113 H = $(window).height(),
2114 W = ( 1040 < width ) ? 1040 : width,
2115 adminbar_height = 0;
2116
2117 if ( $('#wpadminbar').length ) {
2118 adminbar_height = parseInt( $('#wpadminbar').css('height'), 10 );
2119 }
2120
2121 if ( tbWindow.length >= 1 ) {
2122 tbWindow.width( W - 50 ).height( H - 45 - adminbar_height );
2123 $('#TB_iframeContent').width( W - 50 ).height( H - 75 - adminbar_height );
2124 tbWindow.css({'margin-left': '-' + parseInt( ( ( W - 50 ) / 2 ), 10 ) + 'px'});
2125 if ( typeof document.body.style.maxWidth !== 'undefined' ) {
2126 tbWindow.css({'top': 20 + adminbar_height + 'px', 'margin-top': '0'});
2127 }
2128 }
2129 };
2130
2131 $(window).on( 'resize', function(){ tb_position(); });
2132});
2133
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