UNPKG

@dialpad/dialtone

Version:

Dialpad's Dialtone design system monorepo

463 lines (462 loc) 15.2 kB
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