1import retrieveSchema from './schema/retriev';
2import { getPathVal } from './vueUtils';
3
4import { getSchemaType, isObject } from './utils';
5
6// 通用的处理表达式方法
7// 这里打破 JSON Schema 规范
8const regExpression = /{{(.*)}}/;
9function handleExpression(rootFormData, curNodePath, expression, fallBack) {
10    // 未配置
11    if (undefined === expression) {
12        return undefined;
13    }
14
15    // 配置了 mustache 表达式
16    const matchExpression = regExpression.exec(expression);
17    regExpression.lastIndex = 0; // 重置索引
18    if (matchExpression) {
19        const code = matchExpression[1].trim();
20
21        // eslint-disable-next-line no-new-func
22        const fn = new Function('parentFormData', 'rootFormData', `return ${code}`);
23
24        return fn(getPathVal(rootFormData, curNodePath, 1), rootFormData);
25    }
26
27    // 回退
28    return fallBack();
29}
30
31export function replaceArrayIndex({ schema, uiSchema } = {}, index) {
32    const itemUiOptions = getUiOptions({
33        schema,
34        uiSchema,
35        containsSpec: false
36    });
37
38    return ['title', 'description'].reduce((preVal, curItem) => {
39        if (itemUiOptions[curItem]) {
40            preVal[`ui:${curItem}`] = String(itemUiOptions[curItem]).replace(/\$index/g, index + 1);
41        }
42        return preVal;
43    }, {});
44}
45
46// 是否为 hidden Widget
47export function isHiddenWidget({
48    schema = {},
49    uiSchema = {},
50    curNodePath = '',
51    rootFormData = {}
52}) {
53    const widget = uiSchema['ui:widget'] || schema['ui:widget'];
54    const hiddenExpression = uiSchema['ui:hidden'] || schema['ui:hidden'];
55
56    // 支持配置 ui:hidden 表达式
57    return widget === 'HiddenWidget'
58        || widget === 'hidden'
59        || !!handleExpression(rootFormData, curNodePath, hiddenExpression, () => {
60            // 配置了函数 function
61            if (typeof hiddenExpression === 'function') {
62                return hiddenExpression(getPathVal(rootFormData, curNodePath, 1), rootFormData);
63            }
64
65            // 配置了常量 ??
66            return hiddenExpression;
67        });
68}
69
70// 解析当前节点 ui field
71export function getUiField(FIELDS_MAP, {
72    schema = {},
73    uiSchema = {},
74}) {
75    const field = schema['ui:field'] || uiSchema['ui:field'];
76
77    // vue 组件,或者已注册的组件名
78    if (typeof field === 'function' || typeof field === 'object' || typeof field === 'string') {
79        return {
80            field,
81            fieldProps: uiSchema['ui:fieldProps'] || schema['ui:fieldProps'], // 自定义field ,支持传入额外的 props
82        };
83    }
84
85    // 类型默认 field
86    const fieldCtor = FIELDS_MAP[getSchemaType(schema)];
87    if (fieldCtor) {
88        return {
89            field: fieldCtor
90        };
91    }
92
93    // 如果包含 oneOf anyOf 返回空不异常
94    // SchemaField 会附加onyOf anyOf信息
95    if (!fieldCtor && (schema.anyOf || schema.oneOf)) {
96        return {
97            field: null
98        };
99    }
100
101    // 不支持的类型
102    throw new Error(`不支持的field类型 ${schema.type}`);
103}
104
105// 解析用户配置的 uiSchema options
106export function getUserUiOptions({
107    schema = {},
108    uiSchema = {},
109    curNodePath, // undefined 不处理 表达式
110    rootFormData = {}
111}) {
112    // 支持 uiSchema配置在 schema文件中
113    return Object.assign({}, ...[schema, uiSchema].map(itemSchema => Object.keys(itemSchema)
114        .reduce((options, key) => {
115            const value = itemSchema[key];
116            // options 内外合并
117            if (key === 'ui:options' && isObject(value)) {
118                return { ...options, ...value };
119            }
120
121            // https://github.com/lljj-x/vue-json-schema-form/issues/170
122            // ui:hidden需要作为内置属性使用,不能直接透传给widget组件,如果组件需要只能在ui:options 中使用hidden传递
123            if (key !== 'ui:hidden' && key.indexOf('ui:') === 0) {
124                // 只对 ui:xxx 配置形式支持表达式
125                return {
126                    ...options,
127                    [key.substring(3)]: curNodePath === undefined ? value : handleExpression(rootFormData, curNodePath, value, () => value)
128                };
129            }
130
131            return options;
132        }, {})));
133}
134
135// 解析当前节点的ui options参数
136export function getUiOptions({
137    schema = {},
138    uiSchema = {},
139    containsSpec = true,
140    curNodePath,
141    rootFormData,
142}) {
143    const spec = {};
144    if (containsSpec) {
145        spec.readonly = !!schema.readOnly;
146        if (undefined !== schema.multipleOf) {
147            // 组件计数器步长
148            spec.step = schema.multipleOf;
149        }
150        if (schema.minimum || schema.minimum === 0) {
151            spec.min = schema.minimum;
152        }
153        if (schema.maximum || schema.maximum === 0) {
154            spec.max = schema.maximum;
155        }
156
157        if (schema.minLength || schema.minLength === 0) {
158            spec.minlength = schema.minLength;
159        }
160        if (schema.maxLength || schema.maxLength === 0) {
161            spec.maxlength = schema.maxLength;
162        }
163
164        if (schema.format === 'date-time' || schema.format === 'date') {
165            // 数组类型 时间区间
166            // 打破了schema的规范,type array 配置了 format
167            if (schema.type === 'array') {
168                spec.isRange = true;
169                spec.isNumberValue = !(schema.items && schema.items.type === 'string');
170            } else {
171                // 字符串 ISO 时间
172                spec.isNumberValue = !(schema.type === 'string');
173            }
174        }
175    }
176
177    if (schema.title) spec.title = schema.title;
178    if (schema.description) spec.description = schema.description;
179
180    // 计算ui配置
181    return {
182        ...spec,
183
184        // 用户配置最高优先级
185        ...getUserUiOptions({
186            schema,
187            uiSchema,
188            curNodePath,
189            rootFormData
190        })
191    };
192}
193
194// 获取当前节点的ui 配置 (options + widget)
195// 处理成 Widget 组件需要的格式
196export function getWidgetConfig({
197    schema = {},
198    uiSchema = {},
199    curNodePath,
200    rootFormData,
201}, fallback = null) {
202    const uiOptions = getUiOptions({
203        schema,
204        uiSchema,
205        curNodePath,
206        rootFormData,
207    });
208
209    // 没有配置 Widget ,各个Field组件根据类型判断
210    if (!uiOptions.widget && fallback) {
211        Object.assign(uiOptions, fallback({
212            schema,
213            uiSchema
214        }));
215    }
216
217    const {
218        widget,
219        title: label,
220        labelWidth,
221        description,
222        attrs: widgetAttrs,
223        class: widgetClass,
224        style: widgetStyle,
225        widgetListeners,
226        fieldAttrs,
227        fieldStyle,
228        fieldClass,
229        emptyValue,
230        width,
231        getWidget,
232        renderScopedSlots,
233        renderChildren,
234        onChange,
235        ...uiProps
236    } = uiOptions;
237
238    return {
239        widget,
240        label,
241        labelWidth,
242        description,
243        widgetAttrs,
244        widgetClass,
245        widgetStyle,
246        fieldAttrs,
247        width,
248        fieldStyle,
249        fieldClass,
250        emptyValue,
251        getWidget,
252        renderScopedSlots,
253        renderChildren,
254        onChange,
255        widgetListeners,
256        uiProps
257    };
258}
259
260// 解析用户配置的 errorSchema options
261export function getUserErrOptions({
262    schema = {},
263    uiSchema = {},
264    errorSchema = {}
265}) {
266    return Object.assign({}, ...[schema, uiSchema, errorSchema].map(itemSchema => Object.keys(itemSchema)
267        .reduce((options, key) => {
268            const value = itemSchema[key];
269            // options 内外合并
270            if (key === 'err:options' && isObject(value)) {
271                return { ...options, ...value };
272            }
273
274            if (key.indexOf('err:') === 0) {
275                return { ...options, [key.substring(4)]: value };
276            }
277
278            return options;
279        }, {})));
280}
281
282// ui:order object-> properties 排序
283export function orderProperties(properties, order) {
284    if (!Array.isArray(order)) {
285        return properties;
286    }
287
288    const arrayToHash = arr => arr.reduce((prev, curr) => {
289        prev[curr] = true;
290        return prev;
291    }, {});
292    const errorPropList = arr => (arr.length > 1
293        ? `properties '${arr.join("', '")}'`
294        : `property '${arr[0]}'`);
295    const propertyHash = arrayToHash(properties);
296    const orderFiltered = order.filter(
297        prop => prop === '*' || propertyHash[prop]
298    );
299    const orderHash = arrayToHash(orderFiltered);
300
301    const rest = properties.filter(prop => !orderHash[prop]);
302    const restIndex = orderFiltered.indexOf('*');
303    if (restIndex === -1) {
304        if (rest.length) {
305            throw new Error(
306                `uiSchema order list does not contain ${errorPropList(rest)}`
307            );
308        }
309        return orderFiltered;
310    }
311    if (restIndex !== orderFiltered.lastIndexOf('*')) {
312        throw new Error('uiSchema order list contains more than one wildcard item');
313    }
314
315    const complete = [...orderFiltered];
316    complete.splice(restIndex, 1, ...rest);
317    return complete;
318}
319
320/**
321 * 单个匹配
322 * 常量,或者只有一个枚举
323 */
324export function isConstant(schema) {
325    return (
326        (Array.isArray(schema.enum) && schema.enum.length === 1)
327        || schema.hasOwnProperty('const')
328    );
329}
330
331export function toConstant(schema) {
332    if (Array.isArray(schema.enum) && schema.enum.length === 1) {
333        return schema.enum[0];
334    } if (schema.hasOwnProperty('const')) {
335        return schema.const;
336    }
337    throw new Error('schema cannot be inferred as a constant');
338}
339
340/**
341 * 是否为选择列表
342 * 枚举 或者 oneOf anyOf 每项都只有一个固定常量值
343 * @param _schema
344 * @param rootSchema
345 * @returns {boolean|*}
346 */
347export function isSelect(_schema, rootSchema = {}) {
348    const schema = retrieveSchema(_schema, rootSchema);
349    const altSchemas = schema.oneOf || schema.anyOf;
350    if (Array.isArray(schema.enum)) {
351        return true;
352    } if (Array.isArray(altSchemas)) {
353        return altSchemas.every(altSchemasItem => isConstant(altSchemasItem));
354    }
355    return false;
356}
357
358// items 都为一个对象
359export function isFixedItems(schema) {
360    return (
361        Array.isArray(schema.items)
362        && schema.items.length > 0
363        && schema.items.every(item => isObject(item))
364    );
365}
366
367// 是否为多选
368export function isMultiSelect(schema, rootSchema = {}) {
369    if (!schema.uniqueItems || !schema.items) {
370        return false;
371    }
372    return isSelect(schema.items, rootSchema);
373}
374
375// array additionalItems
376// https://json-schema.org/understanding-json-schema/reference/array.html#tuple-validation
377export function allowAdditionalItems(schema) {
378    if (schema.additionalItems === true) {
379        console.warn('additionalItems=true is currently not supported');
380    }
381    return isObject(schema.additionalItems);
382}
383
384// 下拉选项
385export function optionsList(schema, uiSchema, curNodePath, rootFormData) {
386    // enum
387    if (schema.enum) {
388        const uiOptions = getUserUiOptions({
389            schema,
390            uiSchema,
391            curNodePath,
392            rootFormData
393        });
394
395        // ui配置 enumNames 优先
396        const enumNames = uiOptions.enumNames || schema.enumNames;
397        return schema.enum.map((value, i) => {
398            const label = (enumNames && enumNames[i]) || String(value);
399            return { label, value };
400        });
401    }
402
403    // oneOf | anyOf
404    const altSchemas = schema.oneOf || schema.anyOf;
405    const altUiSchemas = uiSchema.oneOf || uiSchema.anyOf;
406    return altSchemas.map((curSchema, i) => {
407        const uiOptions = (altUiSchemas && altUiSchemas[i]) ? getUserUiOptions({
408            schema: curSchema,
409            uiSchema: altUiSchemas[i],
410            curNodePath,
411            rootFormData
412        }) : {};
413        const value = toConstant(curSchema);
414        const label = uiOptions.title || curSchema.title || String(value);
415        return { label, value };
416    });
417
418}
419
420export function fallbackLabel(oriLabel, isFallback, curNodePath) {
421    if (oriLabel) return oriLabel;
422    if (isFallback) {
423        const backLabel = curNodePath.split('.').pop();
424
425        // 过滤纯数字字符串
426        if (backLabel && (backLabel !== `${Number(backLabel)}`)) return backLabel;
427    }
428
429    return '';
430}
431