UNPKG

@adalo/react-indiana-drag-scroll

Version:
641 lines (550 loc) 18.5 kB
'use strict'; function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } var React = require('react'); var React__default = _interopDefault(React); var PropTypes = _interopDefault(require('prop-types')); function createCommonjsModule(fn, module) { return module = { exports: {} }, fn(module, module.exports), module.exports; } var classnames = createCommonjsModule(function (module) { /*! Copyright (c) 2018 Jed Watson. Licensed under the MIT License (MIT), see http://jedwatson.github.io/classnames */ /* global define */ (function () { var hasOwn = {}.hasOwnProperty; function classNames() { var classes = []; for (var i = 0; i < arguments.length; i++) { var arg = arguments[i]; if (!arg) continue; var argType = typeof arg; if (argType === 'string' || argType === 'number') { classes.push(arg); } else if (Array.isArray(arg)) { if (arg.length) { var inner = classNames.apply(null, arg); if (inner) { classes.push(inner); } } } else if (argType === 'object') { if (arg.toString !== Object.prototype.toString && !arg.toString.toString().includes('[native code]')) { classes.push(arg.toString()); continue; } for (var key in arg) { if (hasOwn.call(arg, key) && arg[key]) { classes.push(key); } } } } return classes.join(' '); } if (module.exports) { classNames.default = classNames; module.exports = classNames; } else { window.classNames = classNames; } }()); }); /** * Returns a function, that, as long as it continues to be invoked, will not * be triggered. The function will be called after it stops being called for * N milliseconds. If `immediate` is passed, trigger the function on the * leading edge, instead of the trailing. The function also has a property 'clear' * that is a function which will clear the timer to prevent previously scheduled executions. * * @source underscore.js * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/ * @param {Function} function to wrap * @param {Number} timeout in ms (`100`) * @param {Boolean} whether to execute at the beginning (`false`) * @api public */ function debounce(func, wait, immediate){ var timeout, args, context, timestamp, result; if (null == wait) wait = 100; function later() { var last = Date.now() - timestamp; if (last < wait && last >= 0) { timeout = setTimeout(later, wait - last); } else { timeout = null; if (!immediate) { result = func.apply(context, args); context = args = null; } } } var debounced = function(){ context = this; args = arguments; timestamp = Date.now(); var callNow = immediate && !timeout; if (!timeout) timeout = setTimeout(later, wait); if (callNow) { result = func.apply(context, args); context = args = null; } return result; }; debounced.clear = function() { if (timeout) { clearTimeout(timeout); timeout = null; } }; debounced.flush = function() { if (timeout) { result = func.apply(context, args); context = args = null; clearTimeout(timeout); timeout = null; } }; return debounced; } // Adds compatibility for ES modules debounce.debounce = debounce; var debounce_1 = debounce; /** * BEM class name factory. * * @typedef {Function} Bem * @param {String|Object} [elementOrMods] Element name or hash object with mods * @param {Object} [mods] Hash object with mods * @returns {String} */ /** * Returns BEM class name factory. * * @param {String} componentName Block name * @returns {Bem} */ var easyBem = function bem(componentName) { return function (elementOrMods, mods) { if (!elementOrMods) { return componentName; } var element; if (typeof elementOrMods === 'string') { element = elementOrMods; } else { mods = elementOrMods; } var base = componentName; if (element) { base += '__' + element; } return base + ( mods ? Object.keys(mods).reduce(function (result, name) { var value = mods[name]; if (value) { result += ' ' + ( typeof value === 'boolean' ? (base + '--' + name) : (base + '--' + name + '_' + value) ); } return result; }, '') : '' ); }; }; function styleInject(css, ref) { if ( ref === void 0 ) ref = {}; var insertAt = ref.insertAt; if (!css || typeof document === 'undefined') { return; } var head = document.head || document.getElementsByTagName('head')[0]; var style = document.createElement('style'); style.type = 'text/css'; if (insertAt === 'top') { if (head.firstChild) { head.insertBefore(style, head.firstChild); } else { head.appendChild(style); } } else { head.appendChild(style); } if (style.styleSheet) { style.styleSheet.cssText = css; } else { style.appendChild(document.createTextNode(css)); } } var css = ".indiana-scroll-container {\n overflow: auto;\n cursor: grab;\n}\n.indiana-scroll-container--dragging {\n cursor: grabbing;\n}\n.indiana-scroll-container--hide-scrollbars {\n overflow: hidden;\n overflow: -moz-scrollbars-none;\n -ms-overflow-style: none;\n scrollbar-width: none;\n}\n.indiana-scroll-container--hide-scrollbars::-webkit-scrollbar {\n display: none !important;\n height: 0 !important;\n width: 0 !important;\n background: transparent !important;\n -webkit-appearance: none !important;\n}\n.indiana-scroll-container--native-scroll {\n overflow: auto;\n}\n"; styleInject(css); var classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; var createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); var inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }; var possibleConstructorReturn = function (self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }; var cn = easyBem('indiana-scroll-container'); var SCROLL_END_DEBOUNCE = 300; var ScrollContainer = function (_PureComponent) { inherits(ScrollContainer, _PureComponent); function ScrollContainer(props) { classCallCheck(this, ScrollContainer); var _this = possibleConstructorReturn(this, (ScrollContainer.__proto__ || Object.getPrototypeOf(ScrollContainer)).call(this, props)); _this.onEndScroll = function () { _this.scrolling = false; if (!_this.pressed && _this.started) { _this.processEnd(); } }; _this.onScroll = function (e) { var container = _this.container.current; // Ignore the internal scrolls if (container.scrollLeft !== _this.scrollLeft || container.scrollTop !== _this.scrollTop) { _this.scrolling = true; _this.processScroll(e); _this.onEndScroll(); } }; _this.onTouchStart = function (e) { var nativeMobileScroll = _this.props.nativeMobileScroll; if (_this.isDraggable(e.target)) { if (nativeMobileScroll && _this.scrolling) { _this.pressed = true; } else { var touch = e.touches[0]; _this.processClick(e, touch.clientX, touch.clientY); if (!nativeMobileScroll && _this.props.stopPropagation) { e.stopPropagation(); } } } }; _this.onTouchEnd = function (e) { var nativeMobileScroll = _this.props.nativeMobileScroll; if (_this.pressed) { if (_this.started && (!_this.scrolling || !nativeMobileScroll)) { _this.processEnd(); } else { _this.pressed = false; } _this.forceUpdate(); } }; _this.onTouchMove = function (e) { var nativeMobileScroll = _this.props.nativeMobileScroll; if (_this.pressed && (!nativeMobileScroll || !_this.isMobile)) { var touch = e.touches[0]; if (touch) { _this.processMove(e, touch.clientX, touch.clientY); } e.preventDefault(); if (_this.props.stopPropagation) { e.stopPropagation(); } } }; _this.onMouseDown = function (e) { if (_this.isDraggable(e.target) && _this.isScrollable()) { _this.processClick(e, e.clientX, e.clientY); e.preventDefault(); if (_this.props.stopPropagation) { e.stopPropagation(); } } }; _this.onMouseMove = function (e) { if (_this.pressed) { _this.processMove(e, e.clientX, e.clientY); e.preventDefault(); if (_this.props.stopPropagation) { e.stopPropagation(); } } }; _this.onMouseUp = function (e) { if (_this.pressed) { if (_this.started) { _this.processEnd(); } else { _this.pressed = false; _this.forceUpdate(); if (_this.props.onClick) { _this.props.onClick(e); } } e.preventDefault(); if (_this.props.stopPropagation) { e.stopPropagation(); } } }; _this.container = React__default.createRef(); _this.onEndScroll = debounce_1(_this.onEndScroll, SCROLL_END_DEBOUNCE); // Is container scrolling now (for example by inertia) _this.scrolling = false; // Is scrolling started _this.started = false; // Is touch active or mouse pressed down _this.pressed = false; return _this; } createClass(ScrollContainer, [{ key: 'componentDidMount', value: function componentDidMount() { var nativeMobileScroll = this.props.nativeMobileScroll; var container = this.container.current; window.addEventListener('mouseup', this.onMouseUp); window.addEventListener('mousemove', this.onMouseMove); window.addEventListener('touchmove', this.onTouchMove, { passive: false }); window.addEventListener('touchend', this.onTouchEnd); // due to https://github.com/facebook/react/issues/9809#issuecomment-414072263 container.addEventListener('touchstart', this.onTouchStart, { passive: false }); container.addEventListener('mousedown', this.onMouseDown, { passive: false }); if (nativeMobileScroll) { // We should check if it's the mobile device after page was loaded // to prevent breaking SSR this.isMobile = this.isMobileDevice(); // If it's the mobile device, we should rerender to change styles if (this.isMobile) { this.forceUpdate(); } } } }, { key: 'componentWillUnmount', value: function componentWillUnmount() { window.removeEventListener('mouseup', this.onMouseUp); window.removeEventListener('mousemove', this.onMouseMove); window.removeEventListener('touchmove', this.onTouchMove); window.removeEventListener('touchend', this.onTouchEnd); } }, { key: 'getElement', value: function getElement() { return this.container.current; } }, { key: 'isMobileDevice', value: function isMobileDevice() { return typeof window.orientation !== 'undefined' || navigator.userAgent.indexOf('IEMobile') !== -1; } }, { key: 'isDraggable', value: function isDraggable(target) { var ignoreElements = this.props.ignoreElements; if (ignoreElements) { var closest = target.closest(ignoreElements); return closest === null || closest.contains(this.getElement()); } else { return true; } } }, { key: 'isScrollable', value: function isScrollable() { var container = this.container.current; return container && (container.scrollWidth > container.clientWidth || container.scrollHeight > container.clientHeight); } // Simulate 'onEndScroll' event that fires when scrolling is stopped }, { key: 'processClick', value: function processClick(e, clientX, clientY) { var container = this.container.current; this.scrollLeft = container.scrollLeft; this.scrollTop = container.scrollTop; this.clientX = clientX; this.clientY = clientY; this.pressed = true; } }, { key: 'processStart', value: function processStart(e) { var changeCursor = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; var onStartScroll = this.props.onStartScroll; var container = this.container.current; this.started = true; // Add the class to change displayed cursor if (changeCursor) { document.body.classList.add('indiana-dragging'); } if (onStartScroll) { onStartScroll(container.scrollLeft, container.scrollTop, container.scrollWidth, container.scrollHeight); } this.forceUpdate(); } // Process native scroll (scrollbar, mobile scroll) }, { key: 'processScroll', value: function processScroll(e) { if (this.started) { var onScroll = this.props.onScroll; var container = this.container.current; if (onScroll) { onScroll(container.scrollLeft, container.scrollTop, container.scrollWidth, container.scrollHeight); } } else { this.processStart(e, false); } } // opts: {x: number, y: number, animated: boolean} }, { key: 'scrollTo', value: function scrollTo(opts) { var x = opts.x, y = opts.y, animated = opts.animated; var container = this.container.current; if (animated) { container.scrollTo({ top: y, left: x, behavior: 'smooth' }); } else { container.scrollLeft = x; container.scrollTop = y; } } // Process non-native scroll }, { key: 'processMove', value: function processMove(e, newClientX, newClientY) { var _props = this.props, horizontal = _props.horizontal, vertical = _props.vertical, activationDistance = _props.activationDistance, onScroll = _props.onScroll; var container = this.container.current; if (!this.started) { if (horizontal && Math.abs(newClientX - this.clientX) > activationDistance || vertical && Math.abs(newClientY - this.clientY) > activationDistance) { this.clientX = newClientX; this.clientY = newClientY; this.processStart(); } } else { if (horizontal) { container.scrollLeft -= newClientX - this.clientX; } if (vertical) { container.scrollTop -= newClientY - this.clientY; } if (onScroll) { onScroll(container.scrollLeft, container.scrollTop, container.scrollWidth, container.scrollHeight); } this.clientX = newClientX; this.clientY = newClientY; this.scrollLeft = container.scrollLeft; this.scrollTop = container.scrollTop; } } }, { key: 'processEnd', value: function processEnd(e) { var onEndScroll = this.props.onEndScroll; var container = this.container.current; this.pressed = false; this.started = false; this.scrolling = false; if (container && onEndScroll) { onEndScroll(container.scrollLeft, container.scrollTop, container.scrollWidth, container.scrollHeight); } document.body.classList.remove('indiana-dragging'); this.forceUpdate(); } }, { key: 'render', value: function render() { var _props2 = this.props, children = _props2.children, className = _props2.className, style = _props2.style, hideScrollbars = _props2.hideScrollbars; return React__default.createElement( 'div', { className: classnames(className, cn({ dragging: this.pressed, 'hide-scrollbars': hideScrollbars, 'native-scroll': this.isMobile })), style: style, ref: this.container, onScroll: this.onScroll }, children ); } }]); return ScrollContainer; }(React.PureComponent); ScrollContainer.propTypes = { vertical: PropTypes.bool, horizontal: PropTypes.bool, hideScrollbars: PropTypes.bool, activationDistance: PropTypes.number, children: PropTypes.node, onStartScroll: PropTypes.func, onScroll: PropTypes.func, onEndScroll: PropTypes.func, onClick: PropTypes.func, className: PropTypes.string, style: PropTypes.object, ignoreElements: PropTypes.string, nativeMobileScroll: PropTypes.bool, stopPropagation: PropTypes.bool }; ScrollContainer.defaultProps = { nativeMobileScroll: true, hideScrollbars: true, activationDistance: 10, vertical: true, horizontal: true, stopPropagation: false, style: {} }; module.exports = ScrollContainer; //# sourceMappingURL=index.js.map