PageRenderTime 65ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 1ms

/wordpress-dev/wp-admin/js/theme.js

https://github.com/scottbale/kirkwoodcoc
JavaScript | 2065 lines | 1279 code | 425 blank | 361 comment | 157 complexity | 7f52d864cd927714515906c74582e21b MD5 | raw file

Large files files are truncated, but you can click here to view the full file

  1. /* global _wpThemeSettings, confirm */
  2. window.wp = window.wp || {};
  3. ( function($) {
  4. // Set up our namespace...
  5. var themes, l10n;
  6. themes = wp.themes = wp.themes || {};
  7. // Store the theme data and settings for organized and quick access
  8. // themes.data.settings, themes.data.themes, themes.data.l10n
  9. themes.data = _wpThemeSettings;
  10. l10n = themes.data.l10n;
  11. // Shortcut for isInstall check
  12. themes.isInstall = !! themes.data.settings.isInstall;
  13. // Setup app structure
  14. _.extend( themes, { model: {}, view: {}, routes: {}, router: {}, template: wp.template });
  15. themes.Model = Backbone.Model.extend({
  16. // Adds attributes to the default data coming through the .org themes api
  17. // Map `id` to `slug` for shared code
  18. initialize: function() {
  19. var description;
  20. // If theme is already installed, set an attribute.
  21. if ( _.indexOf( themes.data.installedThemes, this.get( 'slug' ) ) !== -1 ) {
  22. this.set({ installed: true });
  23. }
  24. // Set the attributes
  25. this.set({
  26. // slug is for installation, id is for existing.
  27. id: this.get( 'slug' ) || this.get( 'id' )
  28. });
  29. // Map `section.description` to `description`
  30. // as the API sometimes returns it differently
  31. if ( this.has( 'sections' ) ) {
  32. description = this.get( 'sections' ).description;
  33. this.set({ description: description });
  34. }
  35. }
  36. });
  37. // Main view controller for themes.php
  38. // Unifies and renders all available views
  39. themes.view.Appearance = wp.Backbone.View.extend({
  40. el: '#wpbody-content .wrap .theme-browser',
  41. window: $( window ),
  42. // Pagination instance
  43. page: 0,
  44. // Sets up a throttler for binding to 'scroll'
  45. initialize: function( options ) {
  46. // Scroller checks how far the scroll position is
  47. _.bindAll( this, 'scroller' );
  48. this.SearchView = options.SearchView ? options.SearchView : themes.view.Search;
  49. // Bind to the scroll event and throttle
  50. // the results from this.scroller
  51. this.window.bind( 'scroll', _.throttle( this.scroller, 300 ) );
  52. },
  53. // Main render control
  54. render: function() {
  55. // Setup the main theme view
  56. // with the current theme collection
  57. this.view = new themes.view.Themes({
  58. collection: this.collection,
  59. parent: this
  60. });
  61. // Render search form.
  62. this.search();
  63. this.$el.removeClass( 'search-loading' );
  64. // Render and append
  65. this.view.render();
  66. this.$el.empty().append( this.view.el ).addClass( 'rendered' );
  67. },
  68. // Defines search element container
  69. searchContainer: $( '.search-form' ),
  70. // Search input and view
  71. // for current theme collection
  72. search: function() {
  73. var view,
  74. self = this;
  75. // Don't render the search if there is only one theme
  76. if ( themes.data.themes.length === 1 ) {
  77. return;
  78. }
  79. view = new this.SearchView({
  80. collection: self.collection,
  81. parent: this
  82. });
  83. self.SearchView = view;
  84. // Render and append after screen title
  85. view.render();
  86. this.searchContainer
  87. .append( $.parseHTML( '<label class="screen-reader-text" for="wp-filter-search-input">' + l10n.search + '</label>' ) )
  88. .append( view.el )
  89. .on( 'submit', function( event ) {
  90. event.preventDefault();
  91. });
  92. },
  93. // Checks when the user gets close to the bottom
  94. // of the mage and triggers a theme:scroll event
  95. scroller: function() {
  96. var self = this,
  97. bottom, threshold;
  98. bottom = this.window.scrollTop() + self.window.height();
  99. threshold = self.$el.offset().top + self.$el.outerHeight( false ) - self.window.height();
  100. threshold = Math.round( threshold * 0.9 );
  101. if ( bottom > threshold ) {
  102. this.trigger( 'theme:scroll' );
  103. }
  104. }
  105. });
  106. // Set up the Collection for our theme data
  107. // @has 'id' 'name' 'screenshot' 'author' 'authorURI' 'version' 'active' ...
  108. themes.Collection = Backbone.Collection.extend({
  109. model: themes.Model,
  110. // Search terms
  111. terms: '',
  112. // Controls searching on the current theme collection
  113. // and triggers an update event
  114. doSearch: function( value ) {
  115. // Don't do anything if we've already done this search
  116. // Useful because the Search handler fires multiple times per keystroke
  117. if ( this.terms === value ) {
  118. return;
  119. }
  120. // Updates terms with the value passed
  121. this.terms = value;
  122. // If we have terms, run a search...
  123. if ( this.terms.length > 0 ) {
  124. this.search( this.terms );
  125. }
  126. // If search is blank, show all themes
  127. // Useful for resetting the views when you clean the input
  128. if ( this.terms === '' ) {
  129. this.reset( themes.data.themes );
  130. $( 'body' ).removeClass( 'no-results' );
  131. }
  132. // Trigger a 'themes:update' event
  133. this.trigger( 'themes:update' );
  134. },
  135. // Performs a search within the collection
  136. // @uses RegExp
  137. search: function( term ) {
  138. var match, results, haystack, name, description, author;
  139. // Start with a full collection
  140. this.reset( themes.data.themes, { silent: true } );
  141. // Escape the term string for RegExp meta characters
  142. term = term.replace( /[-\/\\^$*+?.()|[\]{}]/g, '\\$&' );
  143. // Consider spaces as word delimiters and match the whole string
  144. // so matching terms can be combined
  145. term = term.replace( / /g, ')(?=.*' );
  146. match = new RegExp( '^(?=.*' + term + ').+', 'i' );
  147. // Find results
  148. // _.filter and .test
  149. results = this.filter( function( data ) {
  150. name = data.get( 'name' ).replace( /(<([^>]+)>)/ig, '' );
  151. description = data.get( 'description' ).replace( /(<([^>]+)>)/ig, '' );
  152. author = data.get( 'author' ).replace( /(<([^>]+)>)/ig, '' );
  153. haystack = _.union( [ name, data.get( 'id' ), description, author, data.get( 'tags' ) ] );
  154. if ( match.test( data.get( 'author' ) ) && term.length > 2 ) {
  155. data.set( 'displayAuthor', true );
  156. }
  157. return match.test( haystack );
  158. });
  159. if ( results.length === 0 ) {
  160. this.trigger( 'query:empty' );
  161. } else {
  162. $( 'body' ).removeClass( 'no-results' );
  163. }
  164. this.reset( results );
  165. },
  166. // Paginates the collection with a helper method
  167. // that slices the collection
  168. paginate: function( instance ) {
  169. var collection = this;
  170. instance = instance || 0;
  171. // Themes per instance are set at 20
  172. collection = _( collection.rest( 20 * instance ) );
  173. collection = _( collection.first( 20 ) );
  174. return collection;
  175. },
  176. count: false,
  177. // Handles requests for more themes
  178. // and caches results
  179. //
  180. // When we are missing a cache object we fire an apiCall()
  181. // which triggers events of `query:success` or `query:fail`
  182. query: function( request ) {
  183. /**
  184. * @static
  185. * @type Array
  186. */
  187. var queries = this.queries,
  188. self = this,
  189. query, isPaginated, count;
  190. // Store current query request args
  191. // for later use with the event `theme:end`
  192. this.currentQuery.request = request;
  193. // Search the query cache for matches.
  194. query = _.find( queries, function( query ) {
  195. return _.isEqual( query.request, request );
  196. });
  197. // If the request matches the stored currentQuery.request
  198. // it means we have a paginated request.
  199. isPaginated = _.has( request, 'page' );
  200. // Reset the internal api page counter for non paginated queries.
  201. if ( ! isPaginated ) {
  202. this.currentQuery.page = 1;
  203. }
  204. // Otherwise, send a new API call and add it to the cache.
  205. if ( ! query && ! isPaginated ) {
  206. query = this.apiCall( request ).done( function( data ) {
  207. // Update the collection with the queried data.
  208. if ( data.themes ) {
  209. self.reset( data.themes );
  210. count = data.info.results;
  211. // Store the results and the query request
  212. queries.push( { themes: data.themes, request: request, total: count } );
  213. }
  214. // Trigger a collection refresh event
  215. // and a `query:success` event with a `count` argument.
  216. self.trigger( 'themes:update' );
  217. self.trigger( 'query:success', count );
  218. if ( data.themes && data.themes.length === 0 ) {
  219. self.trigger( 'query:empty' );
  220. }
  221. }).fail( function() {
  222. self.trigger( 'query:fail' );
  223. });
  224. } else {
  225. // If it's a paginated request we need to fetch more themes...
  226. if ( isPaginated ) {
  227. return this.apiCall( request, isPaginated ).done( function( data ) {
  228. // Add the new themes to the current collection
  229. // @todo update counter
  230. self.add( data.themes );
  231. self.trigger( 'query:success' );
  232. // We are done loading themes for now.
  233. self.loadingThemes = false;
  234. }).fail( function() {
  235. self.trigger( 'query:fail' );
  236. });
  237. }
  238. if ( query.themes.length === 0 ) {
  239. self.trigger( 'query:empty' );
  240. } else {
  241. $( 'body' ).removeClass( 'no-results' );
  242. }
  243. // Only trigger an update event since we already have the themes
  244. // on our cached object
  245. if ( _.isNumber( query.total ) ) {
  246. this.count = query.total;
  247. }
  248. this.reset( query.themes );
  249. if ( ! query.total ) {
  250. this.count = this.length;
  251. }
  252. this.trigger( 'themes:update' );
  253. this.trigger( 'query:success', this.count );
  254. }
  255. },
  256. // Local cache array for API queries
  257. queries: [],
  258. // Keep track of current query so we can handle pagination
  259. currentQuery: {
  260. page: 1,
  261. request: {}
  262. },
  263. // Send request to api.wordpress.org/themes
  264. apiCall: function( request, paginated ) {
  265. return wp.ajax.send( 'query-themes', {
  266. data: {
  267. // Request data
  268. request: _.extend({
  269. per_page: 100,
  270. fields: {
  271. description: true,
  272. tested: true,
  273. requires: true,
  274. rating: true,
  275. downloaded: true,
  276. downloadLink: true,
  277. last_updated: true,
  278. homepage: true,
  279. num_ratings: true
  280. }
  281. }, request)
  282. },
  283. beforeSend: function() {
  284. if ( ! paginated ) {
  285. // Spin it
  286. $( 'body' ).addClass( 'loading-content' ).removeClass( 'no-results' );
  287. }
  288. }
  289. });
  290. },
  291. // Static status controller for when we are loading themes.
  292. loadingThemes: false
  293. });
  294. // This is the view that controls each theme item
  295. // that will be displayed on the screen
  296. themes.view.Theme = wp.Backbone.View.extend({
  297. // Wrap theme data on a div.theme element
  298. className: 'theme',
  299. // Reflects which theme view we have
  300. // 'grid' (default) or 'detail'
  301. state: 'grid',
  302. // The HTML template for each element to be rendered
  303. html: themes.template( 'theme' ),
  304. events: {
  305. 'click': themes.isInstall ? 'preview': 'expand',
  306. 'keydown': themes.isInstall ? 'preview': 'expand',
  307. 'touchend': themes.isInstall ? 'preview': 'expand',
  308. 'keyup': 'addFocus',
  309. 'touchmove': 'preventExpand',
  310. 'click .theme-install': 'installTheme',
  311. 'click .update-message': 'updateTheme'
  312. },
  313. touchDrag: false,
  314. initialize: function() {
  315. this.model.on( 'change', this.render, this );
  316. },
  317. render: function() {
  318. var data = this.model.toJSON();
  319. // Render themes using the html template
  320. this.$el.html( this.html( data ) ).attr({
  321. tabindex: 0,
  322. 'aria-describedby' : data.id + '-action ' + data.id + '-name',
  323. 'data-slug': data.id
  324. });
  325. // Renders active theme styles
  326. this.activeTheme();
  327. if ( this.model.get( 'displayAuthor' ) ) {
  328. this.$el.addClass( 'display-author' );
  329. }
  330. },
  331. // Adds a class to the currently active theme
  332. // and to the overlay in detailed view mode
  333. activeTheme: function() {
  334. if ( this.model.get( 'active' ) ) {
  335. this.$el.addClass( 'active' );
  336. }
  337. },
  338. // Add class of focus to the theme we are focused on.
  339. addFocus: function() {
  340. var $themeToFocus = ( $( ':focus' ).hasClass( 'theme' ) ) ? $( ':focus' ) : $(':focus').parents('.theme');
  341. $('.theme.focus').removeClass('focus');
  342. $themeToFocus.addClass('focus');
  343. },
  344. // Single theme overlay screen
  345. // It's shown when clicking a theme
  346. expand: function( event ) {
  347. var self = this;
  348. event = event || window.event;
  349. // 'enter' and 'space' keys expand the details view when a theme is :focused
  350. if ( event.type === 'keydown' && ( event.which !== 13 && event.which !== 32 ) ) {
  351. return;
  352. }
  353. // Bail if the user scrolled on a touch device
  354. if ( this.touchDrag === true ) {
  355. return this.touchDrag = false;
  356. }
  357. // Prevent the modal from showing when the user clicks
  358. // one of the direct action buttons
  359. if ( $( event.target ).is( '.theme-actions a' ) ) {
  360. return;
  361. }
  362. // Prevent the modal from showing when the user clicks one of the direct action buttons.
  363. if ( $( event.target ).is( '.theme-actions a, .update-message, .button-link, .notice-dismiss' ) ) {
  364. return;
  365. }
  366. // Set focused theme to current element
  367. themes.focusedTheme = this.$el;
  368. this.trigger( 'theme:expand', self.model.cid );
  369. },
  370. preventExpand: function() {
  371. this.touchDrag = true;
  372. },
  373. preview: function( event ) {
  374. var self = this,
  375. current, preview;
  376. event = event || window.event;
  377. // Bail if the user scrolled on a touch device
  378. if ( this.touchDrag === true ) {
  379. return this.touchDrag = false;
  380. }
  381. // Allow direct link path to installing a theme.
  382. if ( $( event.target ).not( '.install-theme-preview' ).parents( '.theme-actions' ).length ) {
  383. return;
  384. }
  385. // 'enter' and 'space' keys expand the details view when a theme is :focused
  386. if ( event.type === 'keydown' && ( event.which !== 13 && event.which !== 32 ) ) {
  387. return;
  388. }
  389. // pressing enter while focused on the buttons shouldn't open the preview
  390. if ( event.type === 'keydown' && event.which !== 13 && $( ':focus' ).hasClass( 'button' ) ) {
  391. return;
  392. }
  393. event.preventDefault();
  394. event = event || window.event;
  395. // Set focus to current theme.
  396. themes.focusedTheme = this.$el;
  397. // Construct a new Preview view.
  398. themes.preview = preview = new themes.view.Preview({
  399. model: this.model
  400. });
  401. // Render the view and append it.
  402. preview.render();
  403. this.setNavButtonsState();
  404. // Hide previous/next navigation if there is only one theme
  405. if ( this.model.collection.length === 1 ) {
  406. preview.$el.addClass( 'no-navigation' );
  407. } else {
  408. preview.$el.removeClass( 'no-navigation' );
  409. }
  410. // Append preview
  411. $( 'div.wrap' ).append( preview.el );
  412. // Listen to our preview object
  413. // for `theme:next` and `theme:previous` events.
  414. this.listenTo( preview, 'theme:next', function() {
  415. // Keep local track of current theme model.
  416. current = self.model;
  417. // If we have ventured away from current model update the current model position.
  418. if ( ! _.isUndefined( self.current ) ) {
  419. current = self.current;
  420. }
  421. // Get next theme model.
  422. self.current = self.model.collection.at( self.model.collection.indexOf( current ) + 1 );
  423. // If we have no more themes, bail.
  424. if ( _.isUndefined( self.current ) ) {
  425. self.options.parent.parent.trigger( 'theme:end' );
  426. return self.current = current;
  427. }
  428. preview.model = self.current;
  429. // Render and append.
  430. preview.render();
  431. this.setNavButtonsState();
  432. $( '.next-theme' ).focus();
  433. })
  434. .listenTo( preview, 'theme:previous', function() {
  435. // Keep track of current theme model.
  436. current = self.model;
  437. // Bail early if we are at the beginning of the collection
  438. if ( self.model.collection.indexOf( self.current ) === 0 ) {
  439. return;
  440. }
  441. // If we have ventured away from current model update the current model position.
  442. if ( ! _.isUndefined( self.current ) ) {
  443. current = self.current;
  444. }
  445. // Get previous theme model.
  446. self.current = self.model.collection.at( self.model.collection.indexOf( current ) - 1 );
  447. // If we have no more themes, bail.
  448. if ( _.isUndefined( self.current ) ) {
  449. return;
  450. }
  451. preview.model = self.current;
  452. // Render and append.
  453. preview.render();
  454. this.setNavButtonsState();
  455. $( '.previous-theme' ).focus();
  456. });
  457. this.listenTo( preview, 'preview:close', function() {
  458. self.current = self.model;
  459. });
  460. },
  461. // Handles .disabled classes for previous/next buttons in theme installer preview
  462. setNavButtonsState: function() {
  463. var $themeInstaller = $( '.theme-install-overlay' ),
  464. current = _.isUndefined( this.current ) ? this.model : this.current;
  465. // Disable previous at the zero position
  466. if ( 0 === this.model.collection.indexOf( current ) ) {
  467. $themeInstaller.find( '.previous-theme' ).addClass( 'disabled' );
  468. }
  469. // Disable next if the next model is undefined
  470. if ( _.isUndefined( this.model.collection.at( this.model.collection.indexOf( current ) + 1 ) ) ) {
  471. $themeInstaller.find( '.next-theme' ).addClass( 'disabled' );
  472. }
  473. },
  474. installTheme: function( event ) {
  475. var _this = this;
  476. event.preventDefault();
  477. wp.updates.maybeRequestFilesystemCredentials( event );
  478. $( document ).on( 'wp-theme-install-success', function( event, response ) {
  479. if ( _this.model.get( 'id' ) === response.slug ) {
  480. _this.model.set( { 'installed': true } );
  481. }
  482. } );
  483. wp.updates.installTheme( {
  484. slug: $( event.target ).data( 'slug' )
  485. } );
  486. },
  487. updateTheme: function( event ) {
  488. var _this = this;
  489. if ( ! this.model.get( 'hasPackage' ) ) {
  490. return;
  491. }
  492. event.preventDefault();
  493. wp.updates.maybeRequestFilesystemCredentials( event );
  494. $( document ).on( 'wp-theme-update-success', function( event, response ) {
  495. _this.model.off( 'change', _this.render, _this );
  496. if ( _this.model.get( 'id' ) === response.slug ) {
  497. _this.model.set( {
  498. hasUpdate: false,
  499. version: response.newVersion
  500. } );
  501. }
  502. _this.model.on( 'change', _this.render, _this );
  503. } );
  504. wp.updates.updateTheme( {
  505. slug: $( event.target ).parents( 'div.theme' ).first().data( 'slug' )
  506. } );
  507. }
  508. });
  509. // Theme Details view
  510. // Set ups a modal overlay with the expanded theme data
  511. themes.view.Details = wp.Backbone.View.extend({
  512. // Wrap theme data on a div.theme element
  513. className: 'theme-overlay',
  514. events: {
  515. 'click': 'collapse',
  516. 'click .delete-theme': 'deleteTheme',
  517. 'click .left': 'previousTheme',
  518. 'click .right': 'nextTheme',
  519. 'click #update-theme': 'updateTheme'
  520. },
  521. // The HTML template for the theme overlay
  522. html: themes.template( 'theme-single' ),
  523. render: function() {
  524. var data = this.model.toJSON();
  525. this.$el.html( this.html( data ) );
  526. // Renders active theme styles
  527. this.activeTheme();
  528. // Set up navigation events
  529. this.navigation();
  530. // Checks screenshot size
  531. this.screenshotCheck( this.$el );
  532. // Contain "tabbing" inside the overlay
  533. this.containFocus( this.$el );
  534. },
  535. // Adds a class to the currently active theme
  536. // and to the overlay in detailed view mode
  537. activeTheme: function() {
  538. // Check the model has the active property
  539. this.$el.toggleClass( 'active', this.model.get( 'active' ) );
  540. },
  541. // Set initial focus and constrain tabbing within the theme browser modal.
  542. containFocus: function( $el ) {
  543. // Set initial focus on the primary action control.
  544. _.delay( function() {
  545. $( '.theme-overlay' ).focus();
  546. }, 100 );
  547. // Constrain tabbing within the modal.
  548. $el.on( 'keydown.wp-themes', function( event ) {
  549. var $firstFocusable = $el.find( '.theme-header button:not(.disabled)' ).first(),
  550. $lastFocusable = $el.find( '.theme-actions a:visible' ).last();
  551. // Check for the Tab key.
  552. if ( 9 === event.which ) {
  553. if ( $firstFocusable[0] === event.target && event.shiftKey ) {
  554. $lastFocusable.focus();
  555. event.preventDefault();
  556. } else if ( $lastFocusable[0] === event.target && ! event.shiftKey ) {
  557. $firstFocusable.focus();
  558. event.preventDefault();
  559. }
  560. }
  561. });
  562. },
  563. // Single theme overlay screen
  564. // It's shown when clicking a theme
  565. collapse: function( event ) {
  566. var self = this,
  567. scroll;
  568. event = event || window.event;
  569. // Prevent collapsing detailed view when there is only one theme available
  570. if ( themes.data.themes.length === 1 ) {
  571. return;
  572. }
  573. // Detect if the click is inside the overlay
  574. // and don't close it unless the target was
  575. // the div.back button
  576. if ( $( event.target ).is( '.theme-backdrop' ) || $( event.target ).is( '.close' ) || event.keyCode === 27 ) {
  577. // Add a temporary closing class while overlay fades out
  578. $( 'body' ).addClass( 'closing-overlay' );
  579. // With a quick fade out animation
  580. this.$el.fadeOut( 130, function() {
  581. // Clicking outside the modal box closes the overlay
  582. $( 'body' ).removeClass( 'closing-overlay' );
  583. // Handle event cleanup
  584. self.closeOverlay();
  585. // Get scroll position to avoid jumping to the top
  586. scroll = document.body.scrollTop;
  587. // Clean the url structure
  588. themes.router.navigate( themes.router.baseUrl( '' ) );
  589. // Restore scroll position
  590. document.body.scrollTop = scroll;
  591. // Return focus to the theme div
  592. if ( themes.focusedTheme ) {
  593. themes.focusedTheme.focus();
  594. }
  595. });
  596. }
  597. },
  598. // Handles .disabled classes for next/previous buttons
  599. navigation: function() {
  600. // Disable Left/Right when at the start or end of the collection
  601. if ( this.model.cid === this.model.collection.at(0).cid ) {
  602. this.$el.find( '.left' )
  603. .addClass( 'disabled' )
  604. .prop( 'disabled', true );
  605. }
  606. if ( this.model.cid === this.model.collection.at( this.model.collection.length - 1 ).cid ) {
  607. this.$el.find( '.right' )
  608. .addClass( 'disabled' )
  609. .prop( 'disabled', true );
  610. }
  611. },
  612. // Performs the actions to effectively close
  613. // the theme details overlay
  614. closeOverlay: function() {
  615. $( 'body' ).removeClass( 'modal-open' );
  616. this.remove();
  617. this.unbind();
  618. this.trigger( 'theme:collapse' );
  619. },
  620. updateTheme: function( event ) {
  621. var _this = this;
  622. event.preventDefault();
  623. wp.updates.maybeRequestFilesystemCredentials( event );
  624. $( document ).on( 'wp-theme-update-success', function( event, response ) {
  625. if ( _this.model.get( 'id' ) === response.slug ) {
  626. _this.model.set( {
  627. hasUpdate: false,
  628. version: response.newVersion
  629. } );
  630. }
  631. _this.render();
  632. } );
  633. wp.updates.updateTheme( {
  634. slug: $( event.target ).data( 'slug' )
  635. } );
  636. },
  637. deleteTheme: function( event ) {
  638. var _this = this,
  639. _collection = _this.model.collection,
  640. _themes = themes;
  641. event.preventDefault();
  642. // Confirmation dialog for deleting a theme.
  643. if ( ! window.confirm( wp.themes.data.settings.confirmDelete ) ) {
  644. return;
  645. }
  646. wp.updates.maybeRequestFilesystemCredentials( event );
  647. $( document ).one( 'wp-theme-delete-success', function( event, response ) {
  648. _this.$el.find( '.close' ).trigger( 'click' );
  649. $( '[data-slug="' + response.slug + '"]' ).css( { backgroundColor:'#faafaa' } ).fadeOut( 350, function() {
  650. $( this ).remove();
  651. _themes.data.themes = _.without( _themes.data.themes, _.findWhere( _themes.data.themes, { id: response.slug } ) );
  652. $( '.wp-filter-search' ).val( '' );
  653. _collection.doSearch( '' );
  654. _collection.remove( _this.model );
  655. _collection.trigger( 'themes:update' );
  656. } );
  657. } );
  658. wp.updates.deleteTheme( {
  659. slug: this.model.get( 'id' )
  660. } );
  661. },
  662. nextTheme: function() {
  663. var self = this;
  664. self.trigger( 'theme:next', self.model.cid );
  665. return false;
  666. },
  667. previousTheme: function() {
  668. var self = this;
  669. self.trigger( 'theme:previous', self.model.cid );
  670. return false;
  671. },
  672. // Checks if the theme screenshot is the old 300px width version
  673. // and adds a corresponding class if it's true
  674. screenshotCheck: function( el ) {
  675. var screenshot, image;
  676. screenshot = el.find( '.screenshot img' );
  677. image = new Image();
  678. image.src = screenshot.attr( 'src' );
  679. // Width check
  680. if ( image.width && image.width <= 300 ) {
  681. el.addClass( 'small-screenshot' );
  682. }
  683. }
  684. });
  685. // Theme Preview view
  686. // Set ups a modal overlay with the expanded theme data
  687. themes.view.Preview = themes.view.Details.extend({
  688. className: 'wp-full-overlay expanded',
  689. el: '.theme-install-overlay',
  690. events: {
  691. 'click .close-full-overlay': 'close',
  692. 'click .collapse-sidebar': 'collapse',
  693. 'click .devices button': 'previewDevice',
  694. 'click .previous-theme': 'previousTheme',
  695. 'click .next-theme': 'nextTheme',
  696. 'keyup': 'keyEvent',
  697. 'click .theme-install': 'installTheme'
  698. },
  699. // The HTML template for the theme preview
  700. html: themes.template( 'theme-preview' ),
  701. render: function() {
  702. var self = this,
  703. currentPreviewDevice,
  704. data = this.model.toJSON(),
  705. $body = $( document.body );
  706. $body.attr( 'aria-busy', 'true' );
  707. this.$el.removeClass( 'iframe-ready' ).html( this.html( data ) );
  708. currentPreviewDevice = this.$el.data( 'current-preview-device' );
  709. if ( currentPreviewDevice ) {
  710. self.tooglePreviewDeviceButtons( currentPreviewDevice );
  711. }
  712. themes.router.navigate( themes.router.baseUrl( themes.router.themePath + this.model.get( 'id' ) ), { replace: false } );
  713. this.$el.fadeIn( 200, function() {
  714. $body.addClass( 'theme-installer-active full-overlay-active' );
  715. });
  716. this.$el.find( 'iframe' ).one( 'load', function() {
  717. self.iframeLoaded();
  718. });
  719. },
  720. iframeLoaded: function() {
  721. this.$el.addClass( 'iframe-ready' );
  722. $( document.body ).attr( 'aria-busy', 'false' );
  723. },
  724. close: function() {
  725. this.$el.fadeOut( 200, function() {
  726. $( 'body' ).removeClass( 'theme-installer-active full-overlay-active' );
  727. // Return focus to the theme div
  728. if ( themes.focusedTheme ) {
  729. themes.focusedTheme.focus();
  730. }
  731. }).removeClass( 'iframe-ready' );
  732. // Restore the previous browse tab if available.
  733. if ( themes.router.selectedTab ) {
  734. themes.router.navigate( themes.router.baseUrl( '?browse=' + themes.router.selectedTab ) );
  735. themes.router.selectedTab = false;
  736. } else {
  737. themes.router.navigate( themes.router.baseUrl( '' ) );
  738. }
  739. this.trigger( 'preview:close' );
  740. this.undelegateEvents();
  741. this.unbind();
  742. return false;
  743. },
  744. collapse: function( event ) {
  745. var $button = $( event.currentTarget );
  746. if ( 'true' === $button.attr( 'aria-expanded' ) ) {
  747. $button.attr({ 'aria-expanded': 'false', 'aria-label': l10n.expandSidebar });
  748. } else {
  749. $button.attr({ 'aria-expanded': 'true', 'aria-label': l10n.collapseSidebar });
  750. }
  751. this.$el.toggleClass( 'collapsed' ).toggleClass( 'expanded' );
  752. return false;
  753. },
  754. previewDevice: function( event ) {
  755. var device = $( event.currentTarget ).data( 'device' );
  756. this.$el
  757. .removeClass( 'preview-desktop preview-tablet preview-mobile' )
  758. .addClass( 'preview-' + device )
  759. .data( 'current-preview-device', device );
  760. this.tooglePreviewDeviceButtons( device );
  761. },
  762. tooglePreviewDeviceButtons: function( newDevice ) {
  763. var $devices = $( '.wp-full-overlay-footer .devices' );
  764. $devices.find( 'button' )
  765. .removeClass( 'active' )
  766. .attr( 'aria-pressed', false );
  767. $devices.find( 'button.preview-' + newDevice )
  768. .addClass( 'active' )
  769. .attr( 'aria-pressed', true );
  770. },
  771. keyEvent: function( event ) {
  772. // The escape key closes the preview
  773. if ( event.keyCode === 27 ) {
  774. this.undelegateEvents();
  775. this.close();
  776. }
  777. // The right arrow key, next theme
  778. if ( event.keyCode === 39 ) {
  779. _.once( this.nextTheme() );
  780. }
  781. // The left arrow key, previous theme
  782. if ( event.keyCode === 37 ) {
  783. this.previousTheme();
  784. }
  785. },
  786. installTheme: function( event ) {
  787. var _this = this,
  788. $target = $( event.target );
  789. event.preventDefault();
  790. if ( $target.hasClass( 'disabled' ) ) {
  791. return;
  792. }
  793. wp.updates.maybeRequestFilesystemCredentials( event );
  794. $( document ).on( 'wp-theme-install-success', function() {
  795. _this.model.set( { 'installed': true } );
  796. } );
  797. wp.updates.installTheme( {
  798. slug: $target.data( 'slug' )
  799. } );
  800. }
  801. });
  802. // Controls the rendering of div.themes,
  803. // a wrapper that will hold all the theme elements
  804. themes.view.Themes = wp.Backbone.View.extend({
  805. className: 'themes wp-clearfix',
  806. $overlay: $( 'div.theme-overlay' ),
  807. // Number to keep track of scroll position
  808. // while in theme-overlay mode
  809. index: 0,
  810. // The theme count element
  811. count: $( '.wrap .theme-count' ),
  812. // The live themes count
  813. liveThemeCount: 0,
  814. initialize: function( options ) {
  815. var self = this;
  816. // Set up parent
  817. this.parent = options.parent;
  818. // Set current view to [grid]
  819. this.setView( 'grid' );
  820. // Move the active theme to the beginning of the collection
  821. self.currentTheme();
  822. // When the collection is updated by user input...
  823. this.listenTo( self.collection, 'themes:update', function() {
  824. self.parent.page = 0;
  825. self.currentTheme();
  826. self.render( this );
  827. } );
  828. // Update theme count to full result set when available.
  829. this.listenTo( self.collection, 'query:success', function( count ) {
  830. if ( _.isNumber( count ) ) {
  831. self.count.text( count );
  832. self.announceSearchResults( count );
  833. } else {
  834. self.count.text( self.collection.length );
  835. self.announceSearchResults( self.collection.length );
  836. }
  837. });
  838. this.listenTo( self.collection, 'query:empty', function() {
  839. $( 'body' ).addClass( 'no-results' );
  840. });
  841. this.listenTo( this.parent, 'theme:scroll', function() {
  842. self.renderThemes( self.parent.page );
  843. });
  844. this.listenTo( this.parent, 'theme:close', function() {
  845. if ( self.overlay ) {
  846. self.overlay.closeOverlay();
  847. }
  848. } );
  849. // Bind keyboard events.
  850. $( 'body' ).on( 'keyup', function( event ) {
  851. if ( ! self.overlay ) {
  852. return;
  853. }
  854. // Bail if the filesystem credentials dialog is shown.
  855. if ( $( '#request-filesystem-credentials-dialog' ).is( ':visible' ) ) {
  856. return;
  857. }
  858. // Pressing the right arrow key fires a theme:next event
  859. if ( event.keyCode === 39 ) {
  860. self.overlay.nextTheme();
  861. }
  862. // Pressing the left arrow key fires a theme:previous event
  863. if ( event.keyCode === 37 ) {
  864. self.overlay.previousTheme();
  865. }
  866. // Pressing the escape key fires a theme:collapse event
  867. if ( event.keyCode === 27 ) {
  868. self.overlay.collapse( event );
  869. }
  870. });
  871. },
  872. // Manages rendering of theme pages
  873. // and keeping theme count in sync
  874. render: function() {
  875. // Clear the DOM, please
  876. this.$el.empty();
  877. // If the user doesn't have switch capabilities
  878. // or there is only one theme in the collection
  879. // render the detailed view of the active theme
  880. if ( themes.data.themes.length === 1 ) {
  881. // Constructs the view
  882. this.singleTheme = new themes.view.Details({
  883. model: this.collection.models[0]
  884. });
  885. // Render and apply a 'single-theme' class to our container
  886. this.singleTheme.render();
  887. this.$el.addClass( 'single-theme' );
  888. this.$el.append( this.singleTheme.el );
  889. }
  890. // Generate the themes
  891. // Using page instance
  892. // While checking the collection has items
  893. if ( this.options.collection.size() > 0 ) {
  894. this.renderThemes( this.parent.page );
  895. }
  896. // Display a live theme count for the collection
  897. this.liveThemeCount = this.collection.count ? this.collection.count : this.collection.length;
  898. this.count.text( this.liveThemeCount );
  899. /*
  900. * In the theme installer the themes count is already announced
  901. * because `announceSearchResults` is called on `query:success`.
  902. */
  903. if ( ! themes.isInstall ) {
  904. this.announceSearchResults( this.liveThemeCount );
  905. }
  906. },
  907. // Iterates through each instance of the collection
  908. // and renders each theme module
  909. renderThemes: function( page ) {
  910. var self = this;
  911. self.instance = self.collection.paginate( page );
  912. // If we have no more themes bail
  913. if ( self.instance.size() === 0 ) {
  914. // Fire a no-more-themes event.
  915. this.parent.trigger( 'theme:end' );
  916. return;
  917. }
  918. // Make sure the add-new stays at the end
  919. if ( ! themes.isInstall && page >= 1 ) {
  920. $( '.add-new-theme' ).remove();
  921. }
  922. // Loop through the themes and setup each theme view
  923. self.instance.each( function( theme ) {
  924. self.theme = new themes.view.Theme({
  925. model: theme,
  926. parent: self
  927. });
  928. // Render the views...
  929. self.theme.render();
  930. // and append them to div.themes
  931. self.$el.append( self.theme.el );
  932. // Binds to theme:expand to show the modal box
  933. // with the theme details
  934. self.listenTo( self.theme, 'theme:expand', self.expand, self );
  935. });
  936. // 'Add new theme' element shown at the end of the grid
  937. if ( ! themes.isInstall && themes.data.settings.canInstall ) {
  938. this.$el.append( '<div class="theme add-new-theme"><a href="' + themes.data.settings.installURI + '"><div class="theme-screenshot"><span></span></div><h2 class="theme-name">' + l10n.addNew + '</h2></a></div>' );
  939. }
  940. this.parent.page++;
  941. },
  942. // Grabs current theme and puts it at the beginning of the collection
  943. currentTheme: function() {
  944. var self = this,
  945. current;
  946. current = self.collection.findWhere({ active: true });
  947. // Move the active theme to the beginning of the collection
  948. if ( current ) {
  949. self.collection.remove( current );
  950. self.collection.add( current, { at:0 } );
  951. }
  952. },
  953. // Sets current view
  954. setView: function( view ) {
  955. return view;
  956. },
  957. // Renders the overlay with the ThemeDetails view
  958. // Uses the current model data
  959. expand: function( id ) {
  960. var self = this, $card, $modal;
  961. // Set the current theme model
  962. this.model = self.collection.get( id );
  963. // Trigger a route update for the current model
  964. themes.router.navigate( themes.router.baseUrl( themes.router.themePath + this.model.id ) );
  965. // Sets this.view to 'detail'
  966. this.setView( 'detail' );
  967. $( 'body' ).addClass( 'modal-open' );
  968. // Set up the theme details view
  969. this.overlay = new themes.view.Details({
  970. model: self.model
  971. });
  972. this.overlay.render();
  973. if ( this.model.get( 'hasUpdate' ) ) {
  974. $card = $( '[data-slug="' + this.model.id + '"]' );
  975. $modal = $( this.overlay.el );
  976. if ( $card.find( '.updating-message' ).length ) {
  977. $modal.find( '.notice-warning h3' ).remove();
  978. $modal.find( '.notice-warning' )
  979. .removeClass( 'notice-large' )
  980. .addClass( 'updating-message' )
  981. .find( 'p' ).text( wp.updates.l10n.updating );
  982. } else if ( $card.find( '.notice-error' ).length ) {
  983. $modal.find( '.notice-warning' ).remove();
  984. }
  985. }
  986. this.$overlay.html( this.overlay.el );
  987. // Bind to theme:next and theme:previous
  988. // triggered by the arrow keys
  989. //
  990. // Keep track of the current model so we
  991. // can infer an index position
  992. this.listenTo( this.overlay, 'theme:next', function() {
  993. // Renders the next theme on the overlay
  994. self.next( [ self.model.cid ] );
  995. })
  996. .listenTo( this.overlay, 'theme:previous', function() {
  997. // Renders the previous theme on the overlay
  998. self.previous( [ self.model.cid ] );
  999. });
  1000. },
  1001. // This method renders the next theme on the overlay modal
  1002. // based on the current position in the collection
  1003. // @params [model cid]
  1004. next: function( args ) {
  1005. var self = this,
  1006. model, nextModel;
  1007. // Get the current theme
  1008. model = self.collection.get( args[0] );
  1009. // Find the next model within the collection
  1010. nextModel = self.collection.at( self.collection.indexOf( model ) + 1 );
  1011. // Sanity check which also serves as a boundary test
  1012. if ( nextModel !== undefined ) {
  1013. // We have a new theme...
  1014. // Close the overlay
  1015. this.overlay.closeOverlay();
  1016. // Trigger a route update for the current model
  1017. self.theme.trigger( 'theme:expand', nextModel.cid );
  1018. }
  1019. },
  1020. // This method renders the previous theme on the overlay modal
  1021. // based on the current position in the collection
  1022. // @params [model cid]
  1023. previous: function( args ) {
  1024. var self = this,
  1025. model, previousModel;
  1026. // Get the current theme
  1027. model = self.collection.get( args[0] );
  1028. // Find the previous model within the collection
  1029. previousModel = self.collection.at( self.collection.indexOf( model ) - 1 );
  1030. if ( previousModel !== undefined ) {
  1031. // We have a new theme...
  1032. // Close the overlay
  1033. this.overlay.closeOverlay();
  1034. // Trigger a route update for the current model
  1035. self.theme.trigger( 'theme:expand', previousModel.cid );
  1036. }
  1037. },
  1038. // Dispatch audible search results feedback message
  1039. announceSearchResults: function( count ) {
  1040. if ( 0 === count ) {
  1041. wp.a11y.speak( l10n.noThemesFound );
  1042. } else {
  1043. wp.a11y.speak( l10n.themesFound.replace( '%d', count ) );
  1044. }
  1045. }
  1046. });
  1047. // Search input view controller.
  1048. themes.view.Search = wp.Backbone.View.extend({
  1049. tagName: 'input',
  1050. className: 'wp-filter-search',
  1051. id: 'wp-filter-search-input',
  1052. searching: false,
  1053. attributes: {
  1054. placeholder: l10n.searchPlaceholder,
  1055. type: 'search',
  1056. 'aria-describedby': 'live-search-desc'
  1057. },
  1058. events: {
  1059. 'input': 'search',
  1060. 'keyup': 'search',
  1061. 'blur': 'pushState'
  1062. },
  1063. initialize: function( options ) {
  1064. this.parent = options.parent;
  1065. this.listenTo( this.parent, 'theme:close', function() {
  1066. this.searching = false;
  1067. } );
  1068. },
  1069. search: function( event ) {
  1070. // Clear on escape.
  1071. if ( event.type === 'keyup' && event.which === 27 ) {
  1072. event.target.value = '';
  1073. }
  1074. // Since doSearch is debounced, it will only run when user input comes to a rest.
  1075. this.doSearch( event );
  1076. },
  1077. // Runs a search on the theme collection.
  1078. doSearch: function( event ) {
  1079. var options = {};
  1080. this.collection.doSearch( event.target.value.replace( /\+/g, ' ' ) );
  1081. // if search is initiated and key is not return
  1082. if ( this.searching && event.which !== 13 ) {
  1083. options.replace = true;
  1084. } else {
  1085. this.searching = true;
  1086. }
  1087. // Update the URL hash
  1088. if ( event.target.value ) {
  1089. themes.router.navigate( themes.router.baseUrl( themes.router.searchPath + event.target.value ), options );
  1090. } else {
  1091. themes.router.navigate( themes.router.baseUrl( '' ) );
  1092. }
  1093. },
  1094. pushState: function( event ) {
  1095. var url = themes.router.baseUrl( '' );
  1096. if ( event.target.value ) {
  1097. url = themes.router.baseUrl( themes.router.searchPath + encodeURIComponent( event.target.value ) );
  1098. }
  1099. this.searching = false;
  1100. themes.router.navigate( url );
  1101. }
  1102. });
  1103. /**
  1104. * Navigate router.
  1105. *
  1106. * @since 4.9.0
  1107. *
  1108. * @param {string} url - URL to navigate to.
  1109. * @param {object} state - State.
  1110. * @returns {void}
  1111. */
  1112. function navigateRouter( url, state ) {
  1113. var router = this;
  1114. if ( Backbone.history._hasPushState ) {
  1115. Backbone.Router.prototype.navigate.call( router, url, state );
  1116. }
  1117. }
  1118. // Sets up the routes events for relevant url queries
  1119. // Listens to [theme] and [search] params
  1120. themes.Router = Backbone.Router.extend({
  1121. routes: {
  1122. 'themes.php?theme=:slug': 'theme',
  1123. 'themes.php?search=:query': 'search',
  1124. 'themes.php?s=:query': 'search',
  1125. 'themes.php': 'themes',
  1126. '': 'themes'
  1127. },
  1128. baseUrl: function( url ) {
  1129. return 'themes.php' + url;
  1130. },
  1131. themePath: '?theme=',
  1132. searchPath: '?search=',
  1133. search: function( query ) {
  1134. $( '.wp-filter-search' ).val( query.replace( /\+/g, ' ' ) );
  1135. },
  1136. themes: function() {
  1137. $( '.wp-filter-search' ).val( '' );
  1138. },
  1139. navigate: navigateRouter
  1140. });
  1141. // Execute and setup the application
  1142. themes.Run = {
  1143. init: function() {
  1144. // Initializes the blog's theme library view
  1145. // Create a new collection with data
  1146. this.themes = new themes.Collection( themes.data.themes );
  1147. // Set up the view
  1148. this.view = new themes.view.Appearance({
  1149. collection: this.themes
  1150. });
  1151. this.render();
  1152. // Start debouncing user searches after Backbone.history.start().
  1153. this.view.SearchView.doSearch = _.debounce( this.view.SearchView.doSearch, 500 );
  1154. },
  1155. render: function() {
  1156. // Render results
  1157. this.view.render();
  1158. this.routes();
  1159. if ( Backbone.History.started ) {
  1160. Backbone.history.stop();
  1161. }
  1162. Backbone.history.start({
  1163. root: themes.data.settings.adminUrl,
  1164. pushState: true,
  1165. hashChange: false
  1166. });
  1167. },
  1168. routes: function() {
  1169. var self = this;
  1170. // Bind to our global thx object
  1171. // so that the object is available to sub-views
  1172. themes.router = new themes.Router();
  1173. // Handles theme details route event
  1174. themes.router.on( 'route:theme', function( slug ) {
  1175. self.view.view.expand( slug );
  1176. });
  1177. themes.router.on( 'route:themes', function() {
  1178. self.themes.doSearch( '' );
  1179. self.view.trigger( 'theme:close' );
  1180. });
  1181. // Handles search route event
  1182. themes.router.on( 'route:search', function() {
  1183. $( '.wp-filter-search' ).trigger( 'keyup' );
  1184. });
  1185. this.extraRoutes();
  1186. },
  1187. extraRoutes: function() {
  1188. return false;
  1189. }
  1190. };
  1191. // Extend the main Search view
  1192. themes.view.InstallerSearch = themes.view.Search.extend({
  1193. events: {
  1194. 'input': 'search',
  1195. 'keyup': 'search'
  1196. },
  1197. terms: '',
  1198. // Handles Ajax request for searching through themes in public repo
  1199. search: function( event ) {
  1200. // Tabbing or reverse tabbing into the search input shouldn't trigger a search
  1201. if ( event.type === 'keyup' && ( event.which === 9 || event.which === 16 ) ) {
  1202. return;
  1203. }
  1204. this.collection = this.options.parent.view.collection;
  1205. // Clear on escape.
  1206. if ( event.type === 'keyup' && event.which === 27 ) {
  1207. event.target.value = '';
  1208. }
  1209. this.doSearch( event.target.value );
  1210. },
  1211. doSearch: function( value ) {
  1212. var request = {};
  1213. // Don't do anything if the search terms haven't changed.
  1214. if ( this.terms === value ) {
  1215. return;
  1216. }
  1217. // Updates terms with the value passed.
  1218. this.terms = value;
  1219. request.search = value;
  1220. // Intercept an [author] search.
  1221. //
  1222. // If input value starts with `author:` send a request
  1223. // for `author` instead of a regular `search`
  1224. if ( value.substring( 0, 7 ) === 'author:' ) {
  1225. request.search = '';
  1226. request.author = value.slice( 7 );
  1227. }
  1228. // Intercept a [tag] search.
  1229. //
  1230. // If input value starts with `tag:` send a request
  1231. // for `tag` instead of a regular `search`
  1232. if ( value.substring( 0, 4 ) === 'tag:' ) {
  1233. request.search = '';
  1234. request.tag = [ value.slice( 4 ) ];
  1235. }
  1236. $( '.filter-links li > a.current' )
  1237. .removeClass( 'current' )
  1238. .removeAttr( 'aria-current' );
  1239. $( 'body' ).removeClass( 'show-filters filters-applied show-favorites-form' );
  1240. $( '.drawer-toggle' ).attr( 'aria-expanded', 'false' );
  1241. // Get the themes by sending Ajax POST request to api.wordpress.org/themes
  1242. // or searching the local cache
  1243. this.collection.query( request );
  1244. // Set route
  1245. themes.router.navigate( themes.router.baseUrl( themes.router.searchPath + encodeURIComponent( value ) ), { replace: true } );
  1246. }
  1247. });
  1248. themes.view.Installer = themes.view.Appearance.extend({
  1249. el: '#wpbody-content .wrap',
  1250. // Register events for sorting and filters in theme-navigation
  1251. events: {
  1252. 'click .filter-links li > a': 'onSort',
  1253. 'click .theme-filter': 'onFilter',
  1254. 'click .drawer-toggle': 'moreFilters',
  1255. 'click .filter-drawer .apply-filters': 'applyFilters',
  1256. 'click .filter-group [type="checkbox"]': 'addFilter',
  1257. 'click .filter-drawer .clear-filters': 'clearFilters',
  1258. 'click .edit-filters': 'backToFilters',
  1259. 'click .favorites-form-submit' : 'saveUsername',
  1260. 'keyup #wporg-username-input': 'saveUsername'
  1261. },
  1262. // Initial render method
  1263. render: function() {
  1264. var self = this;
  1265. this.search();
  1266. this.uploader();
  1267. this.collection = new themes.Collection();
  1268. // Bump `collection.currentQuery.page` and request more themes if we hit the end of the page.
  1269. this.listenTo( this, 'theme:end', function() {
  1270. // Make sure we are not already loading
  1271. if ( self.collection.loadingThemes ) {
  1272. return;
  1273. }
  1274. // Set loadingThemes to true and bump page instance of currentQuery.
  1275. self.collection.loadingThemes = true;
  1276. self.collection.currentQuery.page++;
  1277. // Use currentQuery.page to build the themes request.
  1278. _.extend( self.collection.currentQuery.request, { page: self.collection.currentQuery.page } );
  1279. self.collection.query( self.collection.currentQuery.request );
  1280. });
  1281. this.listenTo( this.collection, 'query:success', function() {
  1282. $( 'body' ).removeClass( 'loading-content' );
  1283. $( '.theme-browser' ).find( 'div.error' ).remove();
  1284. });
  1285. this.listenTo( this.collection, 'query:fail', function() {
  1286. $( 'body' ).removeClass( 'loading-content' );
  1287. $( '.theme-browser' ).find( 'div.error' ).remove();
  1288. $( '.theme-browser' ).find( 'div.themes' ).before( '<div class="error"><p>' + l10n.error + '</p><p><button class="button try-again">' + l10n.tryAgain + '</button></p></div>' );
  1289. $( '.theme-browser .error .try-again' ).on( 'click', function( e ) {
  1290. e.preventDefault();
  1291. $( 'input.wp-filter-search' ).trigger( 'input' );
  1292. } );
  1293. });
  1294. if ( this.view ) {
  1295. this.view.remove();
  1296. }
  1297. // Set ups the view and passes the section argument
  1298. this.view = new themes.view.Themes({
  1299. collection: this.collection,
  1300. parent: this
  1301. });
  1302. // Reset pagination every time the install view handler is run
  1303. this.page = 0;
  1304. // Render and append
  1305. this.$el.find( '.themes' ).remove();
  1306. this.view.render();
  1307. this.$el.find( '.theme-browser' ).append( this.view.el ).addClass( 'rendered' );
  1308. },
  1309. // Handles all the rendering of the public theme directory
  1310. browse: function( section ) {
  1311. // Create a new collection with the proper theme data
  1312. // for each section
  1313. this.collection.query( { browse: section } );
  1314. },
  1315. // Sorting navigation
  1316. onSort: function( event ) {
  1317. var $el = $( event.target ),
  1318. sort = $el.data( 'sort' );
  1319. event.preventDefault();
  1320. $( 'body' ).removeClass( 'filters-applied show-filters' );
  1321. $( '.drawer-toggle' ).attr( 'aria-expanded', 'false' );
  1322. // Bail if this is already active
  1323. if ( $el.hasClass( this.activeClass ) ) {
  1324. return;
  1325. }
  1326. this.sort( sort );
  1327. // Trigger a router.naviagte update
  1328. themes.router.navigate( themes.router.baseUrl( themes.router.browsePath + sort ) );
  1329. },
  1330. sort: function( sort ) {
  1331. this.clearSearch();
  1332. // Track sorting so we can restore the correct tab when closing preview.
  1333. themes.router.selectedTab = sort;
  1334. $( '.filter-links li > a, .theme-filter' )
  1335. .removeClass( this.activeClass )
  1336. .removeAttr( 'aria-current' );
  1337. $( '[data-sort="' + sort + '"]' )
  1338. .addClass( this.activeClass )
  1339. .attr( 'aria-current', 'page' );
  1340. if ( 'favorites' === sort ) {
  1341. $( 'body' ).addClass( 'show-favorites-form' );
  1342. } else {
  1343. $( 'body' ).removeClass( 'show-favorites-form' );
  1344. }
  1345. this.browse( sort );
  1346. },
  1347. // Filters and Tags
  1348. onFilter: function( event ) {
  1349. var request,
  1350. $el = $( event.target ),
  1351. filter = $el.data( 'filter' );
  1352. // Bail if this is already active
  1353. if ( $el.hasClass( this.activeClass ) ) {
  1354. return;
  1355. }
  1356. $( '.filter-links li > a, .theme-section' )
  1357. .removeClass( this.activeClass )
  1358. .removeAttr( 'aria-current' );
  1359. $el
  1360. .addClass( this.activeClass )
  1361. .attr( 'aria-current', 'page' );
  1362. if ( ! filter ) {
  1363. return;
  1364. }
  1365. // Construct the filter request
  1366. // using the default values
  1367. filter = _.union( [ filter, this.filtersChecked() ] );
  1368. request = { tag: [ filter ] };
  1369. // Get the themes by sending Ajax POST request to api.wordpress.org/themes
  1370. // or searching the local cache
  1371. this.collection.query( request );
  1372. },
  1373. // Clicking on a checkbox to add another filter to the request
  1374. addFilter: function() {
  1375. this.filtersChecked();
  1376. },
  1377. // Applying filters triggers a tag request
  1378. applyFilters: function( event ) {
  1379. var name,
  1380. tags = this.filtersChecked(),
  1381. request = { tag: tags },
  1382. filteringBy = $( '.filtered-by .tags' );
  1383. if ( event ) {
  1384. event.preventDefault();
  1385. }
  1386. if ( ! tags ) {
  1387. wp.a11y.speak( l10n.selectFeatureFilter );
  1388. return;
  1389. }
  1390. $( 'body' ).addClass( 'filters-applied' );
  1391. $( '.filter-links li > a.current' )
  1392. .removeClass( 'current' )
  1393. .removeAttr( 'aria-current' );
  1394. filteringBy.empty();
  1395. _.each( tags, function( tag ) {
  1396. name = $( 'label[for="filter-id-' + tag + '"]' ).text();
  1397. filteringBy.append( '<span class="tag">' + name + '</span>' );
  1398. });
  1399. // Get the themes by sending Ajax POST request to api.wordpress.org/themes
  1400. // or searching the local cache
  1401. this.collection.query( request );
  1402. },
  1403. // Save the user's WordPress.org username and get his favorite themes.
  1404. saveUsername: function ( event ) {
  1405. var username = $( '#wporg-username-input' ).val(),
  1406. nonce = $( '#wporg-username-nonce' ).val(),
  1407. request = { browse: 'favorites', user: username },
  1408. that = this;
  1409. if ( event ) {
  1410. event.preventDefault();
  1411. }
  1412. // save username on enter
  1413. if ( event.type === 'keyup' && event.which !== 13 ) {
  1414. return;
  1415. }
  1416. return wp.ajax.send( 'save-wporg-username', {
  1417. data: {
  1418. _wpnonce: nonce,
  1419. username: username
  1420. },
  1421. success: function () {
  1422. // Get the themes by sending Ajax POST request to api.wordpress.org/themes
  1423. // or searching the local cache
  1424. that.collection.query( request );
  1425. }
  1426. } );
  1427. },
  1428. // Get the checked filters
  1429. // @return {array} of tags or false
  1430. filtersChecked: function() {
  1431. var items = $( '.filter-group' ).find( ':checkbox' ),
  1432. tags = [];
  1433. _.each( items.filter( ':checked' ), function( item ) {
  1434. tags.push( $( item ).prop( 'value' ) );
  1435. });
  1436. // When no filters are checked, restore initial state and return
  1437. if ( tags.length === 0 ) {
  1438. $( '.filter-drawer .apply-filters' ).find( 'span' ).text( '' );
  1439. $( '.filter-drawer .clear-filters' ).hide();
  1440. $( 'body' ).removeClass( 'filters-applied' );
  1441. return false;
  1442. }
  1443. $( '.filter-drawer .apply-filters' ).find( 'span' ).text( tags.length );
  1444. $( '.filter-drawer .clear-filters' ).css( 'display', 'inline-block' );
  1445. return tags;
  1446. },
  1447. activeClass: 'current',
  1448. /*
  1449. * When users press the "Upload Theme" button, show the upload form in place.
  1450. */
  1451. uploader: function() {
  1452. var uploadViewToggle = $( '.upload-view-toggle' ),
  1453. $body = $( document.body );
  1454. uploadViewToggle.on( 'click', function() {
  1455. // Toggle the upload view.
  1456. $body.toggleClass( 'show-upload-view' );
  1457. // Toggle the `aria-expanded` button attribute.
  1458. uploadViewToggle.attr( 'aria-expanded', $body.hasClass( 'show-upload-view' ) );
  1459. });
  1460. },
  1461. // Toggle the full filters navigation
  1462. moreFilters: function( event ) {
  1463. var $body = $( 'body' ),
  1464. $toggleButton = $( '.drawer-toggle' );
  1465. event.preventDefault();
  1466. if ( $body.hasClass( 'filters-applied' ) ) {
  1467. return this.backToFilters();
  1468. }
  1469. this.clearSearch();
  1470. themes.router.navigate( themes.router.baseUrl( '' ) );
  1471. // Toggle the feature filters view.
  1472. $body.toggleClass( 'show-filters' );
  1473. // Toggle the `aria-expanded` button attribute.
  1474. $toggleButton.attr( 'aria-expanded', $body.hasClass( 'show-filters' ) );
  1475. },
  1476. // Clears all the checked filters
  1477. // @uses filtersChecked()
  1478. clearFilters: function( event ) {
  1479. var items = $( '.filter-group' ).find( ':checkbox' ),
  1480. self = this;
  1481. event.preventDefault();
  1482. _.each( items.filter( ':checked' ), function( item ) {
  1483. $( item ).prop( 'checked', false );
  1484. return self.filtersChecked();
  1485. });
  1486. },
  1487. backToFilters: function( event ) {
  1488. if ( event ) {
  1489. event.preventDefault();
  1490. }
  1491. $( 'body' ).removeClass( 'filters-applied' );
  1492. },
  1493. clearSearch: function() {
  1494. $( '#wp-filter-search-input').val( '' );
  1495. }
  1496. });
  1497. themes.InstallerRouter = Backbone.Router.extend({
  1498. routes: {
  1499. 'theme-install.php?theme=:slug': 'preview',
  1500. 'theme-install.php?browse=:sort': 'sort',

Large files files are truncated, but you can click here to view the full file