UNPKG

rabbit-simple-ui

Version:

A simple UI component library based on JavaScript

330 lines (265 loc) 10.9 kB
/* 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;