Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

支持虚拟滚动 #570

Merged
merged 8 commits into from
Oct 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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;
}