@dialpad/dialtone
Version:
Dialpad's Dialtone design system monorepo
463 lines (462 loc) • 15.2 kB
JavaScript
import { reactive, ref, computed, watch, onMounted, nextTick, openBlock, createElementBlock, normalizeClass, createBlock, resolveDynamicComponent, normalizeStyle, unref, withCtx, Fragment, renderList, mergeProps, toHandlers, renderSlot, markRaw, shallowReactive } from "vue";
const _sfc_main = {
__name: "core_scroller",
props: {
/**
* List of items you want to display in the scroller.
*/
items: {
type: Array,
required: true
},
/**
*
* Field used to identify items and optimize managing rendered views
*/
keyField: {
type: String,
default: "id"
},
/**
* Direction of the scroller. Can be either `vertical` or `horizontal`.
*/
direction: {
type: String,
default: "vertical",
validator: (value) => ["vertical", "horizontal"].includes(value)
},
/**
* Size of the items in the list.
* If it is set to null (the default value), it will use variable size mode.
*/
itemSize: {
type: Number,
default: null
},
/**
* Minimum size used if the height (or width in horizontal mode) of an item is unknown.
*/
minItemSize: {
type: [Number, String],
default: null
},
/**
* Field used to get the item's size in variable size mode.
*/
sizeField: {
type: String,
default: "size"
},
/**
* Amount of pixel to add to edges of the scrolling visible area to start rendering items further away.
*/
buffer: {
type: Number,
default: 200
},
/**
* If true, the hover state will be skipped.
* This can be useful if you want to use the hover state for other purposes.
*/
skipHover: {
type: Boolean,
default: false
},
/**
* The element to render as the list's wrapper.
*/
listTag: {
type: String,
default: "div"
},
/**
* The element to render as the list item.
*/
itemTag: {
type: String,
default: "div"
},
/**
* The custom classes added to the item list wrapper.
*/
listClass: {
type: [String, Object, Array],
default: ""
},
/**
* The custom classes added to each item.
*/
itemClass: {
type: [String, Object, Array],
default: ""
}
},
emits: ["user-position"],
setup(__props, { expose: __expose, emit: __emit }) {
const props = __props;
const emit = __emit;
const views = reactive(/* @__PURE__ */ new Map());
const unusedViews = reactive(/* @__PURE__ */ new Map());
const pool = ref([]);
const hoverKey = ref(null);
const ready = ref(false);
const scroller = ref(null);
const userPosition = ref("top");
let endIndex = 0;
let scrollDirty = false;
let lastUpdateScrollPosition = 0;
let sortTimer = null;
let computedMinItemSize = null;
let totalSize = 0;
let uid = 0;
const sizes = computed(() => {
if (props.itemSize === null) {
const sizes2 = {
"-1": { accumulator: 0 }
};
const items = props.items;
const field = props.sizeField;
const minItemSize = props.minItemSize;
let computedMinSize = 1e4;
let accumulator = 0;
let current;
for (let i = 0, l = items.length; i < l; i++) {
current = items[i][field] || minItemSize;
if (current < computedMinSize) {
computedMinSize = current;
}
accumulator += current;
sizes2[i] = { accumulator, size: current };
}
computedMinItemSize = computedMinSize;
return sizes2;
}
return [];
});
const simpleArray = computed(() => {
return props.items.length && typeof props.items[0] !== "object";
});
const itemIndexByKey = computed(() => {
const result = {};
for (let i = 0, l = props.items.length; i < l; i++) {
result[props.items[i][props.keyField]] = i;
}
return result;
});
watch(sizes, () => {
_updateVisibleItems(false);
}, { deep: true });
onMounted(() => {
nextTick(() => {
_updateVisibleItems(true);
ready.value = true;
});
});
const _addView = (pool2, index, item, key, type) => {
const nr = markRaw({
id: uid++,
index,
used: true,
key,
type
});
const view = shallowReactive({
item,
position: 0,
nr
});
pool2.value.push(view);
return view;
};
const _unuseView = (view, fake = false) => {
const _unusedViews = unusedViews;
const type = view.nr.type;
let unusedPool = _unusedViews.get(type);
if (!unusedPool) {
unusedPool = [];
_unusedViews.set(type, unusedPool);
}
unusedPool.push(view);
if (!fake) {
view.nr.used = false;
view.position = -9999;
}
};
const _getScroll = () => {
const isVertical = props.direction === "vertical";
let scrollState;
if (isVertical) {
scrollState = {
start: scroller.value.scrollTop,
end: scroller.value.scrollTop + scroller.value.clientHeight
};
} else {
scrollState = {
start: scroller.value.scrollLeft,
end: scroller.value.scrollLeft + scroller.value.clientWidth
};
}
return scrollState;
};
const _itemsLimitError = () => {
setTimeout(() => {
console.error("It seems the scroller element isn't scrolling, so it tries to render all the items at once.", "Scroller:", scroller);
console.error("Make sure the scroller has a fixed height (or width) and 'overflow-y' (or 'overflow-x') set to 'auto' so it can scroll correctly and only render the items visible in the scroll viewport.");
});
throw new Error("Rendered items limit reached");
};
const _sortViews = () => {
pool.value.sort((viewA, viewB) => viewA.nr.index - viewB.nr.index);
};
const _updateVisibleItems = (checkItem, checkPositionDiff = false) => {
var _a, _b, _c, _d, _e, _f;
const itemSize = props.itemSize;
const minItemSize = computedMinItemSize;
const keyField = simpleArray.value ? null : props.keyField;
const items = props.items;
const count = items.length;
const _sizes = sizes.value;
const _views = views;
const _unusedViews = unusedViews;
const _pool = pool;
const _itemIndexByKey = itemIndexByKey;
let _startIndex, _endIndex;
let _totalSize;
if (!count) {
_startIndex = _endIndex = _totalSize = 0;
} else {
const scroll = _getScroll();
if (checkPositionDiff) {
let positionDiff = scroll.start - lastUpdateScrollPosition.value;
if (positionDiff < 0) positionDiff = -positionDiff;
if (itemSize === null && positionDiff < minItemSize.value || positionDiff < itemSize) {
return {
continuous: true
};
}
}
lastUpdateScrollPosition = scroll.start;
const _buffer = props.buffer;
scroll.start -= _buffer;
scroll.end += _buffer;
if (itemSize === null) {
let h;
let a = 0;
let b = count - 1;
let i = ~~(count / 2);
let oldI;
do {
oldI = i;
h = (_a = _sizes[i]) == null ? void 0 : _a.accumulator;
if (h < scroll.start) {
a = i;
} else if (i < count - 1 && ((_b = _sizes[i + 1]) == null ? void 0 : _b.accumulator) > scroll.start) {
b = i;
}
i = ~~((a + b) / 2);
} while (i !== oldI);
i < 0 && (i = 0);
_startIndex = i;
_totalSize = (_c = _sizes[count - 1]) == null ? void 0 : _c.accumulator;
for (_endIndex = i; _endIndex < count && ((_d = _sizes[_endIndex]) == null ? void 0 : _d.accumulator) < scroll.end; _endIndex++) ;
if (_endIndex === -1) {
_endIndex = items.length - 1;
} else {
_endIndex++;
_endIndex > count && (_endIndex = count);
}
} else {
_startIndex = ~~(scroll.start / itemSize);
const remainer = _startIndex % 1;
_startIndex -= remainer;
_endIndex = Math.ceil(scroll.end / itemSize);
_startIndex < 0 && (_startIndex = 0);
_endIndex > count && (_endIndex = count);
_totalSize = Math.ceil(count / 1) * itemSize;
}
}
if (_endIndex - _startIndex > 1e3) {
_itemsLimitError();
}
totalSize = _totalSize;
let view;
const continuous = _startIndex <= endIndex && _endIndex >= _startIndex;
if (continuous) {
for (let i = 0, l = _pool.value.length; i < l; i++) {
view = _pool.value[i];
if (view == null ? void 0 : view.nr.used) {
if (checkItem) {
view.nr.index = _itemIndexByKey[view.item[keyField]];
}
if (view.nr.index == null || view.nr.index < _startIndex || view.nr.index >= _endIndex) {
_unuseView(view);
}
}
}
}
const unusedIndex = continuous ? null : /* @__PURE__ */ new Map();
let item, type;
let v;
for (let i = _startIndex; i < _endIndex; i++) {
item = items[i];
const key = keyField ? item == null ? void 0 : item[keyField] : item;
if (key == null) {
throw new Error(`Key is ${key} on item (keyField is '${keyField}')`);
}
view = _views.get(key);
if (!itemSize && !((_e = _sizes[i]) == null ? void 0 : _e.size)) {
if (view) _unuseView(view);
continue;
}
type = item.type;
let unusedPool = _unusedViews.get(type);
if (!view) {
if (continuous) {
if (unusedPool && unusedPool.length) {
view = unusedPool.pop();
} else {
view = _addView(_pool, i, item, key, type);
}
} else {
v = unusedIndex.get(type) || 0;
if (!unusedPool || v >= unusedPool.length) {
view = _addView(_pool, i, item, key, type);
_unuseView(view, true);
unusedPool = _unusedViews.get(type);
}
view = unusedPool[v];
unusedIndex.set(type, v + 1);
}
_views.delete(view.nr.key);
view.nr.used = true;
view.nr.index = i;
view.nr.key = key;
view.nr.type = type;
_views.set(key, view);
} else {
if (!view.nr.used) {
view.nr.used = true;
if (unusedPool) {
const index = unusedPool.indexOf(view);
if (index !== -1) unusedPool.splice(index, 1);
}
}
}
view.item = item;
if (itemSize === null) {
view.position = (_f = _sizes[i - 1]) == null ? void 0 : _f.accumulator;
view.offset = 0;
} else {
view.position = Math.floor(i) * itemSize;
view.offset = i % 1 * itemSize;
}
}
endIndex = _endIndex;
clearTimeout(sortTimer);
sortTimer = setTimeout(_sortViews, 300);
return {
continuous
};
};
const _scrollToPosition = (position) => {
const direction = props.direction === "vertical" ? { scroll: "scrollTop", start: "top" } : { scroll: "scrollLeft", start: "left" };
const viewport = scroller.value;
const scrollDirection = direction.scroll;
viewport[scrollDirection] = position;
};
const scrollToItem = (index) => {
var _a;
let scroll;
if (props.itemSize === null) {
scroll = index > 0 ? (_a = sizes.value[index - 1]) == null ? void 0 : _a.accumulator : 0;
} else {
scroll = Math.floor(index) * props.itemSize;
}
_scrollToPosition(scroll);
};
const handleScroll = () => {
const container = scroller.value;
if (userPosition.value !== "middle") {
userPosition.value = "middle";
emit("user-position", "middle");
}
if (container.scrollTop === 0) {
userPosition.value = "top";
emit("user-position", "top");
}
if (container.scrollTop + container.clientHeight === container.scrollHeight) {
userPosition.value = "bottom";
emit("user-position", "bottom");
}
if (!scrollDirty) {
scrollDirty = true;
const requestUpdate = () => requestAnimationFrame(() => {
scrollDirty = false;
_updateVisibleItems(false, true);
});
requestUpdate();
}
};
__expose({
scrollToItem,
_updateVisibleItems
});
return (_ctx, _cache) => {
return openBlock(), createElementBlock("div", {
ref_key: "scroller",
ref: scroller,
class: normalizeClass(["vue-recycle-scroller", {
ready: ready.value,
[`direction-${__props.direction}`]: true
}]),
onScrollPassive: handleScroll
}, [
(openBlock(), createBlock(resolveDynamicComponent(__props.listTag), {
ref: "wrapper",
style: normalizeStyle({ [__props.direction === "vertical" ? "minHeight" : "minWidth"]: `${unref(totalSize)}px` }),
class: normalizeClass(["vue-recycle-scroller__item-wrapper", __props.listClass])
}, {
default: withCtx(() => [
(openBlock(true), createElementBlock(Fragment, null, renderList(pool.value, (view) => {
return openBlock(), createBlock(resolveDynamicComponent(__props.itemTag), mergeProps({
key: view.nr.id,
style: ready.value ? {
transform: `translate${__props.direction === "vertical" ? "Y" : "X"}(${view.position}px) translate${__props.direction === "vertical" ? "X" : "Y"}(${view.offset}px)`,
width: void 0,
height: void 0
} : null,
class: ["vue-recycle-scroller__item-view", [
__props.itemClass,
{
hover: !__props.skipHover && hoverKey.value === view.nr.key
}
]]
}, toHandlers(__props.skipHover ? {} : {
mouseenter: () => {
hoverKey.value = view.nr.key;
},
mouseleave: () => {
hoverKey.value = null;
}
})), {
default: withCtx(() => [
renderSlot(_ctx.$slots, "default", {
item: view.item,
index: view.nr.index,
active: view.nr.used
})
]),
_: 2
}, 1040, ["style", "class"]);
}), 128))
]),
_: 3
}, 8, ["style", "class"]))
], 34);
};
}
};
export {
_sfc_main as default
};
//# sourceMappingURL=core_scroller.vue.js.map