diff --git a/packages/devui-vue/devui/form/index.ts b/packages/devui-vue/devui/form/index.ts index 6da1c16a32..3c3f5d2a1f 100644 --- a/packages/devui-vue/devui/form/index.ts +++ b/packages/devui-vue/devui/form/index.ts @@ -1,31 +1,10 @@ import type { App } from 'vue'; import Form from './src/form'; -import FormLabel from './src/form-label/form-label'; -import FormItem from './src/form-item/form-item'; -import FormControl from './src/form-control/form-control'; -import FormOperation from './src/form-operation/form-operation'; -import dValidateRules from './src/directive/d-validate-rules'; - -Form.install = function(app: App) { - app.component(Form.name, Form); - app.directive('d-validate-rules', dValidateRules); -}; - -FormLabel.install = function(app: App) { - app.component(FormLabel.name, FormLabel); -}; - -FormItem.install = function(app: App) { - app.component(FormItem.name, FormItem); -}; - -FormControl.install = function(app: App) { - app.component(FormControl.name, FormControl); -}; - -FormOperation.install = function(app: App) { - app.component(FormOperation.name, FormOperation); -}; +import FormLabel from './src/components/form-label/form-label'; +import FormItem from './src/components/form-item/form-item'; +import FormControl from './src/components/form-control/form-control'; +import FormOperation from './src/components/form-operation/form-operation'; +import dValidateRules from './src/directives/d-validate-rules'; export { Form, FormLabel, FormItem, FormControl, FormOperation }; @@ -34,10 +13,11 @@ export default { category: '数据录入', status: '75%', install(app: App): void { - app.use(Form as any); - app.use(FormLabel as any); - app.use(FormItem as any); - app.use(FormControl as any); - app.use(FormOperation as any); - } + app.component(Form.name, Form); + app.directive('d-validate-rules', dValidateRules); + app.component(FormLabel.name, FormLabel); + app.component(FormItem.name, FormItem); + app.component(FormControl.name, FormControl); + app.component(FormOperation.name, FormOperation); + }, }; diff --git a/packages/devui-vue/devui/form/src/form-control/form-control.scss b/packages/devui-vue/devui/form/src/components/form-control/form-control.scss similarity index 100% rename from packages/devui-vue/devui/form/src/form-control/form-control.scss rename to packages/devui-vue/devui/form/src/components/form-control/form-control.scss diff --git a/packages/devui-vue/devui/form/src/components/form-control/form-control.tsx b/packages/devui-vue/devui/form/src/components/form-control/form-control.tsx new file mode 100644 index 0000000000..bd41466419 --- /dev/null +++ b/packages/devui-vue/devui/form/src/components/form-control/form-control.tsx @@ -0,0 +1,123 @@ +import { defineComponent, inject, ref, computed, reactive, onMounted, Teleport } from 'vue'; +import { uniqueId } from 'lodash'; +import { IForm, formControlProps, formInjectionKey } from '../../form-types'; +import { ShowPopoverErrorMessageEventData } from '../../directives/d-validate-rules'; +import clickoutsideDirective from '../../../../shared/devui-directive/clickoutside'; +import { EventBus, getElOffset } from '../../utils'; +import Icon from '../../../../icon/src/icon'; +import Popover from '../../../../popover/src/popover'; +import './form-control.scss'; + +type positionType = 'top' | 'right' | 'bottom' | 'left'; + +export default defineComponent({ + name: 'DFormControl', + directives: { + clickoutside: clickoutsideDirective, + }, + props: formControlProps, + setup(props, ctx) { + const formControl = ref(); + const dForm = reactive(inject(formInjectionKey, {} as IForm)); + const labelData = reactive(dForm.labelData); + const isHorizontal = labelData.layout === 'horizontal'; + const uid = uniqueId('dfc-'); + const showPopover = ref(false); + const updateOn = ref('change'); + const tipMessage = ref(''); + const popPosition = ref('bottom'); + let rectInfo: Partial = { + width: 0, + height: 0, + }; + let elOffset = { + left: 0, + top: 0, + }; + let popoverLeftPosition = 0; + let popoverTopPosition = 0; + + onMounted(() => { + const el = document.getElementById(uid); + elOffset = getElOffset(el); + EventBus.on('showPopoverErrorMessage', (data: ShowPopoverErrorMessageEventData) => { + if (uid === data.uid) { + rectInfo = el.getBoundingClientRect(); + showPopover.value = data.showPopover; + tipMessage.value = data.message; + popPosition.value = data.popPosition as any; // todo: 待popover组件positionType完善类型之后再替换类型 + popoverLeftPosition = + popPosition.value === 'top' || popPosition.value === 'bottom' ? rectInfo.right - rectInfo.width / 2 : rectInfo.right; + popoverTopPosition = + popPosition.value === 'top' ? elOffset.top + rectInfo.height / 2 - rectInfo.height : elOffset.top + rectInfo.height / 2; + updateOn.value = data.updateOn ?? 'change'; + } + }); + }); + + const iconData = computed(() => { + switch (props.feedbackStatus) { + case 'pending': + return { name: 'priority', color: '#e9edfa' }; + case 'success': + return { name: 'right-o', color: 'rgb(61, 204, 166)' }; + case 'error': + return { name: 'error-o', color: 'rgb(249, 95, 91)' }; + default: + return { name: '', color: '' }; + } + }); + + const handleClickOutside = () => { + if (updateOn.value !== 'change') { + showPopover.value = false; + } + }; + + return () => { + const { feedbackStatus, extraInfo } = props; + return ( +
+ {showPopover.value && ( + +
+ +
+
+ )} +
+
+ {ctx.slots.default?.()} +
+ {(feedbackStatus || ctx.slots.suffixTemplate?.()) && ( + + {ctx.slots.suffixTemplate?.() ? ( + ctx.slots.suffixTemplate?.() + ) : ( + + )} + + )} +
+ {extraInfo &&
{extraInfo}
} +
+ ); + }; + }, +}); diff --git a/packages/devui-vue/devui/form/src/form-item/form-item.scss b/packages/devui-vue/devui/form/src/components/form-item/form-item.scss similarity index 100% rename from packages/devui-vue/devui/form/src/form-item/form-item.scss rename to packages/devui-vue/devui/form/src/components/form-item/form-item.scss diff --git a/packages/devui-vue/devui/form/src/form-item/form-item.tsx b/packages/devui-vue/devui/form/src/components/form-item/form-item.tsx similarity index 69% rename from packages/devui-vue/devui/form/src/form-item/form-item.tsx rename to packages/devui-vue/devui/form/src/components/form-item/form-item.tsx index 76d2b02a74..52783de339 100644 --- a/packages/devui-vue/devui/form/src/form-item/form-item.tsx +++ b/packages/devui-vue/devui/form/src/components/form-item/form-item.tsx @@ -1,10 +1,9 @@ -import { defineComponent, reactive, inject, onMounted, onBeforeUnmount, provide, ref} from 'vue'; +import { defineComponent, reactive, inject, onMounted, onBeforeUnmount, provide, ref } from 'vue'; import AsyncValidator, { Rules } from 'async-validator'; import mitt from 'mitt'; -import { dFormEvents, dFormItemEvents, IForm, formItemProps, formInjectionKey, formItemInjectionKey } from '../form-types'; +import { dFormEvents, dFormItemEvents, IForm, formItemProps, formInjectionKey, formItemInjectionKey } from '../../form-types'; import './form-item.scss'; - export default defineComponent({ name: 'DFormItem', props: formItemProps, @@ -18,9 +17,9 @@ export default defineComponent({ const rules = reactive(dForm.rules); const resetField = () => { - if(Array.isArray(initFormItemData)) { + if (Array.isArray(initFormItemData)) { formData[props.prop] = [...initFormItemData]; - }else { + } else { formData[props.prop] = initFormItemData; } }; @@ -29,7 +28,7 @@ export default defineComponent({ dHasFeedback: props.dHasFeedback, prop: props.prop, formItemMitt, - resetField + resetField, }); provide(formItemInjectionKey, formItem); @@ -48,39 +47,42 @@ export default defineComponent({ const validator = new AsyncValidator(descriptor); - validator.validate({[ruleKey]: formData[ruleKey]}).then(() => { - showMessage.value = false; - tipMessage.value = ''; - }).catch(({ errors }) => { - showMessage.value = true; - tipMessage.value = errors[0].message; - }); + validator + .validate({ [ruleKey]: formData[ruleKey] }) + .then(() => { + showMessage.value = false; + tipMessage.value = ''; + }) + .catch(({ errors }) => { + showMessage.value = true; + tipMessage.value = errors[0].message; + }); }; const validateEvents = []; const addValidateEvents = () => { - if(rules && rules[props.prop]) { + if (rules && rules[props.prop]) { const ruleItem = rules[props.prop]; let eventName = ruleItem['trigger']; - if(Array.isArray(ruleItem)) { + if (Array.isArray(ruleItem)) { ruleItem.forEach((item) => { eventName = item['trigger']; const cb = () => validate(eventName); - validateEvents.push({eventName: cb}); + validateEvents.push({ eventName: cb }); formItem.formItemMitt.on(dFormItemEvents[eventName], cb); }); - }else { + } else { const cb = () => validate(eventName); - validateEvents.push({eventName: cb}); + validateEvents.push({ eventName: cb }); ruleItem && formItem.formItemMitt.on(dFormItemEvents[eventName], cb); } } }; const removeValidateEvents = () => { - if(rules && rules[props.prop] && validateEvents.length > 0) { - validateEvents.forEach(item => { + if (rules && rules[props.prop] && validateEvents.length > 0) { + validateEvents.forEach((item) => { formItem.formItemMitt.off(item.eventName, item.cb); }); } @@ -97,9 +99,14 @@ export default defineComponent({ }); return () => { return ( -
+
{ctx.slots.default?.()} -
{showMessage.value && tipMessage.value}
+
+ {showMessage.value && tipMessage.value} +
); }; diff --git a/packages/devui-vue/devui/form/src/form-label/form-label.scss b/packages/devui-vue/devui/form/src/components/form-label/form-label.scss similarity index 100% rename from packages/devui-vue/devui/form/src/form-label/form-label.scss rename to packages/devui-vue/devui/form/src/components/form-label/form-label.scss diff --git a/packages/devui-vue/devui/form/src/components/form-label/form-label.tsx b/packages/devui-vue/devui/form/src/components/form-label/form-label.tsx new file mode 100644 index 0000000000..c20b88e35b --- /dev/null +++ b/packages/devui-vue/devui/form/src/components/form-label/form-label.tsx @@ -0,0 +1,50 @@ +import { defineComponent, inject, reactive, computed } from 'vue'; +import { IForm, formLabelProps, FormLabelProps, formInjectionKey } from '../../form-types'; +import Icon from '../../../../icon/src/icon'; +import Popover from '../../../../popover/src/popover'; +import './form-label.scss'; + +export default defineComponent({ + name: 'DFormLabel', + props: formLabelProps, + setup(props: FormLabelProps, ctx) { + const dForm = reactive(inject(formInjectionKey, {} as IForm)); + const labelData = reactive(dForm.labelData); + + const isHorizontal = computed(() => labelData.layout === 'horizontal').value; + const isLg = computed(() => labelData.labelSize === 'lg').value; + const isSm = computed(() => labelData.labelSize === 'sm').value; + const isCenter = computed(() => labelData.labelAlign === 'center').value; + const isEnd = computed(() => labelData.labelAlign === 'end').value; + + const wrapperCls = `devui-form-label${ + isHorizontal ? (isSm ? ' devui-form-label_sm' : isLg ? ' devui-form-label_lg' : ' devui-form-label_sd') : '' + }${isCenter ? ' devui-form-label_center' : isEnd ? ' devui-form-label_end' : ''}`; + const className = `${props.required ? ' devui-required' : ''}`; + const style = { display: isHorizontal ? 'inline' : 'inline-block' }; + + return () => { + return ( + + + {ctx.slots.default?.()} + {props.hasHelp && props.helpTips && ( + ( + + + + ), + }}> + )} + + + ); + }; + }, +}); diff --git a/packages/devui-vue/devui/form/src/form-operation/form-operation.scss b/packages/devui-vue/devui/form/src/components/form-operation/form-operation.scss similarity index 100% rename from packages/devui-vue/devui/form/src/form-operation/form-operation.scss rename to packages/devui-vue/devui/form/src/components/form-operation/form-operation.scss diff --git a/packages/devui-vue/devui/form/src/form-operation/form-operation.tsx b/packages/devui-vue/devui/form/src/components/form-operation/form-operation.tsx similarity index 100% rename from packages/devui-vue/devui/form/src/form-operation/form-operation.tsx rename to packages/devui-vue/devui/form/src/components/form-operation/form-operation.tsx diff --git a/packages/devui-vue/devui/form/src/use-validate.ts b/packages/devui-vue/devui/form/src/composables/use-validate.ts similarity index 100% rename from packages/devui-vue/devui/form/src/use-validate.ts rename to packages/devui-vue/devui/form/src/composables/use-validate.ts diff --git a/packages/devui-vue/devui/form/src/directive/d-validate-rules.ts b/packages/devui-vue/devui/form/src/directive/d-validate-rules.ts deleted file mode 100644 index b1099ad447..0000000000 --- a/packages/devui-vue/devui/form/src/directive/d-validate-rules.ts +++ /dev/null @@ -1,445 +0,0 @@ -import AsyncValidator, { RuleItem } from 'async-validator'; -import { VNode, DirectiveBinding } from 'vue'; -import { debounce } from 'lodash'; -import { EventBus, isObject, hasKey } from '../util'; -import './style.scss'; - -interface ValidateFnParam { - validator: AsyncValidator; - modelValue: Record; - el: HTMLElement; - tipEl: HTMLElement; - isFormTag: boolean; - message: string; - messageShowType: MessageShowType; - dfcUID: string; - popPosition: PopPosition | Array; - updateOn?: UpdateOn; -} - -interface CustomValidatorRuleObject { - message: string; - validator: (rule, value) => boolean; - asyncValidator: (rule, value) => Promise; -} - -interface DirectiveValidateRuleOptions { - updateOn?: UpdateOn; - errorStrategy?: ErrorStrategy; - asyncDebounceTime?: number; - popPosition?: PopPosition | Array; -} - -interface DirectiveBindingValue { - rules: Partial[]; - options: DirectiveValidateRuleOptions; - messageShowType: MessageShowType; - errorStrategy: ErrorStrategy; -} - -interface DirectiveCustomRuleItem extends RuleItem { - validators: CustomValidatorRuleObject[]; - asyncValidators: CustomValidatorRuleObject[]; -} - -export interface ShowPopoverErrorMessageEventData { - showPopover?: boolean; - message?: string; - uid?: string; - popPosition?: PopPosition; - [prop: string]: any; -} - -type MessageShowType = 'popover' | 'text' | 'none' | 'toast'; -type UpdateOn = 'input' | 'focus' | 'change' | 'blur' | 'submit'; -type ErrorStrategy = 'dirty' | 'pristine'; -type BasePopPosition = 'left' | 'right' | 'top' | 'bottom'; -type PopPosition = BasePopPosition | 'left-top' | 'left-bottom' | 'top-left' | 'top-right' | 'right-top' | 'right-bottom' | 'bottom-left' | 'bottom-right'; - -enum ErrorStrategyEnum { - dirty = 'dirty', - pristine = 'pristine' -} - -enum UpdateOnEnum { - input = 'input', - focus = 'focus', - change = 'change', - blur = 'blur', - submit = 'submit', -} - -enum MessageShowTypeEnum { - popover = 'popover', - text = 'text', - none = 'none', - toast = 'toast' -} - -// 获取async-validator可用的规则名 -function getAvaliableRuleObj(ruleName: string, value: any) { - if(!ruleName) { - console.error("[v-d-validate] validator's key is invalid"); - return null; - } - switch(ruleName) { - case 'maxlength': - return { - type: 'string', - max: value, - asyncValidator: (rule, val) => { - return new Promise((resolve, reject) => { - if(val.length > value) { - reject('最大长度为' + value); - }else { - resolve('校验通过'); - } - }); - } - }; - case 'minlength': - return { - type: 'string', - min: value, - asyncValidator: (rule, val) => { - return new Promise((resolve, reject) => { - if(val.length < value) { - reject('最小长度为' + value); - }else { - resolve('校验通过'); - } - }); - } - }; - case 'min': - return { - type: 'number', - asyncValidator: (rule, val) => { - return new Promise((resolve, reject) => { - if(val < value) { - reject('最小值为' + value); - }else { - resolve('校验通过'); - } - }); - } - }; - case 'max': - return { - type: 'number', - asyncValidator: (rule, val) => { - return new Promise((resolve, reject) => { - if(val > value) { - reject('最大值为' + value); - }else { - resolve('校验通过'); - } - }); - } - }; - case 'required': - return { - reqiured: true, - asyncValidator: (rule, val) => { - return new Promise((resolve, reject) => { - if(!val) { - reject('必填项'); - }else { - resolve('校验通过'); - } - }); - } - }; - case 'requiredTrue': - return { - asyncValidator: (rule, val) => { - return new Promise((resolve, reject) => { - if(!val) { - reject('必须为true值'); - }else { - resolve('校验通过'); - } - }); - } - }; - case 'email': - return { - type: 'email', - message: '邮箱格式不正确' - }; - case 'pattern': - return { - type: 'regexp', - pattern: value, - message: '只能包含数字与大小写字符', - validator: (rule, val) => value.test(val), - }; - case 'whitespace': - return { - message: '输入不能全部为空格或空字符', - validator: (rule, val) => !!val.trim() - }; - default: - return { - [ruleName]: value, - }; - } -} - -function getKeyValueOfObjectList(obj): {key: string; value: any}[] { - const kvArr = []; - for (const key in obj) { - if (Object.prototype.hasOwnProperty.call(obj, key)) { - kvArr.push({ - key, - value: obj[key] - }); - } - } - return kvArr; -} - -function handleErrorStrategy(el: HTMLElement): void { - const classList: Array = [...el.classList]; - classList.push('devui-validate-rules-error-pristine'); - el.setAttribute('class', classList.join(' ')); -} - -function handleErrorStrategyPass(el: HTMLElement): void { - const classList: Array = [...el.classList]; - const index = classList.indexOf('devui-validate-rules-error-pristine'); - index !== -1 && classList.splice(index, 1); - el.setAttribute('class', classList.join(' ')); -} - -function getFormControlUID(el: HTMLElement): string { - if(el.tagName.toLocaleLowerCase() === 'body') {return '';} - let uid = ''; - if(el.parentElement.id.startsWith('dfc-')) { - return el.parentElement.id; - }else { - uid = getFormControlUID(el.parentElement); - } -} - -function handleValidateError({el, tipEl, message = '', isFormTag, messageShowType, dfcUID, popPosition = 'right-bottom', updateOn}: Partial): void { - // 如果该指令用在form标签上,这里做特殊处理 - if(isFormTag && messageShowType === MessageShowTypeEnum.toast) { - // todo:待替换为toast - alert(message); - return; - } - - if(!dfcUID) { - dfcUID = getFormControlUID(el); - } - - // messageShowType为popover时,设置popover - if(MessageShowTypeEnum.popover === messageShowType) { - EventBus.emit('showPopoverErrorMessage', {showPopover: true, message, uid: dfcUID, popPosition, updateOn} as ShowPopoverErrorMessageEventData); - return; - } - - tipEl.innerText = '' + message; - tipEl.style.display = 'inline-flex'; - tipEl.setAttribute('class', 'devui-validate-tip'); - handleErrorStrategy(el); -} - -function handleValidatePass(el: HTMLElement, tipEl: HTMLElement): void { - tipEl.style.display = 'none'; - handleErrorStrategyPass(el); -} - -// 获取ref的name -function getRefName(binding: DirectiveBinding): string { - const _refs = binding.instance.$refs; - const refName = Object.keys(_refs)[0]; - return refName; -} - -// 获取表单name -function getFormName(binding: DirectiveBinding): string { - const _refs = binding.instance.$refs; - const key = Object.keys(_refs)[0]; - return _refs[key]['name']; -} - -// 校验处理函数 -function validateFn({validator, modelValue, el, tipEl, isFormTag, messageShowType, dfcUID, popPosition, updateOn}: Partial) { - validator.validate({modelName: modelValue}).then(() => { - handleValidatePass(el, tipEl); - }).catch((err) => { - const { errors } = err; - if(!errors || errors.length === 0) {return;} - let msg = ''; - - // todo: 待支持国际化 - if(typeof errors[0].message === 'object') { - msg = errors[0].message.default; - }else { - msg = errors[0].message; - } - - handleValidateError({el, tipEl, message: msg, isFormTag, messageShowType, dfcUID, popPosition, updateOn}); - }); -} - -// 检测popover的position是否是正确值 -function checkValidPopsition(positionStr: string): boolean { - const validPosition = ['left', 'right', 'top', 'bottom', 'left-top', 'left-bottom', 'top-left', 'top-right', 'right-top', 'right-bottom', 'bottom-left', 'bottom-right']; - const isValid = validPosition.includes(positionStr); - !isValid && console.warn(`invalid popPosition value '${positionStr}'.`); - return isValid; -} - -export default { - mounted(el: HTMLElement, binding: DirectiveBinding, vnode: VNode): void { - const isFormTag = el.tagName === 'FORM'; - const dfcUID = el.parentNode.parentNode.parentElement.dataset.uid; - const refName = getRefName(binding); - - const hasOptions = isObject(binding.value) && hasKey(binding.value, 'options'); - - // 获取指令绑定的值 - let { - rules: bindingRules, - options = {}, - messageShowType = MessageShowTypeEnum.popover - }: DirectiveBindingValue = binding.value; - let { errorStrategy }: DirectiveBindingValue = binding.value; - - if(refName) { - // 判断d-form是否传递了messageShowType属性 - messageShowType = binding.instance[refName]['messageShowType'] ?? 'popover'; - } - - // errorStrategy可配置在options对象中 - let { - updateOn = UpdateOnEnum.change, - errorStrategy: ErrorStrategy = ErrorStrategyEnum.dirty, - asyncDebounceTime = 300, - popPosition = ['right', 'bottom'] - }: DirectiveValidateRuleOptions = options; - - // 设置popover的位置 - if(messageShowType === MessageShowTypeEnum.popover) { - if(Array.isArray(popPosition)) { - popPosition = (popPosition.length > 1 ? popPosition.join('-') : popPosition[0]) as PopPosition; - if(!checkValidPopsition(popPosition)) { - popPosition = 'right-bottom'; - } - }else if(!checkValidPopsition(popPosition)) { - popPosition = 'right-bottom'; - } - } - - if(!errorStrategy) { - errorStrategy = ErrorStrategy; - } - - // 判断是否有options,有就取binding.value对象中的rules对象,再判断有没有rules对象,没有就取binding.value - let customRule: Partial | DirectiveBindingValue = {}; - if(hasOptions) { - customRule = bindingRules ?? binding.value; - }else { - customRule = binding.value as DirectiveBindingValue; - } - - const isCustomValidator = customRule && isObject(customRule) && (hasKey(customRule, 'validators') || hasKey(customRule, 'asyncValidators')); - - const rules = Array.isArray(customRule) ? customRule : [customRule]; - const tipEl = document.createElement('span'); - - // messageShowType控制是否显示文字提示 - if(messageShowType !== MessageShowTypeEnum.none) { - el.parentNode.append(tipEl); - } - - const descriptor = { - modelName: [] - }; - - rules.forEach((rule) => { - const kvObjList = !Array.isArray(rule) && getKeyValueOfObjectList(rule); - let ruleObj: Partial = {}; - let avaliableRuleObj = {}; - kvObjList.forEach(item => { - avaliableRuleObj = getAvaliableRuleObj(item.key, item.value); - ruleObj = {...ruleObj, ...avaliableRuleObj}; - }); - descriptor.modelName.push(ruleObj); - }); - - // 使用自定义的验证器 - if(isCustomValidator) { - // descriptor.modelName = []; - const {validators, asyncValidators} = customRule as DirectiveCustomRuleItem; - - // 校验器 - validators && validators.forEach(item => { - const ruleObj: Partial = { - message: item?.message || '', - validator: (rule, value) => item.validator(rule, value), - }; - descriptor.modelName.push(ruleObj); - }); - - // 异步校验器 - asyncValidators && asyncValidators.forEach(item => { - const ruleObj: Partial = { - message: item?.message || '', - asyncValidator: (rule, value) => { - return new Promise(debounce((resolve, reject) => { - const res = item.asyncValidator(rule, value); - if(res) { - resolve(''); - }else { - reject(rule.message); - } - }, asyncDebounceTime)); - }, - }; - descriptor.modelName.push(ruleObj); - }); - } - - // 校验器对象 - const validator = new AsyncValidator(descriptor); - - const htmlEventValidateHandler = (e) => { - const modelValue = e.target.value; - if(messageShowType === MessageShowTypeEnum.popover) { - EventBus.emit('showPopoverErrorMessage', {showPopover: false, message: '', uid: dfcUID, popPosition, updateOn} as ShowPopoverErrorMessageEventData); - } - validateFn({validator, modelValue, el, tipEl, isFormTag: false, messageShowType, dfcUID, popPosition, updateOn}); - }; - - // 监听事件验证 - vnode.children[0].el.addEventListener(updateOn, htmlEventValidateHandler); - - // 如果校验时机为change,则在focus时关闭popover - if(messageShowType === MessageShowTypeEnum.popover && updateOn === UpdateOnEnum.change) { - vnode.children[0].el.addEventListener('focus', () => { - EventBus.emit('showPopoverErrorMessage', {showPopover: false, uid: dfcUID, updateOn} as ShowPopoverErrorMessageEventData); - }); - } - - // 设置errorStrategy - if(errorStrategy === ErrorStrategyEnum.pristine) { - handleErrorStrategy(el); - // pristine为初始化验证,初始化时需改变下原始值才能出发验证 - vnode.children[0].props.value = '' + vnode.children[0].props.value; - } - - const formName = getFormName(binding); - // 处理表单提交验证 - formName && EventBus.on(`formSubmit:${formName}`, () => { - const modelValue = isFormTag ? '' : vnode.children[0].el.value; - - // 进行提交验证 - validateFn({validator, modelValue, el, tipEl, isFormTag, messageShowType, updateOn: 'submit'}); - }); - - } -}; diff --git a/packages/devui-vue/devui/form/src/directives/d-validate-rules.ts b/packages/devui-vue/devui/form/src/directives/d-validate-rules.ts new file mode 100644 index 0000000000..3143ff194f --- /dev/null +++ b/packages/devui-vue/devui/form/src/directives/d-validate-rules.ts @@ -0,0 +1,506 @@ +import AsyncValidator, { RuleItem } from 'async-validator'; +import { VNode, DirectiveBinding } from 'vue'; +import { debounce } from 'lodash'; +import { EventBus, isObject, hasKey } from '../utils'; +import './style.scss'; + +interface ValidateFnParam { + validator: AsyncValidator; + modelValue: Record; + el: HTMLElement; + tipEl: HTMLElement; + isFormTag: boolean; + message: string; + messageShowType: MessageShowType; + dfcUID: string; + popPosition: PopPosition | Array; + updateOn?: UpdateOn; +} + +interface CustomValidatorRuleObject { + message: string; + validator: (rule, value) => boolean; + asyncValidator: (rule, value) => Promise; +} + +interface DirectiveValidateRuleOptions { + updateOn?: UpdateOn; + errorStrategy?: ErrorStrategy; + asyncDebounceTime?: number; + popPosition?: PopPosition | Array; +} + +interface DirectiveBindingValue { + rules: Partial[]; + options: DirectiveValidateRuleOptions; + messageShowType: MessageShowType; + errorStrategy: ErrorStrategy; +} + +interface DirectiveCustomRuleItem extends RuleItem { + validators: CustomValidatorRuleObject[]; + asyncValidators: CustomValidatorRuleObject[]; +} + +export interface ShowPopoverErrorMessageEventData { + showPopover?: boolean; + message?: string; + uid?: string; + popPosition?: PopPosition; + [prop: string]: any; +} + +type MessageShowType = 'popover' | 'text' | 'none' | 'toast'; +type UpdateOn = 'input' | 'focus' | 'change' | 'blur' | 'submit'; +type ErrorStrategy = 'dirty' | 'pristine'; +type BasePopPosition = 'left' | 'right' | 'top' | 'bottom'; +type PopPosition = + | BasePopPosition + | 'left-top' + | 'left-bottom' + | 'top-left' + | 'top-right' + | 'right-top' + | 'right-bottom' + | 'bottom-left' + | 'bottom-right'; + +enum ErrorStrategyEnum { + dirty = 'dirty', + pristine = 'pristine', +} + +enum UpdateOnEnum { + input = 'input', + focus = 'focus', + change = 'change', + blur = 'blur', + submit = 'submit', +} + +enum MessageShowTypeEnum { + popover = 'popover', + text = 'text', + none = 'none', + toast = 'toast', +} + +// 获取async-validator可用的规则名 +function getAvaliableRuleObj(ruleName: string, value: any) { + if (!ruleName) { + console.error("[v-d-validate] validator's key is invalid"); + return null; + } + switch (ruleName) { + case 'maxlength': + return { + type: 'string', + max: value, + asyncValidator: (rule, val) => { + return new Promise((resolve, reject) => { + if (val.length > value) { + reject('最大长度为' + value); + } else { + resolve('校验通过'); + } + }); + }, + }; + case 'minlength': + return { + type: 'string', + min: value, + asyncValidator: (rule, val) => { + return new Promise((resolve, reject) => { + if (val.length < value) { + reject('最小长度为' + value); + } else { + resolve('校验通过'); + } + }); + }, + }; + case 'min': + return { + type: 'number', + asyncValidator: (rule, val) => { + return new Promise((resolve, reject) => { + if (val < value) { + reject('最小值为' + value); + } else { + resolve('校验通过'); + } + }); + }, + }; + case 'max': + return { + type: 'number', + asyncValidator: (rule, val) => { + return new Promise((resolve, reject) => { + if (val > value) { + reject('最大值为' + value); + } else { + resolve('校验通过'); + } + }); + }, + }; + case 'required': + return { + reqiured: true, + asyncValidator: (rule, val) => { + return new Promise((resolve, reject) => { + if (!val) { + reject('必填项'); + } else { + resolve('校验通过'); + } + }); + }, + }; + case 'requiredTrue': + return { + asyncValidator: (rule, val) => { + return new Promise((resolve, reject) => { + if (!val) { + reject('必须为true值'); + } else { + resolve('校验通过'); + } + }); + }, + }; + case 'email': + return { + type: 'email', + message: '邮箱格式不正确', + }; + case 'pattern': + return { + type: 'regexp', + pattern: value, + message: '只能包含数字与大小写字符', + validator: (rule, val) => value.test(val), + }; + case 'whitespace': + return { + message: '输入不能全部为空格或空字符', + validator: (rule, val) => !!val.trim(), + }; + default: + return { + [ruleName]: value, + }; + } +} + +function getKeyValueOfObjectList(obj): { key: string; value: any }[] { + const kvArr = []; + for (const key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + kvArr.push({ + key, + value: obj[key], + }); + } + } + return kvArr; +} + +function handleErrorStrategy(el: HTMLElement): void { + const classList: Array = [...el.classList]; + classList.push('devui-validate-rules-error-pristine'); + el.setAttribute('class', classList.join(' ')); +} + +function handleErrorStrategyPass(el: HTMLElement): void { + const classList: Array = [...el.classList]; + const index = classList.indexOf('devui-validate-rules-error-pristine'); + index !== -1 && classList.splice(index, 1); + el.setAttribute('class', classList.join(' ')); +} + +function getFormControlUID(el: HTMLElement): string { + if (el.tagName.toLocaleLowerCase() === 'body') { + return ''; + } + let uid = ''; + if (el.parentElement.id.startsWith('dfc-')) { + return el.parentElement.id; + } else { + uid = getFormControlUID(el.parentElement); + } +} + +function handleValidateError({ + el, + tipEl, + message = '', + isFormTag, + messageShowType, + dfcUID, + popPosition = 'right-bottom', + updateOn, +}: Partial): void { + // 如果该指令用在form标签上,这里做特殊处理 + if (isFormTag && messageShowType === MessageShowTypeEnum.toast) { + // todo:待替换为toast + alert(message); + return; + } + + if (!dfcUID) { + dfcUID = getFormControlUID(el); + } + + // messageShowType为popover时,设置popover + if (MessageShowTypeEnum.popover === messageShowType) { + EventBus.emit('showPopoverErrorMessage', { + showPopover: true, + message, + uid: dfcUID, + popPosition, + updateOn, + } as ShowPopoverErrorMessageEventData); + return; + } + + tipEl.innerText = '' + message; + tipEl.style.display = 'inline-flex'; + tipEl.setAttribute('class', 'devui-validate-tip'); + handleErrorStrategy(el); +} + +function handleValidatePass(el: HTMLElement, tipEl: HTMLElement): void { + tipEl.style.display = 'none'; + handleErrorStrategyPass(el); +} + +// 获取ref的name +function getRefName(binding: DirectiveBinding): string { + const _refs = binding.instance.$refs; + const refName = Object.keys(_refs)[0]; + return refName; +} + +// 获取表单name +function getFormName(binding: DirectiveBinding): string { + const _refs = binding.instance.$refs; + const key = Object.keys(_refs)[0]; + return _refs[key]['name']; +} + +// 校验处理函数 +function validateFn({ + validator, + modelValue, + el, + tipEl, + isFormTag, + messageShowType, + dfcUID, + popPosition, + updateOn, +}: Partial) { + validator + .validate({ modelName: modelValue }) + .then(() => { + handleValidatePass(el, tipEl); + }) + .catch((err) => { + const { errors } = err; + if (!errors || errors.length === 0) { + return; + } + let msg = ''; + + // todo: 待支持国际化 + if (typeof errors[0].message === 'object') { + msg = errors[0].message.default; + } else { + msg = errors[0].message; + } + + handleValidateError({ el, tipEl, message: msg, isFormTag, messageShowType, dfcUID, popPosition, updateOn }); + }); +} + +// 检测popover的position是否是正确值 +function checkValidPopsition(positionStr: string): boolean { + const validPosition = [ + 'left', + 'right', + 'top', + 'bottom', + 'left-top', + 'left-bottom', + 'top-left', + 'top-right', + 'right-top', + 'right-bottom', + 'bottom-left', + 'bottom-right', + ]; + const isValid = validPosition.includes(positionStr); + !isValid && console.warn(`invalid popPosition value '${positionStr}'.`); + return isValid; +} + +export default { + mounted(el: HTMLElement, binding: DirectiveBinding, vnode: VNode): void { + const isFormTag = el.tagName === 'FORM'; + const dfcUID = el.parentNode.parentNode.parentElement.dataset.uid; + const refName = getRefName(binding); + + const hasOptions = isObject(binding.value) && hasKey(binding.value, 'options'); + + // 获取指令绑定的值 + let { rules: bindingRules, options = {}, messageShowType = MessageShowTypeEnum.popover }: DirectiveBindingValue = binding.value; + let { errorStrategy }: DirectiveBindingValue = binding.value; + + if (refName) { + // 判断d-form是否传递了messageShowType属性 + messageShowType = binding.instance[refName]['messageShowType'] ?? 'popover'; + } + + // errorStrategy可配置在options对象中 + let { + updateOn = UpdateOnEnum.change, + errorStrategy: ErrorStrategy = ErrorStrategyEnum.dirty, + asyncDebounceTime = 300, + popPosition = ['right', 'bottom'], + }: DirectiveValidateRuleOptions = options; + + // 设置popover的位置 + if (messageShowType === MessageShowTypeEnum.popover) { + if (Array.isArray(popPosition)) { + popPosition = (popPosition.length > 1 ? popPosition.join('-') : popPosition[0]) as PopPosition; + if (!checkValidPopsition(popPosition)) { + popPosition = 'right-bottom'; + } + } else if (!checkValidPopsition(popPosition)) { + popPosition = 'right-bottom'; + } + } + + if (!errorStrategy) { + errorStrategy = ErrorStrategy; + } + + // 判断是否有options,有就取binding.value对象中的rules对象,再判断有没有rules对象,没有就取binding.value + let customRule: Partial | DirectiveBindingValue = {}; + if (hasOptions) { + customRule = bindingRules ?? binding.value; + } else { + customRule = binding.value as DirectiveBindingValue; + } + + const isCustomValidator = + customRule && isObject(customRule) && (hasKey(customRule, 'validators') || hasKey(customRule, 'asyncValidators')); + + const rules = Array.isArray(customRule) ? customRule : [customRule]; + const tipEl = document.createElement('span'); + + // messageShowType控制是否显示文字提示 + if (messageShowType !== MessageShowTypeEnum.none) { + el.parentNode.append(tipEl); + } + + const descriptor = { + modelName: [], + }; + + rules.forEach((rule) => { + const kvObjList = !Array.isArray(rule) && getKeyValueOfObjectList(rule); + let ruleObj: Partial = {}; + let avaliableRuleObj = {}; + kvObjList.forEach((item) => { + avaliableRuleObj = getAvaliableRuleObj(item.key, item.value); + ruleObj = { ...ruleObj, ...avaliableRuleObj }; + }); + descriptor.modelName.push(ruleObj); + }); + + // 使用自定义的验证器 + if (isCustomValidator) { + // descriptor.modelName = []; + const { validators, asyncValidators } = customRule as DirectiveCustomRuleItem; + + // 校验器 + validators && + validators.forEach((item) => { + const ruleObj: Partial = { + message: item?.message || '', + validator: (rule, value) => item.validator(rule, value), + }; + descriptor.modelName.push(ruleObj); + }); + + // 异步校验器 + asyncValidators && + asyncValidators.forEach((item) => { + const ruleObj: Partial = { + message: item?.message || '', + asyncValidator: (rule, value) => { + return new Promise( + debounce((resolve, reject) => { + const res = item.asyncValidator(rule, value); + if (res) { + resolve(''); + } else { + reject(rule.message); + } + }, asyncDebounceTime) + ); + }, + }; + descriptor.modelName.push(ruleObj); + }); + } + + // 校验器对象 + const validator = new AsyncValidator(descriptor); + + const htmlEventValidateHandler = (e) => { + const modelValue = e.target.value; + if (messageShowType === MessageShowTypeEnum.popover) { + EventBus.emit('showPopoverErrorMessage', { + showPopover: false, + message: '', + uid: dfcUID, + popPosition, + updateOn, + } as ShowPopoverErrorMessageEventData); + } + validateFn({ validator, modelValue, el, tipEl, isFormTag: false, messageShowType, dfcUID, popPosition, updateOn }); + }; + + // 监听事件验证 + vnode.children[0].el.addEventListener(updateOn, htmlEventValidateHandler); + + // 如果校验时机为change,则在focus时关闭popover + if (messageShowType === MessageShowTypeEnum.popover && updateOn === UpdateOnEnum.change) { + vnode.children[0].el.addEventListener('focus', () => { + EventBus.emit('showPopoverErrorMessage', { showPopover: false, uid: dfcUID, updateOn } as ShowPopoverErrorMessageEventData); + }); + } + + // 设置errorStrategy + if (errorStrategy === ErrorStrategyEnum.pristine) { + handleErrorStrategy(el); + // pristine为初始化验证,初始化时需改变下原始值才能出发验证 + vnode.children[0].props.value = '' + vnode.children[0].props.value; + } + + const formName = getFormName(binding); + // 处理表单提交验证 + formName && + EventBus.on(`formSubmit:${formName}`, () => { + const modelValue = isFormTag ? '' : vnode.children[0].el.value; + + // 进行提交验证 + validateFn({ validator, modelValue, el, tipEl, isFormTag, messageShowType, updateOn: 'submit' }); + }); + }, +}; diff --git a/packages/devui-vue/devui/form/src/directive/d-validate.ts b/packages/devui-vue/devui/form/src/directives/d-validate.ts similarity index 61% rename from packages/devui-vue/devui/form/src/directive/d-validate.ts rename to packages/devui-vue/devui/form/src/directives/d-validate.ts index 34c66c8d0e..ff957611ef 100644 --- a/packages/devui-vue/devui/form/src/directive/d-validate.ts +++ b/packages/devui-vue/devui/form/src/directives/d-validate.ts @@ -1,9 +1,9 @@ import { VNode, DirectiveBinding, h, render, nextTick } from 'vue'; import { debounce } from 'lodash-es'; -import { EventBus, transformCamelToDash } from '../util'; -import useValidate from '../use-validate'; +import { EventBus, transformCamelToDash } from '../utils'; +import useValidate from '../composables/use-validate'; import dPopover from '../../../popover/src/popover'; -import {DFormValidateSubmitData, positionType} from '../form-types'; +import { DFormValidateSubmitData, positionType } from '../form-types'; import './style.scss'; interface BindingValueRules { @@ -26,10 +26,12 @@ interface BindingValue { } const getTargetElement = (el: HTMLElement, targetTag: string) => { - if (!el) {return;} + if (!el) { + return; + } let tempEl: HTMLElement = el; - while(tempEl?.tagName && tempEl.tagName.toLocaleLowerCase() !== 'body') { - if(tempEl.tagName.toLocaleLowerCase() === targetTag) { + while (tempEl?.tagName && tempEl.tagName.toLocaleLowerCase() !== 'body') { + if (tempEl.tagName.toLocaleLowerCase() === targetTag) { return tempEl; } tempEl = tempEl.parentElement; @@ -38,11 +40,22 @@ const getTargetElement = (el: HTMLElement, targetTag: string) => { export default { mounted(el: HTMLElement, binding: DirectiveBinding): void { - let { prop, rules, validators, asyncValidators, errorStrategy, updateOn = 'input', asyncDebounceTime = 300, messageShowType = 'popover', messageChange, popPosition = ['right', 'bottom'] }: BindingValue = binding.value; - const {instance, arg: modelName} = binding; + let { + prop, + rules, + validators, + asyncValidators, + errorStrategy, + updateOn = 'input', + asyncDebounceTime = 300, + messageShowType = 'popover', + messageChange, + popPosition = ['right', 'bottom'], + }: BindingValue = binding.value; + const { instance, arg: modelName } = binding; const instanceRef = instance[Object.keys(instance.$refs)[0]]; - if(instanceRef && instanceRef?.messageShowType) { + if (instanceRef && instanceRef?.messageShowType) { messageShowType = instanceRef.messageShowType; } const hasModelName = !!modelName; @@ -56,7 +69,9 @@ export default { }; const renderPopover = (msg, visible = true) => { - if(messageShowType !== 'popover') {return;} + if (messageShowType !== 'popover') { + return; + } el.style.position = 'relative'; const popoverPosition = () => { return Array.isArray(popPosition) ? popPosition.join('-') : popPosition; @@ -72,9 +87,9 @@ export default { // 这里使用比较hack的方法控制popover显隐,因为点击popover外部元素隐藏popover之后,再重新传入visible不起作用了,popover不会重新渲染了 nextTick(() => { - if(visible) { + if (visible) { addElClass(popover.el as HTMLElement, 'devui-popover-isVisible'); - }else { + } else { removeElClass(popover.el as HTMLElement, 'devui-popover-isVisible'); } }); @@ -84,50 +99,54 @@ export default { const style: any = { position: 'absolute', height: 0, - top: (rect.height / 2) + 'px', + top: rect.height / 2 + 'px', right: 0, }; const p = popoverPosition(); - if(popPosition === 'bottom' || popPosition === 'top') { + if (popPosition === 'bottom' || popPosition === 'top') { style.left = '50%'; } - if(popPosition === 'left' || popPosition === 'right') { + if (popPosition === 'left' || popPosition === 'right') { style.top = 0; } - if(p.includes('top')) { + if (p.includes('top')) { style.top = -(rect.height / 2) + 'px'; } - if(p.endsWith('-bottom')) { - style.top = (rect.height / 2) + 'px'; + if (p.endsWith('-bottom')) { + style.top = rect.height / 2 + 'px'; } - if(p.includes('left')) { + if (p.includes('left')) { style.left = 0; } - if(p.includes('right')) { + if (p.includes('right')) { delete style.left; style.right = 0; } - if(p.startsWith('bottom')) { + if (p.startsWith('bottom')) { delete style.top; style.bottom = 0; } - if(p.startsWith('top')) { + if (p.startsWith('top')) { delete style.bottom; } return objToStyleString(style); }; - const vn = h('div', { - style: popoverWrapperStyle() - }, popover); + const vn = h( + 'div', + { + style: popoverWrapperStyle(), + }, + popover + ); render(vn, el); }; const tipEl = document.createElement('div'); - if(messageShowType === 'text') { + if (messageShowType === 'text') { el.parentNode.appendChild(tipEl); } @@ -139,7 +158,7 @@ export default { const addElClass = (el: HTMLElement, className: string) => { let currentClasses = el.getAttribute('class'); - if(!currentClasses.includes(className)) { + if (!currentClasses.includes(className)) { currentClasses = currentClasses.trim() + (currentClasses.trim() ? ' ' : '') + className; } el.setAttribute('class', currentClasses); @@ -151,77 +170,81 @@ export default { el.setAttribute('class', currentClasses); }; - const {validate, createDevUIBuiltinValidator} = useValidate(); - const propRule = {} || [] as any; // 值为对象数组或单个对象 + const { validate, createDevUIBuiltinValidator } = useValidate(); + const propRule = {} || ([] as any); // 值为对象数组或单个对象 const isCustomValidator = validators !== undefined || asyncValidators !== undefined; - if(isCustomValidator) { + if (isCustomValidator) { validators && (rules = validators); asyncValidators && (rules = asyncValidators); - if(asyncValidators) { + if (asyncValidators) { let time = Number(asyncDebounceTime); - if(isNaN(time)) { + if (isNaN(time)) { console.warn('[v-d-validate] invalid asyncDebounceTime'); time = 300; } - rules = asyncValidators.map(item => { + rules = asyncValidators.map((item) => { const res = { message: item.message, asyncValidator: (rule, value) => { - return new Promise(debounce((resolve, reject) => { - const res = item.asyncValidator(rule, value); - if(res) { - resolve(''); - }else { - reject(rule.message); - } - }, time)); + return new Promise( + debounce((resolve, reject) => { + const res = item.asyncValidator(rule, value); + if (res) { + resolve(''); + } else { + reject(rule.message); + } + }, time) + ); }, } as any; return res; }); } - }else { - if(Array.isArray(rules)) { - rules.map(item => { + } else { + if (Array.isArray(rules)) { + rules.map((item) => { return createDevUIBuiltinValidator(item); }); - }else { + } else { rules = createDevUIBuiltinValidator(rules); } } const descriptor: any = { - [prop]: rules + [prop]: rules, }; const validateFn = async () => { const validateModel = { - [prop]: hasModelName ? instance[modelName][prop] : instance[prop] + [prop]: hasModelName ? instance[modelName][prop] : instance[prop], }; - return validate(descriptor, validateModel).then(res => { - renderPopover('', false); - removeElClass(el, 'devui-error'); - messageShowType === 'text' && renderTipEl('', true); - return res; - }).catch(({ errors, fields }) => { - const msg = propRule.message ?? fields[prop][0].message; - renderPopover(msg); - addElClass(el, 'devui-error'); - messageShowType === 'text' && renderTipEl(msg, true); - if(messageChange && typeof messageChange === 'function') { - messageChange(msg, { errors, fields }); - } - return { errors, fields }; - }); + return validate(descriptor, validateModel) + .then((res) => { + renderPopover('', false); + removeElClass(el, 'devui-error'); + messageShowType === 'text' && renderTipEl('', true); + return res; + }) + .catch(({ errors, fields }) => { + const msg = propRule.message ?? fields[prop][0].message; + renderPopover(msg); + addElClass(el, 'devui-error'); + messageShowType === 'text' && renderTipEl(msg, true); + if (messageChange && typeof messageChange === 'function') { + messageChange(msg, { errors, fields }); + } + return { errors, fields }; + }); }; - if(errorStrategy === 'pristine') { + if (errorStrategy === 'pristine') { validateFn(); - }else { + } else { el.childNodes[0].addEventListener(updateOn, () => { validateFn(); }); - if(updateOn === 'change') { + if (updateOn === 'change') { el.childNodes[0].addEventListener('focus', () => { renderPopover('', false); }); @@ -230,14 +253,16 @@ export default { // 处理表单提交校验 const formTag = getTargetElement(el, 'form') as HTMLFormElement; - if(formTag && updateOn === 'submit') { + if (formTag && updateOn === 'submit') { const formName = formTag.name; const formSubmitDataCallback: any = (val: DFormValidateSubmitData) => { - validateFn().then((res: any) => { - val.callback(!!!res?.errors, { errors: res?.errors, fields: res?.fields }); - }).catch(({errors, fields}) => { - console.log('validateFn {errors, fields}', {errors, fields}); - }); + validateFn() + .then((res: any) => { + val.callback(!!!res?.errors, { errors: res?.errors, fields: res?.fields }); + }) + .catch(({ errors, fields }) => { + console.log('validateFn {errors, fields}', { errors, fields }); + }); }; EventBus.on(`formSubmit:${formName}`, formSubmitDataCallback); EventBus.on(`formReset:${formName}:${prop}`, () => { @@ -249,11 +274,11 @@ export default { }, beforeUnmount(el: HTMLElement, binding: DirectiveBinding) { - const {prop} = binding.value; + const { prop } = binding.value; const formTag = getTargetElement(el, 'form') as HTMLFormElement; const formName = formTag.name; EventBus.off(`formSubmit:${formName}`); EventBus.off(`formReset:${formName}:${prop}`); - } + }, }; diff --git a/packages/devui-vue/devui/form/src/directive/style.scss b/packages/devui-vue/devui/form/src/directives/style.scss similarity index 100% rename from packages/devui-vue/devui/form/src/directive/style.scss rename to packages/devui-vue/devui/form/src/directives/style.scss diff --git a/packages/devui-vue/devui/form/src/form-control/form-control.tsx b/packages/devui-vue/devui/form/src/form-control/form-control.tsx deleted file mode 100644 index 9a514f5f4a..0000000000 --- a/packages/devui-vue/devui/form/src/form-control/form-control.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import { defineComponent, inject, ref, computed, reactive, onMounted, Teleport } from 'vue'; -import { uniqueId } from 'lodash'; -import { IForm, formControlProps, formInjectionKey } from '../form-types'; -import { ShowPopoverErrorMessageEventData } from '../directive/d-validate-rules'; -import clickoutsideDirective from '../../../shared/devui-directive/clickoutside'; -import { EventBus, getElOffset } from '../util'; -import Icon from '../../../icon/src/icon'; -import Popover from '../../../popover/src/popover'; -import './form-control.scss'; - -type positionType = 'top' | 'right' | 'bottom' | 'left'; - -export default defineComponent({ - name: 'DFormControl', - directives: { - clickoutside: clickoutsideDirective - }, - props: formControlProps, - setup(props, ctx) { - const formControl = ref(); - const dForm = reactive(inject(formInjectionKey, {} as IForm)); - const labelData = reactive(dForm.labelData); - const isHorizontal = labelData.layout === 'horizontal'; - const uid = uniqueId('dfc-'); - const showPopover = ref(false); - const updateOn = ref('change'); - const tipMessage = ref(''); - const popPosition = ref('bottom'); - let rectInfo: Partial = { - width: 0, - height: 0 - }; - let elOffset = { - left: 0, - top: 0 - }; - let popoverLeftPosition = 0 ; - let popoverTopPosition = 0 ; - - onMounted(() => { - const el = document.getElementById(uid); - elOffset = getElOffset(el); - EventBus.on('showPopoverErrorMessage', (data: ShowPopoverErrorMessageEventData) => { - if (uid === data.uid) { - rectInfo = el.getBoundingClientRect(); - showPopover.value = data.showPopover; - tipMessage.value = data.message; - popPosition.value = data.popPosition as any; // todo: 待popover组件positionType完善类型之后再替换类型 - popoverLeftPosition = popPosition.value === 'top' || popPosition.value === 'bottom' ? rectInfo.right - (rectInfo.width / 2) : rectInfo.right; - popoverTopPosition = popPosition.value === 'top' ? elOffset.top + (rectInfo.height / 2) - rectInfo.height : elOffset.top + (rectInfo.height / 2); - updateOn.value = data.updateOn ?? 'change'; - } - }); - }); - - const iconData = computed(() => { - switch (props.feedbackStatus) { - case 'pending': - return { name: 'priority', color: '#e9edfa' }; - case 'success': - return { name: 'right-o', color: 'rgb(61, 204, 166)' }; - case 'error': - return { name: 'error-o', color: 'rgb(249, 95, 91)' }; - default: - return { name: '', color: '' }; - } - }); - - const handleClickOutside = () => { - if(updateOn.value !== 'change') { - showPopover.value = false; - } - }; - - return () => { - const { - feedbackStatus, - extraInfo, - } = props; - return
- { showPopover.value && - -
- -
-
- } -
-
- {ctx.slots.default?.()} -
- { - (feedbackStatus || ctx.slots.suffixTemplate?.()) && - - {ctx.slots.suffixTemplate?.() ? ctx.slots.suffixTemplate?.() : } - - } -
- {extraInfo &&
{extraInfo}
} -
; - }; - } -}); diff --git a/packages/devui-vue/devui/form/src/form-label/form-label.tsx b/packages/devui-vue/devui/form/src/form-label/form-label.tsx deleted file mode 100644 index abd9ccafde..0000000000 --- a/packages/devui-vue/devui/form/src/form-label/form-label.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { defineComponent, inject, reactive, computed } from 'vue'; -import { IForm, formLabelProps, FormLabelProps, formInjectionKey } from '../form-types'; -import Icon from '../../../icon/src/icon'; -import Popover from '../../../popover/src/popover'; -import './form-label.scss'; - -export default defineComponent({ - name: 'DFormLabel', - props: formLabelProps, - setup(props: FormLabelProps, ctx) { - const dForm = reactive(inject(formInjectionKey, {} as IForm)); - const labelData = reactive(dForm.labelData); - - const isHorizontal = computed(() => labelData.layout === 'horizontal').value; - const isLg = computed(() => labelData.labelSize === 'lg').value; - const isSm = computed(() => labelData.labelSize === 'sm').value; - const isCenter = computed(() => labelData.labelAlign === 'center').value; - const isEnd = computed(() => labelData.labelAlign === 'end').value; - - const wrapperCls = `devui-form-label${isHorizontal ? (isSm ? ' devui-form-label_sm' : (isLg ? ' devui-form-label_lg' : ' devui-form-label_sd')) : ''}${isCenter ? ' devui-form-label_center' : (isEnd ? ' devui-form-label_end' : '')}`; - const className = `${props.required ? ' devui-required' : ''}`; - const style = {display: isHorizontal ? 'inline' : 'inline-block'}; - - return () => { - return - - {ctx.slots.default?.()} - { - props.hasHelp && props.helpTips && ( - ( - - - - ) - }}> - - ) - } - - ; - }; - } -}); diff --git a/packages/devui-vue/devui/form/src/form.tsx b/packages/devui-vue/devui/form/src/form.tsx index dc01d02c17..59c03f7e35 100644 --- a/packages/devui-vue/devui/form/src/form.tsx +++ b/packages/devui-vue/devui/form/src/form.tsx @@ -1,18 +1,16 @@ - import { defineComponent, provide } from 'vue'; import mitt from 'mitt'; import { formProps, FormProps, IFormItem, dFormEvents, formInjectionKey, IForm } from './form-types'; -import { EventBus } from './util'; +import { EventBus } from './utils'; import './form.scss'; - export default defineComponent({ name: 'DForm', props: formProps, emits: ['submit'], setup(props: FormProps, ctx) { const formMitt = mitt(); - const fields: IFormItem[] = []; + const fields: IFormItem[] = []; const resetFormFields = () => { fields.forEach((field: IFormItem) => { field.resetField(); @@ -20,13 +18,13 @@ export default defineComponent({ }; formMitt.on(dFormEvents.addField, (field: any) => { - if(field) { + if (field) { fields.push(field); } }); formMitt.on(dFormEvents.removeField, (field: any) => { - if(field.prop) { + if (field.prop) { fields.splice(fields.indexOf(field), 1); } }); @@ -41,7 +39,7 @@ export default defineComponent({ }, rules: props.rules, columnsClass: props.columnsClass, - messageShowType: 'popover' + messageShowType: 'popover', }); const onSubmit = (e) => { @@ -54,15 +52,15 @@ export default defineComponent({ fields, formMitt, onSubmit, - resetFormFields + resetFormFields, }; }, render() { - const {onSubmit} = this; + const { onSubmit } = this; return (
{this.$slots.default?.()}
); - } + }, }); diff --git a/packages/devui-vue/devui/form/src/util/event-bus.ts b/packages/devui-vue/devui/form/src/utils/event-bus.ts similarity index 100% rename from packages/devui-vue/devui/form/src/util/event-bus.ts rename to packages/devui-vue/devui/form/src/utils/event-bus.ts diff --git a/packages/devui-vue/devui/form/src/util/index.ts b/packages/devui-vue/devui/form/src/utils/index.ts similarity index 100% rename from packages/devui-vue/devui/form/src/util/index.ts rename to packages/devui-vue/devui/form/src/utils/index.ts