]> source.dussan.org Git - gitblit.git/commitdiff
Javascript-based sliders styled with CSS
authorTom <tw201207@gmail.com>
Fri, 14 Nov 2014 21:14:28 +0000 (22:14 +0100)
committerTom <tw201207@gmail.com>
Fri, 14 Nov 2014 21:14:28 +0000 (22:14 +0100)
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.)

src/main/java/com/gitblit/wicket/pages/ImageDiffHandler.java
src/main/java/com/gitblit/wicket/pages/scripts/imgdiff.js
src/main/resources/gitblit.css

index 1232e990d8729b8158efafeabd7cdee8b88baf45..52bf13b9d231aadba0c4f03d0c22bc1de07cfb76 100644 (file)
@@ -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;
index bfde435d787833dc14070cc84e32b2bdac7008d5..2b2f4f9fe6c302b86502dc93491ea91798a80574 100644 (file)
  * 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
index 906b555b237b3d479ed9e26cd38e85e124b915b9..5a62de0b35df58630ab864987b0396bba15f8a7c 100644 (file)
@@ -1438,107 +1438,134 @@ div.diff > table {
        color: #555;\r
 }\r
 \r
-/* Image diffs.\r
-   Kudos to Lea Verou: http://lea.verou.me/2014/07/image-comparison-slider-with-pure-css/ \r
-   Slightly modified by Tom to allow moving the slider fully at the left edge of the images. */\r
+/* Image diffs. */\r
+\r
+/* Set on body during mouse tracking. */\r
+.no-select {\r
+       -webkit-touch-callout:none;\r
+       -webkit-user-select:none;\r
+       -khtml-user-select:none;\r
+       -moz-user-select:none;\r
+       -ms-user-select:none;\r
+       user-select:none;\r
+}\r
+\r
+div.imgdiff-container {\r
+       padding: 10px;\r
+       background: #EEE;\r
+}\r
+\r
 div.imgdiff {\r
-       margin: 5px 2px;\r
-       position: relative;\r
+       margin: 10px 20px;\r
+       position:relative;\r
        display: inline-block;\r
-       line-height: 0;\r
-       padding-left: 18px;\r
+       /* Checkerboard background to reveal transparency. */\r
+    background-color: white;\r
+    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);\r
+    background-size:16px 16px;\r
+    background-position:0 0, 8px 8px;\r
 }\r
 \r
-/* Note: width defines the initial position of the slider. Would have liked to have it\r
-   at 50% initially, but that fails on webkit, which refuses to go below the specified\r
-   width. (min-width won't help.) This is known behavior of webkit, see\r
-   https://codereview.chromium.org/239983004 and https://bugs.webkit.org/show_bug.cgi?id=72948\r
-   There is a hack (setting width to 1px in :hover) to work around this, but that causes\r
-   ugly screen flicker and makes for a dreadful UI. We're better off setting the slider\r
-   to the far left initially. */\r
 div.imgdiff-left {\r
        position: absolute;\r
        top: 0;\r
        bottom: 0;\r
        left: 0;\r
-       width: 18px;\r
+       width: 0;\r
        max-width: 100%;\r
        overflow: hidden;\r
-       resize: horizontal;\r
-       /* Some border that should be visible on most images, combined of a dark color (red)\r
-          and white in case the image was all red itself or used other colors that would make\r
-          a thin red line hard to make out. */\r
-       border-right: 1px solid red;\r
-       box-shadow: 1px 0px 0px 0px white;\r
 }\r
 \r
