From 12de8838924c8f14e803bf090da34fe0ef5de102 Mon Sep 17 00:00:00 2001 From: Tom Date: Fri, 14 Nov 2014 22:14:28 +0100 Subject: [PATCH] Javascript-based sliders styled with CSS This works better for small images. The previous CSS-resize based attempt worked reasonably well, but had two problems on WebKit (Safari): 1. For very small images the red resize handle would overlap the image itself. In that case, the image became un-draggable as soon as the opacity was reduced below 1.0. 2. Safari apparently doesn't send mousemove events during a CSS resize, so the opacity was changed only on mouseup. Both observed on Safari 6.1.6 and 7.1. FF 33.1 had no problems. Therefore I've switched to a Javascript slider. Since I didn't find any that was simple, did not require HTML 5, appeared to be well maintained, had a bug tracker and not too many outstanding bug reports, didn't pull in umpteen other dependencies, didn't suffer from feature bloat, was compatible with jQuery 1.7.1, and was freely licensed, I ended up writing my own. imgdiff.js contains a small Javascript slider (only horizontal) that is styled completely in CSS. It reports ratios in the range [0..1] and fires nice jQuery events 'slider:pos' on value changes. Base element is a plain div that is positioned. It's not a general-purpose do-it-all slider, but it's small, simple, and works for what we need it. (imgdiff.js also sets up the ese sliders on the diff pages.) --- .../wicket/pages/ImageDiffHandler.java | 13 +- .../gitblit/wicket/pages/scripts/imgdiff.js | 143 +++++++++++++-- src/main/resources/gitblit.css | 167 ++++++++++-------- 3 files changed, 233 insertions(+), 90 deletions(-) diff --git a/src/main/java/com/gitblit/wicket/pages/ImageDiffHandler.java b/src/main/java/com/gitblit/wicket/pages/ImageDiffHandler.java index 1232e990..52bf13b9 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 @@ public class ImageDiffHandler implements DiffUtils.BinaryDiffHandler { 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 @@ public class ImageDiffHandler implements DiffUtils.BinaryDiffHandler { // 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 bfde435d..2b2f4f9f 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: clipped to [0..1], default 0 + * - handleClass: 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 = $('
').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 906b555b..5a62de0b 100644 --- a/src/main/resources/gitblit.css +++ b/src/main/resources/gitblit.css @@ -1438,107 +1438,134 @@ div.diff > table { 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. */ +/* 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; +} + +div.imgdiff-container { + padding: 10px; + background: #EEE; +} + div.imgdiff { - margin: 5px 2px; - position: relative; + margin: 10px 20px; + position:relative; display: inline-block; - line-height: 0; - padding-left: 18px; + /* 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; } -/* 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-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; - bottom: 0; - width: 13px; - height: 13px; - background: linear-gradient(-45deg, red 50%, transparent 0); - background-clip: content-box; - cursor: ew-resize; + 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); } -img.imgdiff-left { - margin-left: 18px; /* Compensate for padding on outer div. */ - user-select: none; +.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); } -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; +.imgdiff-opa-slider { + position:absolute; + top : 0; + left: -5px; + bottom: 0; + right: -5px; + text-align: left; } -.diff-img { - margin: 2px; +.imgdiff-opa-handle { + width: 10px; + height: 10px; + position: absolute; + top: -3px; + background-image: radial-gradient(5px at 50% 50%, #444, #888, transparent 5px); } -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 */ -- 2.39.5