Wordpress/wp-admin/js/revisions.js
Daryl Koopersmith f9e4a8500a Revisions: Move data management into the frame state. See #24425.
This helps to solidify the separation between data and UI. We track the diffId internally now, as the property itself was never referenced, and was always derived from the 'from' and 'to' models. This also sets us up for better request debouncing and diff management.



git-svn-id: https://develop.svn.wordpress.org/trunk@24609 602fd350-edb4-49c9-b593-d223f7449a82
2013-07-09 08:50:32 +00:00

853 lines
26 KiB
JavaScript

window.wp = window.wp || {};
(function($) {
var revisions;
revisions = wp.revisions = { model: {}, view: {}, controller: {} };
// Link settings.
revisions.settings = typeof _wpRevisionsSettings === 'undefined' ? {} : _wpRevisionsSettings;
/**
* ========================================================================
* MODELS
* ========================================================================
*/
revisions.model.Slider = Backbone.Model.extend({
defaults: {
value: 0,
min: 0,
max: 1,
step: 1
}
});
revisions.model.Tooltip = Backbone.Model.extend({
defaults: {
revision: null,
position: 0
}
});
revisions.model.Revision = Backbone.Model.extend({});
revisions.model.Revisions = Backbone.Collection.extend({
model: revisions.model.Revision,
comparator: function( revision ) {
return revision.id;
}
});
revisions.model.Field = Backbone.Model.extend({});
revisions.model.Fields = Backbone.Collection.extend({
model: revisions.model.Field
});
revisions.model.Diff = Backbone.Model.extend({
initialize: function( attributes, options ) {
var fields = this.get('fields');
this.unset('fields');
this.fields = new revisions.model.Fields( fields );
}
});
revisions.model.Diffs = Backbone.Collection.extend({
initialize: function( models, options ) {
this.revisions = options.revisions;
this.requests = {};
},
model: revisions.model.Diff,
ensure: function( id, context ) {
var diff = this.get( id );
var request = this.requests[ id ];
var deferred = $.Deferred();
var ids = {};
if ( diff ) {
deferred.resolveWith( context, [ diff ] );
} else {
this.trigger( 'ensure:load', ids );
_.each( ids, _.bind( function(id) {
// Remove anything that has an ongoing request
if ( this.requests[ id ] )
delete ids[ id ];
}, this ) );
if ( ! request ) {
// Always include the ID that started this ensure
ids[ id ] = true;
request = this.load( _.keys( ids ) );
}
request.done( _.bind( function() {
deferred.resolveWith( context, [ this.get( id ) ] );
}, this ) );
}
return deferred.promise();
},
loadNew: function( comparisons ) {
comparisons = _.object( comparisons, comparisons );
_.each( comparisons, _.bind( function( id ) {
// Exists
if ( this.get( id ) )
delete comparisons[ id ];
}, this ) );
comparisons = _.toArray( comparisons );
return this.load( comparisons );
},
load: function( comparisons ) {
// Our collection should only ever grow, never shrink, so remove: false
return this.fetch({ data: { compare: comparisons }, remove: false });
},
loadLast: function( num ) {
num = num || 1;
var ids = this.getProximalDiffIds();
ids = _.last( ids, num );
if ( ids.length ) {
return this.loadNew( ids );
}
},
loadLastUnloaded: function( num ) {
num = num || 1;
var ids = this.getUnloadedProximalDiffIds();
ids = _.last( ids, num );
if ( ids.length ) {
return this.loadNew( ids );
}
},
getProximalDiffIds: function() {
var previous = 0, ids = [];
this.revisions.each( _.bind( function( revision ) {
ids.push( previous + ':' + revision.id );
previous = revision.id;
}, this ) );
return ids;
},
getUnloadedProximalDiffIds: function() {
var comparisons = this.getProximalDiffIds();
comparisons = _.object( comparisons, comparisons );
_.each( comparisons, _.bind( function( id ) {
// Exists
if ( this.get( id ) )
delete comparisons[ id ];
}, this ) );
return _.toArray( comparisons );
},
loadAllBy: function( chunkSize ) {
chunkSize = chunkSize || 20;
var unloaded = this.getUnloadedProximalDiffIds();
if ( unloaded.length ) {
return this.loadLastUnloaded( chunkSize ).always( _.bind( function() {
this.loadAllBy( chunkSize );
}, this ) );
}
},
sync: function( method, model, options ) {
if ( 'read' === method ) {
options = options || {};
options.context = this;
options.data = _.extend( options.data || {}, {
action: 'get-revision-diffs',
post_id: revisions.settings.postId
});
var deferred = wp.xhr.send( options );
var requests = this.requests;
// Record that we're requesting each diff.
if ( options.data.compare ) {
_.each( options.data.compare, function( id ) {
requests[ id ] = deferred;
});
}
// When the request completes, clear the stored request.
deferred.always( function() {
if ( options.data.compare ) {
_.each( options.data.compare, function( id ) {
delete requests[ id ];
});
}
});
return deferred;
// Otherwise, fall back to `Backbone.sync()`.
} else {
return Backbone.Model.prototype.sync.apply( this, arguments );
}
}
});
revisions.model.FrameState = Backbone.Model.extend({
initialize: function( attributes, options ) {
this.revisions = options.revisions;
this.diffs = new revisions.model.Diffs( [], { revisions: this.revisions });
this.listenTo( this, 'change:from', this.updateDiff );
this.listenTo( this, 'change:to', this.updateDiff );
this.router = new revisions.Router({ model: this });
},
// So long as `from` and `to` are changed at the same time, the diff
// will only be updated once. This is because Backbone updates all of
// the changed attributes in `set`, and then fires the `change` events.
updateDiff: function() {
var from, to, diffId;
from = this.get('from');
to = this.get('to');
diffId = ( from ? from.id : 0 ) + ':' + to.id;
// Check if we're actually changing the diff id.
if ( this._diffId === diffId )
return;
this._diffId = diffId;
this.trigger( 'update:revisions', from, to );
this.diffs.ensure( diffId, this ).done( function( diff ) {
// Check if the current diff changed while the request was in flight.
if ( this._diffId !== diff.id )
return;
this.trigger( 'update:diff', diff );
});
}
});
/**
* ========================================================================
* VIEWS
* ========================================================================
*/
// The frame view. This contains the entire page.
revisions.view.Frame = wp.Backbone.View.extend({
className: 'revisions',
template: wp.template('revisions-frame'),
initialize: function() {
this.model = new revisions.model.FrameState({}, {
revisions: this.collection
});
this.listenTo( this.model, 'update:diff', this.renderDiff );
this.listenTo( this.model, 'change:compareTwoMode', this.updateCompareTwoMode );
this.views.set( '.revisions-control-frame', new revisions.view.Controls({
model: this.model
}) );
if ( this.model.revisions.length ) {
var last = this.model.revisions.last(2);
var attributes = { to: last.pop() };
if ( last.length )
attributes.from = last.pop();
this.model.set( attributes );
// Load the rest: first 10, then the rest by 50
this.model.diffs.loadLastUnloaded( 10 ).always( _.bind( function() {
this.model.diffs.loadAllBy( 50 );
}, this ) );
}
Backbone.history.start();
},
render: function() {
wp.Backbone.View.prototype.render.apply( this, arguments );
$('#wpbody-content .wrap').append( this.el );
this.views.ready();
return this;
},
renderDiff: function( diff ) {
this.views.set( '.revisions-diff-frame', new revisions.view.Diff({
model: diff
}) );
},
updateCompareTwoMode: function() {
this.$el.toggleClass( 'comparing-two-revisions', this.model.get('compareTwoMode') );
}
});
// The control view.
// This contains the revision slider, previous/next buttons, the meta info and the compare checkbox.
revisions.view.Controls = wp.Backbone.View.extend({
className: 'revisions-controls',
initialize: function() {
// Add the button view
this.views.add( new revisions.view.Buttons({
model: this.model
}) );
// Add the checkbox view
this.views.add( new revisions.view.Checkbox({
model: this.model
}) );
// Add the tooltip view
var tooltip = new revisions.view.Tooltip({
model: new revisions.model.Tooltip()
});
this.views.add( tooltip );
// Add the Tickmarks view
this.views.add( new revisions.view.Tickmarks({
model: this.model
}) );
// Add the Slider view with a reference to the tooltip view
this.views.add( new revisions.view.Slider({
model: this.model,
tooltip: tooltip
}) );
// Add the Meta view
this.views.add( new revisions.view.Meta({
model: this.model
}) );
}
});
// The tickmarks view
// This contains the slider tickmarks.
revisions.view.Tickmarks = wp.Backbone.View.extend({
className: 'revisions-tickmarks',
numberOfTickmarksSet: function() {
var tickCount = this.model.revisions.length - 1, // One tickmark per model
sliderWidth = $('.wp-slider').parent().width() * 0.7, // Width of slider is 70% of container (reset on resize)
tickWidth = Math.floor( sliderWidth / tickCount ), // Divide width by # of tickmarks, round down
newSiderWidth = ( ( tickWidth + 1 ) * tickCount ) + 1, // Calculate the actual width
tickNumber;
$('.wp-slider').css( 'width', newSiderWidth ); // Reset the slider width to match the calculated tick size
this.$el.css( 'width', newSiderWidth ); // Match the tickmark div width
for ( tickNumber = 0; tickNumber <= tickCount; tickNumber++ ){
this.$el.append('<div style="left:' + ( tickWidth * tickNumber ) + 'px;"></div>');
}
},
ready: function() {
var self = this;
self.numberOfTickmarksSet();
$( window ).on( 'resize', _.debounce( function() {
self.$el.html('');
self.numberOfTickmarksSet();
}, 50 ) );
}
});
// The meta view.
// This contains the revision meta, and the restore button.
revisions.view.Meta = wp.Backbone.View.extend({
className: 'revisions-meta',
template: wp.template('revisions-meta'),
events: {
'click #restore-revision': 'restoreRevision'
},
initialize: function() {
this.listenTo( this.model, 'update:revisions', this.updateMeta );
},
restoreRevision: function() {
var restoreUrl = this.model.get('to').attributes.restoreUrl.replace(/&amp;/g, '&');
document.location = restoreUrl;
},
updateMeta: function( from, to ) {
this.$el.html( this.template( this.model.toJSON() ) );
$('#restore-revision').prop( 'disabled', to.attributes.current );
}
});
// The checkbox view.
// Encapsulates all of the configuration for the compare checkbox.
revisions.view.Checkbox = wp.Backbone.View.extend({
className: 'revisions-checkbox',
template: wp.template('revisions-checkbox'),
events: {
'click .compare-two-revisions': 'compareTwoToggle'
},
initialize: function() {
this.$el.html( this.template() );
},
updateCompareTwoMode: function() {
if ( this.model.get('compareTwoMode') ) {
$('.compare-two-revisions').prop( 'checked', true );
// in RTL mode the 'left handle' is the second in the slider, 'right' is first
$('.wp-slider a.ui-slider-handle').first().addClass( isRtl ? 'right-handle' : 'left-handle' );
$('.wp-slider a.ui-slider-handle').last().addClass( isRtl ? 'left-handle' : 'right-handle' );
} else {
$('.compare-two-revisions').prop( 'checked', false );
$('.wp-slider a.ui-slider-handle').removeClass('left-handle').removeClass('right-handle');
}
},
// Toggle the compare two mode feature when the compare two checkbox is checked.
compareTwoToggle: function( event ) {
// Activate compare two mode?
this.model.set({ compareTwoMode: $('.compare-two-revisions').prop('checked') });
// Update route
this.model.router.updateUrl();
},
ready: function() {
// Hide compare two mode toggle when fewer than three revisions.
if ( this.model.revisions.length < 3 )
$('.revision-toggle-compare-mode').hide();
this.listenTo( this.model, 'change:compareTwoMode', this.updateCompareTwoMode );
// Update the mode in case route has set it
this.updateCompareTwoMode();
}
});
// The tooltip view.
// Encapsulates the tooltip.
revisions.view.Tooltip = wp.Backbone.View.extend({
className: 'revisions-tooltip',
template: wp.template('revisions-tooltip'),
initialize: function() {
this.listenTo( this.model, 'change', this.render );
},
ready: function() {
// Hide tooltip on start.
this.$el.addClass('hidden');
},
show: function() {
this.$el.removeClass('hidden');
},
hide: function() {
this.$el.addClass('hidden');
},
render: function() {
// Check if a revision exists.
if ( null === this.model.get('revision') )
return;
// Insert revision data.
this.$el.html( this.template( this.model.get('revision').toJSON() ) );
// Set the position.
var offset = $('.revisions-buttons').offset().left;
this.$el.css( 'left', this.model.get('position') - offset );
}
});
// The buttons view.
// Encapsulates all of the configuration for the previous/next buttons.
revisions.view.Buttons = wp.Backbone.View.extend({
className: 'revisions-buttons',
template: wp.template('revisions-buttons'),
events: {
'click #next': 'nextRevision',
'click #previous': 'previousRevision'
},
initialize: function() {
this.$el.html( this.template() );
},
ready: function() {
this.listenTo( this.model, 'update:revisions', this.disabledButtonCheck );
},
// Go to a specific modelindex, taking into account RTL mode.
gotoModel: function( toIndex ) {
var attributes = {
to: this.model.revisions.at( isRtl ? this.model.revisions.length - toIndex - 1 : toIndex ) // Reverse directions for RTL.
};
// If we're at the first revision, unset 'from'.
if ( isRtl ? this.model.revisions.length - toIndex - 1 : toIndex ) // Reverse directions for RTL
attributes.from = this.model.revisions.at( isRtl ? this.model.revisions.length - toIndex - 2 : toIndex - 1 );
else
this.model.unset('from', { silent: true });
this.model.set( attributes );
// Update route
this.model.router.updateUrl();
},
// Go to the 'next' revision, direction takes into account RTL mode.
nextRevision: function() {
var toIndex = isRtl ? this.model.revisions.length - this.model.revisions.indexOf( this.model.get('to') ) - 1 : this.model.revisions.indexOf( this.model.get('to') );
toIndex = isRtl ? toIndex - 1 : toIndex + 1;
this.gotoModel( toIndex );
},
// Go to the 'previous' revision, direction takes into account RTL mode.
previousRevision: function() {
var toIndex = isRtl ? this.model.revisions.length - this.model.revisions.indexOf( this.model.get('to') ) - 1 : this.model.revisions.indexOf( this.model.get('to') );
toIndex = isRtl ? toIndex + 1 : toIndex - 1;
this.gotoModel( toIndex );
},
// Check to see if the Previous or Next buttons need to be disabled or enabled.
disabledButtonCheck: function() {
var maxVal = this.model.revisions.length - 1,
minVal = 0,
next = $('.revisions-next .button'),
previous = $('.revisions-previous .button'),
val = this.model.revisions.indexOf( this.model.get('to') );
// Disable "Next" button if you're on the last node.
next.prop( 'disabled', ( maxVal === val ) );
// Disable "Previous" button if you're on the first node.
previous.prop( 'disabled', ( minVal === val ) );
}
});
// The slider view.
// Encapsulates all of the configuration for the jQuery UI slider into a view.
revisions.view.Slider = wp.Backbone.View.extend({
className: 'wp-slider',
events: {
'mousemove' : 'mousemove',
'mouseleave' : 'mouseleave',
'mouseenter' : 'mouseenter'
},
initialize: function( options ) {
_.bindAll( this, 'start', 'slide', 'stop' );
this.tooltip = options.tooltip;
// Create the slider model from the provided collection data.
var latestRevisionIndex = this.model.revisions.length - 1;
// Find the initially selected revision
var initiallySelectedRevisionIndex =
this.model.revisions.indexOf(
this.model.revisions.findWhere({ id: revisions.settings.selectedRevision }) );
this.settings = new revisions.model.Slider({
max: latestRevisionIndex,
value: initiallySelectedRevisionIndex,
start: this.start,
slide: this.slide,
stop: this.stop
});
},
ready: function() {
// Refresh the currently selected revision position in case router has set it.
this.settings.attributes.value = this.model.revisions.indexOf(
this.model.revisions.findWhere({ id: revisions.settings.selectedRevision }) );
// And update the slider in case the route has set it.
this.updateSliderSettings();
this.slide( '', this.settings.attributes );
this.$el.slider( this.settings.toJSON() );
// Listen for changes in Compare Two Mode setting
this.listenTo( this.model, 'change:compareTwoMode', this.updateSliderSettings );
this.settings.on( 'change', function() {
this.updateSliderSettings();
}, this );
// Listen for changes to the revisions
this.listenTo( this.model, 'update:revisions', this.updateRevisions );
},
mousemove: function( e ) {
var tickCount = this.model.revisions.length - 1, // One tickmark per model
sliderLeft = Math.ceil( this.$el.offset().left ), // Left edge of slider
sliderWidth = this.$el.width(), // Width of slider
tickWidth = Math.floor( sliderWidth / tickCount ), // Calculated width of tickmark
actualX = e.clientX - sliderLeft, // Offset of mouse position in slider
currentModelIndex = Math.floor( ( actualX + tickWidth / 2 ) / tickWidth ), // Calculate the model index
tooltipPosition = sliderLeft + 2 + currentModelIndex * tickWidth; // Stick tooltip to tickmark
// Reverse direction in RTL mode.
if ( isRtl )
currentModelIndex = this.model.revisions.length - currentModelIndex - 1;
// Ensure sane value for currentModelIndex.
if ( currentModelIndex < 0 )
currentModelIndex = 0;
else if ( currentModelIndex >= this.model.revisions.length )
currentModelIndex = this.model.revisions.length - 1;
// Update the tooltip model
this.tooltip.model.set( 'revision', this.model.revisions.at( currentModelIndex ) );
this.tooltip.model.set( 'position', tooltipPosition );
},
mouseleave: function( e ) {
this.tooltip.hide();
},
mouseenter: function( e ) {
this.tooltip.show();
},
updateSliderSettings: function() {
if ( this.model.get('compareTwoMode') ) {
var leftValue, rightValue;
// In single handle mode, the 1st stored revision is 'blank' and the 'from' model is not set
// In this case we move the to index over one
if ( 'undefined' == typeof this.model.get('from') ) {
if ( isRtl ) {
leftValue = this.model.revisions.length - this.model.revisions.indexOf( this.model.get('to') ) - 2;
rightValue = leftValue + 1;
} else {
leftValue = this.model.revisions.indexOf( this.model.get('to') );
rightValue = leftValue + 1;
}
} else {
leftValue = isRtl ? this.model.revisions.length - this.model.revisions.indexOf( this.model.get('to') ) - 1 :
this.model.revisions.indexOf( this.model.get('from') ),
rightValue = isRtl ? this.model.revisions.length - this.model.revisions.indexOf( this.model.get('from') ) - 1 :
this.model.revisions.indexOf( this.model.get('to') );
}
// Set handles to current from / to models.
// Reverse order for RTL
this.$el.slider( {
values: [
leftValue,
rightValue
],
value: null,
range: true // Range mode ensures handles can't cross
} );
} else {
this.$el.slider( { // Set handle to current to model
// Reverse order for RTL.
value: isRtl ? this.model.revisions.length - this.model.revisions.indexOf( this.model.get('to') ) - 1 :
this.model.revisions.indexOf( this.model.get('to') ),
values: null, // Clear existing two handled values
range: false
} );
}
if ( this.model.get('compareTwoMode') ){
// in RTL mode the 'left handle' is the second in the slider, 'right' is first
$( 'a.ui-slider-handle', this.$el )
.first()
.addClass( isRtl ? 'right-handle' : 'left-handle' )
.removeClass( isRtl ? 'left-handle' : 'right-handle' );
$( 'a.ui-slider-handle', this.$el )
.last()
.addClass( isRtl ? 'left-handle' : 'right-handle' )
.removeClass( isRtl ? 'right-handle' : 'left-handle' );
}
},
updateRevisions: function( from, to ) {
// Update the view settings when the revisions have changed.
if ( this.model.get('compareTwoMode') ) {
this.settings.set({ 'values': [
this.model.revisions.indexOf( from ),
this.model.revisions.indexOf( to )
] });
} else {
this.settings.set({ 'value': this.model.revisions.indexOf( to ) });
}
},
getSliderPosition: function( ui ){
return isRtl ? this.model.revisions.length - ui.value - 1 : ui.value;
},
start: function( event, ui ) {
// Track the mouse position to enable smooth dragging,
// overrides default jQuery UI step behavior.
$( window ).on( 'mousemove', { view: this }, function( e ) {
var view = e.data.view,
leftDragBoundary = view.$el.offset().left, // Initial left boundary
sliderOffset = leftDragBoundary,
sliderRightEdge = leftDragBoundary + view.$el.width(),
rightDragBoundary = sliderRightEdge, // Initial right boundary
leftDragReset = 0, // Initial left drag reset
rightDragReset = sliderRightEdge - sliderOffset; // Initial right drag reset
// In two handle mode, ensure handles can't be dragged past each other.
// Adjust left/right boundaries and reset points.
if ( view.model.get('compareTwoMode') ) {
var rightHandle = $( ui.handle ).parent().find('.right-handle'),
leftHandle = $( ui.handle ).parent().find('.left-handle');
if ( $( ui.handle ).hasClass('left-handle') ) {
// Dragging the left handle, boundary is right handle.
// RTL mode calculations reverse directions.
if ( isRtl ) {
leftDragBoundary = rightHandle.offset().left + rightHandle.width();
leftDragReset = leftDragBoundary - sliderOffset;
} else {
rightDragBoundary = rightHandle.offset().left;
rightDragReset = rightDragBoundary - sliderOffset;
}
} else {
// Dragging the right handle, boundary is the left handle.
// RTL mode calculations reverse directions.
if ( isRtl ) {
rightDragBoundary = leftHandle.offset().left;
rightDragReset = rightDragBoundary - sliderOffset;
} else {
leftDragBoundary = leftHandle.offset().left + leftHandle.width() ;
leftDragReset = leftDragBoundary - sliderOffset;
}
}
}
// Follow mouse movements, as long as handle remains inside slider.
if ( e.clientX < leftDragBoundary ) {
$( ui.handle ).css( 'left', leftDragReset ); // Mouse to left of slider.
} else if ( e.clientX > rightDragBoundary ) {
$( ui.handle ).css( 'left', rightDragReset ); // Mouse to right of slider.
} else {
$( ui.handle ).css( 'left', e.clientX - sliderOffset ); // Mouse in slider.
}
} );
},
slide: function( event, ui ) {
var attributes;
// Compare two revisions mode
if ( 'undefined' !== typeof ui.values && this.model.get('compareTwoMode') ) {
// Prevent sliders from occupying same spot
if ( ui.values[1] === ui.values[0] )
return false;
attributes = {
to: this.model.revisions.at( isRtl ? this.model.revisions.length - ui.values[0] - 1 : ui.values[1] ), // Reverse directions for RTL.
from: this.model.revisions.at( isRtl ? this.model.revisions.length - ui.values[1] - 1 : ui.values[0] ) // Reverse directions for RTL.
};
} else {
// Compare single revision mode
var sliderPosition = this.getSliderPosition( ui );
attributes = {
to: this.model.revisions.at( sliderPosition )
};
// If we're at the first revision, unset 'from'.
if ( sliderPosition ) // Reverse directions for RTL.
attributes.from = this.model.revisions.at( sliderPosition - 1 );
else
this.model.unset( 'from', { silent: true });
}
this.model.set( attributes );
},
stop: function( event, ui ) {
$( window ).off('mousemove');
// Reset settings props handle back to the step position.
this.settings.trigger('change');
}
});
// The diff view.
// This is the view for the current active diff.
revisions.view.Diff = wp.Backbone.View.extend({
className: 'revisions-diff',
template: wp.template('revisions-diff'),
// Generate the options to be passed to the template.
prepare: function() {
return _.extend({ fields: this.model.fields.toJSON() }, this.options );
}
});
// The revisions router
// takes URLs with #hash fragments and routes them
revisions.Router = Backbone.Router.extend({
initialize: function( options ) {
this.model = options.model;
// Maintain state history when dragging
this.listenTo( this.model, 'renderDiff', _.debounce( this.updateUrl, 250 ) );
},
routes: {
'revision/from/:from/to/:to/handles/:handles': 'gotoRevisionId'
},
updateUrl: function() {
var from = this.model.has('from') ? this.model.get('from').id : 0;
var to = this.model.get('to').id;
var handles = this.model.get('compareTwoMode') ? '2' : '1';
this.navigate( '/revision/from/' + from + '/to/' + to + '/handles/' + handles );
},
gotoRevisionId: function( from, to, handles ) {
from = parseInt( from, 10 );
to = parseInt( to, 10 );
this.model.set({ compareTwoMode: ( '2' === handles ) });
if ( 'undefined' !== typeof this.model ) {
var selectedToRevision = this.model.revisions.findWhere({ id: to }),
selectedFromRevision = this.model.revisions.findWhere({ id: from });
this.model.set({
to: selectedToRevision,
from: selectedFromRevision
});
}
revisions.settings.selectedRevision = to;
}
});
// Initialize the revisions UI.
revisions.init = function() {
revisions.view.frame = new revisions.view.Frame({
collection: new revisions.model.Revisions( revisions.settings.revisionData )
}).render();
};
$( revisions.init );
}(jQuery));