[ Index ] |
WordPress Cross Reference |
[Summary view] [Print] [Text view]
1 /* global _wpRevisionsSettings, isRtl */ 2 window.wp = window.wp || {}; 3 4 (function($) { 5 var revisions; 6 7 revisions = wp.revisions = { model: {}, view: {}, controller: {} }; 8 9 // Link settings. 10 revisions.settings = _.isUndefined( _wpRevisionsSettings ) ? {} : _wpRevisionsSettings; 11 12 // For debugging 13 revisions.debug = false; 14 15 revisions.log = function() { 16 if ( window.console && revisions.debug ) { 17 window.console.log.apply( window.console, arguments ); 18 } 19 }; 20 21 // Handy functions to help with positioning 22 $.fn.allOffsets = function() { 23 var offset = this.offset() || {top: 0, left: 0}, win = $(window); 24 return _.extend( offset, { 25 right: win.width() - offset.left - this.outerWidth(), 26 bottom: win.height() - offset.top - this.outerHeight() 27 }); 28 }; 29 30 $.fn.allPositions = function() { 31 var position = this.position() || {top: 0, left: 0}, parent = this.parent(); 32 return _.extend( position, { 33 right: parent.outerWidth() - position.left - this.outerWidth(), 34 bottom: parent.outerHeight() - position.top - this.outerHeight() 35 }); 36 }; 37 38 // wp_localize_script transforms top-level numbers into strings. Undo that. 39 if ( revisions.settings.to ) { 40 revisions.settings.to = parseInt( revisions.settings.to, 10 ); 41 } 42 if ( revisions.settings.from ) { 43 revisions.settings.from = parseInt( revisions.settings.from, 10 ); 44 } 45 46 // wp_localize_script does not allow for top-level booleans. Fix that. 47 if ( revisions.settings.compareTwoMode ) { 48 revisions.settings.compareTwoMode = revisions.settings.compareTwoMode === '1'; 49 } 50 51 /** 52 * ======================================================================== 53 * MODELS 54 * ======================================================================== 55 */ 56 revisions.model.Slider = Backbone.Model.extend({ 57 defaults: { 58 value: null, 59 values: null, 60 min: 0, 61 max: 1, 62 step: 1, 63 range: false, 64 compareTwoMode: false 65 }, 66 67 initialize: function( options ) { 68 this.frame = options.frame; 69 this.revisions = options.revisions; 70 71 // Listen for changes to the revisions or mode from outside 72 this.listenTo( this.frame, 'update:revisions', this.receiveRevisions ); 73 this.listenTo( this.frame, 'change:compareTwoMode', this.updateMode ); 74 75 // Listen for internal changes 76 this.listenTo( this, 'change:from', this.handleLocalChanges ); 77 this.listenTo( this, 'change:to', this.handleLocalChanges ); 78 this.listenTo( this, 'change:compareTwoMode', this.updateSliderSettings ); 79 this.listenTo( this, 'update:revisions', this.updateSliderSettings ); 80 81 // Listen for changes to the hovered revision 82 this.listenTo( this, 'change:hoveredRevision', this.hoverRevision ); 83 84 this.set({ 85 max: this.revisions.length - 1, 86 compareTwoMode: this.frame.get('compareTwoMode'), 87 from: this.frame.get('from'), 88 to: this.frame.get('to') 89 }); 90 this.updateSliderSettings(); 91 }, 92 93 getSliderValue: function( a, b ) { 94 return isRtl ? this.revisions.length - this.revisions.indexOf( this.get(a) ) - 1 : this.revisions.indexOf( this.get(b) ); 95 }, 96 97 updateSliderSettings: function() { 98 if ( this.get('compareTwoMode') ) { 99 this.set({ 100 values: [ 101 this.getSliderValue( 'to', 'from' ), 102 this.getSliderValue( 'from', 'to' ) 103 ], 104 value: null, 105 range: true // ensures handles cannot cross 106 }); 107 } else { 108 this.set({ 109 value: this.getSliderValue( 'to', 'to' ), 110 values: null, 111 range: false 112 }); 113 } 114 this.trigger( 'update:slider' ); 115 }, 116 117 // Called when a revision is hovered 118 hoverRevision: function( model, value ) { 119 this.trigger( 'hovered:revision', value ); 120 }, 121 122 // Called when `compareTwoMode` changes 123 updateMode: function( model, value ) { 124 this.set({ compareTwoMode: value }); 125 }, 126 127 // Called when `from` or `to` changes in the local model 128 handleLocalChanges: function() { 129 this.frame.set({ 130 from: this.get('from'), 131 to: this.get('to') 132 }); 133 }, 134 135 // Receives revisions changes from outside the model 136 receiveRevisions: function( from, to ) { 137 // Bail if nothing changed 138 if ( this.get('from') === from && this.get('to') === to ) { 139 return; 140 } 141 142 this.set({ from: from, to: to }, { silent: true }); 143 this.trigger( 'update:revisions', from, to ); 144 } 145 146 }); 147 148 revisions.model.Tooltip = Backbone.Model.extend({ 149 defaults: { 150 revision: null, 151 offset: {}, 152 hovering: false, // Whether the mouse is hovering 153 scrubbing: false // Whether the mouse is scrubbing 154 }, 155 156 initialize: function( options ) { 157 this.frame = options.frame; 158 this.revisions = options.revisions; 159 this.slider = options.slider; 160 161 this.listenTo( this.slider, 'hovered:revision', this.updateRevision ); 162 this.listenTo( this.slider, 'change:hovering', this.setHovering ); 163 this.listenTo( this.slider, 'change:scrubbing', this.setScrubbing ); 164 }, 165 166 167 updateRevision: function( revision ) { 168 this.set({ revision: revision }); 169 }, 170 171 setHovering: function( model, value ) { 172 this.set({ hovering: value }); 173 }, 174 175 setScrubbing: function( model, value ) { 176 this.set({ scrubbing: value }); 177 } 178 }); 179 180 revisions.model.Revision = Backbone.Model.extend({}); 181 182 revisions.model.Revisions = Backbone.Collection.extend({ 183 model: revisions.model.Revision, 184 185 initialize: function() { 186 _.bindAll( this, 'next', 'prev' ); 187 }, 188 189 next: function( revision ) { 190 var index = this.indexOf( revision ); 191 192 if ( index !== -1 && index !== this.length - 1 ) { 193 return this.at( index + 1 ); 194 } 195 }, 196 197 prev: function( revision ) { 198 var index = this.indexOf( revision ); 199 200 if ( index !== -1 && index !== 0 ) { 201 return this.at( index - 1 ); 202 } 203 } 204 }); 205 206 revisions.model.Field = Backbone.Model.extend({}); 207 208 revisions.model.Fields = Backbone.Collection.extend({ 209 model: revisions.model.Field 210 }); 211 212 revisions.model.Diff = Backbone.Model.extend({ 213 initialize: function() { 214 var fields = this.get('fields'); 215 this.unset('fields'); 216 217 this.fields = new revisions.model.Fields( fields ); 218 } 219 }); 220 221 revisions.model.Diffs = Backbone.Collection.extend({ 222 initialize: function( models, options ) { 223 _.bindAll( this, 'getClosestUnloaded' ); 224 this.loadAll = _.once( this._loadAll ); 225 this.revisions = options.revisions; 226 this.requests = {}; 227 }, 228 229 model: revisions.model.Diff, 230 231 ensure: function( id, context ) { 232 var diff = this.get( id ), 233 request = this.requests[ id ], 234 deferred = $.Deferred(), 235 ids = {}, 236 from = id.split(':')[0], 237 to = id.split(':')[1]; 238 ids[id] = true; 239 240 wp.revisions.log( 'ensure', id ); 241 242 this.trigger( 'ensure', ids, from, to, deferred.promise() ); 243 244 if ( diff ) { 245 deferred.resolveWith( context, [ diff ] ); 246 } else { 247 this.trigger( 'ensure:load', ids, from, to, deferred.promise() ); 248 _.each( ids, _.bind( function( id ) { 249 // Remove anything that has an ongoing request 250 if ( this.requests[ id ] ) { 251 delete ids[ id ]; 252 } 253 // Remove anything we already have 254 if ( this.get( id ) ) { 255 delete ids[ id ]; 256 } 257 }, this ) ); 258 if ( ! request ) { 259 // Always include the ID that started this ensure 260 ids[ id ] = true; 261 request = this.load( _.keys( ids ) ); 262 } 263 264 request.done( _.bind( function() { 265 deferred.resolveWith( context, [ this.get( id ) ] ); 266 }, this ) ).fail( _.bind( function() { 267 deferred.reject(); 268 }) ); 269 } 270 271 return deferred.promise(); 272 }, 273 274 // Returns an array of proximal diffs 275 getClosestUnloaded: function( ids, centerId ) { 276 var self = this; 277 return _.chain([0].concat( ids )).initial().zip( ids ).sortBy( function( pair ) { 278 return Math.abs( centerId - pair[1] ); 279 }).map( function( pair ) { 280 return pair.join(':'); 281 }).filter( function( diffId ) { 282 return _.isUndefined( self.get( diffId ) ) && ! self.requests[ diffId ]; 283 }).value(); 284 }, 285 286 _loadAll: function( allRevisionIds, centerId, num ) { 287 var self = this, deferred = $.Deferred(), 288 diffs = _.first( this.getClosestUnloaded( allRevisionIds, centerId ), num ); 289 if ( _.size( diffs ) > 0 ) { 290 this.load( diffs ).done( function() { 291 self._loadAll( allRevisionIds, centerId, num ).done( function() { 292 deferred.resolve(); 293 }); 294 }).fail( function() { 295 if ( 1 === num ) { // Already tried 1. This just isn't working. Give up. 296 deferred.reject(); 297 } else { // Request fewer diffs this time 298 self._loadAll( allRevisionIds, centerId, Math.ceil( num / 2 ) ).done( function() { 299 deferred.resolve(); 300 }); 301 } 302 }); 303 } else { 304 deferred.resolve(); 305 } 306 return deferred; 307 }, 308 309 load: function( comparisons ) { 310 wp.revisions.log( 'load', comparisons ); 311 // Our collection should only ever grow, never shrink, so remove: false 312 return this.fetch({ data: { compare: comparisons }, remove: false }).done( function() { 313 wp.revisions.log( 'load:complete', comparisons ); 314 }); 315 }, 316 317 sync: function( method, model, options ) { 318 if ( 'read' === method ) { 319 options = options || {}; 320 options.context = this; 321 options.data = _.extend( options.data || {}, { 322 action: 'get-revision-diffs', 323 post_id: revisions.settings.postId 324 }); 325 326 var deferred = wp.ajax.send( options ), 327 requests = this.requests; 328 329 // Record that we're requesting each diff. 330 if ( options.data.compare ) { 331 _.each( options.data.compare, function( id ) { 332 requests[ id ] = deferred; 333 }); 334 } 335 336 // When the request completes, clear the stored request. 337 deferred.always( function() { 338 if ( options.data.compare ) { 339 _.each( options.data.compare, function( id ) { 340 delete requests[ id ]; 341 }); 342 } 343 }); 344 345 return deferred; 346 347 // Otherwise, fall back to `Backbone.sync()`. 348 } else { 349 return Backbone.Model.prototype.sync.apply( this, arguments ); 350 } 351 } 352 }); 353 354 355 revisions.model.FrameState = Backbone.Model.extend({ 356 defaults: { 357 loading: false, 358 error: false, 359 compareTwoMode: false 360 }, 361 362 initialize: function( attributes, options ) { 363 var properties = {}; 364 365 _.bindAll( this, 'receiveDiff' ); 366 this._debouncedEnsureDiff = _.debounce( this._ensureDiff, 200 ); 367 368 this.revisions = options.revisions; 369 this.diffs = new revisions.model.Diffs( [], { revisions: this.revisions }); 370 371 // Set the initial diffs collection provided through the settings 372 this.diffs.set( revisions.settings.diffData ); 373 374 // Set up internal listeners 375 this.listenTo( this, 'change:from', this.changeRevisionHandler ); 376 this.listenTo( this, 'change:to', this.changeRevisionHandler ); 377 this.listenTo( this, 'change:compareTwoMode', this.changeMode ); 378 this.listenTo( this, 'update:revisions', this.updatedRevisions ); 379 this.listenTo( this.diffs, 'ensure:load', this.updateLoadingStatus ); 380 this.listenTo( this, 'update:diff', this.updateLoadingStatus ); 381 382 // Set the initial revisions, baseUrl, and mode as provided through settings 383 properties.to = this.revisions.get( revisions.settings.to ); 384 properties.from = this.revisions.get( revisions.settings.from ); 385 properties.compareTwoMode = revisions.settings.compareTwoMode; 386 properties.baseUrl = revisions.settings.baseUrl; 387 this.set( properties ); 388 389 // Start the router if browser supports History API 390 if ( window.history && window.history.pushState ) { 391 this.router = new revisions.Router({ model: this }); 392 Backbone.history.start({ pushState: true }); 393 } 394 }, 395 396 updateLoadingStatus: function() { 397 this.set( 'error', false ); 398 this.set( 'loading', ! this.diff() ); 399 }, 400 401 changeMode: function( model, value ) { 402 // If we were on the first revision before switching, we have to bump them over one 403 if ( value && 0 === this.revisions.indexOf( this.get('to') ) ) { 404 this.set({ 405 from: this.revisions.at(0), 406 to: this.revisions.at(1) 407 }); 408 } 409 }, 410 411 updatedRevisions: function( from, to ) { 412 if ( this.get( 'compareTwoMode' ) ) { 413 // TODO: compare-two loading strategy 414 } else { 415 this.diffs.loadAll( this.revisions.pluck('id'), to.id, 40 ); 416 } 417 }, 418 419 // Fetch the currently loaded diff. 420 diff: function() { 421 return this.diffs.get( this._diffId ); 422 }, 423 424 // So long as `from` and `to` are changed at the same time, the diff 425 // will only be updated once. This is because Backbone updates all of 426 // the changed attributes in `set`, and then fires the `change` events. 427 updateDiff: function( options ) { 428 var from, to, diffId, diff; 429 430 options = options || {}; 431 from = this.get('from'); 432 to = this.get('to'); 433 diffId = ( from ? from.id : 0 ) + ':' + to.id; 434 435 // Check if we're actually changing the diff id. 436 if ( this._diffId === diffId ) { 437 return $.Deferred().reject().promise(); 438 } 439 440 this._diffId = diffId; 441 this.trigger( 'update:revisions', from, to ); 442 443 diff = this.diffs.get( diffId ); 444 445 // If we already have the diff, then immediately trigger the update. 446 if ( diff ) { 447 this.receiveDiff( diff ); 448 return $.Deferred().resolve().promise(); 449 // Otherwise, fetch the diff. 450 } else { 451 if ( options.immediate ) { 452 return this._ensureDiff(); 453 } else { 454 this._debouncedEnsureDiff(); 455 return $.Deferred().reject().promise(); 456 } 457 } 458 }, 459 460 // A simple wrapper around `updateDiff` to prevent the change event's 461 // parameters from being passed through. 462 changeRevisionHandler: function() { 463 this.updateDiff(); 464 }, 465 466 receiveDiff: function( diff ) { 467 // Did we actually get a diff? 468 if ( _.isUndefined( diff ) || _.isUndefined( diff.id ) ) { 469 this.set({ 470 loading: false, 471 error: true 472 }); 473 } else if ( this._diffId === diff.id ) { // Make sure the current diff didn't change 474 this.trigger( 'update:diff', diff ); 475 } 476 }, 477 478 _ensureDiff: function() { 479 return this.diffs.ensure( this._diffId, this ).always( this.receiveDiff ); 480 } 481 }); 482 483 484 /** 485 * ======================================================================== 486 * VIEWS 487 * ======================================================================== 488 */ 489 490 // The frame view. This contains the entire page. 491 revisions.view.Frame = wp.Backbone.View.extend({ 492 className: 'revisions', 493 template: wp.template('revisions-frame'), 494 495 initialize: function() { 496 this.listenTo( this.model, 'update:diff', this.renderDiff ); 497 this.listenTo( this.model, 'change:compareTwoMode', this.updateCompareTwoMode ); 498 this.listenTo( this.model, 'change:loading', this.updateLoadingStatus ); 499 this.listenTo( this.model, 'change:error', this.updateErrorStatus ); 500 501 this.views.set( '.revisions-control-frame', new revisions.view.Controls({ 502 model: this.model 503 }) ); 504 }, 505 506 render: function() { 507 wp.Backbone.View.prototype.render.apply( this, arguments ); 508 509 $('html').css( 'overflow-y', 'scroll' ); 510 $('#wpbody-content .wrap').append( this.el ); 511 this.updateCompareTwoMode(); 512 this.renderDiff( this.model.diff() ); 513 this.views.ready(); 514 515 return this; 516 }, 517 518 renderDiff: function( diff ) { 519 this.views.set( '.revisions-diff-frame', new revisions.view.Diff({ 520 model: diff 521 }) ); 522 }, 523 524 updateLoadingStatus: function() { 525 this.$el.toggleClass( 'loading', this.model.get('loading') ); 526 }, 527 528 updateErrorStatus: function() { 529 this.$el.toggleClass( 'diff-error', this.model.get('error') ); 530 }, 531 532 updateCompareTwoMode: function() { 533 this.$el.toggleClass( 'comparing-two-revisions', this.model.get('compareTwoMode') ); 534 } 535 }); 536 537 // The control view. 538 // This contains the revision slider, previous/next buttons, the meta info and the compare checkbox. 539 revisions.view.Controls = wp.Backbone.View.extend({ 540 className: 'revisions-controls', 541 542 initialize: function() { 543 _.bindAll( this, 'setWidth' ); 544 545 // Add the button view 546 this.views.add( new revisions.view.Buttons({ 547 model: this.model 548 }) ); 549 550 // Add the checkbox view 551 this.views.add( new revisions.view.Checkbox({ 552 model: this.model 553 }) ); 554 555 // Prep the slider model 556 var slider = new revisions.model.Slider({ 557 frame: this.model, 558 revisions: this.model.revisions 559 }), 560 561 // Prep the tooltip model 562 tooltip = new revisions.model.Tooltip({ 563 frame: this.model, 564 revisions: this.model.revisions, 565 slider: slider 566 }); 567 568 // Add the tooltip view 569 this.views.add( new revisions.view.Tooltip({ 570 model: tooltip 571 }) ); 572 573 // Add the tickmarks view 574 this.views.add( new revisions.view.Tickmarks({ 575 model: tooltip 576 }) ); 577 578 // Add the slider view 579 this.views.add( new revisions.view.Slider({ 580 model: slider 581 }) ); 582 583 // Add the Metabox view 584 this.views.add( new revisions.view.Metabox({ 585 model: this.model 586 }) ); 587 }, 588 589 ready: function() { 590 this.top = this.$el.offset().top; 591 this.window = $(window); 592 this.window.on( 'scroll.wp.revisions', {controls: this}, function(e) { 593 var controls = e.data.controls, 594 container = controls.$el.parent(), 595 scrolled = controls.window.scrollTop(), 596 frame = controls.views.parent; 597 598 if ( scrolled >= controls.top ) { 599 if ( ! frame.$el.hasClass('pinned') ) { 600 controls.setWidth(); 601 container.css('height', container.height() + 'px' ); 602 controls.window.on('resize.wp.revisions.pinning click.wp.revisions.pinning', {controls: controls}, function(e) { 603 e.data.controls.setWidth(); 604 }); 605 } 606 frame.$el.addClass('pinned'); 607 } else if ( frame.$el.hasClass('pinned') ) { 608 controls.window.off('.wp.revisions.pinning'); 609 controls.$el.css('width', 'auto'); 610 frame.$el.removeClass('pinned'); 611 container.css('height', 'auto'); 612 controls.top = controls.$el.offset().top; 613 } else { 614 controls.top = controls.$el.offset().top; 615 } 616 }); 617 }, 618 619 setWidth: function() { 620 this.$el.css('width', this.$el.parent().width() + 'px'); 621 } 622 }); 623 624 // The tickmarks view 625 revisions.view.Tickmarks = wp.Backbone.View.extend({ 626 className: 'revisions-tickmarks', 627 direction: isRtl ? 'right' : 'left', 628 629 initialize: function() { 630 this.listenTo( this.model, 'change:revision', this.reportTickPosition ); 631 }, 632 633 reportTickPosition: function( model, revision ) { 634 var offset, thisOffset, parentOffset, tick, index = this.model.revisions.indexOf( revision ); 635 thisOffset = this.$el.allOffsets(); 636 parentOffset = this.$el.parent().allOffsets(); 637 if ( index === this.model.revisions.length - 1 ) { 638 // Last one 639 offset = { 640 rightPlusWidth: thisOffset.left - parentOffset.left + 1, 641 leftPlusWidth: thisOffset.right - parentOffset.right + 1 642 }; 643 } else { 644 // Normal tick 645 tick = this.$('div:nth-of-type(' + (index + 1) + ')'); 646 offset = tick.allPositions(); 647 _.extend( offset, { 648 left: offset.left + thisOffset.left - parentOffset.left, 649 right: offset.right + thisOffset.right - parentOffset.right 650 }); 651 _.extend( offset, { 652 leftPlusWidth: offset.left + tick.outerWidth(), 653 rightPlusWidth: offset.right + tick.outerWidth() 654 }); 655 } 656 this.model.set({ offset: offset }); 657 }, 658 659 ready: function() { 660 var tickCount, tickWidth; 661 tickCount = this.model.revisions.length - 1; 662 tickWidth = 1 / tickCount; 663 this.$el.css('width', ( this.model.revisions.length * 50 ) + 'px'); 664 665 _(tickCount).times( function( index ){ 666 this.$el.append( '<div style="' + this.direction + ': ' + ( 100 * tickWidth * index ) + '%"></div>' ); 667 }, this ); 668 } 669 }); 670 671 // The metabox view 672 revisions.view.Metabox = wp.Backbone.View.extend({ 673 className: 'revisions-meta', 674 675 initialize: function() { 676 // Add the 'from' view 677 this.views.add( new revisions.view.MetaFrom({ 678 model: this.model, 679 className: 'diff-meta diff-meta-from' 680 }) ); 681 682 // Add the 'to' view 683 this.views.add( new revisions.view.MetaTo({ 684 model: this.model 685 }) ); 686 } 687 }); 688 689 // The revision meta view (to be extended) 690 revisions.view.Meta = wp.Backbone.View.extend({ 691 template: wp.template('revisions-meta'), 692 693 events: { 694 'click .restore-revision': 'restoreRevision' 695 }, 696 697 initialize: function() { 698 this.listenTo( this.model, 'update:revisions', this.render ); 699 }, 700 701 prepare: function() { 702 return _.extend( this.model.toJSON()[this.type] || {}, { 703 type: this.type 704 }); 705 }, 706 707 restoreRevision: function() { 708 document.location = this.model.get('to').attributes.restoreUrl; 709 } 710 }); 711 712 // The revision meta 'from' view 713 revisions.view.MetaFrom = revisions.view.Meta.extend({ 714 className: 'diff-meta diff-meta-from', 715 type: 'from' 716 }); 717 718 // The revision meta 'to' view 719 revisions.view.MetaTo = revisions.view.Meta.extend({ 720 className: 'diff-meta diff-meta-to', 721 type: 'to' 722 }); 723 724 // The checkbox view. 725 revisions.view.Checkbox = wp.Backbone.View.extend({ 726 className: 'revisions-checkbox', 727 template: wp.template('revisions-checkbox'), 728 729 events: { 730 'click .compare-two-revisions': 'compareTwoToggle' 731 }, 732 733 initialize: function() { 734 this.listenTo( this.model, 'change:compareTwoMode', this.updateCompareTwoMode ); 735 }, 736 737 ready: function() { 738 if ( this.model.revisions.length < 3 ) { 739 $('.revision-toggle-compare-mode').hide(); 740 } 741 }, 742 743 updateCompareTwoMode: function() { 744 this.$('.compare-two-revisions').prop( 'checked', this.model.get('compareTwoMode') ); 745 }, 746 747 // Toggle the compare two mode feature when the compare two checkbox is checked. 748 compareTwoToggle: function() { 749 // Activate compare two mode? 750 this.model.set({ compareTwoMode: $('.compare-two-revisions').prop('checked') }); 751 } 752 }); 753 754 // The tooltip view. 755 // Encapsulates the tooltip. 756 revisions.view.Tooltip = wp.Backbone.View.extend({ 757 className: 'revisions-tooltip', 758 template: wp.template('revisions-meta'), 759 760 initialize: function() { 761 this.listenTo( this.model, 'change:offset', this.render ); 762 this.listenTo( this.model, 'change:hovering', this.toggleVisibility ); 763 this.listenTo( this.model, 'change:scrubbing', this.toggleVisibility ); 764 }, 765 766 prepare: function() { 767 if ( _.isNull( this.model.get('revision') ) ) { 768 return; 769 } else { 770 return _.extend( { type: 'tooltip' }, { 771 attributes: this.model.get('revision').toJSON() 772 }); 773 } 774 }, 775 776 render: function() { 777 var otherDirection, 778 direction, 779 directionVal, 780 flipped, 781 css = {}, 782 position = this.model.revisions.indexOf( this.model.get('revision') ) + 1; 783 784 flipped = ( position / this.model.revisions.length ) > 0.5; 785 if ( isRtl ) { 786 direction = flipped ? 'left' : 'right'; 787 directionVal = flipped ? 'leftPlusWidth' : direction; 788 } else { 789 direction = flipped ? 'right' : 'left'; 790 directionVal = flipped ? 'rightPlusWidth' : direction; 791 } 792 otherDirection = 'right' === direction ? 'left': 'right'; 793 wp.Backbone.View.prototype.render.apply( this, arguments ); 794 css[direction] = this.model.get('offset')[directionVal] + 'px'; 795 css[otherDirection] = ''; 796 this.$el.toggleClass( 'flipped', flipped ).css( css ); 797 }, 798 799 visible: function() { 800 return this.model.get( 'scrubbing' ) || this.model.get( 'hovering' ); 801 }, 802 803 toggleVisibility: function() { 804 if ( this.visible() ) { 805 this.$el.stop().show().fadeTo( 100 - this.el.style.opacity * 100, 1 ); 806 } else { 807 this.$el.stop().fadeTo( this.el.style.opacity * 300, 0, function(){ $(this).hide(); } ); 808 } 809 return; 810 } 811 }); 812 813 // The buttons view. 814 // Encapsulates all of the configuration for the previous/next buttons. 815 revisions.view.Buttons = wp.Backbone.View.extend({ 816 className: 'revisions-buttons', 817 template: wp.template('revisions-buttons'), 818 819 events: { 820 'click .revisions-next .button': 'nextRevision', 821 'click .revisions-previous .button': 'previousRevision' 822 }, 823 824 initialize: function() { 825 this.listenTo( this.model, 'update:revisions', this.disabledButtonCheck ); 826 }, 827 828 ready: function() { 829 this.disabledButtonCheck(); 830 }, 831 832 // Go to a specific model index 833 gotoModel: function( toIndex ) { 834 var attributes = { 835 to: this.model.revisions.at( toIndex ) 836 }; 837 // If we're at the first revision, unset 'from'. 838 if ( toIndex ) { 839 attributes.from = this.model.revisions.at( toIndex - 1 ); 840 } else { 841 this.model.unset('from', { silent: true }); 842 } 843 844 this.model.set( attributes ); 845 }, 846 847 // Go to the 'next' revision 848 nextRevision: function() { 849 var toIndex = this.model.revisions.indexOf( this.model.get('to') ) + 1; 850 this.gotoModel( toIndex ); 851 }, 852 853 // Go to the 'previous' revision 854 previousRevision: function() { 855 var toIndex = this.model.revisions.indexOf( this.model.get('to') ) - 1; 856 this.gotoModel( toIndex ); 857 }, 858 859 // Check to see if the Previous or Next buttons need to be disabled or enabled. 860 disabledButtonCheck: function() { 861 var maxVal = this.model.revisions.length - 1, 862 minVal = 0, 863 next = $('.revisions-next .button'), 864 previous = $('.revisions-previous .button'), 865 val = this.model.revisions.indexOf( this.model.get('to') ); 866 867 // Disable "Next" button if you're on the last node. 868 next.prop( 'disabled', ( maxVal === val ) ); 869 870 // Disable "Previous" button if you're on the first node. 871 previous.prop( 'disabled', ( minVal === val ) ); 872 } 873 }); 874 875 876 // The slider view. 877 revisions.view.Slider = wp.Backbone.View.extend({ 878 className: 'wp-slider', 879 direction: isRtl ? 'right' : 'left', 880 881 events: { 882 'mousemove' : 'mouseMove' 883 }, 884 885 initialize: function() { 886 _.bindAll( this, 'start', 'slide', 'stop', 'mouseMove', 'mouseEnter', 'mouseLeave' ); 887 this.listenTo( this.model, 'update:slider', this.applySliderSettings ); 888 }, 889 890 ready: function() { 891 this.$el.css('width', ( this.model.revisions.length * 50 ) + 'px'); 892 this.$el.slider( _.extend( this.model.toJSON(), { 893 start: this.start, 894 slide: this.slide, 895 stop: this.stop 896 }) ); 897 898 this.$el.hoverIntent({ 899 over: this.mouseEnter, 900 out: this.mouseLeave, 901 timeout: 800 902 }); 903 904 this.applySliderSettings(); 905 }, 906 907 mouseMove: function( e ) { 908 var zoneCount = this.model.revisions.length - 1, // One fewer zone than models 909 sliderFrom = this.$el.allOffsets()[this.direction], // "From" edge of slider 910 sliderWidth = this.$el.width(), // Width of slider 911 tickWidth = sliderWidth / zoneCount, // Calculated width of zone 912 actualX = ( isRtl ? $(window).width() - e.pageX : e.pageX ) - sliderFrom, // Flipped for RTL - sliderFrom; 913 currentModelIndex = Math.floor( ( actualX + ( tickWidth / 2 ) ) / tickWidth ); // Calculate the model index 914 915 // Ensure sane value for currentModelIndex. 916 if ( currentModelIndex < 0 ) { 917 currentModelIndex = 0; 918 } else if ( currentModelIndex >= this.model.revisions.length ) { 919 currentModelIndex = this.model.revisions.length - 1; 920 } 921 922 // Update the tooltip mode 923 this.model.set({ hoveredRevision: this.model.revisions.at( currentModelIndex ) }); 924 }, 925 926 mouseLeave: function() { 927 this.model.set({ hovering: false }); 928 }, 929 930 mouseEnter: function() { 931 this.model.set({ hovering: true }); 932 }, 933 934 applySliderSettings: function() { 935 this.$el.slider( _.pick( this.model.toJSON(), 'value', 'values', 'range' ) ); 936 var handles = this.$('a.ui-slider-handle'); 937 938 if ( this.model.get('compareTwoMode') ) { 939 // in RTL mode the 'left handle' is the second in the slider, 'right' is first 940 handles.first() 941 .toggleClass( 'to-handle', !! isRtl ) 942 .toggleClass( 'from-handle', ! isRtl ); 943 handles.last() 944 .toggleClass( 'from-handle', !! isRtl ) 945 .toggleClass( 'to-handle', ! isRtl ); 946 } else { 947 handles.removeClass('from-handle to-handle'); 948 } 949 }, 950 951 start: function( event, ui ) { 952 this.model.set({ scrubbing: true }); 953 954 // Track the mouse position to enable smooth dragging, 955 // overrides default jQuery UI step behavior. 956 $( window ).on( 'mousemove.wp.revisions', { view: this }, function( e ) { 957 var handles, 958 view = e.data.view, 959 leftDragBoundary = view.$el.offset().left, 960 sliderOffset = leftDragBoundary, 961 sliderRightEdge = leftDragBoundary + view.$el.width(), 962 rightDragBoundary = sliderRightEdge, 963 leftDragReset = '0', 964 rightDragReset = '100%', 965 handle = $( ui.handle ); 966 967 // In two handle mode, ensure handles can't be dragged past each other. 968 // Adjust left/right boundaries and reset points. 969 if ( view.model.get('compareTwoMode') ) { 970 handles = handle.parent().find('.ui-slider-handle'); 971 if ( handle.is( handles.first() ) ) { // We're the left handle 972 rightDragBoundary = handles.last().offset().left; 973 rightDragReset = rightDragBoundary - sliderOffset; 974 } else { // We're the right handle 975 leftDragBoundary = handles.first().offset().left + handles.first().width(); 976 leftDragReset = leftDragBoundary - sliderOffset; 977 } 978 } 979 980 // Follow mouse movements, as long as handle remains inside slider. 981 if ( e.pageX < leftDragBoundary ) { 982 handle.css( 'left', leftDragReset ); // Mouse to left of slider. 983 } else if ( e.pageX > rightDragBoundary ) { 984 handle.css( 'left', rightDragReset ); // Mouse to right of slider. 985 } else { 986 handle.css( 'left', e.pageX - sliderOffset ); // Mouse in slider. 987 } 988 } ); 989 }, 990 991 getPosition: function( position ) { 992 return isRtl ? this.model.revisions.length - position - 1: position; 993 }, 994 995 // Responds to slide events 996 slide: function( event, ui ) { 997 var attributes, movedRevision; 998 // Compare two revisions mode 999 if ( this.model.get('compareTwoMode') ) { 1000 // Prevent sliders from occupying same spot 1001 if ( ui.values[1] === ui.values[0] ) { 1002 return false; 1003 } 1004 if ( isRtl ) { 1005 ui.values.reverse(); 1006 } 1007 attributes = { 1008 from: this.model.revisions.at( this.getPosition( ui.values[0] ) ), 1009 to: this.model.revisions.at( this.getPosition( ui.values[1] ) ) 1010 }; 1011 } else { 1012 attributes = { 1013 to: this.model.revisions.at( this.getPosition( ui.value ) ) 1014 }; 1015 // If we're at the first revision, unset 'from'. 1016 if ( this.getPosition( ui.value ) > 0 ) { 1017 attributes.from = this.model.revisions.at( this.getPosition( ui.value ) - 1 ); 1018 } else { 1019 attributes.from = undefined; 1020 } 1021 } 1022 movedRevision = this.model.revisions.at( this.getPosition( ui.value ) ); 1023 1024 // If we are scrubbing, a scrub to a revision is considered a hover 1025 if ( this.model.get('scrubbing') ) { 1026 attributes.hoveredRevision = movedRevision; 1027 } 1028 1029 this.model.set( attributes ); 1030 }, 1031 1032 stop: function() { 1033 $( window ).off('mousemove.wp.revisions'); 1034 this.model.updateSliderSettings(); // To snap us back to a tick mark 1035 this.model.set({ scrubbing: false }); 1036 } 1037 }); 1038 1039 // The diff view. 1040 // This is the view for the current active diff. 1041 revisions.view.Diff = wp.Backbone.View.extend({ 1042 className: 'revisions-diff', 1043 template: wp.template('revisions-diff'), 1044 1045 // Generate the options to be passed to the template. 1046 prepare: function() { 1047 return _.extend({ fields: this.model.fields.toJSON() }, this.options ); 1048 } 1049 }); 1050 1051 // The revisions router 1052 // takes URLs with #hash fragments and routes them 1053 revisions.Router = Backbone.Router.extend({ 1054 initialize: function( options ) { 1055 this.model = options.model; 1056 this.routes = _.object([ 1057 [ this.baseUrl( '?from=:from&to=:to' ), 'handleRoute' ], 1058 [ this.baseUrl( '?from=:from&to=:to' ), 'handleRoute' ] 1059 ]); 1060 // Maintain state and history when navigating 1061 this.listenTo( this.model, 'update:diff', _.debounce( this.updateUrl, 250 ) ); 1062 this.listenTo( this.model, 'change:compareTwoMode', this.updateUrl ); 1063 }, 1064 1065 baseUrl: function( url ) { 1066 return this.model.get('baseUrl') + url; 1067 }, 1068 1069 updateUrl: function() { 1070 var from = this.model.has('from') ? this.model.get('from').id : 0, 1071 to = this.model.get('to').id; 1072 if ( this.model.get('compareTwoMode' ) ) { 1073 this.navigate( this.baseUrl( '?from=' + from + '&to=' + to ) ); 1074 } else { 1075 this.navigate( this.baseUrl( '?revision=' + to ) ); 1076 } 1077 }, 1078 1079 handleRoute: function( a, b ) { 1080 var compareTwo = _.isUndefined( b ); 1081 1082 if ( ! compareTwo ) { 1083 b = this.model.revisions.get( a ); 1084 a = this.model.revisions.prev( b ); 1085 b = b ? b.id : 0; 1086 a = a ? a.id : 0; 1087 } 1088 1089 this.model.set({ 1090 from: this.model.revisions.get( parseInt( a, 10 ) ), 1091 to: this.model.revisions.get( parseInt( a, 10 ) ), 1092 compareTwoMode: compareTwo 1093 }); 1094 } 1095 }); 1096 1097 // Initialize the revisions UI. 1098 revisions.init = function() { 1099 revisions.view.frame = new revisions.view.Frame({ 1100 model: new revisions.model.FrameState({}, { 1101 revisions: new revisions.model.Revisions( revisions.settings.revisionData ) 1102 }) 1103 }).render(); 1104 }; 1105 1106 $( revisions.init ); 1107 }(jQuery));
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Tue Mar 25 01:41:18 2014 | WordPress honlapkészítés: online1.hu |