1/**
2 * Created by Liu.Jun on 2020/4/23 11:24.
3 */
4
5import {
6    computed, h, ref, watch, inject
7} from 'vue';
8
9import {IconInfo} from '@lljj/vjsf-utils/icons';
10
11import {validateFormDataAndTransformMsg} from '@lljj/vjsf-utils/schema/validate';
12import {fallbackLabel} from '@lljj/vjsf-utils/formUtils';
13
14import {
15    isRootNodePath, path2prop, getPathVal, setPathVal, resolveComponent
16} from '@lljj/vjsf-utils/vue3Utils';
17
18export default {
19    name: 'Widget',
20    props: {
21        // 是否同步formData的值,默认表单元素都需要
22        // oneOf anyOf 中的select属于formData之外的数据
23        isFormData: {
24            type: Boolean,
25            default: true
26        },
27        // isFormData = false时需要传入当前 value 否则会通过 curNodePath 自动计算
28        curValue: {
29            type: null,
30            default: 0
31        },
32        schema: {
33            type: Object,
34            default: () => ({})
35        },
36        uiSchema: {
37            type: Object,
38            default: () => ({})
39        },
40        errorSchema: {
41            type: Object,
42            default: () => ({})
43        },
44        customFormats: {
45            type: Object,
46            default: () => ({})
47        },
48        // 自定义校验
49        customRule: {
50            type: Function,
51            default: null
52        },
53        widget: {
54            type: [String, Function, Object],
55            default: null
56        },
57        required: {
58            type: Boolean,
59            default: false
60        },
61        // 解决 JSON Schema和实际输入元素中空字符串 required 判定的差异性
62        // 元素输入为 '' 使用 emptyValue 的值
63        emptyValue: {
64            type: null,
65            default: undefined
66        },
67        rootFormData: {
68            type: null
69        },
70        curNodePath: {
71            type: String,
72            default: ''
73        },
74        label: {
75            type: String,
76            default: ''
77        },
78        // width -> formItem width
79        width: {
80            type: String,
81            default: ''
82        },
83        labelWidth: {
84            type: String,
85            default: ''
86        },
87        description: {
88            type: String,
89            default: ''
90        },
91        // Widget attrs
92        widgetAttrs: {
93            type: Object,
94            default: () => ({})
95        },
96        // Widget className
97        widgetClass: {
98            type: Object,
99            default: () => ({})
100        },
101        // Widget style
102        widgetStyle: {
103            type: Object,
104            default: () => ({})
105        },
106        // Field attrs
107        fieldAttrs: {
108            type: Object,
109            default: () => ({})
110        },
111        // Field className
112        fieldClass: {
113            type: Object,
114            default: () => ({})
115        },
116        // Field style
117        fieldStyle: {
118            type: Object,
119            default: () => ({})
120        },
121        // props
122        uiProps: {
123            type: Object,
124            default: () => ({})
125        },
126        formProps: null,
127        getWidget: null,
128        renderScopedSlots: null, // 作用域插槽
129        globalOptions: null, // 全局配置
130        onChange: null
131    },
132    emits: ['otherDataChange'],
133    inheritAttrs: true,
134    setup(props, {emit}) {
135        const genFormProvide = inject('genFormProvide');
136        const widgetValue = computed({
137            get() {
138                if (props.isFormData) return getPathVal(props.rootFormData, props.curNodePath);
139
140                return props.curValue;
141            },
142            set(value) {
143                // 大多组件删除为空值会重置为null。
144                const trueValue = (value === '' || value === null) ? props.emptyValue : value;
145                if (props.isFormData) {
146                    setPathVal(props.rootFormData, props.curNodePath, trueValue);
147                } else {
148                    emit('otherDataChange', trueValue);
149                }
150            }
151        });
152
153        // 枚举类型默认值为第一个选项
154        if (props.uiProps.enumOptions
155            && props.uiProps.enumOptions.length > 0
156            && widgetValue.value === undefined
157            && widgetValue.value !== props.uiProps.enumOptions[0]
158        ) {
159            // array 渲染为多选框时默认为空数组
160            if (props.schema.items) {
161                widgetValue.value = [];
162            } else if (props.required) {
163                widgetValue.value = props.uiProps.enumOptions[0].value;
164            }
165        }
166
167        // 获取到widget组件实例
168        const widgetRef = ref(null);
169        // 提供一种特殊的配置 允许直接访问到 widget vm
170        if (typeof props.getWidget === 'function') {
171            watch(widgetRef, () => {
172                props.getWidget.call(null, widgetRef.value);
173            });
174        }
175
176        return () => {
177            // 判断是否为根节点
178            const isRootNode = isRootNodePath(props.curNodePath);
179
180            const isMiniDes = props.formProps && props.formProps.isMiniDes;
181            const miniDesModel = isMiniDes || props.globalOptions.HELPERS.isMiniDes(props.formProps);
182
183            const descriptionVNode = (props.description) ? h(
184                'div',
185                {
186                    innerHTML: props.description,
187                    class: {
188                        genFromWidget_des: true,
189                        genFromWidget_des_mini: miniDesModel
190                    }
191                },
192            ) : null;
193
194            const {COMPONENT_MAP} = props.globalOptions;
195            const miniDescriptionVNode = (miniDesModel && descriptionVNode) ? h(resolveComponent(COMPONENT_MAP.popover), {
196                style: {
197                    margin: '0 2px',
198                    fontSize: '16px',
199                    cursor: 'pointer'
200                },
201                placement: 'top',
202                trigger: 'hover'
203            }, {
204                default: () => descriptionVNode,
205                reference: () => h(IconInfo)
206            }) : null;
207
208            // form-item style
209            const formItemStyle = {
210                ...props.fieldStyle,
211                ...(props.width ? {
212                    width: props.width,
213                    flexBasis: props.width,
214                    paddingRight: '10px'
215                } : {})
216            };
217
218            // 运行配置回退到 属性名
219            const label = fallbackLabel(props.label, (props.widget && genFormProvide.value.fallbackLabel), props.curNodePath);
220            return h(
221                resolveComponent(COMPONENT_MAP.formItem),
222                {
223                    class: {
224                        ...props.fieldClass,
225                        genFormItem: true
226                    },
227                    style: formItemStyle,
228                    ...props.fieldAttrs,
229
230                    ...props.labelWidth ? {labelWidth: props.labelWidth} : {},
231                    ...props.isFormData ? {
232                        // 这里对根节点打特殊标志,绕过elementUi无prop属性不校验
233                        prop: isRootNode ? '__$$root' : path2prop(props.curNodePath),
234                        rules: [
235                            {
236                                validator(rule, value, callback) {
237                                    if (isRootNode) value = props.rootFormData;
238
239                                    // 校验是通过对schema逐级展开校验 这里只捕获根节点错误
240                                    const errors = validateFormDataAndTransformMsg({
241                                        formData: value,
242                                        schema: props.schema,
243                                        uiSchema: props.uiSchema,
244                                        customFormats: props.customFormats,
245                                        errorSchema: props.errorSchema,
246                                        required: props.required,
247                                        propPath: path2prop(props.curNodePath)
248                                    });
249
250                                    // 存在校验不通过字段
251                                    if (errors.length > 0) {
252                                        if (callback) return callback(errors[0].message);
253                                        return Promise.reject(errors[0].message);
254                                    }
255
256                                    // customRule 如果存在自定义校验
257                                    const curCustomRule = props.customRule;
258                                    if (curCustomRule && (typeof curCustomRule === 'function')) {
259                                        return curCustomRule({
260                                            field: props.curNodePath,
261                                            value,
262                                            rootFormData: props.rootFormData,
263                                            callback
264                                        });
265                                    }
266
267                                    // 校验成功
268                                    if (callback) return callback();
269                                    return Promise.resolve();
270                                },
271                                trigger:  ['blur', 'input', 'focus', 'change']
272                            }
273                        ]
274                    } : {},
275                },
276                {
277                    // 错误只能显示一行,多余...
278                    error: slotProps => (slotProps.error ? h('div', {
279                        class: {
280                            formItemErrorBox: true
281                        },
282                        title: slotProps.error
283                    }, [slotProps.error]) : null),
284
285                    // label
286                    /*
287                        TODO:这里slot如果从无到有会导致无法正常渲染出元素 怀疑是vue3 bug
288                        如果使用 error 的形式渲染,ElementPlus label labelWrap 未做判断,使用 slots.default?.() 会得到 undefined
289                    */
290                    ...label ? {
291                        label: () => h('span', {
292                            class: {
293                                genFormLabel: true,
294                                genFormItemRequired: props.required && props.schema['ui:widget'] !== 'b-form-checkbox' && !props.uiProps.enumOptions,
295                            },
296                        }, [
297                            ...miniDescriptionVNode ? [miniDescriptionVNode] : [],
298                            (window.isDev ?
299                                    h('div', {style: {position: 'relative'}}, [
300                                        h('div', {style: {position: 'absolute'}},
301                                            `${props.curNodePath}`)
302                                    ]) :
303                                    ''
304                            ),
305                            `${label}`,
306                            `${(props.formProps && props.formProps.labelSuffix) || ''}`
307                        ])
308                    } : {},
309
310                    // default
311                    default: otherAttrs => [
312                        // description
313                        // 非mini模式显示 description
314                        ...(!miniDesModel && descriptionVNode) ? [descriptionVNode] : [],
315
316                        ...props.widget ? [
317                            h( // 关键输入组件
318                                resolveComponent(props.widget),
319                                {
320                                    style: props.widgetStyle,
321                                    class: props.widgetClass,
322
323                                    ...props.widgetAttrs,
324                                    ...props.uiProps,
325                                    modelValue: widgetValue.value, // v-model
326                                    ref: widgetRef,
327                                    'onUpdate:modelValue': function updateModelValue(event) {
328                                        const preVal = widgetValue.value;
329                                        if (preVal !== event) {
330                                            widgetValue.value = event;
331                                            if (props.onChange) {
332                                                props.onChange({
333                                                    curVal: event,
334                                                    preVal,
335                                                    parentFormData: getPathVal(props.rootFormData, props.curNodePath, 1),
336                                                    rootFormData: props.rootFormData
337                                                });
338                                            }
339                                        }
340                                    },
341                                    ...otherAttrs ? (() => Object.keys(otherAttrs).reduce((pre, k) => {
342                                        pre[k] = otherAttrs[k];
343
344                                        // 保证ui配置同名方法 ui方法先执行
345                                        [
346                                            props.widgetAttrs[k],
347                                            props.uiProps[k]
348                                        ].forEach((uiConfFn) => {
349                                            if (uiConfFn && typeof uiConfFn === 'function') {
350                                                pre[k] = (...args) => {
351                                                    uiConfFn(...args);
352                                                    pre[k](...args);
353                                                };
354                                            }
355                                        });
356
357                                        return pre;
358                                    }, {}))() : {}
359                                },
360                                {
361                                    ...(props.renderScopedSlots ? (
362                                        typeof props.renderScopedSlots === 'function' ? props.renderScopedSlots() : props.renderScopedSlots
363                                    ) : {})
364                                }
365                            )
366                        ] : []
367                    ]
368                }
369            );
370        };
371    }
372};
373