Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion components/dropdown/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import {useMenuKeyboard} from './useKeyboard';
import {useMouseOutsidable} from '../../hooks/useMouseOutsidable';
import {FeedbackCallback} from './usePosition';

export interface DropdownMenuProps { }
export interface DropdownMenuProps {
virtualList?: boolean | string
}
export interface DropdownMenuEvents { }
export interface DropdownMenuBlocks { }

Expand Down
29 changes: 18 additions & 11 deletions components/dropdown/menu.vdt
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import {Transition} from 'intact';
import {makeMenuStyles} from './styles';
import {Virtual} from '../virtualList';

const {value, trigger, container} = this.dropdown.get();
const {children, className} = this.get();
const {children, className, virtualList} = this.get();

const classNameObj = {
'k-dropdown-menu': true,
Expand All @@ -18,14 +19,20 @@ const transition = $props.transition || {css: false, ...this.transition};
appear={true}
{...transition}
>
<div
class={classNameObj}
ev-mouseenter={this.onMouseEnter}
ev-mouseleave={trigger === 'hover' ? this.onMouseLeave : null}
ref={this.elementRef}
<Virtual
enableVirtualList={virtualList}
nativeProps={{
class: classNameObj,
'ev-mouseenter': this.onMouseEnter,
'ev-mouseleave': trigger === 'hover' ? this.onMouseLeave : null,
'ev-scroll': this.onScroll,
ref: this.elementRef,
}}
>
<b:children>
{children}
</b:children>
</div>
</Transition>
<template>
<b:children>
{children}
</b:children>
</template>
</Virtual>
</Transition>
47 changes: 47 additions & 0 deletions components/select/demos/virtualList.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
title: 虚拟列表
order: 12
---

给`Select`添加`virtualList`属性可以在`Option`过多时保证页面的性能,`virtualList`的值为true时开启,为false时关闭,也可以设置为`"auto"`,在`Option`数量超过200时则自动开启虚拟列表功能。

```vdt
import {Select, Option} from 'kpc';

<div style="margin-bottom: 16px;">
<Select v-model="day" virtualList>
<Option value={$value.id} v-for={this.get('list')}>{$value.title}</Option>
</Select>
You selected: {this.get('day')}
</div>
```

```ts
interface Props {
day?: string | null
}

const list = [];
for(let i = 0;i < 2000; i++) {
list.push({
title: `item - ${i}`,
id: i
})
}

export default class extends Component<Props> {
static template = template;
static defaults() {
return {
day: 100,
list: list
};
}
}
```


```styl
.k-select-option
width: 280px;
```
1 change: 1 addition & 0 deletions components/select/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ sidebar: doc
| labelMap | 建立值`value`到展示标签`label`的映射,可以在`value`不在`Option`集合中时,依然能够正确展示相应的`label` | `Map<any, string>` | `new Map()` |
| card | 是否展示`card`模式 | `boolean` | `false` |
| autoDisableArrow | 是否在没有更多可选项时,给箭头一个`disabled`状态来提示用户 | `boolean` | `false` |
| virtualList | 是否开启虚拟列表功能 | `boolean` &#124; `"auto"` | `false` |

```ts
export type Container = string | ((parentDom: Element, anchor: Node | null) => Element)
Expand Down
4 changes: 2 additions & 2 deletions components/select/menu.vdt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {context} from './useSearchable';
import {Tabs, Tab} from '../tabs';

let {children, className} = this.get();
const {card, searchable, multiple} = this.select.get();
const {card, searchable, multiple, virtualList} = this.select.get();

const classNameObj = {
'k-select-menu': true,
Expand Down Expand Up @@ -86,6 +86,6 @@ if (searchable) {
);
}

<DropdownMenu class={classNameObj}>
<DropdownMenu class={classNameObj} virtualList={virtualList}>
{children}
</DropdownMenu>
2 changes: 2 additions & 0 deletions components/select/select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface SelectProps<T = string, Multipe extends boolean = boolean> exte
labelMap?: Map<any, Children>
card?: boolean
autoDisableArrow?: boolean
virtualList?: boolean | string
}

export interface SelectEvents extends BaseSelectEvents { }
Expand All @@ -37,6 +38,7 @@ const typeDefs: Required<TypeDefs<SelectProps>> = {
labelMap: Map,
card: Boolean,
autoDisableArrow: Boolean,
virtualList: Boolean,
};

const defaults = (): Partial<SelectProps> => ({
Expand Down
124 changes: 124 additions & 0 deletions components/virtualList/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import {
Component,
TypeDefs,
VNode,
Children,
createVNode as h,
className as getClassName,
normalizeChildren
} from 'intact';
import {useSize} from './useSize';
import {useRange} from './useRange';
import {useScroll} from './useScroll';

export interface VirtualProps {
keeps?: number
buffer?: number
estimateSize?: number
total: number
isFixedType?: boolean
nativeProps?: any
start: number
end: number
enableVirtualList?: boolean | string
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

建议改为disable属性名,跟其他组件的设计保持一致,不过你这里可以为string,另外string还可以具体到auto字符串

}

export type RangeInfo = {
start: number
end: number
paddingTop: number
paddingBottom: number
}

const typeDefs: Required<TypeDefs<VirtualProps>> = {
keeps: Number,
buffer: Number,
estimateSize: Number,
total: Number,
isFixedType: Boolean,
nativeProps: Object,
start: Number,
end: Number,
enableVirtualList: [Boolean, String]
};

const defaults = (): Partial<VirtualProps> => ({
keeps: 30,
buffer: 10
});

export class Virtual extends Component<VirtualProps> {
static typeDefs = typeDefs;
static defaults = defaults;
static template = function(this: Virtual) {
const children: Children = this.get('children');
const nativeProps = this.$vNode.props?.nativeProps;
const enableVirtualList = this.get('enableVirtualList');

if(nativeProps.class) {
nativeProps.class = getClassName(nativeProps.class);
}

const auto = enableVirtualList === 'auto' && this.normalizedVnodes.length < 200;
if(!enableVirtualList || auto) {
return h('div', nativeProps, children);
}

const outerAttrs = {
...nativeProps,
'ev-scroll': (e: WheelEvent) => this.scroll.handleScroll((e.target as HTMLElement).scrollTop)
}
const {start, end} = this.get();
const {paddingTop, paddingBottom} = this.range.getRange();
const innerAttrs = {style: `padding-top: ${paddingTop}px; padding-bottom: ${paddingBottom}px`};
const croppedChildren = this.normalizedVnodes.slice(start, end + 1);
const vnode = this.vnode = h('div', outerAttrs, h('div', innerAttrs, croppedChildren));

return vnode;
};

vnode: VNode | null = null;
offset: number = 0;

private normalizedVnodes: VNode[] = [];

rangeInfo: RangeInfo = {
start: 0,
end: 0,
paddingTop: 0,
paddingBottom: 0
};

private size = useSize();
private range = useRange(this.size);
private scroll = useScroll(this.size, this.range);

init() {
if(!this.get('enableVirtualList')) return;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

有没有考虑这个属性,可能会动态改变的情况


const keeps = this.get('keeps')!;
const children = this.get('children') as VNode;

this.set('start', 0);
this.set('end', keeps - 1);

if(Array.isArray(children) && children.length == 2) {
this.set('total', children[1].length);
}

this.watch('children', (newValue) => {
const fakeVNode = {} as VNode;
normalizeChildren(fakeVNode, newValue);
this.normalizedVnodes = fakeVNode.children as VNode[];
})
}

mounted() {
if(!this.get('enableVirtualList')) return;
this.size.setItemSize();
}

initItems(): void {
this.range.checkRange(0, this.size.getEndByStart(0));
}
}
68 changes: 68 additions & 0 deletions components/virtualList/useRange.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import {useInstance, nextTick} from 'intact';
import {Virtual} from './index';
import {Size} from './useSize';

type RangeInfo = {
start: number
end: number
paddingTop: number
paddingBottom: number
}

export type Range = {
checkRange: (start: number, end: number) => void
getRange: () => RangeInfo
}

export function useRange(size: Size): Range {
const instance = useInstance() as Virtual;

function getRange(): RangeInfo {
const range: RangeInfo = Object.create(null);
const tmpRange = instance.rangeInfo;
range.start = tmpRange.start;
range.end = tmpRange.end;
range.paddingTop = tmpRange.paddingTop;
range.paddingBottom = tmpRange.paddingBottom;

return range;
}

function checkRange(start: number, end: number): void {
const keeps = instance.get('keeps')!;
const total = instance.get('total');

// Render all if total <= keeps
if(total <= keeps) {
start = 0;
end = size.getLastIndex();
} else if(end - start < keeps - 1) {
start = end - keeps + 1;
}

updateRange(start, end);
}

function setRange(): void {
const {start, end} = getRange();
instance.set('start', start);
instance.set('end', end);

nextTick(size.setItemSize.bind(instance));
}

function updateRange(start: number, end: number): void {
const tmp = instance.rangeInfo;
tmp.start = start;
tmp.end = end;
tmp.paddingTop = size.getPadFront();
tmp.paddingBottom = size.getPadBehind();

setRange();
}

return {
checkRange,
getRange
}
}
49 changes: 49 additions & 0 deletions components/virtualList/useScroll.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import {useInstance} from 'intact';
import {Virtual} from './index';
import {Size} from './useSize';
import {Range} from './useRange';

type ScrollObj = {
handleScroll: (offset: number) => void
}

export function useScroll(size: Size, range: Range): ScrollObj {
const instance = useInstance() as Virtual;

function handleScroll(offset: number): void {
const isDown = offset > instance.offset;
instance.offset = offset;

if(isDown) {
handleBehind();
} else {
handleFront();
}
}

function handleBehind(): void {
const overs = size.getPassedItems();

// Range should not change if scroll overs within buffer
if(overs < instance.rangeInfo.start + instance.get('buffer')!) {
return;
}

range.checkRange(overs, size.getEndByStart(overs));
}

function handleFront(): void {
const overs = size.getPassedItems();

if(overs > instance.rangeInfo.start) {
return;
}

const start = Math.max(overs - instance.get('buffer')!, 0);
range.checkRange(start, size.getEndByStart(start));
}

return {
handleScroll
}
}
Loading