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

/wordpress/wp-admin/js/revisions.js

https://gitlab.com/Blueprint-Marketing/wordpress-unit-tests
JavaScript | 974 lines | 704 code | 167 blank | 103 comment | 76 complexity | 99c224d39514612eb22e75e08bc97b4b MD5 | raw file
  1. window.wp = window.wp || {};
  2. (function($) {
  3. var revisions;
  4. revisions = wp.revisions = { model: {}, view: {}, controller: {} };
  5. // Link settings.
  6. revisions.settings = _.isUndefined( _wpRevisionsSettings ) ? {} : _wpRevisionsSettings;
  7. // For debugging
  8. revisions.debug = false;
  9. revisions.log = function() {
  10. if ( window.console && revisions.debug )
  11. console.log.apply( console, arguments );
  12. };
  13. // wp_localize_script transforms top-level numbers into strings. Undo that.
  14. if ( revisions.settings.to )
  15. revisions.settings.to = parseInt( revisions.settings.to, 10 );
  16. if ( revisions.settings.from )
  17. revisions.settings.from = parseInt( revisions.settings.from, 10 );
  18. // wp_localize_script does not allow for top-level booleans. Fix that.
  19. if ( revisions.settings.compareTwoMode )
  20. revisions.settings.compareTwoMode = revisions.settings.compareTwoMode === '1';
  21. /**
  22. * ========================================================================
  23. * MODELS
  24. * ========================================================================
  25. */
  26. revisions.model.Slider = Backbone.Model.extend({
  27. defaults: {
  28. value: null,
  29. values: null,
  30. min: 0,
  31. max: 1,
  32. step: 1,
  33. range: false,
  34. compareTwoMode: false
  35. },
  36. initialize: function( options ) {
  37. this.frame = options.frame;
  38. this.revisions = options.revisions;
  39. // Listen for changes to the revisions or mode from outside
  40. this.listenTo( this.frame, 'update:revisions', this.receiveRevisions );
  41. this.listenTo( this.frame, 'change:compareTwoMode', this.updateMode );
  42. // Listen for internal changes
  43. this.listenTo( this, 'change:from', this.handleLocalChanges );
  44. this.listenTo( this, 'change:to', this.handleLocalChanges );
  45. this.listenTo( this, 'change:compareTwoMode', this.updateSliderSettings );
  46. this.listenTo( this, 'update:revisions', this.updateSliderSettings );
  47. // Listen for changes to the hovered revision
  48. this.listenTo( this, 'change:hoveredRevision', this.hoverRevision );
  49. this.set({
  50. max: this.revisions.length - 1,
  51. compareTwoMode: this.frame.get('compareTwoMode'),
  52. from: this.frame.get('from'),
  53. to: this.frame.get('to')
  54. });
  55. this.updateSliderSettings();
  56. },
  57. getSliderValue: function( a, b ) {
  58. return isRtl ? this.revisions.length - this.revisions.indexOf( this.get(a) ) - 1 : this.revisions.indexOf( this.get(b) );
  59. },
  60. updateSliderSettings: function() {
  61. if ( this.get('compareTwoMode') ) {
  62. this.set({
  63. values: [
  64. this.getSliderValue( 'to', 'from' ),
  65. this.getSliderValue( 'from', 'to' )
  66. ],
  67. value: null,
  68. range: true // ensures handles cannot cross
  69. });
  70. } else {
  71. this.set({
  72. value: this.getSliderValue( 'to', 'to' ),
  73. values: null,
  74. range: false
  75. });
  76. }
  77. this.trigger( 'update:slider' );
  78. },
  79. // Called when a revision is hovered
  80. hoverRevision: function( model, value ) {
  81. this.trigger( 'hovered:revision', value );
  82. },
  83. // Called when `compareTwoMode` changes
  84. updateMode: function( model, value ) {
  85. this.set({ compareTwoMode: value });
  86. },
  87. // Called when `from` or `to` changes in the local model
  88. handleLocalChanges: function() {
  89. this.frame.set({
  90. from: this.get('from'),
  91. to: this.get('to')
  92. });
  93. },
  94. // Receives revisions changes from outside the model
  95. receiveRevisions: function( from, to ) {
  96. // Bail if nothing changed
  97. if ( this.get('from') === from && this.get('to') === to )
  98. return;
  99. this.set({ from: from, to: to }, { silent: true });
  100. this.trigger( 'update:revisions', from, to );
  101. }
  102. });
  103. revisions.model.Tooltip = Backbone.Model.extend({
  104. defaults: {
  105. revision: null,
  106. hovering: false, // Whether the mouse is hovering
  107. scrubbing: false // Whether the mouse is scrubbing
  108. },
  109. initialize: function( options ) {
  110. this.revisions = options.revisions;
  111. this.slider = options.slider;
  112. this.listenTo( this.slider, 'hovered:revision', this.updateRevision );
  113. this.listenTo( this.slider, 'change:hovering', this.setHovering );
  114. this.listenTo( this.slider, 'change:scrubbing', this.setScrubbing );
  115. },
  116. updateRevision: function( revision ) {
  117. this.set({ revision: revision });
  118. },
  119. setHovering: function( model, value ) {
  120. this.set({ hovering: value });
  121. },
  122. setScrubbing: function( model, value ) {
  123. this.set({ scrubbing: value });
  124. }
  125. });
  126. revisions.model.Revision = Backbone.Model.extend({});
  127. revisions.model.Revisions = Backbone.Collection.extend({
  128. model: revisions.model.Revision,
  129. initialize: function() {
  130. _.bindAll( this, 'next', 'prev' );
  131. },
  132. next: function( revision ) {
  133. var index = this.indexOf( revision );
  134. if ( index !== -1 && index !== this.length - 1 )
  135. return this.at( index + 1 );
  136. },
  137. prev: function( revision ) {
  138. var index = this.indexOf( revision );
  139. if ( index !== -1 && index !== 0 )
  140. return this.at( index - 1 );
  141. }
  142. });
  143. revisions.model.Field = Backbone.Model.extend({});
  144. revisions.model.Fields = Backbone.Collection.extend({
  145. model: revisions.model.Field
  146. });
  147. revisions.model.Diff = Backbone.Model.extend({
  148. initialize: function( attributes, options ) {
  149. var fields = this.get('fields');
  150. this.unset('fields');
  151. this.fields = new revisions.model.Fields( fields );
  152. }
  153. });
  154. revisions.model.Diffs = Backbone.Collection.extend({
  155. initialize: function( models, options ) {
  156. _.bindAll( this, 'getClosestUnloaded' );
  157. this.loadAll = _.once( this._loadAll );
  158. this.revisions = options.revisions;
  159. this.requests = {};
  160. },
  161. model: revisions.model.Diff,
  162. ensure: function( id, context ) {
  163. var diff = this.get( id );
  164. var request = this.requests[ id ];
  165. var deferred = $.Deferred();
  166. var ids = {};
  167. var from = id.split(':')[0];
  168. var to = id.split(':')[1];
  169. ids[id] = true;
  170. wp.revisions.log( 'ensure', id );
  171. this.trigger( 'ensure', ids, from, to, deferred.promise() );
  172. if ( diff ) {
  173. deferred.resolveWith( context, [ diff ] );
  174. } else {
  175. this.trigger( 'ensure:load', ids, from, to, deferred.promise() );
  176. _.each( ids, _.bind( function( id ) {
  177. // Remove anything that has an ongoing request
  178. if ( this.requests[ id ] )
  179. delete ids[ id ];
  180. // Remove anything we already have
  181. if ( this.get( id ) )
  182. delete ids[ id ];
  183. }, this ) );
  184. if ( ! request ) {
  185. // Always include the ID that started this ensure
  186. ids[ id ] = true;
  187. request = this.load( _.keys( ids ) );
  188. }
  189. request.done( _.bind( function() {
  190. deferred.resolveWith( context, [ this.get( id ) ] );
  191. }, this ) ).fail( _.bind( function() {
  192. deferred.reject();
  193. }) );
  194. }
  195. return deferred.promise();
  196. },
  197. // Returns an array of proximal diffs
  198. getClosestUnloaded: function( ids, centerId ) {
  199. var self = this;
  200. return _.chain([0].concat( ids )).initial().zip( ids ).sortBy( function( pair ) {
  201. return Math.abs( centerId - pair[1] );
  202. }).map( function( pair ) {
  203. return pair.join(':');
  204. }).filter( function( diffId ) {
  205. return _.isUndefined( self.get( diffId ) ) && ! self.requests[ diffId ];
  206. }).value();
  207. },
  208. _loadAll: function( allRevisionIds, centerId, num ) {
  209. var self = this, deferred = $.Deferred();
  210. diffs = _.first( this.getClosestUnloaded( allRevisionIds, centerId ), num );
  211. if ( _.size( diffs ) > 0 ) {
  212. this.load( diffs ).done( function() {
  213. self._loadAll( allRevisionIds, centerId, num ).done( function() {
  214. deferred.resolve();
  215. });
  216. }).fail( function() {
  217. if ( 1 === num ) { // Already tried 1. This just isn't working. Give up.
  218. deferred.reject();
  219. } else { // Request fewer diffs this time
  220. self._loadAll( allRevisionIds, centerId, Math.ceil( num / 2 ) ).done( function() {
  221. deferred.resolve();
  222. });
  223. }
  224. });
  225. } else {
  226. deferred.resolve();
  227. }
  228. return deferred;
  229. },
  230. load: function( comparisons ) {
  231. wp.revisions.log( 'load', comparisons );
  232. // Our collection should only ever grow, never shrink, so remove: false
  233. return this.fetch({ data: { compare: comparisons }, remove: false }).done( function(){
  234. wp.revisions.log( 'load:complete', comparisons );
  235. });
  236. },
  237. sync: function( method, model, options ) {
  238. if ( 'read' === method ) {
  239. options = options || {};
  240. options.context = this;
  241. options.data = _.extend( options.data || {}, {
  242. action: 'get-revision-diffs',
  243. post_id: revisions.settings.postId
  244. });
  245. var deferred = wp.ajax.send( options );
  246. var requests = this.requests;
  247. // Record that we're requesting each diff.
  248. if ( options.data.compare ) {
  249. _.each( options.data.compare, function( id ) {
  250. requests[ id ] = deferred;
  251. });
  252. }
  253. // When the request completes, clear the stored request.
  254. deferred.always( function() {
  255. if ( options.data.compare ) {
  256. _.each( options.data.compare, function( id ) {
  257. delete requests[ id ];
  258. });
  259. }
  260. });
  261. return deferred;
  262. // Otherwise, fall back to `Backbone.sync()`.
  263. } else {
  264. return Backbone.Model.prototype.sync.apply( this, arguments );
  265. }
  266. }
  267. });
  268. revisions.model.FrameState = Backbone.Model.extend({
  269. defaults: {
  270. loading: false,
  271. error: false,
  272. compareTwoMode: false
  273. },
  274. initialize: function( attributes, options ) {
  275. var properties = {};
  276. _.bindAll( this, 'receiveDiff' );
  277. this._debouncedEnsureDiff = _.debounce( this._ensureDiff, 200 );
  278. this.revisions = options.revisions;
  279. this.diffs = new revisions.model.Diffs( [], { revisions: this.revisions });
  280. // Set the initial diffs collection provided through the settings
  281. this.diffs.set( revisions.settings.diffData );
  282. // Set up internal listeners
  283. this.listenTo( this, 'change:from', this.changeRevisionHandler );
  284. this.listenTo( this, 'change:to', this.changeRevisionHandler );
  285. this.listenTo( this, 'change:compareTwoMode', this.changeMode );
  286. this.listenTo( this, 'update:revisions', this.updatedRevisions );
  287. this.listenTo( this.diffs, 'ensure:load', this.updateLoadingStatus );
  288. this.listenTo( this, 'update:diff', this.updateLoadingStatus );
  289. // Set the initial revisions, baseUrl, and mode as provided through settings
  290. properties.to = this.revisions.get( revisions.settings.to );
  291. properties.from = this.revisions.get( revisions.settings.from );
  292. properties.compareTwoMode = revisions.settings.compareTwoMode;
  293. properties.baseUrl = revisions.settings.baseUrl;
  294. this.set( properties );
  295. // Start the router if browser supports History API
  296. if ( window.history && window.history.pushState ) {
  297. this.router = new revisions.Router({ model: this });
  298. Backbone.history.start({ pushState: true });
  299. }
  300. },
  301. updateLoadingStatus: function() {
  302. this.set( 'error', false );
  303. this.set( 'loading', ! this.diff() );
  304. },
  305. changeMode: function( model, value ) {
  306. // If we were on the first revision before switching, we have to bump them over one
  307. if ( value && 0 === this.revisions.indexOf( this.get('to') ) ) {
  308. this.set({
  309. from: this.revisions.at(0),
  310. to: this.revisions.at(1)
  311. });
  312. }
  313. },
  314. updatedRevisions: function( from, to ) {
  315. if ( this.get( 'compareTwoMode' ) ) {
  316. // TODO: compare-two loading strategy
  317. } else {
  318. this.diffs.loadAll( this.revisions.pluck('id'), to.id, 40 );
  319. }
  320. },
  321. // Fetch the currently loaded diff.
  322. diff: function() {
  323. return this.diffs.get( this._diffId );
  324. },
  325. // So long as `from` and `to` are changed at the same time, the diff
  326. // will only be updated once. This is because Backbone updates all of
  327. // the changed attributes in `set`, and then fires the `change` events.
  328. updateDiff: function( options ) {
  329. var from, to, diffId, diff;
  330. options = options || {};
  331. from = this.get('from');
  332. to = this.get('to');
  333. diffId = ( from ? from.id : 0 ) + ':' + to.id;
  334. // Check if we're actually changing the diff id.
  335. if ( this._diffId === diffId )
  336. return $.Deferred().reject().promise();
  337. this._diffId = diffId;
  338. this.trigger( 'update:revisions', from, to );
  339. diff = this.diffs.get( diffId );
  340. // If we already have the diff, then immediately trigger the update.
  341. if ( diff ) {
  342. this.receiveDiff( diff );
  343. return $.Deferred().resolve().promise();
  344. // Otherwise, fetch the diff.
  345. } else {
  346. if ( options.immediate ) {
  347. return this._ensureDiff();
  348. } else {
  349. this._debouncedEnsureDiff();
  350. return $.Deferred().reject().promise();
  351. }
  352. }
  353. },
  354. // A simple wrapper around `updateDiff` to prevent the change event's
  355. // parameters from being passed through.
  356. changeRevisionHandler: function( model, value, options ) {
  357. this.updateDiff();
  358. },
  359. receiveDiff: function( diff ) {
  360. // Did we actually get a diff?
  361. if ( _.isUndefined( diff ) || _.isUndefined( diff.id ) ) {
  362. this.set({
  363. loading: false,
  364. error: true
  365. });
  366. } else if ( this._diffId === diff.id ) { // Make sure the current diff didn't change
  367. this.trigger( 'update:diff', diff );
  368. }
  369. },
  370. _ensureDiff: function() {
  371. return this.diffs.ensure( this._diffId, this ).always( this.receiveDiff );
  372. }
  373. });
  374. /**
  375. * ========================================================================
  376. * VIEWS
  377. * ========================================================================
  378. */
  379. // The frame view. This contains the entire page.
  380. revisions.view.Frame = wp.Backbone.View.extend({
  381. className: 'revisions',
  382. template: wp.template('revisions-frame'),
  383. initialize: function() {
  384. this.listenTo( this.model, 'update:diff', this.renderDiff );
  385. this.listenTo( this.model, 'change:compareTwoMode', this.updateCompareTwoMode );
  386. this.listenTo( this.model, 'change:loading', this.updateLoadingStatus );
  387. this.listenTo( this.model, 'change:error', this.updateErrorStatus );
  388. this.views.set( '.revisions-control-frame', new revisions.view.Controls({
  389. model: this.model
  390. }) );
  391. },
  392. render: function() {
  393. wp.Backbone.View.prototype.render.apply( this, arguments );
  394. $('#wpbody-content .wrap').append( this.el );
  395. this.updateCompareTwoMode();
  396. this.renderDiff( this.model.diff() );
  397. this.views.ready();
  398. return this;
  399. },
  400. renderDiff: function( diff ) {
  401. this.views.set( '.revisions-diff-frame', new revisions.view.Diff({
  402. model: diff
  403. }) );
  404. },
  405. updateLoadingStatus: function() {
  406. this.$el.toggleClass( 'loading', this.model.get('loading') );
  407. },
  408. updateErrorStatus: function() {
  409. this.$el.toggleClass( 'diff-error', this.model.get('error') );
  410. },
  411. updateCompareTwoMode: function() {
  412. this.$el.toggleClass( 'comparing-two-revisions', this.model.get('compareTwoMode') );
  413. }
  414. });
  415. // The control view.
  416. // This contains the revision slider, previous/next buttons, the meta info and the compare checkbox.
  417. revisions.view.Controls = wp.Backbone.View.extend({
  418. className: 'revisions-controls',
  419. initialize: function() {
  420. // Add the button view
  421. this.views.add( new revisions.view.Buttons({
  422. model: this.model
  423. }) );
  424. // Add the checkbox view
  425. this.views.add( new revisions.view.Checkbox({
  426. model: this.model
  427. }) );
  428. // Prep the slider model
  429. var slider = new revisions.model.Slider({
  430. frame: this.model,
  431. revisions: this.model.revisions
  432. });
  433. // Add the tooltip view
  434. this.views.add( new revisions.view.Tooltip({
  435. model: new revisions.model.Tooltip({
  436. revisions: this.model.revisions,
  437. slider: slider
  438. })
  439. }) );
  440. // Add the tickmarks view
  441. this.views.add( new revisions.view.Tickmarks({
  442. model: this.model
  443. }) );
  444. // Add the slider view
  445. this.views.add( new revisions.view.Slider({
  446. model: slider
  447. }) );
  448. // Add the Meta view
  449. this.views.add( new revisions.view.Meta({
  450. model: this.model
  451. }) );
  452. }
  453. });
  454. // The tickmarks view
  455. revisions.view.Tickmarks = wp.Backbone.View.extend({
  456. className: 'revisions-tickmarks',
  457. ready: function() {
  458. var tickCount, tickWidth;
  459. tickCount = this.model.revisions.length - 1;
  460. tickWidth = 1 / tickCount;
  461. _(tickCount).times( function(){ this.$el.append( '<div></div>' ); }, this );
  462. this.$('div').css( 'width', ( 100 * tickWidth ) + '%' );
  463. }
  464. });
  465. // The meta view
  466. revisions.view.Meta = wp.Backbone.View.extend({
  467. className: 'revisions-meta',
  468. template: wp.template('revisions-meta'),
  469. events: {
  470. 'click .restore-revision': 'restoreRevision'
  471. },
  472. initialize: function() {
  473. this.listenTo( this.model, 'update:revisions', this.ready );
  474. },
  475. prepare: function() {
  476. return this.model.toJSON();
  477. },
  478. ready: function() {
  479. this.$('.restore-revision').prop( 'disabled', this.model.get('to').get('current') );
  480. },
  481. restoreRevision: function() {
  482. var restoreUrl = this.model.get('to').attributes.restoreUrl.replace(/&amp;/g, '&');
  483. document.location = restoreUrl;
  484. }
  485. });
  486. // The checkbox view.
  487. revisions.view.Checkbox = wp.Backbone.View.extend({
  488. className: 'revisions-checkbox',
  489. template: wp.template('revisions-checkbox'),
  490. events: {
  491. 'click .compare-two-revisions': 'compareTwoToggle'
  492. },
  493. initialize: function() {
  494. this.listenTo( this.model, 'change:compareTwoMode', this.updateCompareTwoMode );
  495. },
  496. ready: function() {
  497. if ( this.model.revisions.length < 3 )
  498. $('.revision-toggle-compare-mode').hide();
  499. },
  500. updateCompareTwoMode: function() {
  501. this.$('.compare-two-revisions').prop( 'checked', this.model.get('compareTwoMode') );
  502. },
  503. // Toggle the compare two mode feature when the compare two checkbox is checked.
  504. compareTwoToggle: function( event ) {
  505. // Activate compare two mode?
  506. this.model.set({ compareTwoMode: $('.compare-two-revisions').prop('checked') });
  507. }
  508. });
  509. // The tooltip view.
  510. // Encapsulates the tooltip.
  511. revisions.view.Tooltip = wp.Backbone.View.extend({
  512. className: 'revisions-tooltip',
  513. template: wp.template('revisions-tooltip'),
  514. initialize: function( options ) {
  515. this.listenTo( this.model, 'change:revision', this.render );
  516. this.listenTo( this.model, 'change:hovering', this.toggleVisibility );
  517. this.listenTo( this.model, 'change:scrubbing', this.toggleVisibility );
  518. },
  519. visible: function() {
  520. return this.model.get( 'scrubbing' ) || this.model.get( 'hovering' );
  521. },
  522. toggleVisibility: function( options ) {
  523. if ( this.visible() )
  524. this.$el.stop().show().fadeTo( 100 - this.el.style.opacity * 100, 1 );
  525. else
  526. this.$el.stop().fadeTo( this.el.style.opacity * 300, 0, function(){ $(this).hide(); } );
  527. return;
  528. },
  529. render: function() {
  530. var offset;
  531. // Check if a revision exists.
  532. if ( _.isNull( this.model.get('revision') ) )
  533. return;
  534. this.$el.html( this.template( this.model.get('revision').toJSON() ) );
  535. // Set the position.
  536. offset = this.model.revisions.indexOf( this.model.get('revision') ) / ( this.model.revisions.length - 1 );
  537. // 15% to get us to the start of the slider
  538. // 0.7 to convert the slider-relative percentage to a page-relative percentage
  539. // 100 to convert to a percentage
  540. offset = 15 + (0.7 * offset * 100 ); // Now in a percentage
  541. this.$el.css( isRtl ? 'right' : 'left', offset + '%' );
  542. }
  543. });
  544. // The buttons view.
  545. // Encapsulates all of the configuration for the previous/next buttons.
  546. revisions.view.Buttons = wp.Backbone.View.extend({
  547. className: 'revisions-buttons',
  548. template: wp.template('revisions-buttons'),
  549. events: {
  550. 'click #next': 'nextRevision',
  551. 'click #previous': 'previousRevision'
  552. },
  553. initialize: function() {
  554. this.listenTo( this.model, 'update:revisions', this.disabledButtonCheck );
  555. },
  556. ready: function() {
  557. this.disabledButtonCheck();
  558. },
  559. // Go to a specific modelindex, taking into account RTL mode.
  560. gotoModel: function( toIndex ) {
  561. var attributes = {
  562. to: this.model.revisions.at( isRtl ? this.model.revisions.length - toIndex - 1 : toIndex ) // Reverse directions for RTL.
  563. };
  564. // If we're at the first revision, unset 'from'.
  565. if ( isRtl ? this.model.revisions.length - toIndex - 1 : toIndex ) // Reverse directions for RTL
  566. attributes.from = this.model.revisions.at( isRtl ? this.model.revisions.length - toIndex - 2 : toIndex - 1 );
  567. else
  568. this.model.unset('from', { silent: true });
  569. this.model.set( attributes );
  570. },
  571. // Go to the 'next' revision, direction takes into account RTL mode.
  572. nextRevision: function() {
  573. var toIndex = isRtl ? this.model.revisions.length - this.model.revisions.indexOf( this.model.get('to') ) - 1 : this.model.revisions.indexOf( this.model.get('to') );
  574. toIndex = isRtl ? toIndex - 1 : toIndex + 1;
  575. this.gotoModel( toIndex );
  576. },
  577. // Go to the 'previous' revision, direction takes into account RTL mode.
  578. previousRevision: function() {
  579. var toIndex = isRtl ? this.model.revisions.length - this.model.revisions.indexOf( this.model.get('to') ) - 1 : this.model.revisions.indexOf( this.model.get('to') );
  580. toIndex = isRtl ? toIndex + 1 : toIndex - 1;
  581. this.gotoModel( toIndex );
  582. },
  583. // Check to see if the Previous or Next buttons need to be disabled or enabled.
  584. disabledButtonCheck: function() {
  585. var maxVal = this.model.revisions.length - 1,
  586. minVal = 0,
  587. next = $('.revisions-next .button'),
  588. previous = $('.revisions-previous .button'),
  589. val = this.model.revisions.indexOf( this.model.get('to') );
  590. // Disable "Next" button if you're on the last node.
  591. next.prop( 'disabled', ( maxVal === val ) );
  592. // Disable "Previous" button if you're on the first node.
  593. previous.prop( 'disabled', ( minVal === val ) );
  594. }
  595. });
  596. // The slider view.
  597. revisions.view.Slider = wp.Backbone.View.extend({
  598. className: 'wp-slider',
  599. events: {
  600. 'mousemove' : 'mouseMove'
  601. },
  602. initialize: function() {
  603. _.bindAll( this, 'start', 'slide', 'stop', 'mouseMove', 'mouseEnter', 'mouseLeave' );
  604. this.listenTo( this.model, 'update:slider', this.applySliderSettings );
  605. },
  606. ready: function() {
  607. this.$el.slider( _.extend( this.model.toJSON(), {
  608. start: this.start,
  609. slide: this.slide,
  610. stop: this.stop
  611. }) );
  612. this.$el.hoverIntent({
  613. over: this.mouseEnter,
  614. out: this.mouseLeave,
  615. timeout: 800
  616. });
  617. this.applySliderSettings();
  618. },
  619. mouseMove: function( e ) {
  620. var zoneCount = this.model.revisions.length - 1, // One fewer zone than models
  621. sliderLeft = this.$el.offset().left, // Left edge of slider
  622. sliderWidth = this.$el.width(), // Width of slider
  623. tickWidth = sliderWidth / zoneCount, // Calculated width of zone
  624. actualX = e.clientX - sliderLeft, // Offset of mouse position in slider
  625. currentModelIndex = Math.floor( ( actualX + ( tickWidth / 2 ) ) / tickWidth ); // Calculate the model index
  626. // Reverse direction in RTL mode.
  627. if ( isRtl )
  628. currentModelIndex = this.model.revisions.length - currentModelIndex - 1;
  629. // Ensure sane value for currentModelIndex.
  630. if ( currentModelIndex < 0 )
  631. currentModelIndex = 0;
  632. else if ( currentModelIndex >= this.model.revisions.length )
  633. currentModelIndex = this.model.revisions.length - 1;
  634. // Update the tooltip model
  635. this.model.set({ hoveredRevision: this.model.revisions.at( currentModelIndex ) });
  636. },
  637. mouseLeave: function() {
  638. this.model.set({ hovering: false });
  639. },
  640. mouseEnter: function() {
  641. this.model.set({ hovering: true });
  642. },
  643. applySliderSettings: function() {
  644. this.$el.slider( _.pick( this.model.toJSON(), 'value', 'values', 'range' ) );
  645. var handles = this.$('a.ui-slider-handle');
  646. if ( this.model.get('compareTwoMode') ) {
  647. // in RTL mode the 'left handle' is the second in the slider, 'right' is first
  648. handles.first()
  649. .toggleClass( 'right-handle', !! isRtl )
  650. .toggleClass( 'left-handle', ! isRtl );
  651. handles.last()
  652. .toggleClass( 'left-handle', !! isRtl )
  653. .toggleClass( 'right-handle', ! isRtl );
  654. } else {
  655. handles.removeClass('left-handle right-handle');
  656. }
  657. },
  658. getSliderPosition: function( ui ){
  659. return isRtl ? this.model.revisions.length - ui.value - 1 : ui.value;
  660. },
  661. start: function( event, ui ) {
  662. this.model.set({ scrubbing: true });
  663. // Track the mouse position to enable smooth dragging,
  664. // overrides default jQuery UI step behavior.
  665. $( window ).on( 'mousemove.wp.revisions', { view: this }, function( e ) {
  666. var view = e.data.view,
  667. leftDragBoundary = view.$el.offset().left, // Initial left boundary
  668. sliderOffset = leftDragBoundary,
  669. sliderRightEdge = leftDragBoundary + view.$el.width(),
  670. rightDragBoundary = sliderRightEdge, // Initial right boundary
  671. leftDragReset = 0, // Initial left drag reset
  672. rightDragReset = sliderRightEdge - sliderOffset; // Initial right drag reset
  673. // In two handle mode, ensure handles can't be dragged past each other.
  674. // Adjust left/right boundaries and reset points.
  675. if ( view.model.get('compareTwoMode') ) {
  676. var rightHandle = $( ui.handle ).parent().find('.right-handle'),
  677. leftHandle = $( ui.handle ).parent().find('.left-handle');
  678. if ( $( ui.handle ).hasClass('left-handle') ) {
  679. // Dragging the left handle, boundary is right handle.
  680. // RTL mode calculations reverse directions.
  681. if ( isRtl ) {
  682. leftDragBoundary = rightHandle.offset().left + rightHandle.width();
  683. leftDragReset = leftDragBoundary - sliderOffset;
  684. } else {
  685. rightDragBoundary = rightHandle.offset().left;
  686. rightDragReset = rightDragBoundary - sliderOffset;
  687. }
  688. } else {
  689. // Dragging the right handle, boundary is the left handle.
  690. // RTL mode calculations reverse directions.
  691. if ( isRtl ) {
  692. rightDragBoundary = leftHandle.offset().left;
  693. rightDragReset = rightDragBoundary - sliderOffset;
  694. } else {
  695. leftDragBoundary = leftHandle.offset().left + leftHandle.width() ;
  696. leftDragReset = leftDragBoundary - sliderOffset;
  697. }
  698. }
  699. }
  700. // Follow mouse movements, as long as handle remains inside slider.
  701. if ( e.clientX < leftDragBoundary ) {
  702. $( ui.handle ).css( 'left', leftDragReset ); // Mouse to left of slider.
  703. } else if ( e.clientX > rightDragBoundary ) {
  704. $( ui.handle ).css( 'left', rightDragReset ); // Mouse to right of slider.
  705. } else {
  706. $( ui.handle ).css( 'left', e.clientX - sliderOffset ); // Mouse in slider.
  707. }
  708. } );
  709. },
  710. // Responds to slide events
  711. slide: function( event, ui ) {
  712. var attributes, movedRevision, sliderPosition;
  713. // Compare two revisions mode
  714. if ( this.model.get('compareTwoMode') ) {
  715. // Prevent sliders from occupying same spot
  716. if ( ui.values[1] === ui.values[0] )
  717. return false;
  718. attributes = {
  719. to: this.model.revisions.at( isRtl ? this.model.revisions.length - ui.values[0] - 1 : ui.values[1] ),
  720. from: this.model.revisions.at( isRtl ? this.model.revisions.length - ui.values[1] - 1 : ui.values[0] )
  721. };
  722. if ( isRtl )
  723. movedRevision = ui.value === ui.values[1] ? attributes.from : attributes.to;
  724. else
  725. movedRevision = ui.value === ui.values[0] ? attributes.from : attributes.to;
  726. } else {
  727. sliderPosition = this.getSliderPosition( ui );
  728. attributes = {
  729. to: this.model.revisions.at( sliderPosition )
  730. };
  731. movedRevision = attributes.to;
  732. // If we're at the first revision, unset 'from'.
  733. if ( sliderPosition ) // Reverse directions for RTL.
  734. attributes.from = this.model.revisions.at( sliderPosition - 1 );
  735. else
  736. attributes.from = undefined;
  737. }
  738. // If we are scrubbing, a scrub to a revision is considered a hover
  739. if ( this.model.get('scrubbing') )
  740. attributes.hoveredRevision = movedRevision;
  741. this.model.set( attributes );
  742. },
  743. stop: function( event, ui ) {
  744. $( window ).off('mousemove.wp.revisions');
  745. this.model.updateSliderSettings(); // To snap us back to a tick mark
  746. this.model.set({ scrubbing: false });
  747. }
  748. });
  749. // The diff view.
  750. // This is the view for the current active diff.
  751. revisions.view.Diff = wp.Backbone.View.extend({
  752. className: 'revisions-diff',
  753. template: wp.template('revisions-diff'),
  754. // Generate the options to be passed to the template.
  755. prepare: function() {
  756. return _.extend({ fields: this.model.fields.toJSON() }, this.options );
  757. }
  758. });
  759. // The revisions router
  760. // takes URLs with #hash fragments and routes them
  761. revisions.Router = Backbone.Router.extend({
  762. initialize: function( options ) {
  763. this.model = options.model;
  764. this.routes = _.object([
  765. [ this.baseUrl( '?from=:from&to=:to' ), 'handleRoute' ],
  766. [ this.baseUrl( '?from=:from&to=:to' ), 'handleRoute' ]
  767. ]);
  768. // Maintain state and history when navigating
  769. this.listenTo( this.model, 'update:diff', _.debounce( this.updateUrl, 250 ) );
  770. this.listenTo( this.model, 'change:compareTwoMode', this.updateUrl );
  771. },
  772. baseUrl: function( url ) {
  773. return this.model.get('baseUrl') + url;
  774. },
  775. updateUrl: function() {
  776. var from = this.model.has('from') ? this.model.get('from').id : 0;
  777. var to = this.model.get('to').id;
  778. if ( this.model.get('compareTwoMode' ) )
  779. this.navigate( this.baseUrl( '?from=' + from + '&to=' + to ) );
  780. else
  781. this.navigate( this.baseUrl( '?revision=' + to ) );
  782. },
  783. handleRoute: function( a, b ) {
  784. var from, to, compareTwo = _.isUndefined( b );
  785. if ( ! compareTwo ) {
  786. b = this.model.revisions.get( a );
  787. a = this.model.revisions.prev( b );
  788. b = b ? b.id : 0;
  789. a = a ? a.id : 0;
  790. }
  791. this.model.set({
  792. from: this.model.revisions.get( parseInt( a, 10 ) ),
  793. to: this.model.revisions.get( parseInt( a, 10 ) ),
  794. compareTwoMode: compareTwo
  795. });
  796. }
  797. });
  798. // Initialize the revisions UI.
  799. revisions.init = function() {
  800. revisions.view.frame = new revisions.view.Frame({
  801. model: new revisions.model.FrameState({}, {
  802. revisions: new revisions.model.Revisions( revisions.settings.revisionData )
  803. })
  804. }).render();
  805. };
  806. $( revisions.init );
  807. }(jQuery));