Skip to content

Commit

Permalink
Merge pull request #64 from duenyang/feature/loading
Browse files Browse the repository at this point in the history
feat(loading): add loading adn portal 🎉
  • Loading branch information
dntzhang authored Jun 27, 2024
2 parents 7d6d58d + e8f1d63 commit 37325f5
Show file tree
Hide file tree
Showing 22 changed files with 963 additions and 0 deletions.
6 changes: 6 additions & 0 deletions site/sidebar.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,12 @@ export default [
path: '/components/tooltip',
component: () => import('tdesign-web-components/tooltip/README.md'),
},
{
title: 'Loading 加载',
name: 'loading',
path: '/components/loading',
component: () => import('tdesign-web-components/loading/README.md'),
},
],
},
{
Expand Down
146 changes: 146 additions & 0 deletions src/_util/dom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import raf from 'raf';

import { AttachNode, AttachNodeReturnValue } from '../common';
import { easeInOutCubic, EasingFunction } from './easing';
// 用于判断是否可使用 dom
export const canUseDocument = !!(typeof window !== 'undefined' && window.document && window.document.createElement);

// 获取 css vars
export const getCssVarsValue = (name: string, element?: HTMLElement) => {
if (!canUseDocument) return;

const el = element || document.documentElement;
return getComputedStyle(el).getPropertyValue(name);
};

function isWindow(obj: any) {
return obj && obj === obj.window;
}

type ScrollTarget = HTMLElement | Window | Document;

export function getScroll(target: ScrollTarget, isLeft?: boolean): number {
// node环境或者target为空
if (typeof window === 'undefined' || !target) {
return 0;
}
const method = isLeft ? 'scrollLeft' : 'scrollTop';
let result = 0;
if (isWindow(target)) {
result = (target as Window)[isLeft ? 'pageXOffset' : 'pageYOffset'];
} else if (target instanceof Document) {
result = target.documentElement[method];
} else if (target) {
result = (target as HTMLElement)[method];
}
return result;
}

interface ScrollTopOptions {
container?: ScrollTarget;
duration?: number;
easing?: EasingFunction;
}

export const scrollTo = (target: number, opt: ScrollTopOptions) => {
const { container = window, duration = 450, easing = easeInOutCubic } = opt;
const scrollTop = getScroll(container);
const startTime = Date.now();
return new Promise((res) => {
const fnc = () => {
const timestamp = Date.now();
const time = timestamp - startTime;
const nextScrollTop = easing(Math.min(time, duration), scrollTop, target, duration);
if (isWindow(container)) {
(container as Window).scrollTo(window.pageXOffset, nextScrollTop);
} else if (container instanceof HTMLDocument || container.constructor.name === 'HTMLDocument') {
(container as HTMLDocument).documentElement.scrollTop = nextScrollTop;
} else {
(container as HTMLElement).scrollTop = nextScrollTop;
}
if (time < duration) {
raf(fnc);
} else {
// 由于上面步骤设置了 scrollTop, 滚动事件可能未触发完毕
// 此时应该在下一帧再执行 res
raf(res);
}
};
raf(fnc);
});
};

export function getAttach(attach: AttachNode, triggerNode?: HTMLElement): AttachNodeReturnValue {
if (!canUseDocument) return null;

let el: AttachNodeReturnValue;
if (typeof attach === 'string') {
el = document.querySelector(attach);
}
if (typeof attach === 'function') {
el = attach(triggerNode);
}
if (typeof attach === 'object') {
if ((attach as any) instanceof window.HTMLElement) {
el = attach;
}
}

// fix el in iframe
if (el && el.nodeType === 1) return el;

return document.body;
}

export const addClass = function (el: Element, cls: string) {
if (!el) return;
let curClass = el.className;
const classes = (cls || '').split(' ');

for (let i = 0, j = classes.length; i < j; i++) {
const clsName = classes[i];
if (!clsName) continue;

if (el.classList) {
el.classList.add(clsName);
} else if (!hasClass(el, clsName)) {
curClass += ` ${clsName}`;
}
}
if (!el.classList) {
// eslint-disable-next-line
el.className = curClass;
}
};

const trim = (str: string): string => (str || '').replace(/^[\s\uFEFF]+|[\s\uFEFF]+$/g, '');

export const removeClass = function (el: Element, cls: string) {
if (!el || !cls) return;
const classes = cls.split(' ');
let curClass = ` ${el.className} `;

for (let i = 0, j = classes.length; i < j; i++) {
const clsName = classes[i];
if (!clsName) continue;

if (el.classList) {
el.classList.remove(clsName);
} else if (hasClass(el, clsName)) {
curClass = curClass.replace(` ${clsName} `, ' ');
}
}
if (!el.classList) {
// eslint-disable-next-line
el.className = trim(curClass);
}
};

export function hasClass(el: Element, cls: string) {
if (!el || !cls) return false;
if (cls.indexOf(' ') !== -1) throw new Error('className should not contain space.');
if (el.classList) {
return el.classList.contains(cls);
}
return ` ${el.className} `.indexOf(` ${cls} `) > -1;
}
42 changes: 42 additions & 0 deletions src/_util/easing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* @file
* 缓动函数
* 参考自: https://github.com/bameyrick/js-easing-functions/blob/master/src/index.ts
*/

export interface EasingFunction {
(current: number, start: number, end: number, duration: number): number;
}

/**
* @export
* @param {number} current 当前时间
* @param {number} start 开始值
* @param {number} end 结束值
* @param {number} duration 持续时间
* @returns
*/
export const linear: EasingFunction = function (current, start, end, duration) {
const change = end - start;
const offset = (change * current) / duration;
return offset + start;
};

/**
* @export
* @param {number} current 当前时间
* @param {number} start 开始值
* @param {number} end 结束值
* @param {number} duration 持续时间
* @returns
*/
export const easeInOutCubic: EasingFunction = function (current, start, end, duration) {
const change = (end - start) / 2;
let time = current / (duration / 2);
if (time < 1) {
return change * time * time * time + start;
}
time -= 2;
// eslint-disable-next-line no-return-assign
return change * (time * time * time + 2) + start;
};
5 changes: 5 additions & 0 deletions src/common/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import _Portal from './portal';

export type { PortalProps } from './portal';

export const Partal = _Portal;
70 changes: 70 additions & 0 deletions src/common/portal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Component, render, tag } from 'omi';

