rabbit-simple-ui
Version:
A simple UI component library based on JavaScript
330 lines (265 loc) • 10.9 kB
text/typescript
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import {
$el,
getNumTypeAttr,
getStrTypeAttr,
nextAll,
prevAll,
removeAttrs,
setCss,
setHtml
} from '../../dom-utils';
import { warn } from '../../mixins';
import { type, validComps } from '../../utils';
import PREFIX from '../prefix';
interface Config {
config(
el: string
): {
current: number;
title: string;
content: string;
status: string;
itemStatus: string[];
};
}
class Steps implements Config {
readonly VERSION: string;
readonly COMPONENTS: NodeListOf<Element>;
constructor() {
this.VERSION = '1.0';
this.COMPONENTS = $el('r-steps', { all: true });
this._create(this.COMPONENTS);
}
public config(
el: string
): {
current: number;
title: string;
content: string;
status: string;
itemStatus: string[];
} {
const target = $el(el) as HTMLElement;
validComps(target, 'steps');
const { _setCurrentStep, _setStatus, _setStatusIcon } = Steps.prototype;
const _current = target.dataset['current']!;
const StepsTitle = target.querySelector(`.${PREFIX.steps}-title`)!;
const StepsContent = target.querySelector(`.${PREFIX.steps}-content`)!;
const StepsStep = target.querySelectorAll('r-step')!;
const setTitleOrContent = (elem: Element, val: string) => {
if (val && !type.isStr(val)) return;
setHtml(elem, val);
};
return {
get current() {
return Number(target.dataset['current']);
},
set current(newVal: number) {
if (!type.isNum(newVal)) return;
_setCurrentStep(target, newVal, target.dataset['status']!);
},
get title() {
return setHtml(StepsTitle);
},
set title(newVal: string) {
setTitleOrContent(StepsTitle, newVal);
},
get content() {
return setHtml(StepsContent);
},
set content(newVal: string) {
setTitleOrContent(StepsContent, newVal);
},
get status() {
return target.dataset['status']!;
},
set status(newVal: string) {
if (newVal && !type.isStr(newVal)) return;
const currentStep = target.querySelector(`r-step[data-index="${_current}"]`)!;
_setStatus(target, currentStep, newVal);
},
get itemStatus() {
return [];
},
set itemStatus(newVal: string[]) {
if (newVal && !type.isArr(newVal)) return;
const changeStatus = (elem: Element, status: string) => {
elem.setAttribute('status', status);
_setStatusIcon(status, elem);
};
if (newVal.length == 1) {
const step = StepsStep[0];
changeStatus(step, newVal[0]);
return;
}
StepsStep.forEach((step, idx) =>
newVal[idx] ? changeStatus(step, newVal[idx]) : ''
);
}
};
}
private _create(COMPONENTS: NodeListOf<Element>): void {
COMPONENTS.forEach((node) => {
const { current, status, direction } = this._attrs(node);
const StepsStepItem = node.querySelectorAll('r-step');
this._setDirection(node, direction);
this._setStepChildren(StepsStepItem);
this._setCurrentStep(node, current, status);
removeAttrs(node, ['current', 'status']);
});
}
private _setDirection(node: Element, direction: string): void {
node.setAttribute('direction', `${direction}`);
}
private _setStepChildren(stepItem: NodeListOf<Element>): void {
stepItem.forEach((step, idx) => {
// @ts-ignore
step.dataset['index'] = `${idx}`;
this._setStatusFlag(step);
const { icon, title, content } = this._attrs(step);
const stepsText = idx + 1;
const template = `
<div class="${PREFIX.steps}-tail"><i></i></div>
<div class="${PREFIX.steps}-head">
<div class="${PREFIX.steps}-head-inner">
<span id="stepsIcon"></span>
<span id="stepsText">${stepsText}</span>
</div>
</div>
<div class="${PREFIX.steps}-main">
<div class="${PREFIX.steps}-title">${title}</div>
<div class="${PREFIX.steps}-content">${content}</div>
</div>
`;
setHtml(step, template);
this._setCustomIcon(step, icon);
removeAttrs(step, ['title', 'content', 'icon']);
});
}
private _setStatusFlag(step: Element): void {
const status = step.getAttribute('status');
// 如果用户在步骤项设置了status则为该项打上标记,避免被自动设置的默认状态覆盖
if (status) {
// @ts-ignore
step.dataset['specifiesStatus'] = status;
}
}
private _setCurrentStep(node: Element, current: number, status: string): void {
const len = node.childElementCount - 1;
// 防止溢出边界
if (current > len) {
warn(
`The currently active step item you set does not exist in the <r-steps>. --> "${current}"`
);
console.error(node);
current = len;
}
// @ts-ignore
node.dataset['current'] = current;
const { _setStatus } = Steps.prototype;
const currentStep = node.querySelector(`r-step[data-index="${current}"]`)!;
_setStatus(node, currentStep, status);
}
private _setStatus(node: Element, currentStep: Element, status: string): void {
// @ts-ignore
node.dataset['status'] = status;
const { _setStatusIcon, _setPrevAndNextStatus, _setNextError } = Steps.prototype;
// @ts-ignore
const isAutoStatus = currentStep.dataset['autoStatus'];
const selfStatus = currentStep.getAttribute('status');
// 1.如果步骤项设置了status则优先使用该状态,不包括打上autoStatus的标记项。
// 2.如果步骤项父容器指定了某项步骤项为活跃状态,并且指定了 status 则使用该状态。
if (selfStatus && isAutoStatus !== '') {
currentStep.setAttribute('status', selfStatus);
_setStatusIcon(selfStatus, currentStep);
} else {
currentStep.setAttribute('status', status);
_setStatusIcon(status, currentStep);
}
_setPrevAndNextStatus('prev', currentStep, _setStatusIcon);
_setPrevAndNextStatus('next', currentStep, _setStatusIcon);
_setNextError(node);
}
private _setPrevAndNextStatus(
type: 'prev' | 'next',
currentStep: Element,
setStatusIcon: any
): void {
// @ts-ignore
const func = type === 'prev' ? prevAll : nextAll;
const defaultStatus = type === 'prev' ? 'finish' : 'wait';
func(currentStep).forEach((step) => {
// @ts-ignore
const hasSetStatus = step.dataset['specifiesStatus'];
// 当前步骤项位置的其他节点如果没有提示设置status,则默认设置为 finish / wait,并打上标记
// 如果其中有某个设置了则略过
if (!hasSetStatus) {
// @ts-ignore
step.dataset['autoStatus'] = '';
step.setAttribute('status', defaultStatus);
setStatusIcon(defaultStatus, step);
} else {
setStatusIcon(hasSetStatus, step);
}
});
}
private _setStatusIcon(status: string, step: Element): void {
// @ts-ignore
const isUseCustomIcon: boolean = step.dataset['useIcon'] === 'true';
// 如果使用了自定义图标则略过
if (isUseCustomIcon) return;
const StepsIcon = step.querySelector('#stepsIcon')!;
const StepsText = StepsIcon.nextElementSibling!;
// 步骤项状态不为finish或error则显示步骤数字、隐藏图标容器,反之。
if (status !== 'finish' && status !== 'error') {
setCss(StepsIcon, 'display', 'none');
setCss(StepsText, 'display', '');
return;
}
setCss(StepsIcon, 'display', '');
setCss(StepsText, 'display', 'none');
let iconType = '';
if (status === 'finish') {
iconType = 'ios-checkmark';
}
if (status === 'error') {
iconType = 'ios-close';
}
StepsIcon.className = `${PREFIX.steps}-icon ${PREFIX.icon} ${PREFIX.icon}-${iconType}`;
}
private _setCustomIcon(step: Element, icon: string): void {
if (!icon) return;
// @ts-ignore
step.dataset['useIcon'] = 'true';
step.classList.add(`${PREFIX.steps}-custom`);
const StepsIcon = step.querySelector('#stepsIcon')!;
StepsIcon.classList.add(`${PREFIX.icon}`);
StepsIcon.classList.add(`${PREFIX.icon}-${icon}`);
setCss(StepsIcon.nextElementSibling!, 'display', 'none');
}
private _setNextError(node: Element): void {
const StepsStep = node.querySelectorAll('r-step');
StepsStep.forEach((step, idx) => {
if (step.getAttribute('status') === 'error' && idx !== 0) {
const prevStep = StepsStep[idx - 1];
if (prevStep.getAttribute('status') === 'error') {
prevStep.classList.add(`${PREFIX.steps}-next-error`);
} else {
prevStep.classList.remove(`${PREFIX.steps}-next-error`);
}
}
});
}
private _attrs(node: Element) {
return {
current: getNumTypeAttr(node, 'current', 0),
icon: getStrTypeAttr(node, 'icon', ''),
title: getStrTypeAttr(node, 'title', ''),
status: getStrTypeAttr(node, 'status', 'process'),
content: getStrTypeAttr(node, 'content', ''),
direction: getStrTypeAttr(node, 'direction', 'horizontal')
};
}
}
export default Steps;