form.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648
  1. import XEUtils from 'xe-utils'
  2. import GlobalConfig from '../../v-x-e-table/src/conf'
  3. import vSize from '../../mixins/size'
  4. import VXETable from '../../v-x-e-table'
  5. import { UtilTools, DomTools, isEnableConf } from '../../tools'
  6. import { createItem } from './util'
  7. class Rule {
  8. constructor (rule) {
  9. Object.assign(this, {
  10. $options: rule,
  11. required: rule.required,
  12. min: rule.min,
  13. max: rule.min,
  14. type: rule.type,
  15. pattern: rule.pattern,
  16. validator: rule.validator,
  17. trigger: rule.trigger,
  18. maxWidth: rule.maxWidth
  19. })
  20. }
  21. get message () {
  22. return UtilTools.getFuncText(this.$options.message)
  23. }
  24. }
  25. function getResetValue (value, resetValue) {
  26. if (XEUtils.isArray(value)) {
  27. resetValue = []
  28. }
  29. return resetValue
  30. }
  31. function callSlot (_vm, slotFunc, params, h) {
  32. if (slotFunc) {
  33. const { $scopedSlots } = _vm
  34. if (XEUtils.isString(slotFunc)) {
  35. slotFunc = $scopedSlots[slotFunc] || null
  36. }
  37. if (XEUtils.isFunction(slotFunc)) {
  38. return slotFunc.call(_vm, params, h)
  39. }
  40. }
  41. return []
  42. }
  43. function renderPrefixIcon (h, titlePrefix) {
  44. return h('span', {
  45. class: 'vxe-form--item-title-prefix'
  46. }, [
  47. h('i', {
  48. class: titlePrefix.icon || GlobalConfig.icon.FORM_PREFIX
  49. })
  50. ])
  51. }
  52. function renderSuffixIcon (h, titleSuffix) {
  53. return h('span', {
  54. class: 'vxe-form--item-title-suffix'
  55. }, [
  56. h('i', {
  57. class: titleSuffix.icon || GlobalConfig.icon.FORM_SUFFIX
  58. })
  59. ])
  60. }
  61. function renderTitle (h, _vm, item) {
  62. const { data } = _vm
  63. const { slots, field, itemRender, titlePrefix, titleSuffix } = item
  64. const compConf = isEnableConf(itemRender) ? VXETable.renderer.get(itemRender.name) : null
  65. const params = { data, property: field, item, $form: _vm }
  66. const tss = []
  67. if (titlePrefix) {
  68. tss.push(
  69. titlePrefix.message
  70. ? h('vxe-tooltip', {
  71. props: {
  72. content: UtilTools.getFuncText(titlePrefix.message),
  73. enterable: titlePrefix.enterable,
  74. theme: titlePrefix.theme
  75. }
  76. }, [
  77. renderPrefixIcon(h, titlePrefix)
  78. ])
  79. : renderPrefixIcon(h, titlePrefix)
  80. )
  81. }
  82. tss.push(
  83. h('span', {
  84. class: 'vxe-form--item-title-label'
  85. }, compConf && compConf.renderItemTitle ? compConf.renderItemTitle(itemRender, params) : (slots && slots.title ? callSlot(_vm, slots.title, params, h) : UtilTools.getFuncText(item.title)))
  86. )
  87. if (titleSuffix) {
  88. tss.push(
  89. titleSuffix.message
  90. ? h('vxe-tooltip', {
  91. props: {
  92. content: UtilTools.getFuncText(titleSuffix.message),
  93. enterable: titleSuffix.enterable,
  94. theme: titleSuffix.theme
  95. }
  96. }, [
  97. renderSuffixIcon(h, titleSuffix)
  98. ])
  99. : renderSuffixIcon(h, titleSuffix)
  100. )
  101. }
  102. return tss
  103. }
  104. function renderItems (h, _vm, itemList) {
  105. const { _e, rules, data, collapseAll, validOpts, titleOverflow: allTitleOverflow } = _vm
  106. return itemList.map((item, index) => {
  107. const { slots, title, folding, visible, visibleMethod, field, collapseNode, itemRender, showError, errRule, className, titleOverflow, children } = item
  108. const compConf = isEnableConf(itemRender) ? VXETable.renderer.get(itemRender.name) : null
  109. const span = item.span || _vm.span
  110. const align = item.align || _vm.align
  111. const titleAlign = item.titleAlign || _vm.titleAlign
  112. const titleWidth = item.titleWidth || _vm.titleWidth
  113. let itemVisibleMethod = visibleMethod
  114. const itemOverflow = (XEUtils.isUndefined(titleOverflow) || XEUtils.isNull(titleOverflow)) ? allTitleOverflow : titleOverflow
  115. const showEllipsis = itemOverflow === 'ellipsis'
  116. const showTitle = itemOverflow === 'title'
  117. const showTooltip = itemOverflow === true || itemOverflow === 'tooltip'
  118. const hasEllipsis = showTitle || showTooltip || showEllipsis
  119. const params = { data, property: field, item, $form: _vm }
  120. let isRequired
  121. if (visible === false) {
  122. return _e()
  123. }
  124. // 如果为项集合
  125. const isGather = children && children.length > 0
  126. if (isGather) {
  127. const childVNs = renderItems(h, _vm, item.children)
  128. return childVNs.length ? h('div', {
  129. class: ['vxe-form--gather vxe-row', item.id, span ? `vxe-col--${span} is--span` : '', className ? (XEUtils.isFunction(className) ? className(params) : className) : '']
  130. }, childVNs) : _e()
  131. }
  132. if (!itemVisibleMethod && compConf && compConf.itemVisibleMethod) {
  133. itemVisibleMethod = compConf.itemVisibleMethod
  134. }
  135. if (rules) {
  136. const itemRules = rules[field]
  137. if (itemRules) {
  138. isRequired = itemRules.some(rule => rule.required)
  139. }
  140. }
  141. let contentVNs = []
  142. if (slots && slots.default) {
  143. contentVNs = callSlot(_vm, slots.default, params, h)
  144. } else if (compConf && compConf.renderItemContent) {
  145. contentVNs = compConf.renderItemContent.call(_vm, h, itemRender, params)
  146. } else if (compConf && compConf.renderItem) {
  147. contentVNs = compConf.renderItem.call(_vm, h, itemRender, params)
  148. } else if (field) {
  149. contentVNs = [`${XEUtils.get(data, field)}`]
  150. }
  151. const ons = showTooltip ? {
  152. mouseenter (evnt) {
  153. _vm.triggerHeaderHelpEvent(evnt, params)
  154. },
  155. mouseleave: _vm.handleTargetLeaveEvent
  156. } : {}
  157. return h('div', {
  158. class: ['vxe-form--item', item.id, span ? `vxe-col--${span} is--span` : null, className ? (XEUtils.isFunction(className) ? className(params) : className) : '', {
  159. 'is--title': title,
  160. 'is--required': isRequired,
  161. 'is--hidden': folding && collapseAll,
  162. 'is--active': !itemVisibleMethod || itemVisibleMethod(params),
  163. 'is--error': showError
  164. }],
  165. key: index
  166. }, [
  167. h('div', {
  168. class: 'vxe-form--item-inner'
  169. }, [
  170. title || (slots && slots.title) ? h('div', {
  171. class: ['vxe-form--item-title', titleAlign ? `align--${titleAlign}` : null, {
  172. 'is--ellipsis': hasEllipsis
  173. }],
  174. style: titleWidth ? {
  175. width: isNaN(titleWidth) ? titleWidth : `${titleWidth}px`
  176. } : null,
  177. attrs: {
  178. title: showTitle ? UtilTools.getFuncText(title) : null
  179. },
  180. on: ons
  181. }, renderTitle(h, _vm, item)) : null,
  182. h('div', {
  183. class: ['vxe-form--item-content', align ? `align--${align}` : null]
  184. }, contentVNs.concat(
  185. [
  186. collapseNode ? h('div', {
  187. class: 'vxe-form--item-trigger-node',
  188. on: {
  189. click: _vm.toggleCollapseEvent
  190. }
  191. }, [
  192. h('span', {
  193. class: 'vxe-form--item-trigger-text'
  194. }, collapseAll ? GlobalConfig.i18n('vxe.form.unfolding') : GlobalConfig.i18n('vxe.form.folding')),
  195. h('i', {
  196. class: ['vxe-form--item-trigger-icon', collapseAll ? GlobalConfig.icon.FORM_FOLDING : GlobalConfig.icon.FORM_UNFOLDING]
  197. })
  198. ]) : null,
  199. errRule && validOpts.showMessage ? h('div', {
  200. class: 'vxe-form--item-valid',
  201. style: errRule.maxWidth ? {
  202. width: `${errRule.maxWidth}px`
  203. } : null
  204. }, errRule.message) : null
  205. ])
  206. )
  207. ])
  208. ])
  209. })
  210. }
  211. export default {
  212. name: 'VxeForm',
  213. mixins: [vSize],
  214. props: {
  215. loading: Boolean,
  216. data: Object,
  217. size: { type: String, default: () => GlobalConfig.form.size || GlobalConfig.size },
  218. span: [String, Number],
  219. align: { type: String, default: () => GlobalConfig.form.align },
  220. titleAlign: { type: String, default: () => GlobalConfig.form.titleAlign },
  221. titleWidth: [String, Number],
  222. titleColon: { type: Boolean, default: () => GlobalConfig.form.titleColon },
  223. titleAsterisk: { type: Boolean, default: () => GlobalConfig.form.titleAsterisk },
  224. titleOverflow: { type: [Boolean, String], default: null },
  225. items: Array,
  226. rules: Object,
  227. preventSubmit: { type: Boolean, default: () => GlobalConfig.form.preventSubmit },
  228. validConfig: Object
  229. },
  230. data () {
  231. return {
  232. collapseAll: true,
  233. staticItems: [],
  234. formItems: [],
  235. tooltipTimeout: null,
  236. tooltipActive: false,
  237. tooltipStore: {
  238. item: null,
  239. visible: false
  240. }
  241. }
  242. },
  243. provide () {
  244. return {
  245. $xeform: this
  246. }
  247. },
  248. computed: {
  249. validOpts () {
  250. return Object.assign({}, GlobalConfig.form.validConfig, this.validConfig)
  251. },
  252. tooltipOpts () {
  253. const opts = Object.assign({ leaveDelay: 300 }, GlobalConfig.form.tooltipConfig, this.tooltipConfig)
  254. if (opts.enterable) {
  255. opts.leaveMethod = this.handleTooltipLeaveMethod
  256. }
  257. return opts
  258. }
  259. },
  260. created () {
  261. this.$nextTick(() => {
  262. const { items } = this
  263. if (items) {
  264. this.loadItem(items)
  265. }
  266. })
  267. },
  268. watch: {
  269. staticItems (value) {
  270. this.formItems = value
  271. },
  272. items (value) {
  273. this.loadItem(value)
  274. }
  275. },
  276. render (h) {
  277. const { _e, loading, vSize, tooltipOpts, formItems } = this
  278. const hasUseTooltip = VXETable._tooltip
  279. return h('form', {
  280. class: ['vxe-form', {
  281. [`size--${vSize}`]: vSize,
  282. 'is--colon': this.titleColon,
  283. 'is--asterisk': this.titleAsterisk,
  284. 'is--loading': loading
  285. }],
  286. on: {
  287. submit: this.submitEvent,
  288. reset: this.resetEvent
  289. }
  290. }, [
  291. h('div', {
  292. class: 'vxe-form--wrapper vxe-row'
  293. }, renderItems(h, this, formItems)),
  294. h('div', {
  295. class: 'vxe-form-slots',
  296. ref: 'hideItem'
  297. }, this.$slots.default),
  298. h('div', {
  299. class: ['vxe-loading', {
  300. 'is--visible': loading
  301. }]
  302. }, [
  303. h('div', {
  304. class: 'vxe-loading--spinner'
  305. })
  306. ]),
  307. /**
  308. * 工具提示
  309. */
  310. hasUseTooltip ? h('vxe-tooltip', {
  311. ref: 'tooltip',
  312. ...tooltipOpts
  313. }) : _e()
  314. ])
  315. },
  316. methods: {
  317. loadItem (list) {
  318. if (process.env.VUE_APP_VXE_TABLE_ENV === 'development') {
  319. const { $scopedSlots } = this
  320. list.forEach(item => {
  321. if (item.slots) {
  322. XEUtils.each(item.slots, (func) => {
  323. if (!XEUtils.isFunction(func)) {
  324. if (!$scopedSlots[func]) {
  325. UtilTools.error('vxe.error.notSlot', [func])
  326. }
  327. }
  328. })
  329. }
  330. })
  331. }
  332. this.staticItems = list.map(item => createItem(this, item))
  333. return this.$nextTick()
  334. },
  335. getItems () {
  336. const itemList = []
  337. XEUtils.eachTree(this.formItems, item => {
  338. itemList.push(item)
  339. }, { children: 'children' })
  340. return itemList
  341. },
  342. toggleCollapse () {
  343. this.collapseAll = !this.collapseAll
  344. return this.$nextTick()
  345. },
  346. toggleCollapseEvent (evnt) {
  347. this.toggleCollapse()
  348. this.$emit('toggle-collapse', { collapse: !this.collapseAll, data: this.data, $form: this, $event: evnt }, evnt)
  349. },
  350. submitEvent (evnt) {
  351. evnt.preventDefault()
  352. if (!this.preventSubmit) {
  353. this.beginValidate().then(() => {
  354. this.$emit('submit', { data: this.data, $form: this, $event: evnt })
  355. }).catch(errMap => {
  356. this.$emit('submit-invalid', { data: this.data, errMap, $form: this, $event: evnt })
  357. })
  358. }
  359. },
  360. reset () {
  361. const { data } = this
  362. if (data) {
  363. const itemList = this.getItems()
  364. itemList.forEach(item => {
  365. const { field, resetValue, itemRender } = item
  366. if (isEnableConf(itemRender)) {
  367. const compConf = VXETable.renderer.get(itemRender.name)
  368. if (compConf && compConf.itemResetMethod) {
  369. compConf.itemResetMethod({ data, property: field, item, $form: this })
  370. } else if (field) {
  371. XEUtils.set(data, field, resetValue === null ? getResetValue(XEUtils.get(data, field), undefined) : resetValue)
  372. }
  373. }
  374. })
  375. }
  376. return this.clearValidate()
  377. },
  378. resetEvent (evnt) {
  379. evnt.preventDefault()
  380. this.reset()
  381. this.$emit('reset', { data: this.data, $form: this, $event: evnt })
  382. },
  383. handleTooltipLeaveMethod () {
  384. const { tooltipOpts } = this
  385. setTimeout(() => {
  386. if (!this.tooltipActive) {
  387. this.closeTooltip()
  388. }
  389. }, tooltipOpts.leaveDelay)
  390. return false
  391. },
  392. closeTooltip () {
  393. const { tooltipStore } = this
  394. const $tooltip = this.$refs.tooltip
  395. if (tooltipStore.visible) {
  396. Object.assign(tooltipStore, {
  397. item: null,
  398. visible: false
  399. })
  400. if ($tooltip) {
  401. $tooltip.close()
  402. }
  403. }
  404. return this.$nextTick()
  405. },
  406. triggerHeaderHelpEvent (evnt, params) {
  407. const { item } = params
  408. const { tooltipStore } = this
  409. const $tooltip = this.$refs.tooltip
  410. const overflowElem = evnt.currentTarget
  411. const content = (overflowElem.textContent || '').trim()
  412. const isCellOverflow = overflowElem.scrollWidth > overflowElem.clientWidth
  413. clearTimeout(this.tooltipTimeout)
  414. this.tooltipActive = true
  415. this.closeTooltip()
  416. if (content && isCellOverflow) {
  417. Object.assign(tooltipStore, {
  418. item,
  419. visible: true
  420. })
  421. if ($tooltip) {
  422. $tooltip.open(overflowElem, content)
  423. }
  424. }
  425. },
  426. handleTargetLeaveEvent () {
  427. const { tooltipOpts } = this
  428. this.tooltipActive = false
  429. if (tooltipOpts.enterable) {
  430. this.tooltipTimeout = setTimeout(() => {
  431. const $tooltip = this.$refs.tooltip
  432. if ($tooltip && !$tooltip.isHover) {
  433. this.closeTooltip()
  434. }
  435. }, tooltipOpts.leaveDelay)
  436. } else {
  437. this.closeTooltip()
  438. }
  439. },
  440. clearValidate (field) {
  441. const itemList = this.getItems()
  442. if (field) {
  443. const item = itemList.find(item => item.field === field)
  444. if (item) {
  445. item.showError = false
  446. }
  447. } else {
  448. itemList.forEach(item => {
  449. item.showError = false
  450. })
  451. }
  452. return this.$nextTick()
  453. },
  454. validate (callback) {
  455. return this.beginValidate('', callback)
  456. },
  457. beginValidate (type, callback) {
  458. const { data, rules: formRules, validOpts } = this
  459. const validRest = {}
  460. const validFields = []
  461. const itemValids = []
  462. const itemList = this.getItems()
  463. this.clearValidate()
  464. clearTimeout(this.showErrTime)
  465. if (data && formRules) {
  466. itemList.forEach(item => {
  467. const { field } = item
  468. if (field) {
  469. itemValids.push(
  470. this.validItemRules(type || 'all', field).then(() => {
  471. item.errRule = null
  472. }).catch(({ rule, rules }) => {
  473. const rest = { rule, rules, data, property: field, $form: this }
  474. if (!validRest[field]) {
  475. validRest[field] = []
  476. }
  477. validRest[field].push(rest)
  478. validFields.push(field)
  479. item.errRule = rule
  480. return Promise.reject(rest)
  481. })
  482. )
  483. }
  484. })
  485. return Promise.all(itemValids).then(() => {
  486. if (callback) {
  487. callback()
  488. }
  489. }).catch(() => {
  490. this.showErrTime = setTimeout(() => {
  491. itemList.forEach(item => {
  492. if (item.errRule) {
  493. item.showError = true
  494. }
  495. })
  496. }, 20)
  497. if (callback) {
  498. callback(validRest)
  499. }
  500. if (validOpts.autoPos) {
  501. this.$nextTick(() => {
  502. this.handleFocus(validFields)
  503. })
  504. }
  505. return Promise.reject(validRest)
  506. })
  507. }
  508. if (callback) {
  509. callback()
  510. }
  511. return Promise.resolve()
  512. },
  513. /**
  514. * 校验数据
  515. * 按表格行、列顺序依次校验(同步或异步)
  516. * 校验规则根据索引顺序依次校验,如果是异步则会等待校验完成才会继续校验下一列
  517. * 如果校验失败则,触发回调或者 Promise<(ErrMap 校验不通过列的信息)>
  518. * 如果是传回调方式这返回一个 (ErrMap 校验不通过列的信息)
  519. *
  520. * rule 配置:
  521. * required=Boolean 是否必填
  522. * min=Number 最小长度
  523. * max=Number 最大长度
  524. * validator=Function({ itemValue, rule, rules, data, property }) 自定义校验,接收一个 Promise
  525. * trigger=change 触发方式
  526. */
  527. validItemRules (type, property, val) {
  528. const { data, rules: formRules } = this
  529. const errorRules = []
  530. const syncVailds = []
  531. if (property && formRules) {
  532. const rules = XEUtils.get(formRules, property)
  533. if (rules) {
  534. const itemValue = XEUtils.isUndefined(val) ? XEUtils.get(data, property) : val
  535. rules.forEach(rule => {
  536. if (type === 'all' || !rule.trigger || type === rule.trigger) {
  537. if (XEUtils.isFunction(rule.validator)) {
  538. const customValid = rule.validator({
  539. itemValue,
  540. rule,
  541. rules,
  542. data,
  543. property,
  544. $form: this
  545. })
  546. if (customValid) {
  547. if (XEUtils.isError(customValid)) {
  548. errorRules.push(new Rule({ type: 'custom', trigger: rule.trigger, message: customValid.message, rule: new Rule(rule) }))
  549. } else if (customValid.catch) {
  550. // 如果为异步校验(注:异步校验是并发无序的)
  551. syncVailds.push(
  552. customValid.catch(e => {
  553. errorRules.push(new Rule({ type: 'custom', trigger: rule.trigger, message: e ? e.message : rule.message, rule: new Rule(rule) }))
  554. })
  555. )
  556. }
  557. }
  558. } else {
  559. const isNumber = rule.type === 'number'
  560. const numVal = isNumber ? XEUtils.toNumber(itemValue) : XEUtils.getSize(itemValue)
  561. if (itemValue === null || itemValue === undefined || itemValue === '') {
  562. if (rule.required) {
  563. errorRules.push(new Rule(rule))
  564. }
  565. } else if (
  566. (isNumber && isNaN(itemValue)) ||
  567. (!isNaN(rule.min) && numVal < parseFloat(rule.min)) ||
  568. (!isNaN(rule.max) && numVal > parseFloat(rule.max)) ||
  569. (rule.pattern && !(rule.pattern.test ? rule.pattern : new RegExp(rule.pattern)).test(itemValue))
  570. ) {
  571. errorRules.push(new Rule(rule))
  572. }
  573. }
  574. }
  575. })
  576. }
  577. }
  578. return Promise.all(syncVailds).then(() => {
  579. if (errorRules.length) {
  580. const rest = { rules: errorRules, rule: errorRules[0] }
  581. return Promise.reject(rest)
  582. }
  583. })
  584. },
  585. handleFocus (fields) {
  586. const { $el } = this
  587. const itemList = this.getItems()
  588. fields.some(property => {
  589. const item = itemList.find(item => item.field === property)
  590. if (item && isEnableConf(item.itemRender)) {
  591. const { itemRender } = item
  592. const compConf = VXETable.renderer.get(itemRender.name)
  593. let inputElem
  594. // 如果指定了聚焦 class
  595. if (itemRender.autofocus) {
  596. inputElem = $el.querySelector(`.${item.id} ${itemRender.autofocus}`)
  597. }
  598. // 渲染器的聚焦处理
  599. if (!inputElem && compConf && compConf.autofocus) {
  600. inputElem = $el.querySelector(`.${item.id} ${compConf.autofocus}`)
  601. }
  602. if (inputElem) {
  603. inputElem.focus()
  604. // 保持一致行为,光标移到末端
  605. if (DomTools.browse.msie) {
  606. const textRange = inputElem.createTextRange()
  607. textRange.collapse(false)
  608. textRange.select()
  609. }
  610. return true
  611. }
  612. }
  613. })
  614. },
  615. /**
  616. * 更新项状态
  617. * 如果组件值 v-model 发生 change 时,调用改函数用于更新某一项编辑状态
  618. * 如果单元格配置了校验规则,则会进行校验
  619. */
  620. updateStatus (scope, itemValue) {
  621. const { property } = scope
  622. if (property) {
  623. this.validItemRules('change', property, itemValue)
  624. .then(() => {
  625. this.clearValidate(property)
  626. })
  627. .catch(({ rule }) => {
  628. const itemList = this.getItems()
  629. const item = itemList.find(item => item.field === property)
  630. if (item) {
  631. item.showError = true
  632. item.errRule = rule
  633. }
  634. })
  635. }
  636. }
  637. }
  638. }