import { getClassPrefix } from '../_util/classname';
import { canUseDocument } from '../_util/dom';
import { AttachNode, AttachNodeReturnValue, TNode } from '../common';

export interface PortalProps {
/**
* 指定挂载的 HTML 节点, false 为挂载在 body
*/
attach?: AttachNode;
/**
* 触发元素
*/
triggerNode?: HTMLElement;
children: TNode;
}

export function getAttach(attach: PortalProps['attach'], triggerNode?: HTMLElement): AttachNodeReturnValue {
if (!canUseDocument) return null;

let el: AttachNodeReturnValue;
if (typeof attach === 'string') {
el = document.querySelector(attach);
}
if (typeof attach === 'function') {
el = attach(triggerNode);
}
if (typeof attach === 'object' && (attach as any) instanceof window.HTMLElement) {
el = attach;
}

// fix el in iframe
if (el && el.nodeType === 1) return el;

return document.body;
}

@tag('t-portal')
export default class Portal extends Component<PortalProps> {
static css = [];

static defaultProps = {
attach: false,
};

parentElement: HTMLElement | null = null;

container: HTMLElement = null;

classPrefix = getClassPrefix();

getContainer() {
if (!canUseDocument) return null;
const el = document.createElement('div');
el.className = `${this.classPrefix}-portal-wrapper`;
return el;
}

install(): void {
this.container = this.getContainer();
this.parentElement = getAttach(this.props.attach, this.props.triggerNode) as HTMLElement;
this.parentElement?.appendChild?.(this.container);
}

render() {
const { children } = this.props;
return render(children, this.container);
}
}
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
export * from './button';
export * from './common';
export * from './divider';
export * from './icon';
export * from './image';
export * from './input';
export * from './loading';
export * from './popup';
export * from './space';
export * from './switch';
Expand Down
89 changes: 89 additions & 0 deletions src/loading/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
---
title: Loading 加载中
description: 在网络较慢或数据较多时,表示数据正在加载的状态。
isComponent: true
usage: { title: '', description: '' }
spline: message
---

