UNPKG

el-table-virtual-scroll-next

Version:

The virtual scrolling component developed based on the Table component of Element-Plus supports dynamic height and solves the problem of scrolling stuck when the amount of data is large.

1,551 lines (1,440 loc) 59 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('vue')) : typeof define === 'function' && define.amd ? define(['exports', 'vue'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global["el-table-virtual-scroll"] = {}, global.vue)); })(this, (function (exports, vue) { 'use strict'; function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } } function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread2(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); } function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); } function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); } function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter); } function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; } function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } /** * Checks if `value` is the * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types) * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) * * @static * @memberOf _ * @since 0.1.0 * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is an object, else `false`. * @example * * _.isObject({}); * // => true * * _.isObject([1, 2, 3]); * // => true * * _.isObject(_.noop); * // => true * * _.isObject(null); * // => false */ function isObject(value) { var type = typeof value; return value != null && (type == 'object' || type == 'function'); } var isObject_1 = isObject; var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; /** Detect free variable `global` from Node.js. */ var freeGlobal = typeof commonjsGlobal == 'object' && commonjsGlobal && commonjsGlobal.Object === Object && commonjsGlobal; var _freeGlobal = freeGlobal; /** Detect free variable `self`. */ var freeSelf = typeof self == 'object' && self && self.Object === Object && self; /** Used as a reference to the global object. */ var root = _freeGlobal || freeSelf || Function('return this')(); var _root = root; /** * Gets the timestamp of the number of milliseconds that have elapsed since * the Unix epoch (1 January 1970 00:00:00 UTC). * * @static * @memberOf _ * @since 2.4.0 * @category Date * @returns {number} Returns the timestamp. * @example * * _.defer(function(stamp) { * console.log(_.now() - stamp); * }, _.now()); * // => Logs the number of milliseconds it took for the deferred invocation. */ var now = function() { return _root.Date.now(); }; var now_1 = now; /** Used to match a single whitespace character. */ var reWhitespace = /\s/; /** * Used by `_.trim` and `_.trimEnd` to get the index of the last non-whitespace * character of `string`. * * @private * @param {string} string The string to inspect. * @returns {number} Returns the index of the last non-whitespace character. */ function trimmedEndIndex(string) { var index = string.length; while (index-- && reWhitespace.test(string.charAt(index))) {} return index; } var _trimmedEndIndex = trimmedEndIndex; /** Used to match leading whitespace. */ var reTrimStart = /^\s+/; /** * The base implementation of `_.trim`. * * @private * @param {string} string The string to trim. * @returns {string} Returns the trimmed string. */ function baseTrim(string) { return string ? string.slice(0, _trimmedEndIndex(string) + 1).replace(reTrimStart, '') : string; } var _baseTrim = baseTrim; /** Built-in value references. */ var Symbol$1 = _root.Symbol; var _Symbol = Symbol$1; /** Used for built-in method references. */ var objectProto$1 = Object.prototype; /** Used to check objects for own properties. */ var hasOwnProperty = objectProto$1.hasOwnProperty; /** * Used to resolve the * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) * of values. */ var nativeObjectToString$1 = objectProto$1.toString; /** Built-in value references. */ var symToStringTag$1 = _Symbol ? _Symbol.toStringTag : undefined; /** * A specialized version of `baseGetTag` which ignores `Symbol.toStringTag` values. * * @private * @param {*} value The value to query. * @returns {string} Returns the raw `toStringTag`. */ function getRawTag(value) { var isOwn = hasOwnProperty.call(value, symToStringTag$1), tag = value[symToStringTag$1]; try { value[symToStringTag$1] = undefined; var unmasked = true; } catch (e) {} var result = nativeObjectToString$1.call(value); if (unmasked) { if (isOwn) { value[symToStringTag$1] = tag; } else { delete value[symToStringTag$1]; } } return result; } var _getRawTag = getRawTag; /** Used for built-in method references. */ var objectProto = Object.prototype; /** * Used to resolve the * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) * of values. */ var nativeObjectToString = objectProto.toString; /** * Converts `value` to a string using `Object.prototype.toString`. * * @private * @param {*} value The value to convert. * @returns {string} Returns the converted string. */ function objectToString(value) { return nativeObjectToString.call(value); } var _objectToString = objectToString; /** `Object#toString` result references. */ var nullTag = '[object Null]', undefinedTag = '[object Undefined]'; /** Built-in value references. */ var symToStringTag = _Symbol ? _Symbol.toStringTag : undefined; /** * The base implementation of `getTag` without fallbacks for buggy environments. * * @private * @param {*} value The value to query. * @returns {string} Returns the `toStringTag`. */ function baseGetTag(value) { if (value == null) { return value === undefined ? undefinedTag : nullTag; } return (symToStringTag && symToStringTag in Object(value)) ? _getRawTag(value) : _objectToString(value); } var _baseGetTag = baseGetTag; /** * Checks if `value` is object-like. A value is object-like if it's not `null` * and has a `typeof` result of "object". * * @static * @memberOf _ * @since 4.0.0 * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is object-like, else `false`. * @example * * _.isObjectLike({}); * // => true * * _.isObjectLike([1, 2, 3]); * // => true * * _.isObjectLike(_.noop); * // => false * * _.isObjectLike(null); * // => false */ function isObjectLike(value) { return value != null && typeof value == 'object'; } var isObjectLike_1 = isObjectLike; /** `Object#toString` result references. */ var symbolTag = '[object Symbol]'; /** * Checks if `value` is classified as a `Symbol` primitive or object. * * @static * @memberOf _ * @since 4.0.0 * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is a symbol, else `false`. * @example * * _.isSymbol(Symbol.iterator); * // => true * * _.isSymbol('abc'); * // => false */ function isSymbol(value) { return typeof value == 'symbol' || (isObjectLike_1(value) && _baseGetTag(value) == symbolTag); } var isSymbol_1 = isSymbol; /** Used as references for various `Number` constants. */ var NAN = 0 / 0; /** Used to detect bad signed hexadecimal string values. */ var reIsBadHex = /^[-+]0x[0-9a-f]+$/i; /** Used to detect binary string values. */ var reIsBinary = /^0b[01]+$/i; /** Used to detect octal string values. */ var reIsOctal = /^0o[0-7]+$/i; /** Built-in method references without a dependency on `root`. */ var freeParseInt = parseInt; /** * Converts `value` to a number. * * @static * @memberOf _ * @since 4.0.0 * @category Lang * @param {*} value The value to process. * @returns {number} Returns the number. * @example * * _.toNumber(3.2); * // => 3.2 * * _.toNumber(Number.MIN_VALUE); * // => 5e-324 * * _.toNumber(Infinity); * // => Infinity * * _.toNumber('3.2'); * // => 3.2 */ function toNumber(value) { if (typeof value == 'number') { return value; } if (isSymbol_1(value)) { return NAN; } if (isObject_1(value)) { var other = typeof value.valueOf == 'function' ? value.valueOf() : value; value = isObject_1(other) ? (other + '') : other; } if (typeof value != 'string') { return value === 0 ? value : +value; } value = _baseTrim(value); var isBinary = reIsBinary.test(value); return (isBinary || reIsOctal.test(value)) ? freeParseInt(value.slice(2), isBinary ? 2 : 8) : (reIsBadHex.test(value) ? NAN : +value); } var toNumber_1 = toNumber; /** Error message constants. */ var FUNC_ERROR_TEXT$1 = 'Expected a function'; /* Built-in method references for those with the same name as other `lodash` methods. */ var nativeMax = Math.max, nativeMin = Math.min; /** * Creates a debounced function that delays invoking `func` until after `wait` * milliseconds have elapsed since the last time the debounced function was * invoked. The debounced function comes with a `cancel` method to cancel * delayed `func` invocations and a `flush` method to immediately invoke them. * Provide `options` to indicate whether `func` should be invoked on the * leading and/or trailing edge of the `wait` timeout. The `func` is invoked * with the last arguments provided to the debounced function. Subsequent * calls to the debounced function return the result of the last `func` * invocation. * * **Note:** If `leading` and `trailing` options are `true`, `func` is * invoked on the trailing edge of the timeout only if the debounced function * is invoked more than once during the `wait` timeout. * * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred * until to the next tick, similar to `setTimeout` with a timeout of `0`. * * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) * for details over the differences between `_.debounce` and `_.throttle`. * * @static * @memberOf _ * @since 0.1.0 * @category Function * @param {Function} func The function to debounce. * @param {number} [wait=0] The number of milliseconds to delay. * @param {Object} [options={}] The options object. * @param {boolean} [options.leading=false] * Specify invoking on the leading edge of the timeout. * @param {number} [options.maxWait] * The maximum time `func` is allowed to be delayed before it's invoked. * @param {boolean} [options.trailing=true] * Specify invoking on the trailing edge of the timeout. * @returns {Function} Returns the new debounced function. * @example * * // Avoid costly calculations while the window size is in flux. * jQuery(window).on('resize', _.debounce(calculateLayout, 150)); * * // Invoke `sendMail` when clicked, debouncing subsequent calls. * jQuery(element).on('click', _.debounce(sendMail, 300, { * 'leading': true, * 'trailing': false * })); * * // Ensure `batchLog` is invoked once after 1 second of debounced calls. * var debounced = _.debounce(batchLog, 250, { 'maxWait': 1000 }); * var source = new EventSource('/stream'); * jQuery(source).on('message', debounced); * * // Cancel the trailing debounced invocation. * jQuery(window).on('popstate', debounced.cancel); */ function debounce(func, wait, options) { var lastArgs, lastThis, maxWait, result, timerId, lastCallTime, lastInvokeTime = 0, leading = false, maxing = false, trailing = true; if (typeof func != 'function') { throw new TypeError(FUNC_ERROR_TEXT$1); } wait = toNumber_1(wait) || 0; if (isObject_1(options)) { leading = !!options.leading; maxing = 'maxWait' in options; maxWait = maxing ? nativeMax(toNumber_1(options.maxWait) || 0, wait) : maxWait; trailing = 'trailing' in options ? !!options.trailing : trailing; } function invokeFunc(time) { var args = lastArgs, thisArg = lastThis; lastArgs = lastThis = undefined; lastInvokeTime = time; result = func.apply(thisArg, args); return result; } function leadingEdge(time) { // Reset any `maxWait` timer. lastInvokeTime = time; // Start the timer for the trailing edge. timerId = setTimeout(timerExpired, wait); // Invoke the leading edge. return leading ? invokeFunc(time) : result; } function remainingWait(time) { var timeSinceLastCall = time - lastCallTime, timeSinceLastInvoke = time - lastInvokeTime, timeWaiting = wait - timeSinceLastCall; return maxing ? nativeMin(timeWaiting, maxWait - timeSinceLastInvoke) : timeWaiting; } function shouldInvoke(time) { var timeSinceLastCall = time - lastCallTime, timeSinceLastInvoke = time - lastInvokeTime; // Either this is the first call, activity has stopped and we're at the // trailing edge, the system time has gone backwards and we're treating // it as the trailing edge, or we've hit the `maxWait` limit. return (lastCallTime === undefined || (timeSinceLastCall >= wait) || (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait)); } function timerExpired() { var time = now_1(); if (shouldInvoke(time)) { return trailingEdge(time); } // Restart the timer. timerId = setTimeout(timerExpired, remainingWait(time)); } function trailingEdge(time) { timerId = undefined; // Only invoke if we have `lastArgs` which means `func` has been // debounced at least once. if (trailing && lastArgs) { return invokeFunc(time); } lastArgs = lastThis = undefined; return result; } function cancel() { if (timerId !== undefined) { clearTimeout(timerId); } lastInvokeTime = 0; lastArgs = lastCallTime = lastThis = timerId = undefined; } function flush() { return timerId === undefined ? result : trailingEdge(now_1()); } function debounced() { var time = now_1(), isInvoking = shouldInvoke(time); lastArgs = arguments; lastThis = this; lastCallTime = time; if (isInvoking) { if (timerId === undefined) { return leadingEdge(lastCallTime); } if (maxing) { // Handle invocations in a tight loop. clearTimeout(timerId); timerId = setTimeout(timerExpired, wait); return invokeFunc(lastCallTime); } } if (timerId === undefined) { timerId = setTimeout(timerExpired, wait); } return result; } debounced.cancel = cancel; debounced.flush = flush; return debounced; } var debounce_1 = debounce; /** Error message constants. */ var FUNC_ERROR_TEXT = 'Expected a function'; /** * Creates a throttled function that only invokes `func` at most once per * every `wait` milliseconds. The throttled function comes with a `cancel` * method to cancel delayed `func` invocations and a `flush` method to * immediately invoke them. Provide `options` to indicate whether `func` * should be invoked on the leading and/or trailing edge of the `wait` * timeout. The `func` is invoked with the last arguments provided to the * throttled function. Subsequent calls to the throttled function return the * result of the last `func` invocation. * * **Note:** If `leading` and `trailing` options are `true`, `func` is * invoked on the trailing edge of the timeout only if the throttled function * is invoked more than once during the `wait` timeout. * * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred * until to the next tick, similar to `setTimeout` with a timeout of `0`. * * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) * for details over the differences between `_.throttle` and `_.debounce`. * * @static * @memberOf _ * @since 0.1.0 * @category Function * @param {Function} func The function to throttle. * @param {number} [wait=0] The number of milliseconds to throttle invocations to. * @param {Object} [options={}] The options object. * @param {boolean} [options.leading=true] * Specify invoking on the leading edge of the timeout. * @param {boolean} [options.trailing=true] * Specify invoking on the trailing edge of the timeout. * @returns {Function} Returns the new throttled function. * @example * * // Avoid excessively updating the position while scrolling. * jQuery(window).on('scroll', _.throttle(updatePosition, 100)); * * // Invoke `renewToken` when the click event is fired, but not more than once every 5 minutes. * var throttled = _.throttle(renewToken, 300000, { 'trailing': false }); * jQuery(element).on('click', throttled); * * // Cancel the trailing throttled invocation. * jQuery(window).on('popstate', throttled.cancel); */ function throttle(func, wait, options) { var leading = true, trailing = true; if (typeof func != 'function') { throw new TypeError(FUNC_ERROR_TEXT); } if (isObject_1(options)) { leading = 'leading' in options ? !!options.leading : leading; trailing = 'trailing' in options ? !!options.trailing : trailing; } return debounce_1(func, wait, { 'leading': leading, 'maxWait': wait, 'trailing': trailing }); } var throttle_1 = throttle; // 判断是否是滚动容器 function isScroller(el) { var style = window.getComputedStyle(el, null); var scrollValues = ['auto', 'scroll']; return scrollValues.includes(style.overflow) || scrollValues.includes(style['overflow-y']); } // 获取容器滚动位置 function getScrollTop(el) { return el === window ? window.pageYOffset : el.scrollTop; } // 获取容器滚动位置 function getScrollLeft(el) { return el === window ? window.pageXOffset : el.scrollLeft; } // 设置容器滚动位置 function setScrollTop(el, pos) { return el === window ? window.scroll(window.pageXOffset, pos) : el.scrollTop = pos; } // 设置容器滚动位置 function setScrollLeft(el, pos) { return el === window ? window.scroll(pos, window.pageYOffset) : el.scrollLeft = pos; } // 获取容器高度 function getOffsetHeight(el) { return el === window ? window.innerHeight : el.offsetHeight; } // 滚动到某个位置 function scrollToY(el, y) { if (el === window) { window.scroll(0, y); } else { el.scrollTop = y; } } // 是否为空 undefine or null function isEmpty(val) { return typeof val === 'undefined' || val === null; } // 表格body class名称 var TableBodyClassNames = ['.el-table__body-wrapper .el-scrollbar__view']; var checkOrder = 0; // 多选:记录多选选项改变的顺序 var script$1 = { inheritAttrs: false, name: 'el-table-virtual-scroll', props: { // 总数据 data: { type: Array, required: true }, // 每一行的预估高度 itemSize: { type: Number, "default": 60 }, // 指定滚动容器 scrollBox: { type: String }, // 顶部和底部缓冲区域,值越大显示表格的行数越多 buffer: { type: Number, "default": 200 }, // key值,data数据中的唯一id keyProp: { type: String, "default": 'id' }, // 滚动事件的节流时间 throttleTime: { type: Number, "default": 10 }, // 是否获取表格行动态高度 dynamic: { type: Boolean, "default": true }, // 是否开启虚拟滚动 virtualized: { type: Boolean, "default": true }, // 表格行合并时,合并在一起的行返回相同的key值 rowSpanKey: { type: Function } }, provide: function provide() { return { virtualScroll: this }; }, data: function data() { return { sizes: {}, // 尺寸映射(依赖响应式) start: 0, end: undefined, curRow: null, // 表格单选:选中的行 isExpanding: false, // 列是否正在展开 columnVms: [], // virtual-column 组件实例 isHideAppend: false, stopAmin: false // 是否停止row过渡动画 }; }, computed: { // 计算出每个item(的key值)到滚动容器顶部的距离 offsetMap: function offsetMap(_ref) { var keyProp = _ref.keyProp, itemSize = _ref.itemSize, sizes = _ref.sizes, data = _ref.data; if (!this.dynamic) return {}; var res = {}; var total = 0; for (var i = 0; i < data.length; i++) { var key = data[i][keyProp]; res[key] = total; var curSize = sizes[key]; var size = typeof curSize === 'number' ? curSize : itemSize; total += size; } return res; } }, methods: { // 初始化数据 initData: function initData() { var _this = this; // 可视范围内显示数据 this.renderData = []; // 页面可视范围顶端、底部 this.top = undefined; this.bottom = undefined; // 截取页面可视范围内显示数据的开始和结尾索引 this.start = 0; this.end = undefined; // 设置表格到滚动容器的距离 this.toTop = 0; // 组件是否deactivated状态 this.isDeactivated = false; // 滚动容器滚动位置【0-滚动容器top;1-滚动容器left;2-表格滚动容器top;3-表格滚动容器left】 this.scrollPos = [0, 0, 0, 0]; this.scroller = this.getScroller(); this.setToTop(); this.recordTablePos(); // 初次执行 setTimeout(function () { _this.handleScroll(); }, 100); // 监听事件 this.onScroll = !this.throttleTime ? this.handleScroll : throttle_1(this.handleScroll, this.throttleTime); this.scroller.addEventListener('scroll', this.onScroll, { passive: true }); window.addEventListener('resize', this.onScroll); }, // 获取滚动元素 getScroller: function getScroller() { var el; if (this.scrollBox) { if (this.scrollBox === 'window' || this.scrollBox === window) return window; el = document.querySelector(this.scrollBox); if (!el) throw new Error(" scrollBox prop: '".concat(this.scrollBox, "' is not a valid selector")); if (!isScroller(el)) console.warn("Warning! scrollBox prop: '".concat(this.scrollBox, "' is not a scroll element")); return el; } else { this.innerScroll = true; return this.$el.querySelector('.el-table__body-wrapper .el-scrollbar__wrap'); } }, // 设置表格到滚动容器的距离 setToTop: function setToTop() { if (this.innerScroll) { this.toTop = 0; } else { this.toTop = this.$el.getBoundingClientRect().top - (this.scroller === window ? 0 : this.scroller.getBoundingClientRect().top) + getScrollTop(this.scroller); } }, // 处理滚动事件 handleScroll: function handleScroll() { var _this2 = this; var shouldUpdate = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true; this.stopAmin = true; this.$nextTick(function () { _this2.stopAmin = false; }); // 如果组件失活,则不再执行handleScroll;否则外部容器滚动情况下记录的scrollTop会是0 if (this.isDeactivated) return; // 记录scrollPos this.scrollPos[0] = getScrollTop(this.scroller); this.scrollPos[1] = getScrollLeft(this.scroller); if (!this.virtualized) return; // 更新当前尺寸(高度) this.updateSizes(); // 计算renderData this.calcRenderData(); // 计算位置 this.calcPosition(); shouldUpdate && this.updatePosition(); // 触发事件 this.$emit('change', this.renderData, this.start, this.end); }, // 更新尺寸(高度) updateSizes: function updateSizes() { var _this3 = this; if (!this.dynamic) return; var rows = this.$el.querySelectorAll('.el-table__body > tbody > .el-table__row'); // 处理树形表格 var isTree = false; if (rows[0] && rows[0].classList.contains('el-table__row--level-0')) { isTree = true; rows = this.$el.querySelectorAll('.el-table__body > tbody > .el-table__row.el-table__row--level-0'); } Array.from(rows).forEach(function (row, index) { var item = _this3.renderData[index]; if (!item) return; // 计算表格行的高度 var offsetHeight = row.offsetHeight; // 表格行如果有扩展行,需要加上扩展内容的高度 var next = row.nextSibling; if (next.tagName === 'TR' && next.querySelector('.el-table__expanded-cell')) { offsetHeight += next.offsetHeight; } // 表格行如果有子孙节点,需要加上子孙节点的高度 if (isTree) { var _next = row.nextSibling; while (_next && _next.tagName === 'TR' && !_next.classList.contains('el-table__row--level-0')) { offsetHeight += _next.offsetHeight; _next = _next.nextSibling; } } var key = item[_this3.keyProp]; if (_this3.sizes[key] !== offsetHeight) { _this3.sizes[key] = offsetHeight; } }); }, // 获取某条数据offsetTop getItemOffsetTop: function getItemOffsetTop(index) { if (!this.dynamic) { return this.itemSize * index; } var item = this.data[index]; if (item) { return this.offsetMap[item[this.keyProp]] || 0; } return 0; }, // 获取某条数据的尺寸 getItemSize: function getItemSize(index) { if (index <= -1) return 0; var item = this.data[index]; if (item) { var key = item[this.keyProp]; return this.sizes[key] || this.itemSize; } return this.itemSize; }, // 计算只在视图上渲染的数据 calcRenderData: function calcRenderData() { var scroller = this.scroller, data = this.data, buffer = this.buffer; // 计算可视范围顶部、底部 var top = this.scrollPos[0] - buffer - this.toTop; var bottom = this.scrollPos[0] + getOffsetHeight(scroller) + buffer - this.toTop; var start; var end; if (!this.dynamic) { start = top <= 0 ? 0 : Math.floor(top / this.itemSize); end = bottom <= 0 ? 0 : Math.ceil(bottom / this.itemSize); } else { // 二分法计算可视范围内的开始的第一个内容 var l = 0; var r = data.length - 1; var mid = 0; while (l <= r) { mid = Math.floor((l + r) / 2); var midVal = this.getItemOffsetTop(mid); if (midVal < top) { var midNextVal = this.getItemOffsetTop(mid + 1); if (midNextVal > top) break; l = mid + 1; } else { r = mid - 1; } } // 计算渲染内容的开始、结束索引 start = mid; end = data.length - 1; for (var i = start + 1; i < data.length; i++) { var offsetTop = this.getItemOffsetTop(i); if (offsetTop >= bottom) { end = i; break; } } } if (this.isRowSpan()) { // 计算包含合并行的开始结束区间(⚠️注意:合并行不支持使用斑马纹,因为不能100%确定合并行的开始行是偶数,可能会向上找一直到第一行,导致渲染非常多行,浪费性能) var _this$calcRenderSpanD = this.calcRenderSpanData(start, end); var _this$calcRenderSpanD2 = _slicedToArray(_this$calcRenderSpanD, 2); start = _this$calcRenderSpanD2[0]; end = _this$calcRenderSpanD2[1]; } else { // 开始索引始终保持偶数,如果为奇数,则加1使其保持偶数【确保表格行的偶数数一致,不会导致斑马纹乱序显示】 if (start % 2) start = start - 1; } this.top = top; this.bottom = bottom; this.start = start; this.end = end; this.renderData = data.slice(start, end + 1); }, // 如果存在合并行的情况,渲染的数据范围扩大到包含合并行 calcRenderSpanData: function calcRenderSpanData(start, end) { // 从开始节点向上查找是否有合并行 var prevKey; while (start > 0) { var curRow = this.data[start]; var curkey = this.rowSpanKey(curRow, start); // 如果不存在key,说明当前行不属于合并行 if (isEmpty(curkey)) break; // 如果当前行与后面一行的key不相同,说明则当前行不属于合并行,从后一行开始截断 if (!isEmpty(prevKey) && prevKey !== curkey) { start++; break; } prevKey = curkey; start--; } // 从末端节点向下查找是否有合并行 var len = this.data.length; prevKey = undefined; while (end < len) { var _curRow = this.data[end]; var _curkey = this.rowSpanKey(_curRow, end); // 如果不存在key,说明当前行不属于合并行 if (!_curkey) break; // 如果当前行与前面一行的key不相同,说明则当前行不属于合并行,从前一行开始截断 if (prevKey && prevKey !== _curkey) { end--; break; } prevKey = _curkey; end++; } return [start, end]; }, // 是否是合并行 isRowSpan: function isRowSpan() { return typeof this.rowSpanKey === 'function'; }, // 计算位置 calcPosition: function calcPosition() { var _this4 = this; var last = this.data.length - 1; // 计算内容总高度 var wrapHeight = this.getItemOffsetTop(last) + this.getItemSize(last); // 计算当前滚动位置需要撑起的高度 var offsetTop = this.getItemOffsetTop(this.start); // 设置dom位置 TableBodyClassNames.forEach(function (className) { var el = _this4.$el.querySelector(className); if (!el) return; // 创建wrapEl、innerEl if (!el.wrapEl) { var wrapEl = document.createElement('div'); var innerEl = document.createElement('div'); wrapEl.appendChild(innerEl); innerEl.appendChild(el.children[0]); el.insertBefore(wrapEl, el.firstChild); el.wrapEl = wrapEl; el.innerEl = innerEl; } if (el.wrapEl) { // 设置高度 el.wrapEl.style.height = wrapHeight + 'px'; // 设置transform撑起高度 el.innerEl.style.transform = "translateY(".concat(offsetTop, "px)"); // 设置paddingTop撑起高度 // el.innerEl.style.paddingTop = `${offsetTop}px` } }); }, // 空闲时更新位置(触发时间:滚动停止后等待10ms执行) // 滚动停止之后,偶尔表格的行发生高度变更,那么当前计算的渲染数据是不正确的;那么需要手动触发最后一次handleScroll来重新计算 updatePosition: function updatePosition() { var _this5 = this; this.timer && clearTimeout(this.timer); this.timer = setTimeout(function () { _this5.timer && clearTimeout(_this5.timer); // 传入false,避免一直循环调用 _this5.handleScroll(false); }, this.throttleTime + 10); }, // 渲染全部数据 renderAllData: function renderAllData() { var _this6 = this; this.renderData = this.data; this.$emit('change', this.data, 0, this.data.length - 1); this.$nextTick(function () { // 清除撑起的高度和位置 TableBodyClassNames.forEach(function (className) { var el = _this6.$el.querySelector(className); if (!el) return; if (el.wrapEl) { // 设置高度 el.wrapEl.style.height = 'auto'; // 设置transform撑起高度 el.innerEl.style.transform = "translateY(".concat(0, "px)"); } }); }); }, // 【外部调用】更新 update: function update() { // console.log('update') this.setToTop(); this.handleScroll(); }, // 【外部调用】滚动到第几行 // (不太精确:滚动到第n行时,如果周围的表格行计算出真实高度后会更新高度,导致内容坍塌或撑起) scrollTo: function scrollTo(index) { var _this7 = this; var stop = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; var item = this.data[index]; if (item && this.scroller) { this.updateSizes(); this.calcRenderData(); this.$nextTick(function () { var offsetTop = _this7.getItemOffsetTop(index); scrollToY(_this7.scroller, offsetTop); // 调用两次scrollTo,第一次滚动时,如果表格行初次渲染高度发生变化时,会导致滚动位置有偏差,此时需要第二次执行滚动,确保滚动位置无误 if (!stop) { setTimeout(function () { _this7.scrollTo(index, true); }, 50); } }); } }, // 【外部调用】重置 reset: function reset() { this.sizes = {}; this.scrollTo(0, false); }, // 添加virtual-column实例 addColumn: function addColumn(vm) { this.columnVms.push(vm); }, // 移除virtual-column实例 removeColumn: function removeColumn(vm) { this.columnVms = this.columnVms.filter(function (item) { return item !== vm; }); }, // 多选:选中所有列 checkAll: function checkAll(val) { var _this8 = this; this.data.forEach(function (row) { return _this8.checkRow(row, val, false); }); this.emitSelectionChange(); if (val === false) checkOrder = 0; // 取消全选,则重置checkOrder }, // 多选:选中某一列 checkRow: function checkRow(row, val) { var emit = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true; if (row.$v_checked === val) return; row.$v_checked = val; row.$v_checkedOrder = val ? checkOrder++ : undefined; emit && this.emitSelectionChange(); }, // 多选:兼容表格selection-change事件 emitSelectionChange: function emitSelectionChange() { var selection = this.data.filter(function (row) { return row.$v_checked; }).sort(function (a, b) { return a.$v_checkedOrder - b.$v_checkedOrder; }); this.$emit('selection-change', selection); }, // 多选:兼容表格clearSelection方法 clearSelection: function clearSelection() { this.checkAll(false); this.columnVms.forEach(function (vm) { return vm.syncCheckStatus(); }); }, // 多选:兼容表格toggleRowSelection方法 toggleRowSelection: function toggleRowSelection(row, selected) { var val = typeof selected === 'boolean' ? selected : !row.$v_checked; this.checkRow(row, val); this.columnVms.forEach(function (vm) { return vm.syncCheckStatus(); }); }, // 单选:设置选中行 setCurrentRow: function setCurrentRow(row) { this.curRow = row; this.$emit('current-change', row); }, // 更新数据 updateData: function updateData(data) { this.$emit('update:data', data); }, // 执行update方法更新虚拟滚动,且每次nextTick只能执行一次【在数据大于100条开启虚拟滚动时,由于监听了data、virtualized会连续触发两次update方法:第一次update时,(updateSize)计算尺寸里的渲染数据(renderData)与表格行的dom是一一对应,之后会改变渲染数据(renderData)的值;而第二次执行update时,renderData改变了,而表格行dom未改变,导致renderData与dom不一一对应,从而位置计算错误,最终渲染的数据对应不上。因此使用每次nextTick只能执行一次来避免bug发生】 doUpdate: function doUpdate() { var _this9 = this; if (this.hasDoUpdate) return; // nextTick内已经执行过一次就不执行 if (!this.scroller) return; // scroller不存在说明未初始化完成,不执行 // 启动虚拟滚动的瞬间,需要暂时隐藏el-table__append-wrapper里的内容,不然会导致滚动位置一直到append的内容处 this.isHideAppend = true; this.update(); this.hasDoUpdate = true; this.$nextTick(function () { _this9.hasDoUpdate = false; _this9.isHideAppend = false; }); }, // 记录表格x、y轴滚动位置 recordTablePos: function recordTablePos() { var _this10 = this; if (this.innerScroll || this.isDeactivated) return; this.tableBodyEl = this.$el.querySelector('.el-table__body-wrapper'); this.onTableScroll = throttle_1(function () { _this10.scrollPos[2] = getScrollTop(_this10.tableBodyEl); _this10.scrollPos[3] = getScrollLeft(_this10.tableBodyEl); }, 100); this.tableBodyEl.addEventListener('scroll', this.onTableScroll, { passive: true }); }, // 恢复y轴滚动位置(仅支持表格内部滚动) restoreScrollY: function restoreScrollY() { var _this11 = this; if (!this.scroller) return; // 恢复滚动容器滚动位置 var _this$scrollPos = _slicedToArray(this.scrollPos, 4), top = _this$scrollPos[0], left = _this$scrollPos[1], top2 = _this$scrollPos[2], left2 = _this$scrollPos[3]; if (this.innerScroll) { // 表格内部滚动需要等待一段时间才执行恢复滚动位置,是因为表格需要等待一段时间才设置滚动容器高度,此时设置scrollTop才会生效 setTimeout(function () { setScrollTop(_this11.scroller, top); setScrollLeft(_this11.scroller, left); // 内部滚动且固定列,则固定列也需要恢复y轴滚动位置 var leftScroller = document.querySelector(TableBodyClassNames[1]); var rightScroller = document.querySelector(TableBodyClassNames[2]); if (leftScroller) setScrollTop(leftScroller, top); if (rightScroller) setScrollTop(rightScroller, top); }, 50); } else { setScrollTop(this.scroller, top); setScrollLeft(this.scroller, left); // 恢复表格内滚动位置 setScrollTop(this.tableBodyEl, top2); setScrollLeft(this.tableBodyEl, left2); } } }, watch: { data: function data() { if (!this.virtualized) { this.renderAllData(); } else { this.doUpdate(); } }, 'data.length': function dataLength() { if (!this.virtualized) { this.renderAllData(); } else { this.doUpdate(); } }, virtualized: { immediate: true, handler: function handler(val) { if (!val) { this.renderAllData(); } else { this.doUpdate(); } } } }, created: function created() { var _this12 = this; this.$nextTick(function () { _this12.initData(); }); }, beforeUnmount: function beforeUnmount() { if (this.scroller) { this.scroller.removeEventListener('scroll', this.onScroll); window.removeEventListener('resize', this.onScroll); } }, activated: function activated() { this.isDeactivated = false; this.restoreScrollY(); }, deactivated: function deactivated() { this.isDeactivated = true; } }; function render$1(_ctx, _cache, $props, $setup, $data, $options) { return vue.openBlock(), vue.createElementBlock("div", { "class": vue.normalizeClass(["el-table-virtual-scroll", [$data.isHideAppend ? 'hide-append' : '', $data.stopAmin ? 'no-row-amin' : '']]) }, [vue.renderSlot(_ctx.$slots, "default")], 2 /* CLASS */); } 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_248z = ".hide-append[data-v-6ee4cd4b] [data-v-6ee4cd4b] .el-table__append-wrapper {\n display: none;\n}\n.no-row-amin[data-v-6ee4cd4b] [data-v-6ee4cd4b] .hover-row td.el-table__cell {\n background: inherit !important;\n}\n"; styleInject(css_248z); script$1.render = render$1; script$1.__scopeId = "data-v-6ee4cd4b"; script$1.__file = "src/el-table-virtual-scroll.vue"; // import { // ElTableColumn, // ElCheckbox, // ElRadio // } from 'element-plus' var script = { inheritAttrs: false, name: 'el-table-virtual-column', components: { // ElTableColumn, // ElCheckbox, // ElRadio }, inject: ['virtualScroll'], props: { load: { type: Function }, indent: { type: Number, "default": 16 } }, data: function data() { return { isCheckedAll: false, // 全选 isCheckedImn: false, // 控制半选样式 isTree: false, type: undefined }; }, methods: { // 选择表格所有行 onCheckAllRows: function onCheckAllRows(val) { val = this.isCheckedImn ? true : val; this.virtualScroll.checkAll(val); this.isCheckedAll = val; this.isCheckedImn = false; }, // 选择表格某行 onCheckRow: function onCheckRow(row, val) { this.virtualScroll.checkRow(row, val); this.syncCheckStatus(); }, // 同步全选、半选框状态 syncCheckStatus: function syncCheckStatus() { var list = this.virtualScroll.data; var checkedLen = list.filter(function (row) { return row.$v_checked === true; }).length; if (checkedLen === 0) { this.isCheckedAll = false; this.isCheckedImn = false; } else if (checkedLen === list.length) { this.isCheckedAll = true; this.isCheckedImn = false; } else { this.isCheckedAll = false; this.isCheckedImn = true; } }, onRadioChange: function onRadioChange(row) { this.virtualScroll.setCurrentRow(row); }, // 获取索引值 getIndex: function getIndex(scope) { var index = this.virtualScroll.start + scope.$index; if (typeof this.$attrs.index === 'function') { return this.$attrs.index(index); } return index + 1; }, // 展开收起事件 onTreeNodeExpand: function onTreeNodeExpand(row) { this.$set(row, '$v_expanded', !row.$v_expanded); if (row.$v_expanded) { this.loadChildNodes(row); } else { this.hideChildNodes(row); } }, // 加载子节点 loadChildNodes: function loadChildNodes(row) { // 如果已经加载,则显示隐藏的字节点 if (row.$v_loaded) { var list = this.virtualScroll.data; var index = list.findIndex(function (item) { return item === row; }); if (index > -1) { this.virtualScroll.updateData([].concat(_toConsumableArray(list.slice(0, index + 1)), _toConsumableArray(row.$v_hideNodes), _toConsumableArray(list.slice(index + 1)))); } return; } // 获取子节点数据并显示 this.$set(row, '$v_loading', true); this.load && this.load(row, resolve.bind(this)); function resolve(data) { if (Array.isArray(!data)) data = []; this.$set(row, '$v_loading', false); this.$set(row, '$v_loaded', true); this.$set(row, '$v_hasChildren', !!data.length); data.forEach(function (item) { item.$level = typeof row.$level === 'number' ? row.$level + 1 : 2; }); // 所有子节点插入到当前同级节点下 var list = this.virtualScroll.data; var index = list.findIndex(function (item) { return item === row; }); if (index > -1) { this.virtualScroll.updateData([].concat(_toConsumableArray(list.slice(0, index + 1)), _toConsumableArray(data), _toConsumableArray(list.slice(index + 1)))); } } }, // 隐藏子节点 hideChildNodes: function hideChildNodes(row) { var list = this.virtualScroll.data; var index = list.findIndex(function (item) { return item === row; }); if (index === -1) return; // 查找当前节点的所有子孙节点 var hideNodes = []; for (var i = index + 1; i < list.length; i++) { var curRow = list[i]; if ((curRow.$level || 1) <= (row.$level || 1)) break; hideNodes.push(curRow); } this.$set(row, '$v_hideNodes', hideNodes); // 隐藏所有子孙节点 var newList = list.filter(function (item) { return !hideNodes.includes(item); }); this.virtualScroll.updateData(newList); this.virtualScroll.update(); } }, created: function created() { this.$set = function (data, key, value) { data[key] = value; }; this.virtualScroll.addColumn(this); var type = this.$attrs.type; if (['index', 'selection', 'radio', 'tree'].includes(type)) { this.type = 'v-' + type; } else { this.type = type; } if (type === 'tree') { this.isTree = true; } }, beforeUnmount: function beforeUnmount() { this.virtualScroll.removeColumn(this); } }; var _hoisted_1 = ["onClick"]; var _hoisted_2 = { key: 0, "class": "el-icon is-loading" }; var _hoisted_3 = /*#__PURE__*/vue.createElementVNode("svg", { viewBox: "0 0 1024 1024", xmlns: "http://www.w3.org/2000/svg" }, [/*#__PURE__*/vue.createElementVNode("path", { fill: "currentColor", d: "M512 64a32 32 0 0 1 32 32v192a32 32 0 0 1-64 0V96a32 32 0 0 1 32-32zm0 640a32 32 0 0 1 32 32v192a32 32 0 1 1-64 0V736a32 32 0 0 1 32-32zm448-192a32 32 0 0 1-32 32H736a32 32 0 1 1 0-64h192a32 32 0 0 1 32 32zm-640 0a32 32 0 0 1-32 32H96a32 32 0 0 1 0-64h192a32 32 0 0 1 32 32zM195.2 195.2a32 32 0 0 1 45.248 0L376.32 331.008a32 32 0 0 1-45.248 45.248L195.2 240.448a32 32 0 0 1 0-45.248zm452.544 452.544a32 32 0 0 1 45.248 0L828.8 783.552a32 32 0 0 1-45.248 45.248L647.744 692.992a32 32 0 0 1 0-45.248zM828.8 195.264a32 32 0 0 1 0 45.184L692.992 376.32a32 32 0 0 1-45.248-45.248l135.808-135.808a32 32 0 0 1 45.248 0zm-452.544 452.48a32 32 0 0 1 0 45.248L240.448 828.8a32 32 0 0 1-45.248-45.248l135.808-135.808a32 32 0 0 1 45.248