Skip to content

Commit

Permalink
Merge pull request #570 from Tidyzq/feature-recycle-scroller
Browse files Browse the repository at this point in the history
支持虚拟滚动
  • Loading branch information
Maizify authored Oct 19, 2022
2 parents 61c7061 + 7a491cb commit 3cbde14
Show file tree
Hide file tree
Showing 26 changed files with 1,391 additions and 91 deletions.
8 changes: 7 additions & 1 deletion doc/plugin_event_list.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,13 @@ Trigger while vConsole trying to create a new tab for a plugin. This event will
After binding this event, vConsole will get HTML from your callback to render a tab. A new tab will definitely be added if you bind this event, no matter what tab's HTML you set. Do not bind this event if you don't want to add a new tab.

##### Callback Arguments:
- (required) function(html): a callback function that receives the content HTML of the new tab. `html` can be a HTML `string` or an `HTMLElement` object (Or object which supports `appendTo()` method, like JQuery object).
- (required) function(html, options): a callback function that receives the content HTML of the new tab. `html` can be a HTML `string` or an `HTMLElement` object (Or object which supports `appendTo()` method, like JQuery object), and an optional `object` for tab options.

A tab option is an object with properties:

Property | | | |
------- | ------- | ------- | -------
fixedHeight | boolean | optional | Whether the height of tab is fixed to 100%.

##### Example:

