Make WordPress Core

Changeset 24658


Ignore:
Timestamp:
07/11/2013 09:14:14 AM (13 years ago)
Author:
markjaquith
Message:

Revisions: Bunch of refactoring and code cleanup

  • Extracted a lot of model-y stuff from the view code.
  • Slider now has a proper model, with communication with other models.
  • Use of get( foo ) instead of findWhere({ id: foo }).
  • Properly set the from diff when routing single mode.
  • Bind prev() and next() to their model.
  • Tick marks are now percentage based, which means the slider resizes with the browser, without JS resize events.
  • When scrubbing, the position of the scrubber is considered a hover, so you can fall off the timeline while still scrubbing and the tooltips will persist.
  • Tooltips fade in and out.
  • Tooltips hang around for a grace period instead of immediately going away. More forgiving.
  • Unused code paths removed.
  • Got rid of a bunch of view-to-view communication.
  • Use _.times() instead of a for loop.
  • Removed premature Math.floor() and Math.ceil() calls that were making things not add up to 100%.

See #24425.

Location:
trunk/wp-admin
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • trunk/wp-admin/css/colors-fresh.css

    r24595 r24658  
    13921392
    13931393.revisions-tickmarks > div {
    1394         background-color: #ccc;
     1394        border-color: #aaa;
    13951395}
    13961396
  • trunk/wp-admin/css/wp-admin.css

    r24644 r24658  
    25502550        height: 0;
    25512551        opacity: 0;
    2552         -webkit-transition: height 1s, opacity 500ms;
    2553         transition:         height 1s, opacity 500ms;
    25542552}
    25552553
     
    25572555        height: 22px;
    25582556        opacity: 1;
    2559         -webkit-transition: height 1s, opacity 500ms 500ms;
    2560         transition:         height 1s, opacity 500ms 500ms;
     2557}
     2558
     2559tr.locked-info, tr.wp-locked .locked-info {
     2560        -webkit-transition: height 1s, opacity 500ms;
     2561        -moz-transition:    height 1s, opacity 500ms;
     2562        -ms-transition:     height 1s, opacity 500ms;
     2563        -o-transition:      height 1s, opacity 500ms;
     2564        transition:         height 1s, opacity 500ms;
    25612565}
    25622566
     
    35133517        z-index: 2;
    35143518        top: 7px;
     3519        width: 70%;
     3520        box-sizing: border-box;
    35153521}
    35163522
    35173523.revisions-tickmarks > div {
    3518         height: 0.8em;
    3519         width: 1px;
    3520         float: left;
    35213524        position: relative;
     3525        height: 100%;
     3526        float: left;
    35223527        z-index: 10002;
     3528        border-style: solid;
     3529        border-width: 0 0 0 1px;
     3530        box-sizing: border-box;
     3531}
     3532
     3533.revisions-tickmarks > div:first-of-type {
     3534        border-left-width: 0;
    35233535}
    35243536
     
    36533665        min-width: 130px;
    36543666        padding: 4px;
     3667}
     3668
     3669.revisions-tooltip.fade {
     3670        -webkit-transition: opacity 200ms;
     3671        -ms-transition:     opacity 200ms;
     3672        -moz-transition:    opacity 200ms;
     3673        -o-transition:      opacity 200ms;
     3674        transition:         opacity 200ms;
    36553675}
    36563676
  • trunk/wp-admin/js/revisions.js

    r24652 r24658  
    3232                        min: 0,
    3333                        max: 1,
    34                         step: 1
    35                 }
     34                        step: 1,
     35                        compareTwoMode: false
     36                },
     37
     38                initialize: function( options ) {
     39                        this.frame = options.frame;
     40                        this.revisions = options.revisions;
     41                        this.set({
     42                                max:   this.revisions.length - 1,
     43                                value: this.revisions.indexOf( this.revisions.get( revisions.settings.selectedRevision ) ),
     44                                compareTwoMode: this.frame.get('compareTwoMode')
     45                        });
     46
     47                        // Listen for changes to the revisions or mode from outside
     48                        this.listenTo( this.frame, 'update:revisions', this.receiveRevisions );
     49                        this.listenTo( this.frame, 'change:compareTwoMode', this.updateMode );
     50
     51                        // Listen for internal changes
     52                        this.listenTo( this, 'change:from', this.handleLocalChanges );
     53                        this.listenTo( this, 'change:to', this.handleLocalChanges );
     54
     55                        // Listen for changes to the hovered revision
     56                        this.listenTo( this, 'change:hoveredRevision', this.hoverRevision );
     57                },
     58
     59                // Called when a revision is hovered
     60                hoverRevision: function( model, value ) {
     61                        this.trigger( 'hovered:revision', value );
     62                },
     63
     64                // Called when `compareTwoMode` changes
     65                updateMode: function( model, value ) {
     66                        this.set({ compareTwoMode: value });
     67                },
     68
     69                // Called when `from` or `to` changes in the local model
     70                handleLocalChanges: function() {
     71                        this.frame.set({
     72                                from: this.get('from'),
     73                                to: this.get('to')
     74                        });
     75                },
     76
     77                // Receives revisions changes from outside the model
     78                receiveRevisions: function( from, to ) {
     79                        // Bail if nothing changed
     80                        if ( this.get('from') === from && this.get('to') === to )
     81                                return;
     82
     83                        this.set({ from: from, to: to }, { silent: true });
     84                        this.trigger( 'update:revisions', from, to );
     85                }
     86
    3687        });
    3788
     
    3990                defaults: {
    4091                        revision: null,
    41                         position: 0
     92                        hovering: false, // Whether the mouse is hovering
     93                        scrubbing: false // Whether the mouse is scrubbing
     94                },
     95
     96                initialize: function( options ) {
     97                        this.revisions = options.revisions;
     98                        this.slider = options.slider;
     99
     100                        this.listenTo( this.slider, 'hovered:revision', this.updateRevision );
     101                        this.listenTo( this.slider, 'change:hovering', this.setHovering );
     102                        this.listenTo( this.slider, 'change:scrubbing', this.setScrubbing );
     103                },
     104
     105                updateRevision: function( revision ) {
     106                        this.set({ revision: revision });
     107                },
     108
     109                setHovering: function( model, value ) {
     110                        this.set({ hovering: value });
     111                },
     112
     113                setScrubbing: function( model, value ) {
     114                        this.set({ scrubbing: value });
    42115                }
    43116        });
     
    47120        revisions.model.Revisions = Backbone.Collection.extend({
    48121                model: revisions.model.Revision,
     122
     123                initialize: function() {
     124                        _.bindAll( this, 'next', 'prev' );
     125                },
    49126
    50127                comparator: function( a, b ) {
     
    402479                        }) );
    403480
     481                        // Prep the slider model
     482                        var slider = new revisions.model.Slider({
     483                                frame: this.model,
     484                                revisions: this.model.revisions
     485                        });
     486
    404487                        // Add the tooltip view
    405                         var tooltip = new revisions.view.Tooltip({
    406                                 model: new revisions.model.Tooltip()
    407                         });
    408                         this.views.add( tooltip );
    409 
    410                         // Add the Tickmarks view
     488                        this.views.add( new revisions.view.Tooltip({
     489                                model: new revisions.model.Tooltip({
     490                                        revisions: this.model.revisions,
     491                                        slider: slider
     492                                })
     493                        }) );
     494
     495                        // Add the tickmarks view
    411496                        this.views.add( new revisions.view.Tickmarks({
    412497                                model: this.model
    413498                        }) );
    414499
    415                         // Add the Slider view with a reference to the tooltip view
     500                        // Add the slider view
    416501                        this.views.add( new revisions.view.Slider({
    417                                 model: this.model,
    418                                 tooltip: tooltip
     502                                model: slider
    419503                        }) );
    420504
     
    432516                className: 'revisions-tickmarks',
    433517
    434                 numberOfTickmarksSet: function() {
    435                         var tickCount = this.model.revisions.length - 1, // One tickmark per model
    436                                 sliderWidth = $('.wp-slider').parent().width() * 0.7, // Width of slider is 70% of container (reset on resize)
    437                                 tickWidth = Math.floor( sliderWidth / tickCount ), // Divide width by # of tickmarks, round down
    438                                 newSiderWidth = ( ( tickWidth + 1 ) * tickCount ) + 1, // Calculate the actual width
    439                                 tickNumber;
    440 
    441                         $('.wp-slider').css( 'width', newSiderWidth ); // Reset the slider width to match the calculated tick size
    442                         this.$el.css( 'width', newSiderWidth ); // Match the tickmark div width
    443 
    444                         for ( tickNumber = 0; tickNumber <= tickCount; tickNumber++ ){
    445                                 this.$el.append('<div style="left:' + ( tickWidth * tickNumber ) + 'px;"></div>');
    446                         }
     518                render: function() {
     519                        var tickCount, tickWidth;
     520
     521                        tickCount = this.model.revisions.length - 1;
     522                        tickWidth = 1 / tickCount;
     523
     524                        this.$el.html('');
     525                        _(tickCount).times( function(){ this.$el.append( '<div></div>' ); }, this );
     526
     527                        this.$('div').css( 'width', ( 100 * tickWidth ) + '%' );
    447528                },
    448529
    449530                ready: function() {
    450                         var self = this;
    451                         self.numberOfTickmarksSet();
    452                         $( window ).on( 'resize', _.debounce( function() {
    453                                 self.$el.html('');
    454                                 self.numberOfTickmarksSet();
    455                         }, 50 ) );
     531                        this.render();
    456532                }
    457533        });
     
    528604                template: wp.template('revisions-tooltip'),
    529605
    530                 initialize: function() {
    531                         this.listenTo( this.model, 'change', this.render );
     606                initialize: function( options ) {
     607                        this.listenTo( this.model, 'change:revision', this.render );
     608                        this.listenTo( this.model, 'change:hovering', this.toggleVisibility );
     609                        this.listenTo( this.model, 'change:scrubbing', this.toggleVisibility );
    532610                },
    533611
    534612                ready: function() {
    535                         // Hide tooltip on start.
    536                         this.$el.addClass('hidden');
    537                 },
    538 
    539                 show: function() {
    540                         this.$el.removeClass('hidden');
    541                 },
    542 
    543                 hide: function() {
    544                         this.$el.addClass('hidden');
     613                        this.toggleVisibility({ immediate: true });
     614                },
     615
     616                visible: function() {
     617                        return this.model.get( 'scrubbing' ) || this.model.get( 'hovering' );
     618                },
     619
     620                toggleVisibility: function( options ) {
     621                        options = options || {};
     622                        var visible = this.visible()
     623                        if ( visible ) { // Immediate show
     624                                // this.$el.removeClass('fade');
     625                                this.$el.css( 'opacity', 1 );
     626                        } else if ( options.immediate ) { // Immediate fade out
     627                                this.$el.addClass('fade');
     628                                this.$el.css( 'opacity', 0 );
     629                        } else { // Wait a bit, make sure we're really done, then fade it out
     630                                _.delay( function( view ) {
     631                                        if ( ! view.visible() )
     632                                                view.toggleVisibility({ immediate: true });
     633                                }, 500, this );
     634                        }
    545635                },
    546636
    547637                render: function() {
     638                        var offset;
    548639                        // Check if a revision exists.
    549                         if ( null === this.model.get('revision') )
     640                        if ( _.isNull( this.model.get('revision') ) )
    550641                                return;
    551642
     
    554645
    555646                        // Set the position.
    556                         var offset = $('.revisions-buttons').offset().left;
    557                         this.$el.css( 'left', this.model.get('position') - offset );
     647                        offset = this.model.revisions.indexOf( this.model.get('revision') ) / ( this.model.revisions.length - 1 );
     648                        // 15% to get us to the start of the slider
     649                        // 0.7 to convert the slider-relative percentage to a page-relative percentage
     650                        // 100 to convert to a percentage
     651                        offset = 15 + (0.7 * offset * 100 ); // Now in a percentage
     652                        this.$el.css( 'left', offset + '%' );
    558653                }
    559654        });
     
    627722
    628723        // The slider view.
    629         // Encapsulates all of the configuration for the jQuery UI slider into a view.
    630724        revisions.view.Slider = wp.Backbone.View.extend({
    631725                className: 'wp-slider',
    632726
    633727                events: {
    634                         'mousemove'  : 'mousemove',
    635                         'mouseleave' : 'mouseleave',
    636                         'mouseenter' : 'mouseenter'
    637                 },
    638 
    639                 initialize: function( options ) {
    640                         _.bindAll( this, 'start', 'slide', 'stop' );
    641 
    642                         this.tooltip = options.tooltip;
    643 
    644                         // Create the slider model from the provided collection data.
    645                         var latestRevisionIndex = this.model.revisions.length - 1;
    646 
    647                         // Find the initially selected revision
    648                         var initiallySelectedRevisionIndex =
    649                                 this.model.revisions.indexOf(
    650                                         this.model.revisions.findWhere({ id: revisions.settings.selectedRevision }) );
    651 
    652                         this.settings = new revisions.model.Slider({
    653                                 max:   latestRevisionIndex,
    654                                 value: initiallySelectedRevisionIndex,
     728                        'mousemove'  : 'mouseMove',
     729                        'mouseleave' : 'mouseLeave',
     730                        'mouseenter' : 'mouseEnter'
     731                },
     732
     733                initialize: function() {
     734                        _.bindAll( this, 'start', 'slide', 'stop', 'mouseMove' );
     735                        this.listenTo( this.model, 'change:compareTwoMode', this.updateSliderSettings );
     736                        this.listenTo( this.model, 'update:revisions', this.updateSliderSettings );
     737                },
     738
     739                ready: function() {
     740                        this.$el.slider( _.extend( this.model.toJSON(), {
    655741                                start: this.start,
    656742                                slide: this.slide,
    657743                                stop:  this.stop
    658                         });
    659                 },
    660 
    661                 ready: function() {
    662                         // Refresh the currently selected revision position in case router has set it.
    663                         this.settings.attributes.value = this.model.revisions.indexOf(
    664                                 this.model.revisions.findWhere({ id: revisions.settings.selectedRevision }) );
    665 
    666                         // And update the slider in case the route has set it.
    667                         this.updateSliderSettings();
    668                         this.slide( '', this.settings.attributes );
    669                         this.$el.slider( this.settings.toJSON() );
    670 
    671                         // Listen for changes in Compare Two Mode setting
    672                         this.listenTo( this.model, 'change:compareTwoMode', this.updateSliderSettings );
    673 
    674                         this.settings.on( 'change', function() {
    675                                 this.updateSliderSettings();
    676                         }, this );
    677 
    678                         // Listen for changes to the revisions
    679                         this.listenTo( this.model, 'update:revisions', this.updateRevisions );
    680                 },
    681 
    682                 mousemove: function( e ) {
    683                         var tickCount = this.model.revisions.length - 1, // One tickmark per model
    684                                 sliderLeft = Math.ceil( this.$el.offset().left ), // Left edge of slider
     744                        }) );
     745
     746                        this.listenTo( this, 'slide:stop', this.updateSliderSettings );
     747                },
     748
     749                mouseMove: function( e ) {
     750                        var zoneCount = this.model.revisions.length - 1, // One fewer zone than models
     751                                sliderLeft = this.$el.offset().left, // Left edge of slider
    685752                                sliderWidth = this.$el.width(), // Width of slider
    686                                 tickWidth = Math.floor( sliderWidth / tickCount ), // Calculated width of tickmark
     753                                tickWidth = sliderWidth / zoneCount, // Calculated width of zone
    687754                                actualX = e.clientX - sliderLeft, // Offset of mouse position in slider
    688                                 currentModelIndex = Math.floor( ( actualX + tickWidth / 2 ) / tickWidth ), // Calculate the model index
    689                                 tooltipPosition = sliderLeft + 2 + currentModelIndex * tickWidth; // Stick tooltip to tickmark
     755                                currentModelIndex = Math.floor( ( actualX + ( tickWidth / 2 )  ) / tickWidth ); // Calculate the model index
    690756
    691757                        // Reverse direction in RTL mode.
     
    700766
    701767                        // Update the tooltip model
    702                         this.tooltip.model.set( 'revision', this.model.revisions.at( currentModelIndex ) );
    703                         this.tooltip.model.set( 'position', tooltipPosition );
    704                 },
    705 
    706                 mouseleave: function( e ) {
    707                         this.tooltip.hide();
    708                 },
    709 
    710                 mouseenter: function( e ) {
    711                         this.tooltip.show();
     768                        this.model.set({
     769                                'hoveredRevision': this.model.revisions.at( currentModelIndex )
     770                        });
     771                },
     772
     773                mouseLeave: function() {
     774                        this.model.set({ hovering: false });
     775                },
     776
     777                mouseEnter: function() {
     778                        this.model.set({ hovering: true });
    712779                },
    713780
    714781                updateSliderSettings: function() {
    715                         var handles;
     782                        var handles, leftValue, rightValue;
    716783
    717784                        if ( this.model.get('compareTwoMode') ) {
    718                                 var leftValue, rightValue;
    719 
    720                                 // In single handle mode, the 1st stored revision is 'blank' and the 'from' model is not set
    721                                 // In this case we move the to index over one
    722                                 if ( _.isUndefined( this.model.get('from') ) ) {
    723                                         if ( isRtl ) {
    724                                                 leftValue  = this.model.revisions.length -  this.model.revisions.indexOf( this.model.get('to') ) - 2;
    725                                                 rightValue = leftValue + 1;
    726                                         } else {
    727                                                 leftValue  = this.model.revisions.indexOf( this.model.get('to') );
    728                                                 rightValue = leftValue + 1;
    729                                         }
    730                                 } else {
    731                                         leftValue = isRtl ?     this.model.revisions.length - this.model.revisions.indexOf( this.model.get('to') ) - 1 :
     785                                leftValue = isRtl ?     this.model.revisions.length - this.model.revisions.indexOf( this.model.get('to') ) - 1 :
    732786                                                                                        this.model.revisions.indexOf( this.model.get('from') ),
    733                                         rightValue = isRtl ?    this.model.revisions.length - this.model.revisions.indexOf( this.model.get('from') ) - 1 :
     787                                rightValue = isRtl ?    this.model.revisions.length - this.model.revisions.indexOf( this.model.get('from') ) - 1 :
    734788                                                                                        this.model.revisions.indexOf( this.model.get('to') );
    735                                 }
    736789
    737790                                // Set handles to current from / to models.
     
    767820                },
    768821
    769                 updateRevisions: function( from, to ) {
    770                         // Update the view settings when the revisions have changed.
    771                         if ( this.model.get('compareTwoMode') ) {
    772                                 this.settings.set({ 'values': [
    773                                         this.model.revisions.indexOf( from ),
    774                                         this.model.revisions.indexOf( to )
    775                                 ] });
    776                         } else {
    777                                 this.settings.set({ 'value': this.model.revisions.indexOf( to ) });
    778                         }
    779                 },
    780 
    781822                getSliderPosition: function( ui ){
    782823                        return isRtl ? this.model.revisions.length - ui.value - 1 : ui.value;
     
    784825
    785826                start: function( event, ui ) {
     827                        this.model.set({ scrubbing: true });
     828
    786829                        // Track the mouse position to enable smooth dragging,
    787830                        // overrides default jQuery UI step behavior.
    788                         $( window ).on( 'mousemove', { view: this }, function( e ) {
    789                                 var view              = e.data.view,
     831                        $( window ).on( 'mousemove.wp.revisions', { view: this }, function( e ) {
     832                                var view            = e.data.view,
    790833                                        leftDragBoundary  = view.$el.offset().left, // Initial left boundary
    791834                                        sliderOffset      = leftDragBoundary,
     
    797840                                // In two handle mode, ensure handles can't be dragged past each other.
    798841                                // Adjust left/right boundaries and reset points.
    799                                 if ( view.model.get('compareTwoMode') ) {
     842                                if ( view.model.frame.get('compareTwoMode') ) {
    800843                                        var rightHandle = $( ui.handle ).parent().find('.right-handle'),
    801844                                                leftHandle  = $( ui.handle ).parent().find('.left-handle');
     
    835878                },
    836879
     880                // Responds to slide events
    837881                slide: function( event, ui ) {
    838882                        var attributes;
    839883                        // Compare two revisions mode
    840                         if ( ! _.isUndefined( ui.values ) && this.model.get('compareTwoMode') ) {
     884                        if ( ! _.isUndefined( ui.values ) && this.model.frame.get('compareTwoMode') ) {
    841885                                // Prevent sliders from occupying same spot
    842886                                if ( ui.values[1] === ui.values[0] )
     
    859903                                        attributes.from = this.model.revisions.at( sliderPosition - 1  );
    860904                                else
    861                                         this.model.unset( 'from', { silent: true });
     905                                        attributes.from = undefined;
    862906                        }
    863907                        this.model.set( attributes );
     908
     909                        // If we are scrubbing, a scrub to a revision is considered a hover
     910                        if ( this.model.get( 'scrubbing' ) ) {
     911                                this.model.set({
     912                                        'hoveredRevision': attributes.to
     913                                });
     914                        }
    864915                },
    865916
    866917                stop: function( event, ui ) {
    867                         $( window ).off('mousemove');
    868 
    869                         // Reset settings props handle back to the step position.
    870                         this.settings.trigger('change');
     918                        $( window ).off('mousemove.wp.revisions');
     919                        this.updateSliderSettings();
     920                        this.model.set({ scrubbing: false });
    871921                }
    872922        });
     
    913963                        // If `b` is undefined, this is an 'at/:to' route, for a single revision
    914964                        if ( _.isUndefined( b ) ) {
    915                                 b = a;
    916                                 a = 0;
     965                                b = this.model.revisions.get( a );
     966                                a = this.model.revisions.prev( b );
     967                                b = b ? b.id : 0;
     968                                a = a ? a.id : 0
    917969                                compareTwo = false;
    918970                        } else {
     
    926978
    927979                        if ( ! _.isUndefined( this.model ) ) {
    928                                 var selectedToRevision = this.model.revisions.findWhere({ id: to }),
    929                                         selectedFromRevision = this.model.revisions.findWhere({ id: from });
     980                                var selectedToRevision = this.model.revisions.get( to ),
     981                                        selectedFromRevision = this.model.revisions.get( from );
    930982
    931983                                this.model.set({
Note: See TracChangeset for help on using the changeset viewer.