From 12de8838924c8f14e803bf090da34fe0ef5de102 Mon Sep 17 00:00:00 2001
From: Tom <tw201207@gmail.com>
Date: Fri, 14 Nov 2014 16:14:28 -0500
Subject: [PATCH] Javascript-based sliders styled with CSS

---
 src/main/java/com/gitblit/wicket/pages/ImageDiffHandler.java |   13 +-
 src/main/resources/gitblit.css                               |  177 +++++++++++++++++------------
 src/main/java/com/gitblit/wicket/pages/scripts/imgdiff.js    |  143 +++++++++++++++++++++--
 3 files changed, 238 insertions(+), 95 deletions(-)

diff --git a/src/main/java/com/gitblit/wicket/pages/ImageDiffHandler.java b/src/main/java/com/gitblit/wicket/pages/ImageDiffHandler.java
index 1232e99..52bf13b 100644
--- a/src/main/java/com/gitblit/wicket/pages/ImageDiffHandler.java
+++ b/src/main/java/com/gitblit/wicket/pages/ImageDiffHandler.java
@@ -67,8 +67,9 @@
 				imgDiffCount++;
 				String id = "imgdiff" + imgDiffCount;
 				HtmlBuilder builder = new HtmlBuilder("div");
-				Element container = builder.root().attr("align", "center").appendElement("div").attr("class", "imgdiff");
-				Element resizeable = container.appendElement("div").attr("class", "imgdiff-left");
+				Element wrapper = builder.root().attr("class", "imgdiff-container").attr("id", "imgdiff-" + id);
+				Element container = wrapper.appendElement("div").attr("class", "imgdiff-ovr-slider").appendElement("div").attr("class", "imgdiff");
+				Element old = container.appendElement("div").attr("class", "imgdiff-left");
 				// style='max-width:640px;' is necessary for ensuring that the browser limits large images
 				// to some reasonable width, and to override the "img { max-width: 100%; }" from bootstrap.css,
 				// which would scale the left image to the width of its resizeable container, which isn't what
@@ -77,12 +78,10 @@
 				// is too wide.
 				// XXX: Maybe add a max-height, too, to limit portrait-oriented images to some reasonable height?
 				// (Like a 300x10000px image...)
-				resizeable.appendElement("img").attr("class", "imgdiff-left").attr("id", id).attr("style", "max-width:640px;").attr("src", oldUrl);
+				old.appendElement("img").attr("class", "imgdiff-old").attr("id", id).attr("style", "max-width:640px;").attr("src", oldUrl);
 				container.appendElement("img").attr("class", "imgdiff").attr("style", "max-width:640px;").attr("src", newUrl);
-				builder.root().appendElement("br");
-				Element slider = builder.root().appendElement("div").attr("class", "imgdiff-slider");
-				slider.appendElement("div").attr("class", "imgdiff-slider-resizeable").attr("id", "slider-" + id)
-					.appendElement("div").attr("class", "imgdiff-slider-left");
+				wrapper.appendElement("br");
+				wrapper.appendElement("div").attr("class", "imgdiff-opa-container").appendElement("div").attr("class", "imgdiff-opa-slider");
 				return builder.toString();
 			}
 			break;
