From ba4e049936c282b9cba725b5c08e050415775e0e Mon Sep 17 00:00:00 2001 From: webyom Date: Mon, 14 Sep 2020 22:30:15 +0800 Subject: [PATCH] feat(list): list component --- src/_site/scripts/root-component.tsx | 2 + src/_site/scripts/routes/home/index.tsx | 3 + src/list/demo/index.scss | 10 ++ src/list/demo/index.tsx | 121 ++++++++++++++++ src/list/index.scss | 17 +++ src/list/index.tsx | 175 ++++++++++++++++++++++++ src/styles/var.scss | 6 + 7 files changed, 334 insertions(+) create mode 100644 src/list/demo/index.scss create mode 100644 src/list/demo/index.tsx create mode 100644 src/list/index.scss create mode 100644 src/list/index.tsx diff --git a/src/_site/scripts/root-component.tsx b/src/_site/scripts/root-component.tsx index 8ec8237..3611a1e 100644 --- a/src/_site/scripts/root-component.tsx +++ b/src/_site/scripts/root-component.tsx @@ -17,6 +17,7 @@ import { FormRouteComponent } from '../../form/demo'; import { ImgRouteComponent } from '../../img/demo'; import { LayoutRouteComponent } from '../../col/demo'; import { LazyloadRouteComponent } from '../../lazyload/demo'; +import { ListRouteComponent } from '../../list/demo'; import { LoadingRouteComponent } from '../../loading/demo'; import { NotifyRouteComponent } from '../../notify/demo'; import { NumberKeyboardRouteComponent } from '../../number-keyboard/demo'; @@ -59,6 +60,7 @@ export class RootComponent extends preact.Component { + diff --git a/src/_site/scripts/routes/home/index.tsx b/src/_site/scripts/routes/home/index.tsx index b75ce03..7f25a6d 100644 --- a/src/_site/scripts/routes/home/index.tsx +++ b/src/_site/scripts/routes/home/index.tsx @@ -119,6 +119,9 @@ export class HomeRouteComponent extends preact.Component { Lazyload + + List + Skeleton diff --git a/src/list/demo/index.scss b/src/list/demo/index.scss new file mode 100644 index 0000000..dc14584 --- /dev/null +++ b/src/list/demo/index.scss @@ -0,0 +1,10 @@ +@import '../../styles/var.scss'; + +.demo-list { + padding: 0 0 $padding-md 0; + background-color: $gray-1; + min-height: calc(100vh - 56px); + .pant-cell { + text-align: center; + } +} diff --git a/src/list/demo/index.tsx b/src/list/demo/index.tsx new file mode 100644 index 0000000..abe564b --- /dev/null +++ b/src/list/demo/index.tsx @@ -0,0 +1,121 @@ +import * as preact from 'preact'; +import { Cell } from '../../cell'; +import { Tabs, Tab } from '../../tab'; +import { PullRefresh } from '../../pull-refresh'; +import { List, ListLoadResult } from '../../list'; +import { createBEM } from '../../utils/bem'; +import { NavBar } from '../../_site/scripts/components/nav-bar'; +import './index.scss'; + +const bem = createBEM('demo-list'); + +export class ListRouteComponent extends preact.Component { + private list3Ref = preact.createRef(); + + constructor(props: any) { + super(props); + this.state = { + list1: [], + list2: [], + list3: [], + }; + } + + private genList(start: number, end: number): number[] { + const res: number[] = []; + for (let i = start; i < end; i++) { + res.push(i + 1); + } + return res; + } + + render(): preact.JSX.Element { + return ( + + +
+ + + => { + return new Promise(resolve => { + setTimeout(() => { + const list1 = this.state.list1; + this.setState({ list1: list1.concat(this.genList(list1.length, list1.length + 10)) }, () => { + resolve({ + finished: this.state.list1.length >= 40, + }); + }); + }, 2000); + }); + }} + > + {this.state.list1.map(i => ( + + ))} + + + + => { + return new Promise(resolve => { + setTimeout(() => { + const list2 = this.state.list2; + this.setState({ list2: list2.concat(this.genList(list2.length, list2.length + 10)) }, () => { + resolve({ + error: this.state.list2.length === 10, + finished: this.state.list2.length >= 40, + }); + }); + }, 2000); + }); + }} + > + {this.state.list2.map(i => ( + + ))} + + + + => { + return new Promise(resolve => { + setTimeout(() => { + this.setState({ list3: this.genList(0, 10) }, () => { + this.list3Ref.current.check(); + }); + resolve(); + }, 2000); + }); + }} + > + => { + return new Promise(resolve => { + setTimeout(() => { + const list3 = this.state.list3; + this.setState({ list3: list3.concat(this.genList(list3.length, list3.length + 10)) }, () => { + resolve({ + finished: this.state.list3.length >= 40, + }); + }); + }, 2000); + }); + }} + > + {this.state.list3.map(i => ( + + ))} + + + + +
+
+ ); + } +} diff --git a/src/list/index.scss b/src/list/index.scss new file mode 100644 index 0000000..8321181 --- /dev/null +++ b/src/list/index.scss @@ -0,0 +1,17 @@ +@import '../styles/var.scss'; + +.pant-list { + &__loading, + &__finished-text, + &__error-text { + color: $list-text-color; + font-size: $list-text-font-size; + line-height: $list-text-line-height; + text-align: center; + } + + &__placeholder { + height: 0; + pointer-events: none; + } +} diff --git a/src/list/index.tsx b/src/list/index.tsx new file mode 100644 index 0000000..18c6dfa --- /dev/null +++ b/src/list/index.tsx @@ -0,0 +1,175 @@ +import * as preact from 'preact'; +import { on, off } from '../utils/event'; +import { isHidden } from '../utils/dom'; +import { createBEM } from '../utils/bem'; +import { getScroller, ScrollElement } from '../utils/scroll'; +import { Loading } from '../loading'; +import { i18n } from '../locale'; +import './index.scss'; + +type ListDirection = 'up' | 'down'; + +export type ListLoadResult = { + finished?: boolean; + error?: boolean; + errorText?: string; + errorNode?: preact.VNode; +}; + +export type ListProps = { + direction?: ListDirection; + offset?: number | string; + loadingText?: string; + finishedText?: string; + errorText?: string; + loadingNode?: preact.VNode; + finishedNode?: preact.VNode; + errorNode?: preact.VNode; + onLoad(error?: boolean): Promise; +}; + +type ListState = { + loading: boolean; + loadResult: ListLoadResult; +}; + +const bem = createBEM('pant-list'); + +export class List extends preact.Component { + private containerRef = preact.createRef(); + private placeholderRef = preact.createRef(); + private scroller: ScrollElement; + + constructor(props: ListProps) { + super(props); + this.state = { + loading: false, + loadResult: {}, + }; + this.check = this.check.bind(this); + this.onClickError = this.onClickError.bind(this); + } + + componentDidMount(): void { + this.scroller = getScroller(this.containerRef.current); + on(this.scroller, 'scroll', this.check); + this.check(); + } + + componentWillUnmount(): void { + off(this.scroller, 'scroll', this.check); + this.scroller = null; + } + + check(): void { + const { loading, loadResult } = this.state; + if (loading || loadResult.finished || loadResult.error) { + return; + } + + const scroller = this.scroller as any; + const { offset, direction } = this.props; + let scrollerRect; + + if (scroller.getBoundingClientRect) { + scrollerRect = scroller.getBoundingClientRect(); + } else { + scrollerRect = { + top: 0, + bottom: scroller.innerHeight, + }; + } + + const scrollerHeight = scrollerRect.bottom - scrollerRect.top; + + if (!scrollerHeight || isHidden(this.containerRef.current)) { + return; + } + + let isReachEdge = false; + const placeholderRect = this.placeholderRef.current.getBoundingClientRect(); + + if (direction === 'up') { + isReachEdge = scrollerRect.top - placeholderRect.top <= offset; + } else { + isReachEdge = placeholderRect.bottom - scrollerRect.bottom <= offset; + } + + if (isReachEdge) { + this.load(); + } + } + + private load(error?: boolean): void { + this.setState({ loading: true, loadResult: {} }, () => { + this.props.onLoad(error).then(res => { + this.setState({ loading: false, loadResult: res || {} }, () => { + this.check(); + }); + }); + }); + } + + private genLoading(): preact.JSX.Element { + const { loading, loadResult } = this.state; + const { loadingText, loadingNode } = this.props; + if (loading && !loadResult.finished) { + return ( +
+ {loadingNode || {loadingText || i18n().loading}} +
+ ); + } + } + + private genFinishedText(): preact.JSX.Element { + const { loadResult } = this.state; + const { finishedText, finishedNode } = this.props; + if (loadResult.finished) { + const text = finishedNode || finishedText; + + if (text) { + return
{text}
; + } + } + } + + private genErrorText(): preact.JSX.Element { + const { loadResult } = this.state; + const { errorText, errorNode } = this.props; + if (loadResult.error) { + const text = loadResult.errorNode || loadResult.errorText || errorNode || errorText; + + if (text) { + return ( +
+ {text} +
+ ); + } + } + } + + private onClickError(): void { + this.load(true); + } + + render(): preact.JSX.Element { + const { direction, children } = this.props; + const Placeholder =
; + return ( +
+ {direction === 'down' ? children : Placeholder} + {this.genLoading()} + {this.genFinishedText()} + {this.genErrorText()} + {direction === 'up' ? children : Placeholder} +
+ ); + } +} + +List.defaultProps = { + direction: 'down', + offset: 300, +}; diff --git a/src/styles/var.scss b/src/styles/var.scss index 4ab0229..f44a71a 100644 --- a/src/styles/var.scss +++ b/src/styles/var.scss @@ -235,6 +235,12 @@ $info-dot-color: $red; $info-dot-size: 8px; $info-font-family: PingFang SC, Helvetica Neue, Arial, sans-serif; +// List +$list-icon-margin-right: 5px; +$list-text-color: $gray-6; +$list-text-font-size: $font-size-md; +$list-text-line-height: 50px; + // Loading $loading-text-color: $gray-6; $loading-text-font-size: $font-size-md;