PageRenderTime 57ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 1ms

/wp-admin/js/theme.js

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