### 图标加载

加载过程中只有图标显示。适用于打开页面或操作完成后模块内等待刷新的加载场景。

{{ base }}

### 文字加载

加载过程中有文字显示。适用于打开页面或操作完成后模块内等待刷新的加载场景。

{{ text }}

### 文字和图标共同显示加载

加载过程中有文字和图标共同显示。适用于打开页面或操作完成后页面内等待刷新的加载场景。

{{ icon-text }}

### 不同尺寸的加载
小尺寸适用于组件内加载场景,中尺寸适用于容器如卡片、表格等区域的加载场景,大尺寸适用于页面全屏加载场景。

{{ size }}

### 有包裹的加载
Loading 组件可以作为容器包裹需要显示加载状态的内容。

{{ wrap }}

### 有延时的加载
设置最短延迟响应时间,低于响应时间的操作不显示加载状态。

{{ delay }}

### 全屏加载
全屏展示加载状态,阻止用户操作。

{{ fullscreen }}

### 挂载到指定元素

可通过 `attach` 挂载到指定元素。

注:被挂载元素(loading的父元素)需设置:`position: relative;`

{{ attach }}

### 函数方式调用

{{ service }}


## API
### Loading Props

名称 | 类型 | 默认值 | 说明 | 必传
-- | -- | -- | -- | --
className | String | - | 类名 | N
style | Object | - | 样式,TS 类型:`Styles` | N
attach | String / Function | '' | 挂载元素,默认挂载到组件本身所在的位置。数据类型为 String 时,会被当作选择器处理,进行节点查询。示例:'body' 或 () => document.body。TS 类型:`AttachNode`[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N
children | TNode | - | 子元素,同 content。TS 类型:`string \| TNode`[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N
content | TNode | - | 子元素。TS 类型:`string \| TNode`[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N
delay | Number | 0 | 延迟显示加载效果的时间,用于防止请求速度过快引起的加载闪烁,单位:毫秒 | N
fullscreen | Boolean | false | 是否显示为全屏加载 | N
indicator | TNode | true | 加载指示符,值为 true 显示默认指示符,值为 false 则不显示,也可以自定义指示符。TS 类型:`boolean \| TNode`[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N
inheritColor | Boolean | false | 是否继承父元素颜色 | N
loading | Boolean | true | 是否处于加载状态 | N
preventScrollThrough | Boolean | true | 防止滚动穿透,全屏加载模式有效 | N
showOverlay | Boolean | true | 是否需要遮罩层,遮罩层对包裹元素才有效 | N
size | String | medium | 尺寸,示例:small/medium/large/12px/56px/0.3em | N
text | TNode | - | 加载提示文案。TS 类型:`string \| TNode`[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N
zIndex | Number | - | 消息通知层级,样式默认为 3500 | N

### loading 或 LoadingPlugin

这是一个插件函数,参数形式为顺序参数(形如:(a, b, c)),而非对象参数(形如:`({ a, b, c })`)。顺序参数如下,

参数名称 | 参数类型 | 参数默认值 | 参数说明
-- | -- | -- | --
options | Function | - | 必需。TS 类型:`boolean \| TdLoadingProps`

插件返回值:`LoadingInstance【interface LoadingInstance { hide: () => void }】`
Loading

0 comments on commit 37325f5

Please sign in to comment.