├── README.md ├── css ├── carousel.css ├── carouselDnD.css ├── timer.css └── viewer.css ├── img ├── photo-small.jpg └── photo.jpg ├── index.html └── js ├── RequestAnimationFrame.js ├── TouchScroll.js └── dragDrop.js /README.md: -------------------------------------------------------------------------------- 1 | # Touch Scroll 2 | 3 | Cross browser drag and touch scrolling using the native browser scrollbars 4 | 5 | ## Demo 6 | 7 | You can see a working demo here: 8 | http://kmturley.github.io/touch-scroll/ -------------------------------------------------------------------------------- /css/carousel.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Carousel styling 3 | * @class css.Carousel 4 | * @example 5 | **/ 6 | 7 | .carousel { 8 | position: relative; 9 | } 10 | 11 | .carousel .prev, 12 | .carousel .next { 13 | top: 0; 14 | position: absolute; 15 | padding: 60px 0 60px 0; 16 | font-size: 30px; 17 | text-decoration: none; 18 | } 19 | 20 | .carousel .prev { 21 | left: 0; 22 | padding-right: 50px; 23 | } 24 | 25 | .carousel .next { 26 | right: 0; 27 | padding-left: 50px; 28 | } 29 | 30 | .carousel ul { 31 | margin: 0; 32 | padding: 0 50px 0 50px; 33 | white-space: nowrap; 34 | overflow-x: auto; 35 | overflow-y: hidden; 36 | -webkit-overflow-scrolling: touch; 37 | } 38 | 39 | .carousel li { 40 | display: inline-block; 41 | white-space: normal; 42 | vertical-align: top; 43 | } 44 | 45 | .carousel li a { 46 | padding: 10px; 47 | width: 200px; 48 | display: block; 49 | text-decoration: none; 50 | } 51 | 52 | .carousel li a:hover { 53 | background-color: #e9e8ec; 54 | } 55 | 56 | .carousel li img { 57 | width: 100%; 58 | margin: 0 0 5px 0; 59 | } -------------------------------------------------------------------------------- /css/carouselDnD.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Carousel Drag and Drop styling 3 | * @class css.Carousel 4 | * @example 5 | **/ 6 | 7 | 8 | .carouselDnD ul { 9 | overflow: hidden; 10 | } 11 | 12 | .carouselDnD li { 13 | display: inline-block; 14 | white-space: normal; 15 | vertical-align: top; 16 | 17 | padding: 10px; 18 | width: 200px; 19 | text-decoration: none; 20 | } 21 | 22 | .carouselDnD li img { 23 | width: 100%; 24 | margin: 0 0 5px 0; 25 | } 26 | 27 | .select { 28 | border: 5px solid red 29 | } 30 | 31 | .hover { 32 | background-color: #e9e8ec; 33 | } 34 | 35 | .invisible { 36 | display: none; 37 | opacity: 0; 38 | } -------------------------------------------------------------------------------- /css/timer.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Adapted from bipoza: https://codepen.io/bipoza/pen/QWWveKZ 3 | **/ 4 | 5 | #countdown { 6 | position: relative; 7 | margin: auto; 8 | height: 40px; 9 | width: 40px; 10 | } 11 | 12 | svg { 13 | position: absolute; 14 | top: 0; 15 | right: 0; 16 | width: 40px; 17 | height: 40px; 18 | transform: rotateY(-180deg) rotateZ(-90deg); 19 | } 20 | 21 | svg circle { 22 | width: 40px; 23 | height: 40px; 24 | position: absolute; 25 | stroke-dasharray: 113px; 26 | stroke-dashoffset: 0px; 27 | stroke-linecap: round; 28 | stroke-width: 2px; 29 | stroke: red; 30 | fill: none; 31 | animation: countdown 1s linear forwards; 32 | } 33 | 34 | @keyframes countdown { 35 | from { 36 | stroke-dashoffset: 0px; 37 | } 38 | to { 39 | stroke-dashoffset: 113px; 40 | } 41 | } -------------------------------------------------------------------------------- /css/viewer.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Viewer styling 3 | * @class css.Viewer 4 | * @example
5 | **/ 6 | 7 | .viewer { 8 | width: 100%; 9 | height: 320px; 10 | overflow: auto; 11 | } 12 | 13 | .no-select { 14 | -webkit-touch-callout: none; 15 | -webkit-user-select: none; 16 | -khtml-user-select: none; 17 | -moz-user-select: none; 18 | -ms-user-select: none; 19 | user-select: none; 20 | cursor: move; 21 | cursor: -moz-grab; 22 | cursor: -webkit-grab; 23 | } 24 | 25 | .no-select:active { 26 | cursor: move; 27 | cursor: -moz-grabbing; 28 | cursor: -webkit-grabbing; 29 | } -------------------------------------------------------------------------------- /img/photo-small.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmturley/touch-scroll/0934f77b5447547933b0c09ff6d63b5f3205b9da/img/photo-small.jpg -------------------------------------------------------------------------------- /img/photo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmturley/touch-scroll/0934f77b5447547933b0c09ff6d63b5f3205b9da/img/photo.jpg -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | TouchScroll 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Fork me on GitHub 15 | 16 |

