import { on, off, css, throttle, cancelThrottle, scrollBy, getParentAutoScrollElement, expando, getRect, getWindowScrollingElement } from '../../src/utils.js'; import Sortable from '../../src/Sortable.js'; import { Edge, IE11OrLess, Safari } from '../../src/BrowserInfo.js'; let autoScrolls = [], scrollEl, scrollRootEl, scrolling = false, lastAutoScrollX, lastAutoScrollY, touchEvt, pointerElemChangedInterval; function AutoScrollPlugin() { function AutoScroll() { this.defaults = { scroll: true, scrollSensitivity: 30, scrollSpeed: 10, bubbleScroll: true }; // Bind all private methods for (let fn in this) { if (fn.charAt(0) === '_' && typeof this[fn] === 'function') { this[fn] = this[fn].bind(this); } } } AutoScroll.prototype = { dragStarted({ originalEvent }) { if (this.sortable.nativeDraggable) { on(document, 'dragover', this._handleAutoScroll); } else { if (this.options.supportPointer) { on(document, 'pointermove', this._handleFallbackAutoScroll); } else if (originalEvent.touches) { on(document, 'touchmove', this._handleFallbackAutoScroll); } else { on(document, 'mousemove', this._handleFallbackAutoScroll); } } }, dragOverCompleted({ originalEvent }) { // For when bubbling is canceled and using fallback (fallback 'touchmove' always reached) if (!this.options.dragOverBubble && !originalEvent.rootEl) { this._handleAutoScroll(originalEvent); } }, drop() { if (this.sortable.nativeDraggable) { off(document, 'dragover', this._handleAutoScroll); } else { off(document, 'pointermove', this._handleFallbackAutoScroll); off(document, 'touchmove', this._handleFallbackAutoScroll); off(document, 'mousemove', this._handleFallbackAutoScroll); } clearPointerElemChangedInterval(); clearAutoScrolls(); cancelThrottle(); }, nulling() { touchEvt = scrollRootEl = scrollEl = scrolling = pointerElemChangedInterval = lastAutoScrollX = lastAutoScrollY = null; autoScrolls.length = 0; }, _handleFallbackAutoScroll(evt) { this._handleAutoScroll(evt, true); }, _handleAutoScroll(evt, fallback) { const x = (evt.touches ? evt.touches[0] : evt).clientX, y = (evt.touches ? evt.touches[0] : evt).clientY, elem = document.elementFromPoint(x, y); touchEvt = evt; // IE does not seem to have native autoscroll, // Edge's autoscroll seems too conditional, // MACOS Safari does not have autoscroll, // Firefox and Chrome are good if (fallback || Edge || IE11OrLess || Safari) { autoScroll(evt, this.options, elem, fallback); // Listener for pointer element change let ogElemScroller = getParentAutoScrollElement(elem, true); if ( scrolling && ( !pointerElemChangedInterval || x !== lastAutoScrollX || y !== lastAutoScrollY ) ) { pointerElemChangedInterval && clearPointerElemChangedInterval(); // Detect for pointer elem change, emulating native DnD behaviour pointerElemChangedInterval = setInterval(() => { let newElem = getParentAutoScrollElement(document.elementFromPoint(x, y), true); if (newElem !== ogElemScroller) { ogElemScroller = newElem; clearAutoScrolls(); } autoScroll(evt, this.options, newElem, fallback); }, 10); lastAutoScrollX = x; lastAutoScrollY = y; } } else { // if DnD is enabled (and browser has good autoscrolling), first autoscroll will already scroll, so get parent autoscroll of first autoscroll if (!this.options.bubbleScroll || getParentAutoScrollElement(elem, true) === getWindowScrollingElement()) { clearAutoScrolls(); return; } autoScroll(evt, this.options, getParentAutoScrollElement(elem, false), false); } } }; return Object.assign(AutoScroll, { pluginName: 'scroll', initializeByDefault: true }); } function clearAutoScrolls() { autoScrolls.forEach(function(autoScroll) { clearInterval(autoScroll.pid); }); autoScrolls = []; } function clearPointerElemChangedInterval() { clearInterval(pointerElemChangedInterval); } const autoScroll = throttle(function(evt, options, rootEl, isFallback) { // Bug: https://bugzilla.mozilla.org/show_bug.cgi?id=505521 if (!options.scroll) return; const x = (evt.touches ? evt.touches[0] : evt).clientX, y = (evt.touches ? evt.touches[0] : evt).clientY, sens = options.scrollSensitivity, speed = options.scrollSpeed, winScroller = getWindowScrollingElement(); let scrollThisInstance = false, scrollCustomFn; // New scroll root, set scrollEl if (scrollRootEl !== rootEl) { scrollRootEl = rootEl; clearAutoScrolls(); scrollEl = options.scroll; scrollCustomFn = options.scrollFn; if (scrollEl === true) { scrollEl = getParentAutoScrollElement(rootEl, true); } } let layersOut = 0; let currentParent = scrollEl; do { let el = currentParent, rect = getRect(el), top = rect.top, bottom = rect.bottom, left = rect.left, right = rect.right, width = rect.width, height = rect.height, canScrollX, canScrollY, scrollWidth = el.scrollWidth, scrollHeight = el.scrollHeight, elCSS = css(el), scrollPosX = el.scrollLeft, scrollPosY = el.scrollTop; if (el === winScroller) { canScrollX = width < scrollWidth && (elCSS.overflowX === 'auto' || elCSS.overflowX === 'scroll' || elCSS.overflowX === 'visible'); canScrollY = height < scrollHeight && (elCSS.overflowY === 'auto' || elCSS.overflowY === 'scroll' || elCSS.overflowY === 'visible'); } else { canScrollX = width < scrollWidth && (elCSS.overflowX === 'auto' || elCSS.overflowX === 'scroll'); canScrollY = height < scrollHeight && (elCSS.overflowY === 'auto' || elCSS.overflowY === 'scroll'); } let vx = canScrollX && (Math.abs(right - x) <= sens && (scrollPosX + width) < scrollWidth) - (Math.abs(left - x) <= sens && !!scrollPosX); let vy = canScrollY && (Math.abs(bottom - y) <= sens && (scrollPosY + height) < scrollHeight) - (Math.abs(top - y) <= sens && !!scrollPosY); if (!autoScrolls[layersOut]) { for (let i = 0; i <= layersOut; i++) { if (!autoScrolls[i]) { autoScrolls[i] = {}; } } } if (autoScrolls[layersOut].vx != vx || autoScrolls[layersOut].vy != vy || autoScrolls[layersOut].el !== el) { autoScrolls[layersOut].el = el; autoScrolls[layersOut].vx = vx; autoScrolls[layersOut].vy = vy; clearInterval(autoScrolls[layersOut].pid); if (vx != 0 || vy != 0) { scrollThisInstance = true; /* jshint loopfunc:true */ autoScrolls[layersOut].pid = setInterval((function () { // emulate drag over during autoscroll (fallback), emulating native DnD behaviour if (isFallback && this.layer === 0) { Sortable.active._onTouchMove(touchEvt); // To move ghost if it is positioned absolutely } let scrollOffsetY = autoScrolls[this.layer].vy ? autoScrolls[this.layer].vy * speed : 0; let scrollOffsetX = autoScrolls[this.layer].vx ? autoScrolls[this.layer].vx * speed : 0; if (typeof(scrollCustomFn) === 'function') { if (scrollCustomFn.call(Sortable.dragged.parentNode[expando], scrollOffsetX, scrollOffsetY, evt, touchEvt, autoScrolls[this.layer].el) !== 'continue') { return; } } scrollBy(autoScrolls[this.layer].el, scrollOffsetX, scrollOffsetY); }).bind({layer: layersOut}), 24); } } layersOut++; } while (options.bubbleScroll && currentParent !== winScroller && (currentParent = getParentAutoScrollElement(currentParent, false))); scrolling = scrollThisInstance; // in case another function catches scrolling as false in between when it is not }, 30); export default AutoScrollPlugin;