-div.imgdiff-left:before {\r
+img.imgdiff {\r
+       user-select: none;\r
+       border: 1px solid #0F0;\r
+}\r
+img.imgdiff-old {\r
+       user-select: none;\r
+       border: 1px solid #F00;\r
+}\r
+.imgdiff-opa-container {\r
+       width: 200px;\r
+       height: 4px;\r
+       margin: 12px 35px;\r
+       padding: 0;\r
+       position: relative;\r
+       border-left: 1px solid #888;\r
+       border-right: 1px solid #888;\r
+       background: linear-gradient(to bottom, #888, #EEE 50%, #888);\r
+}\r
+\r
+.imgdiff-opa-container:before {\r
        content: '';\r
        position: absolute;\r
-       right: 0;\r
-       bottom: 0;\r
-       width: 13px;\r
-       height: 13px;\r
-       background: linear-gradient(-45deg, red 50%, transparent 0);\r
-       background-clip: content-box;\r
-       cursor: ew-resize;\r
+       left: -20px;\r
+       top: -4px;\r
+       width : 12px;\r
+       height: 12px;\r
+       background-image: radial-gradient(6px at 50% 50%, rgba(255, 255, 255, 255) 50%, rgba(255, 255, 255, 0) 6px);\r
 }\r
 \r
-img.imgdiff-left {\r
-       margin-left: 18px; /* Compensate for padding on outer div. */\r
-       user-select: none;\r
+.imgdiff-opa-container:after {\r
+       content: '';\r
+       position: absolute;\r
+       right: -20px;\r
+       top: -4px;\r
+       width : 12px;\r
+       height: 12px;\r
+       background-image: radial-gradient(6px at 50% 50%, #888, #888 1px, transparent 6px);\r
 }\r
 \r
-img.imagediff {\r
-       user-select: none;\r
-       /* Checkerboard background */\r
-       background-color: white;\r
-       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);\r
-       background-size: 16px 16px;\r
-       background-position: 0 0, 8px 8px;\r
+.imgdiff-opa-slider {\r
+       position:absolute;\r
+       top : 0;\r
+       left: -5px;\r
+       bottom: 0;\r
+       right: -5px;\r
+       text-align: left;\r
 }\r
 \r
-.diff-img {\r
-       margin: 2px;\r
+.imgdiff-opa-handle {\r
+       width: 10px;\r
+       height: 10px;\r
+       position: absolute;\r
+       top: -3px;\r
+       background-image: radial-gradient(5px at 50% 50%, #444, #888, transparent 5px);\r
 }\r
 \r
-div.imgdiff-slider {\r
+.imgdiff-ovr-slider {\r
        display: inline-block;\r
+       margin: 0;\r
+       padding: 0;\r
        position: relative;\r
-       margin: 0px 5px;\r
-       width: 418px;\r
-       height: 18px;\r
-       background: linear-gradient(to right, #F00, #0F0);\r
-       border: 1px solid #888;\r
+       text-align: left;\r
 }\r
 \r
-div.imgdiff-slider-resizeable {\r
-       position: absolute;\r
+.imgdiff-ovr-handle {\r
+       width : 2px;\r
+       height: 100%;\r
        top: 0px;\r
-       left: 0px;\r
-       bottom: 0px;\r
-       width: 18px;\r
-       min-width: 18px;\r
-       max-width: 100%;\r
-       overflow: hidden;\r
-       resize: horizontal;\r
-       border-right: 1px solid #888;\r
-       /* The "handle" */ \r
-       background-image: linear-gradient(to right, white, white);\r
-       background-size: 18px 18px;\r
-       background-position: top right;\r
-       background-repeat: no-repeat;\r
-       cursor: ew-resize;\r
+       background: linear-gradient(to right, #444, #FFF);\r
 }\r
 \r
-/* Provides the *left* border of the "handle" */\r
-div.imagediff-slider-left {\r
+.imgdiff-ovr-handle:before {\r
+       content: '';\r
        position: absolute;\r
-       top: 0px;\r
-       right: 0px;\r
-       bottom: 0px;\r
-       margin-right:18px;\r
-       border-right: 1px solid #888;\r
+       right: -4px;\r
+       bottom: -5px;\r
+       width : 10px;\r
+       height: 10px;\r
+       background-image: radial-gradient(5px at 50% 50%, #444, #888, transparent 5px);\r
+}\r
+\r
+.imgdiff-ovr-handle:after {\r
+       content: '';\r
+       position: absolute;\r
+       right: -4px;\r
+       top: -5px;\r
+       width : 10px;\r
+       height: 10px;\r
+       /* border: 1px solid red; */\r
+       background-image: radial-gradient(5px at 50% 50%, #444, #888, transparent 5px);\r
 }\r
 \r
 /* End image diffs */\r