Touch Scroll

17 |

Drag a native browser scroll area around! Works on Chrome (latest), Safari (latest), Firefox (latest), IE8+

18 |

Viewer

19 |
20 | 21 |
22 |

Carousel

23 | 77 |

Carousel with Drag and Drop

78 |

Hold and drag to the left or right to scroll

79 |

Hold an image for a sec. to enable drag and drop. Hold it again to deactivate dnd

80 | 118 | 119 | 120 | 121 | 122 | 148 | 149 | -------------------------------------------------------------------------------- /js/RequestAnimationFrame.js: -------------------------------------------------------------------------------- 1 | // http://paulirish.com/2011/requestanimationframe-for-smart-animating/ 2 | // http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating 3 | 4 | // requestAnimationFrame polyfill by Erik Möller. fixes from Paul Irish and Tino Zijdel 5 | 6 | // MIT license 7 | 8 | (function() { 9 | var lastTime = 0; 10 | var vendors = ['ms', 'moz', 'webkit', 'o']; 11 | for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { 12 | window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame']; 13 | window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] 14 | || window[vendors[x]+'CancelRequestAnimationFrame']; 15 | } 16 | 17 | if (!window.requestAnimationFrame) 18 | window.requestAnimationFrame = function(callback, element) { 19 | var currTime = new Date().getTime(); 20 | var timeToCall = Math.max(0, 16 - (currTime - lastTime)); 21 | var id = window.setTimeout(function() { callback(currTime + timeToCall); }, 22 | timeToCall); 23 | lastTime = currTime + timeToCall; 24 | return id; 25 | }; 26 | 27 | if (!window.cancelAnimationFrame) 28 | window.cancelAnimationFrame = function(id) { 29 | clearTimeout(id); 30 | }; 31 | }()); -------------------------------------------------------------------------------- /js/TouchScroll.js: -------------------------------------------------------------------------------- 1 | /* 2 | * TouchScroll - using dom overflow:scroll 3 | * by kmturley 4 | */ 5 | 6 | /*globals window, document */ 7 | 8 | var TouchScroll = function () { 9 | 'use strict'; 10 | 11 | var module = { 12 | axis: 'x', 13 | drag: false, 14 | zoom: 1, 15 | time: 0.04, 16 | ignoreDraggableElements: false, 17 | isIE: window.navigator.userAgent.toLowerCase().indexOf('msie') > -1, 18 | isFirefox: window.navigator.userAgent.toLowerCase().indexOf('firefox') > -1, 19 | /** 20 | * @method init 21 | */ 22 | init: function (options) { 23 | var me = this; 24 | this.options = options; 25 | 26 | // find target element or fall back to body 27 | if (options && options.id) { 28 | this.el = document.getElementById(options.id); 29 | } 30 | if (!this.el) { 31 | if (this.isIE || this.isFirefox) { 32 | this.el = document.documentElement; 33 | } else { 34 | this.el = document.body; 35 | } 36 | } 37 | 38 | // ignore scrolling for draggbles e.g. when img dragable 39 | if (options && options.ignoreDraggableElements) { 40 | this.ignoreDraggableElements = true; 41 | } 42 | 43 | // if draggable option is enabled add events 44 | if (options.draggable === true) { 45 | if (this.isIE) { 46 | document.ondragstart = function () { return false; }; 47 | } 48 | if (this.isIE || this.isFirefox) { 49 | this.body = document.documentElement; 50 | } else { 51 | this.body = document.body; 52 | } 53 | this.addEvent('mousedown', this.el, function (e) { me.onMouseDown(e); }); 54 | this.addEvent('mousemove', this.el, function (e) { me.onMouseMove(e); }); 55 | this.addEvent('mouseup', this.body, function (e) { me.onMouseUp(e); }); 56 | } 57 | 58 | // if zoom option exists add mouse wheel functionality to element 59 | if (options && options.zoom) { 60 | this.elzoom = document.getElementById(options.zoom); 61 | if (this.isFirefox) { 62 | this.addEvent('DOMMouseScroll', this.el, function (e) { me.onMouseWheel(e); }); 63 | } else { 64 | this.addEvent('mousewheel', this.el, function (e) { me.onMouseWheel(e); }); 65 | } 66 | } 67 | 68 | // if scroll options exist add events 69 | if (options && options.prev) { 70 | this.prev = document.getElementById(options.prev); 71 | this.addEvent('mousedown', this.prev, function (e) { 72 | me.onMouseDown(e); 73 | }); 74 | this.addEvent('mouseup', this.prev, function (e) { 75 | me.diffx = options.distance ? (-options.distance / 11) : -11; 76 | me.onMouseUp(e); 77 | }); 78 | } 79 | if (options && options.next) { 80 | this.next = document.getElementById(options.next); 81 | this.addEvent('mousedown', this.next, function (e) { 82 | me.onMouseDown(e); 83 | }); 84 | this.addEvent('mouseup', this.next, function (e) { 85 | me.diffx = options.distance ? (options.distance / 11) : 11; 86 | me.onMouseUp(e); 87 | }); 88 | } 89 | }, 90 | /** 91 | * @method addEvent 92 | */ 93 | addEvent: function (name, el, func) { 94 | if (el.addEventListener) { 95 | el.addEventListener(name, func, false); 96 | } else if (el.attachEvent) { 97 | el.attachEvent('on' + name, func); 98 | } else { 99 | el[name] = func; 100 | } 101 | }, 102 | /** 103 | * @method cancelEvent 104 | */ 105 | cancelEvent: function (e) { 106 | if (!e) { e = window.event; } 107 | if (e.target && e.target.nodeName === 'IMG') { 108 | e.preventDefault(); 109 | } else if (e.srcElement && e.srcElement.nodeName === 'IMG') { 110 | e.returnValue = false; 111 | } 112 | }, 113 | /** 114 | * @method onMouseDown 115 | */ 116 | onMouseDown: function (e) { 117 | if( this.ignoreDraggableElements && e.target.draggable ) { 118 | return; 119 | } 120 | 121 | if (this.drag === false || this.options.wait === false) { 122 | // ignore mousedown event if emitted on scrollbar 123 | this.drag = e.offsetX <= e.target.clientWidth && e.offsetY <= e.target.clientHeight; 124 | this.cancelEvent(e); 125 | this.startx = e.clientX + this.el.scrollLeft; 126 | this.starty = e.clientY + this.el.scrollTop; 127 | this.diffx = 0; 128 | this.diffy = 0; 129 | } 130 | }, 131 | /** 132 | * @method onMouseMove 133 | */ 134 | onMouseMove: function (e) { 135 | if (this.drag === true) { 136 | this.cancelEvent(e); 137 | this.diffx = (this.startx - (e.clientX + this.el.scrollLeft)); 138 | this.diffy = (this.starty - (e.clientY + this.el.scrollTop)); 139 | this.el.scrollLeft += this.diffx; 140 | this.el.scrollTop += this.diffy; 141 | } 142 | }, 143 | /** 144 | * @method onMouseMove 145 | */ 146 | onMouseUp: function (e) { 147 | if (this.drag === true) { 148 | if (!this.options.wait) { 149 | this.drag = null; 150 | } 151 | this.cancelEvent(e); 152 | var me = this, 153 | start = 1, 154 | animate = function () { 155 | var step = Math.sin(start); 156 | if (step <= 0) { 157 | me.diffx = 0; 158 | me.diffy = 0; 159 | window.cancelAnimationFrame(animate); 160 | me.drag = false; 161 | } else { 162 | me.el.scrollLeft += me.diffx * step; 163 | me.el.scrollTop += me.diffy * step; 164 | start -= me.time; 165 | window.requestAnimationFrame(animate); 166 | } 167 | }; 168 | animate(); 169 | } 170 | }, 171 | /** 172 | * @method onMouseMove 173 | */ 174 | onMouseWheel: function (e) { 175 | this.cancelEvent(e); 176 | if (e.detail) { 177 | this.zoom -= e.detail; 178 | } else { 179 | this.zoom += (e.wheelDelta / 1200); 180 | } 181 | if (this.zoom < 1) { 182 | this.zoom = 1; 183 | } else if (this.zoom > 10) { 184 | this.zoom = 10; 185 | } 186 | /* 187 | this.elzoom.style.OTransform = 'scale(' + this.zoom + ', ' + this.zoom + ')'; 188 | this.elzoom.style.MozTransform = 'scale(' + this.zoom + ', ' + this.zoom + ')'; 189 | this.elzoom.style.msTransform = 'scale(' + this.zoom + ', ' + this.zoom + ')'; 190 | this.elzoom.style.WebkitTransform = 'scale(' + this.zoom + ', ' + this.zoom + ')'; 191 | this.elzoom.style.transform = 'scale(' + this.zoom + ', ' + this.zoom + ')'; 192 | */ 193 | this.elzoom.style.zoom = this.zoom * 100 + '%'; 194 | //this.el.scrollLeft += e.wheelDelta / 10; 195 | //this.el.scrollTop += e.wheelDelta / 8; 196 | } 197 | }; 198 | return module; 199 | }; 200 | -------------------------------------------------------------------------------- /js/dragDrop.js: -------------------------------------------------------------------------------- 1 | /* 2 | * create timer animation 3 | */ 4 | let timerAnimation = document.createElement("div"); 5 | timerAnimation.id = "countdown"; 6 | let svg = document.createElementNS('http://www.w3.org/2000/svg', "svg"); 7 | let circle = document.createElementNS('http://www.w3.org/2000/svg', "circle"); 8 | circle.setAttributeNS(null, 'r', 18); 9 | circle.setAttributeNS(null, 'cx', 20); 10 | circle.setAttributeNS(null, 'cy', 20); 11 | svg.appendChild(circle); 12 | timerAnimation.appendChild(svg); 13 | 14 | /* 15 | * init imgages for drag and drop (dnd) 16 | */ 17 | let list = document.getElementById("carousel-draggable"); 18 | let imgs = list.getElementsByTagName("img"); 19 | let deg = -50; 20 | for (const img of imgs) { 21 | // flag to identify dnd items in events 22 | img.selectable = true; 23 | 24 | img.draggable = false; 25 | img.onmousedown = onMouseDown; 26 | img.onmouseup = onMouseUp; 27 | img.onmousemove = onMouseMove; 28 | 29 | img.ondragstart = onDragStart; 30 | img.ondragend = onDragEnd; 31 | img.ondragover = onDragOver; 32 | img.ondrop = onDragDrop; 33 | 34 | // for demo individually recolor imgs 35 | img.style.filter = `hue-rotate(${deg}deg)`; 36 | deg = deg + 50; 37 | } 38 | 39 | /* 40 | * event handler for dnd 41 | */ 42 | let currDrag = null; 43 | 44 | function onMouseDown(e) { 45 | if (!this.selectable) return; 46 | 47 | if (!this.draggable) { 48 | // enable drag and drop when timeout reached and not canceled 49 | const delay = 1000; 50 | this.dragTimeOut = setTimeout(() => { 51 | this.draggable = true; 52 | this.classList.add("select") 53 | }, delay) 54 | // set off timer animation for ux 55 | setTimeout(() => this.parentElement.appendChild(timerAnimation), 25); 56 | } else if (!currDrag) { 57 | this.dragTimeOut = setTimeout(() => { 58 | this.draggable = false; 59 | this.classList.remove("select") 60 | }, 200); 61 | } 62 | } 63 | 64 | function onMouseUp(e) { 65 | if (!this.selectable) return; 66 | 67 | // cancel dnd on mouse release when timeout not reached 68 | if (!this.draggable || currDrag) { 69 | clearTimeout(this.dragTimeOut); 70 | } 71 | 72 | if (this.parentElement.contains(timerAnimation)) { 73 | this.parentElement.removeChild(timerAnimation); 74 | } 75 | } 76 | 77 | function onMouseMove(e) { 78 | if (!this.selectable) return; 79 | 80 | // cancel dnd on hold and move 81 | if (!this.draggable || currDrag) { 82 | clearTimeout(this.dragTimeOut); 83 | } 84 | 85 | if (this.parentElement.contains(timerAnimation)) { 86 | this.parentElement.removeChild(timerAnimation); 87 | } 88 | } 89 | 90 | function onDragStart(e) { 91 | if (!this.selectable) return; 92 | 93 | setTimeout(() => this.classList.add("invisible"), 0); 94 | currDrag = this; 95 | } 96 | 97 | function onDragEnd(e) { 98 | e.preventDefault(); 99 | if (!this.selectable) return; 100 | 101 | currDrag = null; 102 | setTimeout(() => this.classList.remove("invisible"), 0); 103 | } 104 | 105 | function onDragDrop(e) { 106 | e.preventDefault(); 107 | // swap images 108 | if (currDrag) { 109 | let prevParent = currDrag.parentElement; 110 | let newParent = this.parentElement; 111 | prevParent.insertBefore(this, prevParent.children[0]); 112 | newParent.insertBefore(currDrag, newParent.children[0]); 113 | 114 | currDrag.draggable = false; 115 | currDrag.classList.remove("select") 116 | } 117 | } 118 | 119 | function onDragOver(e) { 120 | if (!this.selectable) return; 121 | // without this drop event won't be emitted 122 | e.preventDefault() 123 | } --------------------------------------------------------------------------------