WordPress.org

Make WordPress Core

Changeset 24658


Ignore:
Timestamp:
07/11/2013 09:14:14 AM (9 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.