import XEUtils from 'xe-utils' import GlobalConfig from '../../v-x-e-table/src/conf' import vSize from '../../mixins/size' import VXETable from '../../v-x-e-table' import { UtilTools, DomTools, isEnableConf } from '../../tools' import { createItem } from './util' class Rule { constructor (rule) { Object.assign(this, { $options: rule, required: rule.required, min: rule.min, max: rule.min, type: rule.type, pattern: rule.pattern, validator: rule.validator, trigger: rule.trigger, maxWidth: rule.maxWidth }) } get message () { return UtilTools.getFuncText(this.$options.message) } } function getResetValue (value, resetValue) { if (XEUtils.isArray(value)) { resetValue = [] } return resetValue } function callSlot (_vm, slotFunc, params, h) { if (slotFunc) { const { $scopedSlots } = _vm if (XEUtils.isString(slotFunc)) { slotFunc = $scopedSlots[slotFunc] || null } if (XEUtils.isFunction(slotFunc)) { return slotFunc.call(_vm, params, h) } } return [] } function renderPrefixIcon (h, titlePrefix) { return h('span', { class: 'vxe-form--item-title-prefix' }, [ h('i', { class: titlePrefix.icon || GlobalConfig.icon.FORM_PREFIX }) ]) } function renderSuffixIcon (h, titleSuffix) { return h('span', { class: 'vxe-form--item-title-suffix' }, [ h('i', { class: titleSuffix.icon || GlobalConfig.icon.FORM_SUFFIX }) ]) } function renderTitle (h, _vm, item) { const { data } = _vm const { slots, field, itemRender, titlePrefix, titleSuffix } = item const compConf = isEnableConf(itemRender) ? VXETable.renderer.get(itemRender.name) : null const params = { data, property: field, item, $form: _vm } const tss = [] if (titlePrefix) { tss.push( titlePrefix.message ? h('vxe-tooltip', { props: { content: UtilTools.getFuncText(titlePrefix.message), enterable: titlePrefix.enterable, theme: titlePrefix.theme } }, [ renderPrefixIcon(h, titlePrefix) ]) : renderPrefixIcon(h, titlePrefix) ) } tss.push( h('span', { class: 'vxe-form--item-title-label' }, compConf && compConf.renderItemTitle ? compConf.renderItemTitle(itemRender, params) : (slots && slots.title ? callSlot(_vm, slots.title, params, h) : UtilTools.getFuncText(item.title))) ) if (titleSuffix) { tss.push( titleSuffix.message ? h('vxe-tooltip', { props: { content: UtilTools.getFuncText(titleSuffix.message), enterable: titleSuffix.enterable, theme: titleSuffix.theme } }, [ renderSuffixIcon(h, titleSuffix) ]) : renderSuffixIcon(h, titleSuffix) ) } return tss } function renderItems (h, _vm, itemList) { const { _e, rules, data, collapseAll, validOpts, titleOverflow: allTitleOverflow } = _vm return itemList.map((item, index) => { const { slots, title, folding, visible, visibleMethod, field, collapseNode, itemRender, showError, errRule, className, titleOverflow, children } = item const compConf = isEnableConf(itemRender) ? VXETable.renderer.get(itemRender.name) : null const span = item.span || _vm.span const align = item.align || _vm.align const titleAlign = item.titleAlign || _vm.titleAlign const titleWidth = item.titleWidth || _vm.titleWidth let itemVisibleMethod = visibleMethod const itemOverflow = (XEUtils.isUndefined(titleOverflow) || XEUtils.isNull(titleOverflow)) ? allTitleOverflow : titleOverflow const showEllipsis = itemOverflow === 'ellipsis' const showTitle = itemOverflow === 'title' const showTooltip = itemOverflow === true || itemOverflow === 'tooltip' const hasEllipsis = showTitle || showTooltip || showEllipsis const params = { data, property: field, item, $form: _vm } let isRequired if (visible === false) { return _e() } // 如果为项集合 const isGather = children && children.length > 0 if (isGather) { const childVNs = renderItems(h, _vm, item.children) return childVNs.length ? h('div', { class: ['vxe-form--gather vxe-row', item.id, span ? `vxe-col--${span} is--span` : '', className ? (XEUtils.isFunction(className) ? className(params) : className) : ''] }, childVNs) : _e() } if (!itemVisibleMethod && compConf && compConf.itemVisibleMethod) { itemVisibleMethod = compConf.itemVisibleMethod } if (rules) { const itemRules = rules[field] if (itemRules) { isRequired = itemRules.some(rule => rule.required) } } let contentVNs = [] if (slots && slots.default) { contentVNs = callSlot(_vm, slots.default, params, h) } else if (compConf && compConf.renderItemContent) { contentVNs = compConf.renderItemContent.call(_vm, h, itemRender, params) } else if (compConf && compConf.renderItem) { contentVNs = compConf.renderItem.call(_vm, h, itemRender, params) } else if (field) { contentVNs = [`${XEUtils.get(data, field)}`] } const ons = showTooltip ? { mouseenter (evnt) { _vm.triggerHeaderHelpEvent(evnt, params) }, mouseleave: _vm.handleTargetLeaveEvent } : {} return h('div', { class: ['vxe-form--item', item.id, span ? `vxe-col--${span} is--span` : null, className ? (XEUtils.isFunction(className) ? className(params) : className) : '', { 'is--title': title, 'is--required': isRequired, 'is--hidden': folding && collapseAll, 'is--active': !itemVisibleMethod || itemVisibleMethod(params), 'is--error': showError }], key: index }, [ h('div', { class: 'vxe-form--item-inner' }, [ title || (slots && slots.title) ? h('div', { class: ['vxe-form--item-title', titleAlign ? `align--${titleAlign}` : null, { 'is--ellipsis': hasEllipsis }], style: titleWidth ? { width: isNaN(titleWidth) ? titleWidth : `${titleWidth}px` } : null, attrs: { title: showTitle ? UtilTools.getFuncText(title) : null }, on: ons }, renderTitle(h, _vm, item)) : null, h('div', { class: ['vxe-form--item-content', align ? `align--${align}` : null] }, contentVNs.concat( [ collapseNode ? h('div', { class: 'vxe-form--item-trigger-node', on: { click: _vm.toggleCollapseEvent } }, [ h('span', { class: 'vxe-form--item-trigger-text' }, collapseAll ? GlobalConfig.i18n('vxe.form.unfolding') : GlobalConfig.i18n('vxe.form.folding')), h('i', { class: ['vxe-form--item-trigger-icon', collapseAll ? GlobalConfig.icon.FORM_FOLDING : GlobalConfig.icon.FORM_UNFOLDING] }) ]) : null, errRule && validOpts.showMessage ? h('div', { class: 'vxe-form--item-valid', style: errRule.maxWidth ? { width: `${errRule.maxWidth}px` } : null }, errRule.message) : null ]) ) ]) ]) }) } export default { name: 'VxeForm', mixins: [vSize], props: { loading: Boolean, data: Object, size: { type: String, default: () => GlobalConfig.form.size || GlobalConfig.size }, span: [String, Number], align: { type: String, default: () => GlobalConfig.form.align }, titleAlign: { type: String, default: () => GlobalConfig.form.titleAlign }, titleWidth: [String, Number], titleColon: { type: Boolean, default: () => GlobalConfig.form.titleColon }, titleAsterisk: { type: Boolean, default: () => GlobalConfig.form.titleAsterisk }, titleOverflow: { type: [Boolean, String], default: null }, items: Array, rules: Object, preventSubmit: { type: Boolean, default: () => GlobalConfig.form.preventSubmit }, validConfig: Object }, data () { return { collapseAll: true, staticItems: [], formItems: [], tooltipTimeout: null, tooltipActive: false, tooltipStore: { item: null, visible: false } } }, provide () { return { $xeform: this } }, computed: { validOpts () { return Object.assign({}, GlobalConfig.form.validConfig, this.validConfig) }, tooltipOpts () { const opts = Object.assign({ leaveDelay: 300 }, GlobalConfig.form.tooltipConfig, this.tooltipConfig) if (opts.enterable) { opts.leaveMethod = this.handleTooltipLeaveMethod } return opts } }, created () { this.$nextTick(() => { const { items } = this if (items) { this.loadItem(items) } }) }, watch: { staticItems (value) { this.formItems = value }, items (value) { this.loadItem(value) } }, render (h) { const { _e, loading, vSize, tooltipOpts, formItems } = this const hasUseTooltip = VXETable._tooltip return h('form', { class: ['vxe-form', { [`size--${vSize}`]: vSize, 'is--colon': this.titleColon, 'is--asterisk': this.titleAsterisk, 'is--loading': loading }], on: { submit: this.submitEvent, reset: this.resetEvent } }, [ h('div', { class: 'vxe-form--wrapper vxe-row' }, renderItems(h, this, formItems)), h('div', { class: 'vxe-form-slots', ref: 'hideItem' }, this.$slots.default), h('div', { class: ['vxe-loading', { 'is--visible': loading }] }, [ h('div', { class: 'vxe-loading--spinner' }) ]), /** * 工具提示 */ hasUseTooltip ? h('vxe-tooltip', { ref: 'tooltip', ...tooltipOpts }) : _e() ]) }, methods: { loadItem (list) { if (process.env.VUE_APP_VXE_TABLE_ENV === 'development') { const { $scopedSlots } = this list.forEach(item => { if (item.slots) { XEUtils.each(item.slots, (func) => { if (!XEUtils.isFunction(func)) { if (!$scopedSlots[func]) { UtilTools.error('vxe.error.notSlot', [func]) } } }) } }) } this.staticItems = list.map(item => createItem(this, item)) return this.$nextTick() }, getItems () { const itemList = [] XEUtils.eachTree(this.formItems, item => { itemList.push(item) }, { children: 'children' }) return itemList }, toggleCollapse () { this.collapseAll = !this.collapseAll return this.$nextTick() }, toggleCollapseEvent (evnt) { this.toggleCollapse() this.$emit('toggle-collapse', { collapse: !this.collapseAll, data: this.data, $form: this, $event: evnt }, evnt) }, submitEvent (evnt) { evnt.preventDefault() if (!this.preventSubmit) { this.beginValidate().then(() => { this.$emit('submit', { data: this.data, $form: this, $event: evnt }) }).catch(errMap => { this.$emit('submit-invalid', { data: this.data, errMap, $form: this, $event: evnt }) }) } }, reset () { const { data } = this if (data) { const itemList = this.getItems() itemList.forEach(item => { const { field, resetValue, itemRender } = item if (isEnableConf(itemRender)) { const compConf = VXETable.renderer.get(itemRender.name) if (compConf && compConf.itemResetMethod) { compConf.itemResetMethod({ data, property: field, item, $form: this }) } else if (field) { XEUtils.set(data, field, resetValue === null ? getResetValue(XEUtils.get(data, field), undefined) : resetValue) } } }) } return this.clearValidate() }, resetEvent (evnt) { evnt.preventDefault() this.reset() this.$emit('reset', { data: this.data, $form: this, $event: evnt }) }, handleTooltipLeaveMethod () { const { tooltipOpts } = this setTimeout(() => { if (!this.tooltipActive) { this.closeTooltip() } }, tooltipOpts.leaveDelay) return false }, closeTooltip () { const { tooltipStore } = this const $tooltip = this.$refs.tooltip if (tooltipStore.visible) { Object.assign(tooltipStore, { item: null, visible: false }) if ($tooltip) { $tooltip.close() } } return this.$nextTick() }, triggerHeaderHelpEvent (evnt, params) { const { item } = params const { tooltipStore } = this const $tooltip = this.$refs.tooltip const overflowElem = evnt.currentTarget const content = (overflowElem.textContent || '').trim() const isCellOverflow = overflowElem.scrollWidth > overflowElem.clientWidth clearTimeout(this.tooltipTimeout) this.tooltipActive = true this.closeTooltip() if (content && isCellOverflow) { Object.assign(tooltipStore, { item, visible: true }) if ($tooltip) { $tooltip.open(overflowElem, content) } } }, handleTargetLeaveEvent () { const { tooltipOpts } = this this.tooltipActive = false if (tooltipOpts.enterable) { this.tooltipTimeout = setTimeout(() => { const $tooltip = this.$refs.tooltip if ($tooltip && !$tooltip.isHover) { this.closeTooltip() } }, tooltipOpts.leaveDelay) } else { this.closeTooltip() } }, clearValidate (field) { const itemList = this.getItems() if (field) { const item = itemList.find(item => item.field === field) if (item) { item.showError = false } } else { itemList.forEach(item => { item.showError = false }) } return this.$nextTick() }, validate (callback) { return this.beginValidate('', callback) }, beginValidate (type, callback) { const { data, rules: formRules, validOpts } = this const validRest = {} const validFields = [] const itemValids = [] const itemList = this.getItems() this.clearValidate() clearTimeout(this.showErrTime) if (data && formRules) { itemList.forEach(item => { const { field } = item if (field) { itemValids.push( this.validItemRules(type || 'all', field).then(() => { item.errRule = null }).catch(({ rule, rules }) => { const rest = { rule, rules, data, property: field, $form: this } if (!validRest[field]) { validRest[field] = [] } validRest[field].push(rest) validFields.push(field) item.errRule = rule return Promise.reject(rest) }) ) } }) return Promise.all(itemValids).then(() => { if (callback) { callback() } }).catch(() => { this.showErrTime = setTimeout(() => { itemList.forEach(item => { if (item.errRule) { item.showError = true } }) }, 20) if (callback) { callback(validRest) } if (validOpts.autoPos) { this.$nextTick(() => { this.handleFocus(validFields) }) } return Promise.reject(validRest) }) } if (callback) { callback() } return Promise.resolve() }, /** * 校验数据 * 按表格行、列顺序依次校验(同步或异步) * 校验规则根据索引顺序依次校验,如果是异步则会等待校验完成才会继续校验下一列 * 如果校验失败则,触发回调或者 Promise<(ErrMap 校验不通过列的信息)> * 如果是传回调方式这返回一个 (ErrMap 校验不通过列的信息) * * rule 配置: * required=Boolean 是否必填 * min=Number 最小长度 * max=Number 最大长度 * validator=Function({ itemValue, rule, rules, data, property }) 自定义校验,接收一个 Promise * trigger=change 触发方式 */ validItemRules (type, property, val) { const { data, rules: formRules } = this const errorRules = [] const syncVailds = [] if (property && formRules) { const rules = XEUtils.get(formRules, property) if (rules) { const itemValue = XEUtils.isUndefined(val) ? XEUtils.get(data, property) : val rules.forEach(rule => { if (type === 'all' || !rule.trigger || type === rule.trigger) { if (XEUtils.isFunction(rule.validator)) { const customValid = rule.validator({ itemValue, rule, rules, data, property, $form: this }) if (customValid) { if (XEUtils.isError(customValid)) { errorRules.push(new Rule({ type: 'custom', trigger: rule.trigger, message: customValid.message, rule: new Rule(rule) })) } else if (customValid.catch) { // 如果为异步校验(注:异步校验是并发无序的) syncVailds.push( customValid.catch(e => { errorRules.push(new Rule({ type: 'custom', trigger: rule.trigger, message: e ? e.message : rule.message, rule: new Rule(rule) })) }) ) } } } else { const isNumber = rule.type === 'number' const numVal = isNumber ? XEUtils.toNumber(itemValue) : XEUtils.getSize(itemValue) if (itemValue === null || itemValue === undefined || itemValue === '') { if (rule.required) { errorRules.push(new Rule(rule)) } } else if ( (isNumber && isNaN(itemValue)) || (!isNaN(rule.min) && numVal < parseFloat(rule.min)) || (!isNaN(rule.max) && numVal > parseFloat(rule.max)) || (rule.pattern && !(rule.pattern.test ? rule.pattern : new RegExp(rule.pattern)).test(itemValue)) ) { errorRules.push(new Rule(rule)) } } } }) } } return Promise.all(syncVailds).then(() => { if (errorRules.length) { const rest = { rules: errorRules, rule: errorRules[0] } return Promise.reject(rest) } }) }, handleFocus (fields) { const { $el } = this const itemList = this.getItems() fields.some(property => { const item = itemList.find(item => item.field === property) if (item && isEnableConf(item.itemRender)) { const { itemRender } = item const compConf = VXETable.renderer.get(itemRender.name) let inputElem // 如果指定了聚焦 class if (itemRender.autofocus) { inputElem = $el.querySelector(`.${item.id} ${itemRender.autofocus}`) } // 渲染器的聚焦处理 if (!inputElem && compConf && compConf.autofocus) { inputElem = $el.querySelector(`.${item.id} ${compConf.autofocus}`) } if (inputElem) { inputElem.focus() // 保持一致行为,光标移到末端 if (DomTools.browse.msie) { const textRange = inputElem.createTextRange() textRange.collapse(false) textRange.select() } return true } } }) }, /** * 更新项状态 * 如果组件值 v-model 发生 change 时,调用改函数用于更新某一项编辑状态 * 如果单元格配置了校验规则,则会进行校验 */ updateStatus (scope, itemValue) { const { property } = scope if (property) { this.validItemRules('change', property, itemValue) .then(() => { this.clearValidate(property) }) .catch(({ rule }) => { const itemList = this.getItems() const item = itemList.find(item => item.field === property) if (item) { item.showError = true item.errRule = rule } }) } } } }