Expand Down
8 changes: 7 additions & 1 deletion doc/plugin_event_list_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,13 @@ myPlugin.on('init', function() {
绑定此事件后,vConsole 会认为此插件需要创建新 tab,并会将 callback 中获取的 HTML 用于渲染 tab。因此,只要绑定了此事件,新 tab 肯定会被渲染到页面中,无论 callback 传入的 HTML 是否为空。如果不需要添加新 tab,请不要绑定此事件。

##### Callback 参数
- (必填) function(html): 回调函数,接收一个 HTML 参数用于渲染 tab。`html` 可以为 HTML 字符串,或者 `HTMLElement` 对象(或支持 `appendTo()` 方法的对象,如 jQuery 对象)。
- (必填) function(html, options): 回调函数,第一个参数接收一个 HTML 参数用于渲染 tab。`html` 可以为 HTML 字符串,或者 `HTMLElement` 对象(或支持 `appendTo()` 方法的对象,如 jQuery 对象)。第二个参数接收一个可选配置信息。

配置的参数为:

Property | | | |
------- | ------- | ------- | -------
fixedHeight | boolean | 选填 | tab 高度固定为 100%。

##### 例子:

Expand Down
34 changes: 34 additions & 0 deletions src/component/recycleScroller/recycleItem.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
export let show: boolean
export let top: boolean
export let onResize: (height: number) => void = () => {}
let item: HTMLDivElement | undefined
let observer: ResizeObserver | null = null
onMount(() => {
if (show) onResize(item.getBoundingClientRect().height)
observer = new ResizeObserver((entries) => {
const entry = entries[0];
if (show) onResize(entry.contentRect.height)
})
observer.observe(item)
});
onDestroy(() => {
observer.disconnect()
});
</script>

<div
bind:this={item}
class="vc-scroller-item"
style:display={show ? "block" : "none"}
style:top="{top}px"
>
<slot />
</div>
161 changes: 161 additions & 0 deletions src/component/recycleScroller/recycleManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
const createRecycleManager = () => {
const recycles: { key: number; index: number; show: boolean }[] = [];

const poolKeys: number[] = [];
let poolStartIndex = 0;
let poolEndIndex = 0;

let lastItemCount = 0;
let lastStart = 0;
let lastEnd = 0;

const update = (itemCount: number, start: number, end: number) => {
if (lastItemCount === itemCount && lastStart === start && lastEnd === end)
return recycles;

const poolCount = poolKeys.length;

// 计算新的 visible 区域
const newFirstPool =
start <= poolEndIndex
? // 1. 开头一定在 [0, start]
Math.max(
0,
Math.min(
start,
// 2. 开头一定在 [poolStartIndex, poolEndIndex) 之间
Math.max(
poolStartIndex,
Math.min(poolEndIndex - 1, end - poolCount)
)
)
)
: start; // poolEndIndex 如果比 start 小,则前部无法保留下来

const newLastPool =
poolStartIndex <= end
? // 1. 结尾一定在 [end, itemCount] 之间
Math.max(
end,
Math.min(
itemCount,
// 2. 结尾一定在 (poolStartIndex, poolEndIndex] 之间
Math.max(
poolStartIndex + 1,
Math.min(poolEndIndex, newFirstPool + poolCount)
)
)
)
: end; // end 如果比 poolStartIndex 小,则后部无法保留下来

if (poolCount === 0 || newLastPool - newFirstPool < poolCount) {
// 无法复用,全都重新生成
const count = (recycles.length = poolKeys.length = end - start);
for (let i = 0; i < count; i += 1) {
poolKeys[i] = i;
recycles[i] = {
key: i,
index: i + start,
show: true,
};
}
poolStartIndex = start;
poolEndIndex = end;
lastItemCount = itemCount;
lastStart = start;
lastEnd = end;
return recycles;
}

let usedPoolIndex = 0;
let usedPoolOffset = 0;

// 复用区域
let reuseStart = 0;
let reuseEnd = 0;

if (poolEndIndex < newFirstPool || newLastPool < poolStartIndex) {
// 完全没有交集,随便复用
reuseStart = newFirstPool;
reuseEnd = newFirstPool + poolCount;
} else if (poolStartIndex < newFirstPool) {
// 开头复用
usedPoolOffset = newFirstPool - poolStartIndex;
reuseStart = newFirstPool;
reuseEnd = newFirstPool + poolCount;
} else if (newLastPool < poolEndIndex) {
// 尾部复用
usedPoolOffset = poolCount - (poolEndIndex - newLastPool);
reuseStart = newLastPool - poolCount;
reuseEnd = newLastPool;
} else if (newFirstPool <= poolStartIndex && poolEndIndex <= newLastPool) {
// 新的 visible 是完全子集,直接复用
reuseStart = poolStartIndex;
reuseEnd = poolEndIndex;
}

// 开头不可见区域
// 如果有不可见区域,则一定是来自上一次 visible 的复用 row
for (let i = newFirstPool; i < start; i += 1, usedPoolIndex += 1) {
const poolKey = poolKeys[(usedPoolOffset + usedPoolIndex) % poolCount];
const recycle = recycles[i - newFirstPool];
recycle.key = poolKey;
recycle.index = i;
recycle.show = false;
}

// 可见区域
for (let i = start, keyIndex = 0; i < end; i += 1) {
let poolKey: number;
if (reuseStart <= i && i < reuseEnd) {
// 复用 row
poolKey = poolKeys[(usedPoolOffset + usedPoolIndex) % poolCount];
usedPoolIndex += 1;
} else {
// 新建 row
poolKey = poolCount + keyIndex;
keyIndex += 1;
}
const recycleIndex = i - newFirstPool;
if (recycleIndex < recycles.length) {
const recycle = recycles[recycleIndex];
recycle.key = poolKey;
recycle.index = i;
recycle.show = true;
} else {
recycles.push({
key: poolKey,
index: i,
show: true,
});
}
}

// 末尾不可见区域
// 如果有不可见区域,则一定是来自上一次 visible 的复用 row
for (let i = end; i < newLastPool; i += 1, usedPoolIndex += 1) {
const poolKey = poolKeys[(usedPoolOffset + usedPoolIndex) % poolCount];
const recycle = recycles[i - newFirstPool];
recycle.key = poolKey;
recycle.index = i;
recycle.show = false;
}

// 更新 poolKeys
for (let i = 0; i < recycles.length; i += 1) {
poolKeys[i] = recycles[i].key;
}
recycles.sort((a, b) => a.key - b.key);
poolStartIndex = newFirstPool;
poolEndIndex = newLastPool;
lastItemCount = itemCount;
lastStart = start;
lastEnd = end;

return recycles;
};

return update;
};

export default createRecycleManager;
42 changes: 42 additions & 0 deletions src/component/recycleScroller/recycleScroller.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
.vc-scroller-viewport {
position: relative;
overflow: hidden;
height: 100%;
}

.vc-scroller-contents {
min-height: 100%;
will-change: transform;
}

.vc-scroller-items {
will-change: height;
}

.vc-scroller-item {
display: none;
position: absolute;
left: 0;
right: 0;
}

.vc-scroller-footer {

}

.vc-scroller-scrollbar-track {
width: 4px;
position: absolute;
top: 0;
right: 0;
bottom: 0;
padding: 1px;
}

.vc-scroller-scrollbar-thumb {
position: relative;
width: 100%;
height: 100%;
background: rgba(0,0,0,.5);
border-radius: 999px;
}
Loading

0 comments on commit 3cbde14

Please sign in to comment.