light-vue-tree
Version:
* Support drag, check, multiple check, bigData virtual scroll, lazy load, checkStrictly, add and delete node, etc.
1,652 lines (1,495 loc) • 77 kB
JavaScript
/*!
* light-vue-tree v1.0.1
* open source under the MIT license
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('core-js/modules/es.function.name.js'), require('core-js/modules/es.number.constructor.js'), require('core-js/modules/es.array.filter.js'), require('core-js/modules/web.dom-collections.for-each.js'), require('core-js/modules/es.array.includes.js'), require('core-js/modules/es.string.includes.js'), require('core-js/modules/es.array.splice.js'), require('core-js/modules/es.array.find-index.js'), require('core-js/modules/es.array.map.js'), require('core-js/modules/es.array.concat.js'), require('core-js/modules/es.map.js'), require('core-js/modules/es.object.to-string.js'), require('core-js/modules/es.string.iterator.js'), require('core-js/modules/web.dom-collections.iterator.js'), require('core-js/modules/es.regexp.exec.js'), require('core-js/modules/es.string.split.js'), require('core-js/modules/es.string.replace.js')) :
typeof define === 'function' && define.amd ? define(['core-js/modules/es.function.name.js', 'core-js/modules/es.number.constructor.js', 'core-js/modules/es.array.filter.js', 'core-js/modules/web.dom-collections.for-each.js', 'core-js/modules/es.array.includes.js', 'core-js/modules/es.string.includes.js', 'core-js/modules/es.array.splice.js', 'core-js/modules/es.array.find-index.js', 'core-js/modules/es.array.map.js', 'core-js/modules/es.array.concat.js', 'core-js/modules/es.map.js', 'core-js/modules/es.object.to-string.js', 'core-js/modules/es.string.iterator.js', 'core-js/modules/web.dom-collections.iterator.js', 'core-js/modules/es.regexp.exec.js', 'core-js/modules/es.string.split.js', 'core-js/modules/es.string.replace.js'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.VueTree = factory());
}(this, (function () { 'use strict';
function _defineProperty(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
});
} else {
obj[key] = value;
}
return obj;
}
function ownKeys(object, enumerableOnly) {
var keys = Object.keys(object);
if (Object.getOwnPropertySymbols) {
var symbols = Object.getOwnPropertySymbols(object);
if (enumerableOnly) {
symbols = symbols.filter(function (sym) {
return Object.getOwnPropertyDescriptor(object, sym).enumerable;
});
}
keys.push.apply(keys, symbols);
}
return keys;
}
function _objectSpread2(target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i] != null ? arguments[i] : {};
if (i % 2) {
ownKeys(Object(source), true).forEach(function (key) {
_defineProperty(target, key, source[key]);
});
} else if (Object.getOwnPropertyDescriptors) {
Object.defineProperties(target, Object.getOwnPropertyDescriptors(source));
} else {
ownKeys(Object(source)).forEach(function (key) {
Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key));
});
}
}
return target;
}
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a 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);
}
}
function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
if (staticProps) _defineProperties(Constructor, staticProps);
return Constructor;
}
var TREE_DATA = {
checked: false,
partialSelected: false,
expanded: false,
visible: true,
exceptDisabledChecked: false,
isLeaf: false,
isSearchAdd: false,
isSearchingAdd: false,
level: 0,
selected: false // 设置multiple使用
};
var TreeData = /*#__PURE__*/function () {
function TreeData(data) {
_classCallCheck(this, TreeData);
this.data = _objectSpread2(_objectSpread2({}, TREE_DATA), data);
this.children = [];
}
_createClass(TreeData, [{
key: "setParent",
value: function setParent(node) {
this.parent = node;
}
}, {
key: "prependChild",
value: function prependChild(node) {
if (this === node) return;
node.setParent(this);
this.children.unshift(node);
}
}, {
key: "addChild",
value: function addChild(node) {
if (this === node) return;
this.children.push(node);
node.setParent(this);
}
}, {
key: "isExceptDisabledChecked",
value: function isExceptDisabledChecked() {
return this.children && this.children.length ? this.children.every(function (child) {
return child.data.disabled || child.data.checked || child.data.exceptDisabledChecked;
}) : this.data.checked;
}
}, {
key: "isChecked",
value: function isChecked() {
return this.data.checked || false;
}
}, {
key: "isSelected",
value: function isSelected() {
return this.data.selected || false;
}
}, {
key: "isExpanded",
value: function isExpanded() {
return this.data.expanded || false;
}
}, {
key: "isPartialSelected",
value: function isPartialSelected() {
return this.data.partialSelected || false;
} // TODO 这里两个方法需要合并一下
}, {
key: "isAllChildrenSelected",
value: function isAllChildrenSelected() {
// eslint-disable-next-line no-debugger
// debugger
return this.children.every(function (child) {
return child.isChecked();
});
}
}, {
key: "hasChildrenPartialSelected",
value: function hasChildrenPartialSelected() {
return this.children.some(function (child) {
return child.isChecked() || child.isPartialSelected();
});
}
}, {
key: "contains",
value: function contains(target) {
var deep = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
var walk = function walk(parent) {
var children = parent.children || [];
var result = false;
for (var i = 0, j = children.length; i < j; i++) {
var child = children[i];
if (child === target || deep && walk(child)) {
result = true;
break;
}
}
return result;
};
return walk(this);
}
}, {
key: "nextSibling",
get: function get() {
var parent = this.parent;
if (parent) {
var index = parent.children.indexOf(this);
if (index > -1) {
return parent.children[index + 1];
}
}
return null;
}
}, {
key: "previousSibling",
get: function get() {
var parent = this.parent;
if (parent) {
var index = parent.children.indexOf(this);
if (index > -1) {
return index > 0 ? parent.children[index - 1] : null;
}
}
return null;
}
}, {
key: "insertBefore",
value: function insertBefore(child, ref) {
var index;
if (ref) {
index = this.children.indexOf(ref);
}
this.insertChild(child, index);
}
}, {
key: "insertAfter",
value: function insertAfter(child, ref) {
var index;
if (ref) {
index = this.children.indexOf(ref);
if (index !== -1) index += 1;
}
this.insertChild(child, index);
}
}, {
key: "removeChild",
value: function removeChild(child) {
var dataIndex = this.children.indexOf(child);
if (dataIndex > -1) {
child.parent = null;
this.children.splice(dataIndex, 1);
}
}
}, {
key: "remove",
value: function remove() {
var parent = this.parent;
if (parent) {
parent.removeChild(this);
}
}
}, {
key: "insertChild",
value: function insertChild(child, index) {
child.parent = this;
if (typeof index === 'undefined' || index < 0) {
this.children.push(child);
} else {
this.children.splice(index, 0, child);
}
}
}]);
return TreeData;
}();
var prefixCls$1 = 'vue-checkbox';
var Checkbox = {
name: 'Checkbox',
props: {
value: [Boolean],
indeterminate: [Boolean],
disabled: [Boolean]
},
methods: {
handleClick: function handleClick() {
this.$emit('change', this.value);
}
},
render: function render() {
var h = arguments[0];
var value = this.value,
disabled = this.disabled,
indeterminate = this.indeterminate,
handleClick = this.handleClick;
return h("div", {
"class": prefixCls$1,
"on": {
"click": handleClick
}
}, [indeterminate && h("div", {
"class": "".concat(prefixCls$1, "-indeterminate")
}), h("input", {
"class": "".concat(prefixCls$1, "-input"),
"domProps": {
"checked": value
},
"attrs": {
"disabled": disabled,
"type": "checkbox"
}
})]);
}
};
var TreeContent = {
name: 'TreeContent',
components: {
Checkbox: Checkbox
},
props: {
node: [Object],
tree: [Object]
},
data: function data() {
return {
loading: false
};
},
computed: {
activeNode: function activeNode() {
var node = this.node;
return node.isSelected();
},
visibleExpand: function visibleExpand() {
var node = this.node;
var lazy = this.tree.lazy;
return node.children && node.children.length || lazy && !node.data.isLeaf ? 'inline' : 'none';
}
},
methods: {
toggleFold: function toggleFold(node) {
var _this = this;
var _this$tree = this.tree,
load = _this$tree.load,
lazy = _this$tree.lazy,
appendChildren = _this$tree.appendChildren;
Object.assign(node.data, {
expanded: !node.data.expanded
});
if (this.tree.virtual) {
this.tree.getFlatTree();
} // 当前没有children进行lazyLoad
if (node.data.expanded && lazy && (!node.children || !node.children.length)) {
if (!load) {
throw new Error('[Tree] when lazy is true, load method must be set');
}
this.loading = true;
load(node.data, function (data) {
_this.loading = false;
if (data && data.length) {
appendChildren(data, node);
} else {
Object.assign(node.data, {
isLeaf: true
});
}
});
}
},
selectToggle: function selectToggle(val, node) {
var _this2 = this;
if (!this.tree.checkStrictly) {
var exceptDisabledChecked = node.isExceptDisabledChecked();
Object.assign(node.data, {
checked: !exceptDisabledChecked,
exceptDisabledChecked: !exceptDisabledChecked
});
this.$nextTick(function () {
_this2.tree.refreshNode(node);
_this2.tree.$emit('on-checked-change', {
node: node.data,
selectedData: _this2.tree.checkedNodes
});
_this2.tree.$emit('on-checked-item', {
node: node.data,
vNode: _this2
});
});
} else {
Object.assign(node.data, {
checked: val
});
this.tree.getCheckedValue(node);
this.tree.$emit('on-checked-change', {
node: node.data,
selectedData: this.tree.checkedNodes
});
this.tree.$emit('on-checked-item', {
node: node.data,
vNode: this
});
}
},
clickContent: function clickContent() {
this.tree.handleClickNode(this);
},
handleClickCheckBox: function handleClickCheckBox(e) {
e.stopPropagation();
},
renderExpand: function renderExpand() {
var h = this.$createElement;
var _this$tree2 = this.tree,
expandIconSlot = _this$tree2.$scopedSlots.expandIcon,
lazy = _this$tree2.lazy;
var node = this.node,
toggleFold = this.toggleFold,
visibleExpand = this.visibleExpand;
var _node$data = node.data,
expanded = _node$data.expanded,
isLeaf = _node$data.isLeaf,
children = node.children;
return children && children.length || lazy && !isLeaf ? expandIconSlot ? h("div", {
"style": {
display: visibleExpand
}
}, [expandIconSlot({
expanded: expanded,
node: node,
toggleFold: toggleFold
})]) : h("span", {
"class": ['icon', expanded ? 'rotate180-enter icon-expand' : 'rotate180-leave icon-unexpand'],
"on": {
"click": function click() {
return toggleFold(node);
}
}
}, ["\u25BC"]) : null;
},
renderCheckbox: function renderCheckbox() {
var h = this.$createElement;
var node = this.node,
handleClickCheckBox = this.handleClickCheckBox,
selectToggle = this.selectToggle;
var _this$tree3 = this.tree,
checkboxSlot = _this$tree3.$scopedSlots.checkbox,
showCheckbox = _this$tree3.showCheckbox;
var _node$data2 = node.data,
checked = _node$data2.checked,
partialSelected = _node$data2.partialSelected,
exceptDisabledChecked = _node$data2.exceptDisabledChecked,
disabled = _node$data2.disabled;
return showCheckbox ? checkboxSlot ? checkboxSlot({
handleClickCheckBox: handleClickCheckBox,
selectToggle: selectToggle,
node: node.data
}) : h(Checkbox, {
"attrs": {
"value": checked,
"indeterminate": partialSelected,
"disabled": disabled
},
"style": "margin-right: 10px;",
"key": exceptDisabledChecked,
"nativeOn": {
"click": handleClickCheckBox
},
"on": {
"change": function change(val) {
return selectToggle(val, node);
}
}
}) : null;
},
renderLoading: function renderLoading() {
var h = this.$createElement;
var loadingSlot = this.tree.$scopedSlots.loading;
var loading = this.loading;
return loading ? loadingSlot ? loadingSlot({
loading: loading
}) : h("div", ["\u21BB"]) : null;
},
renderNodeName: function renderNodeName() {
var h = this.$createElement;
var tree = this.tree,
node = this.node;
var name = node.data.name;
var renderTreeNode = tree.renderTreeNode,
defaultSlot = tree.$scopedSlots.default;
return renderTreeNode ? renderTreeNode(node.data) : defaultSlot ? defaultSlot({
node: node.data,
treeNode: node
}) : h("span", [name]);
}
},
render: function render() {
var h = arguments[0];
var clickContent = this.clickContent,
activeNode = this.activeNode,
renderExpand = this.renderExpand,
renderCheckbox = this.renderCheckbox,
renderLoading = this.renderLoading,
renderNodeName = this.renderNodeName;
return h("div", {
"class": ['node-content', {
'active-li': activeNode
}]
}, [renderExpand(), h("div", {
"class": ['inner-wrap'],
"on": {
"click": clickContent
}
}, [renderCheckbox(), renderLoading(), h("div", {
"class": 'node-name'
}, [renderNodeName()])])]);
}
};
var TreeNode = {
name: 'TreeNode',
components: {
TreeContent: TreeContent
},
props: {
node: [Object],
tree: {
type: Object
}
},
computed: {
visible: function visible() {
return this.parentExpanded && this.node.data.expanded;
}
},
methods: {
nodeView: function nodeView(node) {
var h = this.$createElement;
return h(TreeContent, {
"attrs": {
"tree": this.tree,
"node": node,
"icon": this.icon
}
});
},
nodeChildren: function nodeChildren(node) {
var h = this.$createElement;
var tree = this.tree;
return node.data.expanded ? h("div", [node.children && node.children.map(function (subNode, index) {
return subNode.data.visible && h("TreeNode", {
"attrs": {
"tree": tree,
"node": subNode
},
"key": subNode && subNode.data && subNode.data.name || index
});
})]) : null;
}
},
render: function render() {
var _this = this;
var h = arguments[0];
var node = this.node,
nodeChildren = this.nodeChildren;
var currentNode = this.nodeView(node);
var _this$tree = this.tree,
draggable = _this$tree.draggable,
dragStart = _this$tree.dragStart,
dragOver = _this$tree.dragOver,
dragEnd = _this$tree.dragEnd,
handleDrop = _this$tree.handleDrop,
dragInfo = _this$tree.dragInfo,
virtual = _this$tree.virtual;
return h("div", {
"class": ['child-node', dragInfo.dropType || ''],
"attrs": {
"draggable": draggable
},
"on": {
"dragstart": function dragstart(e) {
return dragStart(e, _this);
},
"dragover": function dragover(e) {
return dragOver(e, _this);
},
"dragend": function dragend(e) {
return dragEnd(e, _this);
},
"drop": handleDrop
}
}, [currentNode, h("transition", {
"attrs": {
"name": 'transition-drop'
}
}, [!virtual && nodeChildren(node)])]);
}
};
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 _arrayWithoutHoles(arr) {
if (Array.isArray(arr)) return _arrayLikeToArray(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 _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 _toConsumableArray(arr) {
return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread();
}
var DIRECTION_TYPE = {
FRONT: 'FRONT',
// scroll to up || left
BEHIND: 'BEHIND' // scroll to down || right
};
var CALC_TYPE = {
INIT: 'INIT',
FIXED: 'FIXED',
DYNAMIC: 'DYNAMIC'
};
var LEADING_BUFFER = 2;
var Virtual = /*#__PURE__*/function () {
function Virtual(param, callUpdate) {
_classCallCheck(this, Virtual);
this.init(param, callUpdate);
}
_createClass(Virtual, [{
key: "init",
value: function init(param, callUpdate) {
// param data
this.param = param;
this.callUpdate = callUpdate; // size data
this.sizes = new Map();
this.firstRangeTotalSize = 0;
this.firstRangeAverageSize = 0;
this.lastCalcIndex = 0;
this.fixedSizeValue = 0;
this.calcType = CALC_TYPE.INIT; // scroll data
this.offset = 0;
this.direction = ''; // range data
this.range = Object.create(null);
if (param) {
this.checkRange(0, param.keeps - 1);
} // benchmark test data
// this.__bsearchCalls = 0
// this.__getIndexOffsetCalls = 0
}
}, {
key: "destroy",
value: function destroy() {
this.init(null, null);
} // return current render range
}, {
key: "getRange",
value: function getRange() {
var range = Object.create(null);
range.start = this.range.start;
range.end = this.range.end;
range.padFront = this.range.padFront;
range.padBehind = this.range.padBehind;
return range;
}
}, {
key: "isBehind",
value: function isBehind() {
return this.direction === DIRECTION_TYPE.BEHIND;
}
}, {
key: "isFront",
value: function isFront() {
return this.direction === DIRECTION_TYPE.FRONT;
} // return start index offset
}, {
key: "getOffset",
value: function getOffset(start) {
return (start < 1 ? 0 : this.getIndexOffset(start)) + this.param.slotHeaderSize;
}
}, {
key: "updateParam",
value: function updateParam(key, value) {
var _this = this;
if (this.param && key in this.param) {
// if uniqueIds change, find out deleted id and remove from size map
if (key === 'uniqueIds') {
this.sizes.forEach(function (v, key) {
if (!value.includes(key)) {
_this.sizes.delete(key);
}
});
}
this.param[key] = value;
}
} // save each size map by id
}, {
key: "saveSize",
value: function saveSize(id, size) {
this.sizes.set(id, size); // we assume size type is fixed at the beginning and remember first size value
// if there is no size value different from this at next comming saving
// we think it's a fixed size list, otherwise is dynamic size list
if (this.calcType === CALC_TYPE.INIT) {
this.fixedSizeValue = size;
this.calcType = CALC_TYPE.FIXED;
} else if (this.calcType === CALC_TYPE.FIXED && this.fixedSizeValue !== size) {
this.calcType = CALC_TYPE.DYNAMIC; // it's no use at all
delete this.fixedSizeValue;
} // calculate the average size only in the first range
if (this.calcType !== CALC_TYPE.FIXED && typeof this.firstRangeTotalSize !== 'undefined') {
if (this.sizes.size < Math.min(this.param.keeps, this.param.uniqueIds.length)) {
this.firstRangeTotalSize = _toConsumableArray(this.sizes.values()).reduce(function (acc, val) {
return acc + val;
}, 0);
this.firstRangeAverageSize = Math.round(this.firstRangeTotalSize / this.sizes.size);
} else {
// it's done using
delete this.firstRangeTotalSize;
}
}
} // in some special situation (e.g. length change) we need to update in a row
// try goiong to render next range by a leading buffer according to current direction
}, {
key: "handleDataSourcesChange",
value: function handleDataSourcesChange() {
var start = this.range.start;
if (this.isFront()) {
start = start - LEADING_BUFFER;
} else if (this.isBehind()) {
start = start + LEADING_BUFFER;
}
start = Math.max(start, 0);
this.updateRange(this.range.start, this.getEndByStart(start));
} // when slot size change, we also need force update
}, {
key: "handleSlotSizeChange",
value: function handleSlotSizeChange() {
this.handleDataSourcesChange();
} // calculating range on scroll
}, {
key: "handleScroll",
value: function handleScroll(offset) {
this.direction = offset < this.offset ? DIRECTION_TYPE.FRONT : DIRECTION_TYPE.BEHIND;
this.offset = offset;
if (!this.param) {
return;
}
if (this.direction === DIRECTION_TYPE.FRONT) {
this.handleFront();
} else if (this.direction === DIRECTION_TYPE.BEHIND) {
this.handleBehind();
}
} // ----------- public method end -----------
}, {
key: "handleFront",
value: function handleFront() {
var overs = this.getScrollOvers(); // should not change range if start doesn't exceed overs
if (overs > this.range.start) {
return;
} // move up start by a buffer length, and make sure its safety
var start = Math.max(overs - this.param.buffer, 0);
this.checkRange(start, this.getEndByStart(start));
}
}, {
key: "handleBehind",
value: function handleBehind() {
var overs = this.getScrollOvers(); // range should not change if scroll overs within buffer
if (overs < this.range.start + this.param.buffer) {
return;
}
this.checkRange(overs, this.getEndByStart(overs));
} // return the pass overs according to current scroll offset
}, {
key: "getScrollOvers",
value: function getScrollOvers() {
// if slot header exist, we need subtract its size
var offset = this.offset - this.param.slotHeaderSize;
if (offset <= 0) {
return 0;
} // if is fixed type, that can be easily
if (this.isFixedType()) {
return Math.floor(offset / this.fixedSizeValue);
}
var low = 0;
var middle = 0;
var middleOffset = 0;
var high = this.param.uniqueIds.length;
while (low <= high) {
// this.__bsearchCalls++
middle = low + Math.floor((high - low) / 2);
middleOffset = this.getIndexOffset(middle);
if (middleOffset === offset) {
return middle;
} else if (middleOffset < offset) {
low = middle + 1;
} else if (middleOffset > offset) {
high = middle - 1;
}
}
return low > 0 ? --low : 0;
} // return a scroll offset from given index, can efficiency be improved more here?
// although the call frequency is very high, its only a superposition of numbers
}, {
key: "getIndexOffset",
value: function getIndexOffset(givenIndex) {
if (!givenIndex) {
return 0;
}
var offset = 0;
var indexSize = 0;
for (var index = 0; index < givenIndex; index++) {
// this.__getIndexOffsetCalls++
indexSize = this.sizes.get(this.param.uniqueIds[index]);
offset = offset + (typeof indexSize === 'number' ? indexSize : this.getEstimateSize());
} // remember last calculate index
this.lastCalcIndex = Math.max(this.lastCalcIndex, givenIndex - 1);
this.lastCalcIndex = Math.min(this.lastCalcIndex, this.getLastIndex());
return offset;
} // is fixed size type
}, {
key: "isFixedType",
value: function isFixedType() {
return this.calcType === CALC_TYPE.FIXED;
} // return the real last index
}, {
key: "getLastIndex",
value: function getLastIndex() {
return this.param.uniqueIds.length - 1;
} // in some conditions range is broke, we need correct it
// and then decide whether need update to next range
}, {
key: "checkRange",
value: function checkRange(start, end) {
var keeps = this.param.keeps;
var total = this.param.uniqueIds.length; // datas less than keeps, render all
if (total <= keeps) {
start = 0;
end = this.getLastIndex();
} else if (end - start < keeps - 1) {
// if range length is less than keeps, corrent it base on end
start = end - keeps + 1;
}
if (this.range.start !== start) {
this.updateRange(start, end);
}
} // setting to a new range and rerender
}, {
key: "updateRange",
value: function updateRange(start, end) {
this.range.start = start;
this.range.end = end;
this.range.padFront = this.getPadFront();
this.range.padBehind = this.getPadBehind();
this.callUpdate(this.getRange());
} // return end base on start
}, {
key: "getEndByStart",
value: function getEndByStart(start) {
var theoryEnd = start + this.param.keeps - 1;
var truelyEnd = Math.min(theoryEnd, this.getLastIndex());
return truelyEnd;
} // return total front offset
}, {
key: "getPadFront",
value: function getPadFront() {
if (this.isFixedType()) {
return this.fixedSizeValue * this.range.start;
} else {
return this.getIndexOffset(this.range.start);
}
} // return total behind offset
}, {
key: "getPadBehind",
value: function getPadBehind() {
var end = this.range.end;
var lastIndex = this.getLastIndex();
if (this.isFixedType()) {
return (lastIndex - end) * this.fixedSizeValue;
} // if it's all calculated, return the exactly offset
if (this.lastCalcIndex === lastIndex) {
return this.getIndexOffset(lastIndex) - this.getIndexOffset(end);
} else {
// if not, use a estimated value
return (lastIndex - end) * this.getEstimateSize();
}
} // get the item estimate size
}, {
key: "getEstimateSize",
value: function getEstimateSize() {
return this.isFixedType() ? this.fixedSizeValue : this.firstRangeAverageSize || this.param.estimateSize;
}
}]);
return Virtual;
}();
var VirtualProps = {
rowKey: {
type: [String, Function],
required: true
},
dataSources: {
type: Array,
required: true
},
dataComponent: {
type: [Object, Function]
},
keeps: {
type: Number,
default: 30
},
extraProps: {
type: Object
},
estimateSize: {
type: Number,
default: 50
},
direction: {
type: String,
default: 'vertical' // vertical || horizontal
},
start: {
type: Number,
default: 0
},
offset: {
type: Number,
default: 0
},
topThreshold: {
type: Number,
default: 0
},
bottomThreshold: {
type: Number,
default: 0
},
pageMode: {
type: Boolean,
default: false
},
rootTag: {
type: String,
default: 'div'
},
wrapTag: {
type: String,
default: 'div'
},
wrapClass: {
type: String,
default: ''
},
wrapStyle: {
type: Object
},
itemTag: {
type: String,
default: 'div'
},
itemClass: {
type: String,
default: ''
},
itemClassAdd: {
type: Function
},
itemStyle: {
type: Object
},
headerTag: {
type: String,
default: 'div'
},
headerClass: {
type: String,
default: ''
},
headerStyle: {
type: Object
},
footerTag: {
type: String,
default: 'div'
},
footerClass: {
type: String,
default: ''
},
footerStyle: {
type: Object
},
itemScopedSlots: {
type: Object
},
transitionName: {
type: String
}
};
var ItemProps = {
index: {
type: Number
},
event: {
type: String
},
tag: {
type: String
},
horizontal: {
type: Boolean
},
source: {
type: Object
},
component: {
type: [Object, Function]
},
uniqueKey: {
type: [String, Number]
},
extraProps: {
type: Object
},
scopedSlots: {
type: Object
}
};
var SlotProps = {
event: {
type: String
},
uniqueKey: {
type: String
},
tag: {
type: String
},
horizontal: {
type: Boolean
}
};
var Wrapper = {
created: function created() {
this.shapeKey = this.horizontal ? 'offsetWidth' : 'offsetHeight';
},
mounted: function mounted() {
var _this = this;
if (typeof ResizeObserver !== 'undefined') {
this.resizeObserver = new ResizeObserver(function () {
_this.dispatchSizeChange();
});
this.resizeObserver.observe(this.$el);
}
},
// since componet will be reused, so disptach when updated
updated: function updated() {
this.dispatchSizeChange();
},
beforeDestroy: function beforeDestroy() {
if (this.resizeObserver) {
this.resizeObserver.disconnect();
this.resizeObserver = null;
}
},
methods: {
getCurrentSize: function getCurrentSize() {
return this.$el ? this.$el[this.shapeKey] : 0;
},
// tell parent current size identify by unqiue key
dispatchSizeChange: function dispatchSizeChange() {
this.$parent.$emit(this.event, this.uniqueKey, this.getCurrentSize(), this.hasInitial);
}
}
};
var Item = {
name: 'virtual-list-item',
mixins: [Wrapper],
props: ItemProps,
methods: {
renderItem: function renderItem(_ref) {
var source = _ref.source;
var h = this.$createElement;
return h("div", [source.name]);
}
},
render: function render() {
var h = arguments[0];
// scopedSlots = {}, uniqueKey tag,
var component = this.component,
_this$extraProps = this.extraProps,
extraProps = _this$extraProps === void 0 ? {} : _this$extraProps,
index = this.index,
source = this.source,
uniqueKey = this.uniqueKey,
tag = this.tag;
var props = _objectSpread2(_objectSpread2({}, extraProps), {}, {
source: source,
rowKey: uniqueKey,
index: index
});
var ItemTag = tag;
return h(ItemTag, {
"key": uniqueKey,
"attrs": {
"role": "listitem"
}
}, [component ? component(props) : this.renderItem(props)]);
}
};
var Slot = {
name: 'virtual-list-slot',
mixins: [Wrapper],
props: SlotProps,
render: function render() {
var h = arguments[0];
var tag = this.tag,
uniqueKey = this.uniqueKey;
var Wrapper = tag || 'div';
return h(Wrapper, {
"key": uniqueKey,
"attrs": {
"role": uniqueKey
}
}, [this.$slots.default]);
}
};
var EVENT_TYPE = {
ITEM: 'item_resize',
SLOT: 'slot_resize'
};
var SLOT_TYPE = {
HEADER: 'thead',
// string value also use for aria role attribute
FOOTER: 'tfoot'
};
var VirtualList = {
name: 'virtual-list',
props: VirtualProps,
data: function data() {
return {
range: null,
isBottom: false
};
},
watch: {
'dataSources.length': function dataSourcesLength(oldVal, newVal) {
this.virtual.updateParam('uniqueIds', this.getUniqueIdFromDataSources());
this.virtual.handleDataSourcesChange();
if (newVal !== oldVal) {
this.isBottom = false;
}
},
keeps: function keeps(newValue) {
this.virtual.updateParam('keeps', newValue);
this.virtual.handleSlotSizeChange();
},
start: function start(newValue) {
this.scrollToIndex(newValue);
},
offset: function offset(newValue) {
this.scrollToOffset(newValue);
}
},
created: function created() {
this.isHorizontal = this.direction === 'horizontal';
this.directionKey = this.isHorizontal ? 'scrollLeft' : 'scrollTop';
this.installVirtual(); // listen item size change
this.$on(EVENT_TYPE.ITEM, this.onItemResized); // listen slot size change
if (this.$slots.header || this.$slots.footer) {
this.$on(EVENT_TYPE.SLOT, this.onSlotResized);
}
},
// set back offset when awake from keep-alive
activated: function activated() {
this.scrollToOffset(this.virtual.offset);
},
mounted: function mounted() {
// set position
if (this.start) {
this.scrollToIndex(this.start);
} else if (this.offset) {
this.scrollToOffset(this.offset);
} // in page mode we bind scroll event to document
if (this.pageMode) {
this.updatePageModeFront();
document.addEventListener('scroll', this.onScroll, {
passive: false
});
}
},
beforeDestroy: function beforeDestroy() {
this.virtual.destroy();
if (this.pageMode) {
document.removeEventListener('scroll', this.onScroll);
}
},
methods: {
// get item size by id
getSize: function getSize(id) {
return this.virtual.sizes.get(id);
},
// get the total number of stored (rendered) items
getSizes: function getSizes() {
return this.virtual.sizes.size;
},
// return current scroll offset
getOffset: function getOffset() {
if (this.pageMode) {
return document.documentElement[this.directionKey] || document.body[this.directionKey];
} else {
var root = this.$refs.root;
return root ? Math.ceil(root[this.directionKey]) : 0;
}
},
// return client viewport size
getClientSize: function getClientSize() {
var key = this.isHorizontal ? 'clientWidth' : 'clientHeight';
if (this.pageMode) {
return document.documentElement[key] || document.body[key];
} else {
var root = this.$refs.root;
return root ? Math.ceil(root[key]) : 0;
}
},
// return all scroll size
getScrollSize: function getScrollSize() {
var key = this.isHorizontal ? 'scrollWidth' : 'scrollHeight';
if (this.pageMode) {
return document.documentElement[key] || document.body[key];
} else {
var root = this.$refs.root;
return root ? Math.ceil(root[key]) : 0;
}
},
// set current scroll position to a expectant offset
scrollToOffset: function scrollToOffset(offset) {
if (this.pageMode) {
document.body[this.directionKey] = offset;
document.documentElement[this.directionKey] = offset;
} else {
var root = this.$refs.root;
if (root) {
root[this.directionKey] = offset;
}
}
},
// set current scroll position to a expectant index
scrollToIndex: function scrollToIndex(index) {
// scroll to bottom
if (index >= this.dataSources.length - 1) {
this.scrollToBottom();
} else {
var offset = this.virtual.getOffset(index);
this.scrollToOffset(offset);
}
},
// set current scroll position to bottom
scrollToBottom: function scrollToBottom() {
var _this = this;
var shepherd = this.$refs.shepherd;
if (shepherd) {
var offset = shepherd[this.isHorizontal ? 'offsetLeft' : 'offsetTop'];
this.scrollToOffset(offset); // check if it's really scrolled to the bottom
// maybe list doesn't render and calculate to last range
// so we need retry in next event loop until it really at bottom
setTimeout(function () {
if (_this.getOffset() + _this.getClientSize() < _this.getScrollSize()) {
_this.scrollToBottom();
}
}, 3);
}
},
// when using page mode we need update slot header size manually
// taking root offset relative to the browser as slot header size
updatePageModeFront: function updatePageModeFront() {
var root = this.$refs.root;
if (root) {
var rect = root.getBoundingClientRect();
var defaultView = root.ownerDocument.defaultView;
var offsetFront = this.isHorizontal ? rect.left + defaultView.pageXOffset : rect.top + defaultView.pageYOffset;
this.virtual.updateParam('slotHeaderSize', offsetFront);
}
},
// reset all state back to initial
reset: function reset() {
this.virtual.destroy();
this.scrollToOffset(0);
this.installVirtual();
},
installVirtual: function installVirtual() {
this.virtual = new Virtual({
slotHeaderSize: 0,
slotFooterSize: 0,
keeps: this.keeps,
estimateSize: this.estimateSize,
buffer: Math.round(this.keeps / 3),
// recommend for a third of keeps
uniqueIds: this.getUniqueIdFromDataSources()
}, this.onRangeChanged); // sync initial range
this.range = this.virtual.getRange();
},
getUniqueIdFromDataSources: function getUniqueIdFromDataSources() {
var rowKey = this.rowKey;
return this.dataSources.map(function (dataSource) {
return typeof rowKey === 'function' ? rowKey(dataSource) : dataSource[rowKey];
});
},
// event called when each item mounted or size changed
onItemResized: function onItemResized(id, size) {
this.virtual.saveSize(id, size);
this.$emit('resized', id, size);
},
// event called when slot mounted or size changed
onSlotResized: function onSlotResized(type, size, hasInit) {
if (type === SLOT_TYPE.HEADER) {
this.virtual.updateParam('slotHeaderSize', size);
} else if (type === SLOT_TYPE.FOOTER) {
this.virtual.updateParam('slotFooterSize', size);
}
if (hasInit) {
this.virtual.handleSlotSizeChange();
}
},
// here is the rerendering entry
onRangeChanged: function onRangeChanged(range) {
this.range = range;
},
// debounceVirtualScroll: debounce(function (offset) { this.virtual.handleScroll(offset) }, 200),
onScroll: function onScroll(evt) {
var offset = this.getOffset();
var clientSize = this.getClientSize();
var scrollSize = this.getScrollSize(); // iOS scroll-spring-back behavior will make direction mistake
if (offset < 0 || offset + clientSize > scrollSize + 1 || !scrollSize) {
return;
} // if (this.transitionName) {
// this.debounceVirtualScroll(offset)
// } else {
this.virtual.handleScroll(offset); // }
this.emitEvent(offset, clientSize, scrollSize, evt);
},
// emit event in special position
emitEvent: function emitEvent(offset, clientSize, scrollSize, evt) {
this.$emit('scroll', evt, this.virtual.getRange());
if (this.virtual.isFront() && this.isBottom) {
this.isBottom = false;
}
if (this.virtual.isFront() && !!this.dataSources.length && offset - this.topThreshold <= 0) {
this.$emit('totop');
} else if (!this.isBottom && this.virtual.isBehind() && offset + clientSize + this.bottomThreshold >= scrollSize) {
this.isBottom = true; // 防止滚动到底部时触发两次加载\
this.$emit('tobottom');
}
},
// get the real render slots based on range data
// in-place patch strategy will try to reuse components as possible
// so those components that are reused will not trigger lifecycle mounted
getRenderSlots: function getRenderSlots() {
var h = this.$createElement;
var slots = [];
var _this$range = this.range,
start = _this$range.start,
end = _this$range.end;
var dataSources = this.dataSources,
rowKey = this.rowKey,
itemClass = this.itemClass,
itemTag = this.itemTag,
itemStyle = this.itemStyle,
isHorizontal = this.isHorizontal,
extraProps = this.extraProps,
dataComponent = this.dataComponent,
itemScopedSlots = this.itemScopedSlots;
for (var index = start; index <= end; index++) {
var dataSource = dataSources[index];
if (dataSource) {
var uniqueKey = typeof rowKey === 'function' ? rowKey(dataSource) : dataSource[rowKey];
if (typeof uniqueKey === 'string' || typeof uniqueKey === 'number') {
slots.push(h(Item, {
"props": _objectSpread2({}, {
index: index,
tag: itemTag,
event: EVENT_TYPE.ITEM,
horizontal: isHorizontal,
uniqueKey: uniqueKey,
source: dataSource,
extraProps: extraProps,
component: dataComponent,
scopedSlots: itemScopedSlots
}),
"key": uniqueKey,
"style": itemStyle,
"class": "".concat(itemClass).concat(this.itemClassAdd ? ' ' + this.itemClassAdd(index) : '')
}));
} else {
console.warn("Cannot get the rowKey '".concat(rowKey, "' from data-sources."));
}
} else {
console.warn("Cannot get the index '".concat(index, "' from data-sources."));
}
}
return slots;
} // debounceScroll: debounce(function () { this.onScroll() }, 10)
},
render: function render() {
var h = arguments[0];
var _this$$slots = this.$slots,
header = _this$$slots.header,
footer = _this$$slots.footer;
var _this$range2 = this.range,
padFront = _this$range2.padFront,
padBehind = _this$range2.padBehind;
var isHorizontal = this.isHorizontal,
pageMode = this.pageMode,
RootTag = this.rootTag,
wrapTag = this.wrapTag,
wrapClass = this.wrapClass,
wrapStyle = this.wrapStyle,
headerTag = this.headerTag,
headerClass = this.headerClass,
headerStyle = this.headerStyle,
footerTag = this.footerTag,
footerClass = this.footerClass,
footerStyle = this.footerStyle,
transitionName = this.transitionName,
onScroll = this.onScroll;
var paddingStyle = {
padding: isHorizontal ? "0px ".concat(padBehind, "px 0px ").concat(padFront, "px") : "".concat(padFront, "px 0px ").concat(padBehind, "px")
};
var wrapperStyle = wrapStyle ? Object.assign({}, wrapStyle, paddingStyle) : paddingStyle;
var ListTag = transitionName ? 'transition-group' : wrapTag;
return h(RootTag, {
"ref": 'root',
"on": {
"scroll": !pageMode && onScroll
}
}, [header && h(Slot, {
"class": headerClass,
"style": headerStyle,
"props": _objectSpread2({}, {
tag: headerTag,
event: EVENT_TYPE.SLOT,
uniqueKey: SLOT_TYPE.HEADER
})
}, [header]), h(ListTag, {
"class": wrapClass,
"style": wrapperStyle,
"attrs": {
"role": 'group',
"tag": wrapTag
}
}, [this.getRenderSlots()]), footer && h(Slot, {
"class": footerClass,
"style": footerStyle,
"props": _objectSpread2({}, {
tag: footerTag,
event: EVENT_TYPE.SLOT,
uniqueKey: SLOT_TYPE.FOOTER
})
}, [footer]), h("div", {
"ref": 'shepherd',
"style": {
width: isHorizontal ? '0px' : '100%',
height: isHorizontal ? '100%' : '0px'
}
})]);
}
};
var prefixCls = 'vue-tree';
var VirtualTreeList = {
name: 'VirtualTreeList',
components: {
TreeNode: TreeNode,
VirtualList: VirtualList
},
props: {
treeNodes: [Array],
tree: {
type: Object
},
height: {
type: Number
},
keeps: [Number]
},
computed: {
conStyles: function conStyles() {
return this.height ? {
height: "".concat(this.height, "px"),
overflow: 'scroll'
} : {
height: 'auto'
};
}
},
methods: {
renderTreeNode: function renderTreeNode(_ref) {
var node = _ref.source;
var h = this.$createElement;
var tree = this.tree;
return node.data.visible && node.parent.data.expanded ? h(TreeNode, {
"attrs": {
"tree": tree,
"node": node
},
"style": {
marginLeft: "".concat(10 * node.data.level, "px")
},
"key": node && node.data && node.data.name
}) : null;
},
getRowKey: function getRowKey(node) {
return node.data.id;
}
},
render: function render() {
var h = arguments[0];
var treeNodes = this.treeNodes,
renderTreeNode = this.renderTreeNode,
getRowKey = this.getRowKey,
conStyles = this.conStyles,