diff --git a/src/main/java/com/gitblit/wicket/pages/scripts/imgdiff.js b/src/main/java/com/gitblit/wicket/pages/scripts/imgdiff.js
index bfde435..2b2f4f9 100644
--- a/src/main/java/com/gitblit/wicket/pages/scripts/imgdiff.js
+++ b/src/main/java/com/gitblit/wicket/pages/scripts/imgdiff.js
@@ -13,18 +13,135 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-jQuery(function () {
-	// Runs on jQuery's document.ready and sets up the scroll event handlers for all image diffs.
-	jQuery(".imgdiff-slider-resizeable").each(function () {
-		var $el = jQuery(this);
-		var $img = jQuery('#' + this.id.substr(this.id.indexOf('-') + 1));
-		function fade() {
-			var w = Math.max(0, $el.width() - 18); // Must correspond to CSS: 18 px is handle width, 400 px is slider width
-			w = Math.max(0, 1.0 - w / 400.0);
-			$img.css("opacity", w);
+(function($) {
+
+/**
+ * Sets up elem as a slider; returns an access object. Elem must be positioned!
+ * Note that the element may contain other elements; this is used for instance
+ * for the image diff overlay slider.
+ *
+ * The styling of the slider is to be done in CSS. Currently recognized options:
+ * - initial: <float> clipped to [0..1], default 0
+ * - handleClass: <string> to assign to the handle div element created.
+ * If no handleClass is specified, a very plain default style is assigned.
+ */
+function rangeSlider(elem, options) {
+	options = $.extend({ initial : 0 }, options || {});
+	options.initial = Math.min(1.0, Math.max(0, options.initial));
+	
+	var $elem = $(elem);
+	var $handle = $('<div></div>').css({ position: 'absolute', left: 0, cursor: 'ew-resize' });
+	var $root = $(document.documentElement);
+	var $doc = $(document);	
+	var lastRatio = options.initial;
+
+	/** Mousemove event handler to track the mouse and move the slider. Generates slider:pos events. */
+	function track(e) {
+		var pos = $elem.offset().left;
+		var width = $elem.width();
+		var handleWidth = $handle.width();
+		var range = width - handleWidth;
+		if (range <= 0) return;
+		var delta = Math.min(range, Math.max (0, e.pageX - pos - handleWidth / 2));
+		lastRatio = delta / range;
+		$handle.css('left', "" + (delta * 100 / width) + '%');
+		$elem.trigger('slider:pos', { ratio: lastRatio, handle: $handle[0] });
+	}
+
+	/** Mouseup event handler to stop mouse tracking. */
+	function end(e) {
+		$doc.off('mousemove', track);
+		$doc.off('mouseup', end);
+		$root.removeClass('no-select');
+	}
+
+    /** Snaps the slider to the given ratio and generates a slider:pos event with the new ratio. */
+	function setTo(ratio) {
+		var w = $elem.width();
+		if (w <= 0 || $elem.is(':hidden')) return;
+		lastRatio = Math.min( 1.0, Math.max(0, ratio));
+		$handle.css('left', "" + Math.max(0, 100 * (lastRatio * (w - $handle.width())) / w) + '%');
+		$elem.trigger('slider:pos', { ratio: lastRatio, handle: $handle[0] });
+	}
+	
+	/**
+	 * Moves the slider to the given ratio, clipped to [0..1], in duration milliseconds.
+	 * Generates slider:pos events during the animation. If duration === 0, same as setTo.
+	 * Default duration is 500ms.
+	 */
+	function moveTo(ratio, duration) {
+		ratio = Math.min(1.0, Math.max(0, ratio));
+		if (ratio === lastRatio) return;
+		if (typeof duration == 'undefined') duration = 500;
+		if (duration === 0) {
+			setTo(ratio);
+		} else {
+			var target = ratio * ($elem.width() - $handle.width());
+			if (ratio > lastRatio) target--; else target++;
+			$handle.animate({left: target},
+				{ 'duration' : duration,
+				  'step' : function() {
+						lastRatio = Math.min(1.0, Math.max(0, $handle.offset().left / ($elem.width() - $handle.width())));
+						$elem.trigger('slider:pos', { ratio : lastRatio, handle : $handle[0] });
+					},
+				  'complete' : function() { setTo(ratio); } // Last step gives us a % value again.
+				}
+			);
 		}
-		// Unfortunately, not even jQuery triggers resize events for our resizeable... so let's track the mouse.
-		$el.on('mousedown', function() { $el.on('mousemove', fade); });
-		$el.on('mouseup', function() { $el.off('mousemove', fade); fade(); });
+	}
+	
+	/** Returns the current ratio. */
+	function getValue() {
+		return lastRatio;
+	}
+		
+	$elem.append($handle);
+	if (options.handleClass) {
+		$handle.addClass(options.handleClass);
+	} else { // Provide a default style so that it is at least visible
+		$handle.css({ width: '10px', height: '10px', background: 'white', border: '1px solid black' });
+	}
+	if (options.initial) setTo(options.initial);
+
+	/** Install mousedown handler to start mouse tracking. */
+	$handle.on('mousedown', function(e) {
+		$root.addClass('no-select');
+		$doc.on('mousemove', track);
+		$doc.on('mouseup', end);
 	});
-});
+
+	return { setRatio: setTo, moveRatio: moveTo, getRatio: getValue, handle: $handle[0] };
+}
+
+function setup() {
+	$('.imgdiff-container').each(function() {
+		var $this = $(this);
+		var $overlaySlider = $this.find('.imgdiff-ovr-slider').first();
+		var $opacitySlider = $this.find('.imgdiff-opa-slider').first();
+		var overlayAccess = rangeSlider($overlaySlider, {handleClass: 'imgdiff-ovr-handle'});
+		rangeSlider($opacitySlider, {handleClass: 'imgdiff-opa-handle'});
+		var $img = $('#' + this.id.substr(this.id.indexOf('-')+1)); // Here we change opacity
+		var $div = $img.parent(); // This controls visibility: here we change width.
+		
+		$overlaySlider.on('slider:pos', function(e, data) {
+			var pos = $(data.handle).offset().left;
+			var imgLeft = $img.offset().left; // Global
+			var imgW = $img.width() + $img.position().left; // From left edge of $div
+			if (pos <= imgLeft) {
+				$div.width(0);
+			} else if (pos <= imgLeft + imgW) {
+				$div.width(pos - imgLeft);
+			} else if ($div.width() < imgW) {
+				$div.width(imgW);
+			}
+		});
+		$opacitySlider.on('slider:pos', function(e, data) {
+			if ($div.width() <= 0) overlayAccess.moveRatio(1.0, 500); // Make old image visible in a nice way
+			$img.css('opacity', 1.0 - data.ratio);
+		});
+	});
+}
+
+$(setup); // Run on jQuery's dom-ready
+
+})(jQuery);
\ No newline at end of file
diff --git a/src/main/resources/gitblit.css b/src/main/resources/gitblit.css
index 906b555..5a62de0 100644
--- a/src/main/resources/gitblit.css
+++ b/src/main/resources/gitblit.css
@@ -1438,107 +1438,134 @@
 	color: #555;
 }
 
-/* Image diffs.
-   Kudos to Lea Verou: http://lea.verou.me/2014/07/image-comparison-slider-with-pure-css/ 
-   Slightly modified by Tom to allow moving the slider fully at the left edge of the images. */
-div.imgdiff {
-	margin: 5px 2px;
-	position: relative;
-	display: inline-block;
-	line-height: 0;
-	padding-left: 18px;
+/* Image diffs. */
+
+/* Set on body during mouse tracking. */
+.no-select {
+	-webkit-touch-callout:none;
+	-webkit-user-select:none;
+	-khtml-user-select:none;
+	-moz-user-select:none;
+	-ms-user-select:none;
+	user-select:none;
 }
 
-/* Note: width defines the initial position of the slider. Would have liked to have it
-   at 50% initially, but that fails on webkit, which refuses to go below the specified
-   width. (min-width won't help.) This is known behavior of webkit, see
-   https://codereview.chromium.org/239983004 and https://bugs.webkit.org/show_bug.cgi?id=72948
-   There is a hack (setting width to 1px in :hover) to work around this, but that causes
-   ugly screen flicker and makes for a dreadful UI. We're better off setting the slider
-   to the far left initially. */
+div.imgdiff-container {
+	padding: 10px;
+	background: #EEE;
+}
+
+div.imgdiff {
+	margin: 10px 20px;
+	position:relative;
+	display: inline-block;
+	/* Checkerboard background to reveal transparency. */
+    background-color: white;
+    background-image: linear-gradient(45deg, #DDD 25%, transparent 25%, transparent 75%, #DDD 75%, #DDD), linear-gradient(45deg, #DDD 25%, transparent 25%, transparent 75%, #DDD 75%, #DDD);
+    background-size:16px 16px;
+    background-position:0 0, 8px 8px;
+}
+
 div.imgdiff-left {
 	position: absolute;
 	top: 0;
 	bottom: 0;
 	left: 0;
-	width: 18px;
+	width: 0;
 	max-width: 100%;
 	overflow: hidden;
-	resize: horizontal;
-	/* Some border that should be visible on most images, combined of a dark color (red)
-	   and white in case the image was all red itself or used other colors that would make
-	   a thin red line hard to make out. */
-	border-right: 1px solid red;
-	box-shadow: 1px 0px 0px 0px white;
 }
 
-div.imgdiff-left:before {
+img.imgdiff {
+	user-select: none;
+	border: 1px solid #0F0;
+}
+img.imgdiff-old {
+	user-select: none;
+	border: 1px solid #F00;
+}
+.imgdiff-opa-container {
+	width: 200px;
+	height: 4px;
+	margin: 12px 35px;
+	padding: 0;
+	position: relative;
+	border-left: 1px solid #888;
+	border-right: 1px solid #888;
+	background: linear-gradient(to bottom, #888, #EEE 50%, #888);
+}
+
+.imgdiff-opa-container:before {
 	content: '';
 	position: absolute;
-	right: 0;
+	left: -20px;
+	top: -4px;
+	width : 12px;
+	height: 12px;
+	background-image: radial-gradient(6px at 50% 50%, rgba(255, 255, 255, 255) 50%, rgba(255, 255, 255, 0) 6px);
+}
+
+.imgdiff-opa-container:after {
+	content: '';
+	position: absolute;
+	right: -20px;
+	top: -4px;
+	width : 12px;
+	height: 12px;
+	background-image: radial-gradient(6px at 50% 50%, #888, #888 1px, transparent 6px);
+}
+
+.imgdiff-opa-slider {
+	position:absolute;
+	top : 0;
+	left: -5px;
 	bottom: 0;
-	width: 13px;
-	height: 13px;
-	background: linear-gradient(-45deg, red 50%, transparent 0);
-	background-clip: content-box;
-	cursor: ew-resize;
+	right: -5px;
+	text-align: left;
 }
 
-img.imgdiff-left {
-	margin-left: 18px; /* Compensate for padding on outer div. */
-	user-select: none;
+.imgdiff-opa-handle {
+	width: 10px;
+	height: 10px;
+	position: absolute;
+	top: -3px;
+	background-image: radial-gradient(5px at 50% 50%, #444, #888, transparent 5px);
 }
 
-img.imagediff {
-	user-select: none;
-	/* Checkerboard background */
-	background-color: white;
-	background-image: linear-gradient(45deg, #DDD 25%, transparent 25%, transparent 75%, #DDD 75%, #DDD), linear-gradient(45deg, #DDD 25%, transparent 25%, transparent 75%, #DDD 75%, #DDD);
-	background-size: 16px 16px;
-	background-position: 0 0, 8px 8px;
-}
-
-.diff-img {
-	margin: 2px;
-}
-
-div.imgdiff-slider {
+.imgdiff-ovr-slider {
 	display: inline-block;
+	margin: 0;
+	padding: 0;
 	position: relative;
-	margin: 0px 5px;
-	width: 418px;
-	height: 18px;
-	background: linear-gradient(to right, #F00, #0F0);
-	border: 1px solid #888;
+	text-align: left;
 }
 
-div.imgdiff-slider-resizeable {
-	position: absolute;
+.imgdiff-ovr-handle {
+	width : 2px;
+	height: 100%;
 	top: 0px;
-	left: 0px;
-	bottom: 0px;
-	width: 18px;
-	min-width: 18px;
-	max-width: 100%;
-	overflow: hidden;
-	resize: horizontal;
-	border-right: 1px solid #888;
-	/* The "handle" */ 
-	background-image: linear-gradient(to right, white, white);
-	background-size: 18px 18px;
-	background-position: top right;
-	background-repeat: no-repeat;
-	cursor: ew-resize;
+	background: linear-gradient(to right, #444, #FFF);
 }
 
-/* Provides the *left* border of the "handle" */
-div.imagediff-slider-left {
+.imgdiff-ovr-handle:before {
+	content: '';
 	position: absolute;
-	top: 0px;
-	right: 0px;
-	bottom: 0px;
-	margin-right:18px;
-	border-right: 1px solid #888;
+	right: -4px;
+	bottom: -5px;
+	width : 10px;
+	height: 10px;
+	background-image: radial-gradient(5px at 50% 50%, #444, #888, transparent 5px);
+}
+
+.imgdiff-ovr-handle:after {
+	content: '';
+	position: absolute;
+	right: -4px;
+	top: -5px;
+	width : 10px;
+	height: 10px;
+	/* border: 1px solid red; */
+	background-image: radial-gradient(5px at 50% 50%, #444, #888, transparent 5px);
 }
 
 /* End image diffs */

--
Gitblit v1.9.1