JSON配置编辑器

集成jsoneditor库,用于编辑复杂配置项:https://github.com/json-editor/json-editor

jsoneditor文档: jsonEditor.README.md

1 用法

安装:

./tool/jdcloud-plugin.sh add ../jdcloud-plugin-jsonEditor

在逻辑页对话框中使用示例:page/dlgOrderConfRule.html

    <tr>
        <td>配置值</td>
        <td class="wui-jsonEditor" data-options="schema:'schema/example.js'">
            <textarea name="value" rows=14></textarea>
            <p class="hint">
                <a class="easyui-linkbutton btnEdit" data-options="iconCls: 'icon-edit'" href="javascript:;">修改</a>
                <a class="easyui-linkbutton btnFormat" data-options="iconCls: 'icon-reload'" href="javascript:;">格式化JSON</a>
                <a class="easyui-linkbutton btnEditJson" data-options="iconCls: 'icon-edit'" href="javascript:;">配置</a>
            </p>
        </td>
    </tr>

注意:在data-options中设置schema:'schema/example.js',由该文件定义JSON格式。 为了兼容,也支持在配置按钮(CSS类为btnEditJson)上指定schema:

                <a class="easyui-linkbutton btnEditJson" data-options="iconCls: 'icon-edit'" data-schema="schema/example.js" href="javascript:;">配置</a>

如果想不显示JSON内容,只显示编辑按钮,这时在data-options中设置input:false

    <tr>
        <td>配置值</td>
        <td class="wui-jsonEditor" data-options="schema:'schema/example.js', input:false">
            <textarea name="value" rows=14></textarea>
            <p class="hint">
                <a class="easyui-linkbutton btnEditJson" data-options="iconCls: 'icon-edit'" href="javascript:;">配置</a>
            </p>
        </td>
    </tr>

标识类wui-jsonEditor已被定义为组件,其下面的标识类btnEdit, btnFormat和btnEditJson的三个类自动绑定了相应的操作。 其中btnEditJson则是弹出新的窗口,在该窗口中编辑配置。 配置项的schema定义由data-schema属性定义,示例见web/schema-example.js。

wui-jsonEditor组件有以下事件:

1.1 前端页面接口 - dlgJson

显示JSON编辑对话框:

DlgJson.show(schemaFile, jsonData, onSetJson, showDlgOpt)

schemaFile是json格式说明文件,习惯放在“web/schema”目录下。

示例1:取数据、编辑并保存:

callSvr("JDConf.get", {name: name}, function (data) {
    DlgJson.show("schema/" + name + ".js", data, onSetJson, {modal: false});
});

function onSetJson (data) {
    callSvr("JDConf.set", {name: name}, function () {
        app_show("更新成功");
    }, data);
}

示例2:取数据、编辑并保存。保存前检查是否有变更,无变更则忽略保存。

var initValue = null;
callSvr("UiCfg.getValue", {name: "menu"}, function (data) {
    initValue = data;
    DlgJson.show("schema/menu.js", initValue, onSetJson, {modal: false});
});

function onSetJson (data) {
    var str = JSON.stringify(data, null, 2);
    if (str == initValue)
        return;
    callSvr("UiCfg.setValue", {name: "menu"}, function () {
        UiMeta.handleMenu(data);
        initValue = str;
        app_show("已成功更新");
    }, {value: str});
}

也可以直接调用底层的dlgJson对话框,其扩展对话框参数如下:

示例:显示JSON编辑框编辑数据

var arrEditor = this.parent.parent;
var data = arrEditor.getValue();
WUI.showDlg("#dlgJson_inst_colSeq", {
    editorOpt: {
        schema: colSeqSchema,
        startval: data
    },
    onSetJson: function (data) {
        ...
        arrEditor.setValue(data);
    },
});

2 schema文件格式及常用配置值

schema文件一般存为 schema/xx.js,是一个返回js对象的脚本,它比普通JSON文件更灵活。 返回的对象可以是JSONEditor支持的schema格式(参见schema-example), 如:

{
    type: "object",
    title: "XX配置",
    properties: {
        name: {
            type: "string"
        },
        title: {
            type: "string"
        }
    }
}

也可以是完整的JSONEditor的options,如:

var schema = {
    type: "object",
    title: "XX配置",
    properties: {
        name: {
            type: "string"
        },
        title: {
            type: "string"
        }
    }
}

// 最后返回的JS对象,须用小括号括起来。有schema属性,表示返回的是完整的JSONEditor选项
({
    no_additional_properties:true,
    schema: schema,
})

常用的JSONEditor选项有:

也可以设置各级editor选项(可全局设置,也可在schema各级的options中设置),如对象上

在schema各级中常用选项:

本插件默认设置了如下选项(可在schema文件中再覆盖它们):

theme: "spectre",
iconlib: "spectre", 
remove_empty_properties: true,
use_default_values: false,
show_opt_in: true

注意:此前版本使用的是bootstrap库,换成了轻量的spectre库,且做了scoped处理(须在.spectre类下才生效),避免影响全局。 在dlgJson.html中引入了CSS库并对主题色做了定制适配。

CSS库文件制作于:git@github.com:skyshore2001/json-editor.git 下面的css目录。

2.1 各类型及常用格式

常见类型的示例参考web/schema-example.js。

关于布局:

{
  "type": "object",
  "format": "grid-strict",
  "properties": {
    "a": {
      "title": "a",
      "type": "string",
      "options": {
        "grid_columns": 4
      }
    },
    "b": {
      "title": "b",
      "type": "string",
      "options": {
        "grid_columns": 4,
        "grid_break": true
      }
    },
    "c": {
      "title": "c",
      "type": "string",
      "options": {
        "grid_columns": 6
      }
    },
    "d": {
      "title": "d",
      "type": "string",
      "options": {
        "grid_columns": 6
      }
    }
  }
}

2.2 示例:下拉列表

使用enum和enum_titles:

genSnFlag: {
    title: "genSnFlag/产品序列号规则",
    type: "integer",
    enum: [1, 2],
    options: {
        enum_titles: [
            "1-热像(10位:2位产品线+2位系列+6位数字)",
            "2-OEM(9位:9+4位订单号+4位数字)"
        ]
    }
},

或使用enumSource, source指定一个数组:

{
  "enumSource": [{
      // A watched field source
      "source": [
        {
          "value": 1,
          "title": "One"
        },
        {
          "value": 2,
          "title": "Two"
        }
      ],
      "title": "{{item.title}}",
      "value": "{{item.value}}"
    }]
  ]
}

动态下拉列表,使用enumSource:

{
  "type": "object",
  "properties": {
    "possible_colors": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "text": {
            "type": "string"
          }
        }
      }
    },
    "primary_color": {
      "type": "string",
      "watch": {
        "colors": "possible_colors"
      },
      "enumSource": [{
        "source": "colors",
        "value": "{{item.text}}"
      }]
    }
  }
}

更多用法,参考jsonEditor.README.md中Enum Values章节。

2.3 示例:设置对象上默认显示的选项

默认jsoneditor的配置会显示对象的所有选项,并在每个选项前添加checkbox。

{
    title: "虚拟字段配置",
    type: "array",
    format: "tabs",
    items: {
        title: "字段组",
        type: "object",
        properties: {
            res: {
                ...
                required: true
            },
            join: {
                ...
            },
            require: {
                ...
            },
            default: {
                ...
                required: true
            },
        },
        defaultProperties: ["res", "join", "default"],
    }
}

2.4 示例:重用定义

通过$ref引用:

var schema = {
  "type": "object",
  "properties": {
    "name": {
      "title": "Full Name",
      "$ref": "#/definitions/name"
    },
    "location": {
      "$ref": "http://mydomain.com/geo.json"
    }
  },
  "definitions": {
    "name": {
      "type": "string",
      "minLength": 5
    }
  }
};

({
    schema: schema,
    ajax: true
})

注意:引用外部文件限制只能为json文件,而且要加ajax:true选项。

递归调用,比如菜单结构,menus是menu的数组,每个menu又可以包含menus(子菜单): 使用oneOf区分不同类型。注意oneOf只能根据数据大类型区分(object,string,array,number,null等),如果类型相同,则无法自动区分,比如它无法识别不同结构的object。

{
    title: "菜单配置",
    $ref: "#/definitions/menus",
    format: "tabs-top",
    definitions: {
        menu: {
            title: "菜单项",
            headerTemplate: "{{self.name}}",
            type: "object",
            properties: {
                name: {
                    title: "菜单名",
                    type: "string",
                    required: true
                },
                value: {
                    title: "值",
                    oneOf: [
                        {
                            title: "链接/代码",
                            type: "string",
                            format: "textarea",
                            description: '示例: <code style="margin-left:10px">http://baidu.com</code> <code style="margin-left:10px">WUI.showPage("pageUi", "物料")</code>',
                            options: {
                                input_height: "200px",
                            }
                        },
                        {
                            title: "子菜单",
                            $ref: "#/definitions/menus"
                        }
                    ],
                    required: true
                },
            }
        },
        menus: {
            type: "array",
            format: "tabs",
            items: {
                $ref: "#/definitions/menu"
            },
        }
    }
}

2.5 示例:每种类型显示不同的字段

通过在 dependencies 中指定:

type: {
    title: "类型",
    type: "string",
    enum: [
        "s",
        "i",
        "n",
        "subobj"
    ],
    default: "s",
    required: true,
    options: {
        enum_titles: [
            "s-字符串",
            "i-整数",
            "n-小数",
            "subobj-子对象"
        ]
    }
},
// 仅当type=i时出现
linkTo: {
    type: "string",
    options: {
        dependencies: {
            type: "i"
        }
    }
},
// 仅当type=subobj时出现
uiMeta: {
    title: "uiMeta/页面名",
    type: "string",
    options: {
        dependencies: {
            type: "subobj"
        }
    }
}

2.6 示例:每种类型具有不同的值结构

结构为{type="mail|wxmsg", value},当type不同时,对应value的结构定义也不同。 json editor并不支持这种方式。 我们换用{type, value_mail, value_wxmsg}这种方式, 在显示对话框时(使用onInit回调)将原{type,value}格式转为此种json editor支持的格式, 在保存时(使用onValidate回调),再做相反的转换。

var schema = {
    title: "任务配置",
    type: "array",
    format: "tabs",

    items: {
        title: "任务配置",
        type: "object",
        headerTemplate: "{{self.name}}",
        properties: {
            type: {
                title: "类型",
                type: "string",
                enum: ["mail","wxmsg","httpCall"],
                default: "mail",
                required: true,
                options: {
                    enum_titles: [
                        "mail-邮件",
                        "wxmsg-微信公众号消息",
                    ]
                }
            },
            value_mail: {
                $ref: "#/definitions/mail",
                required: true,
                options: {
                    dependencies: {
                        type: "mail"
                    }
                }
            },
            value_wxmsg: {
                $ref: "#/definitions/wxmsg",
                required: true,
                options: {
                    dependencies: {
                        type: "wxmsg"
                    }
                }
            }
        }
    },
    definitions: {
        mail: {
            title: "邮件发送配置",
            type: "object",
            properties: {
                ...
            }
        },
        wxmsg: {
            title: "微信公众号消息配置",
            type: "object",
            properties: {
                ...
            }
        }
    }
};

({
    schema: schema,
    // {type, value} => {type, value_mail/value_wxmsg}
    onInit: function (data) {
        if (! $.isArray(data))
            return;
        data.forEach(function (e) {
            e['value_'+e.type] = e.value;
            delete e.value;
        });
    },
    // {type, value_mail/value_wxmsg} => {type, value}
    onValidate: function (data) {
        data.forEach(function (e) {
            e.value = e['value_'+e.type];
            for (var k in e) {
                if (k.indexOf('value_') == 0) {
                    delete e[k];
                }
            }
        });
    }
})

2.7 示例:使用代码编辑器

使用js编辑器输入js代码选项。指定type: "javascript"。 已内置ace编辑器,其中会检查代码语法。onNotify中对代码有没有运行错误进行检查(扩展功能,下面章节介绍)。

opt: {
    title: "opt/配置代码",
    type: "string",
    format: "javascript",
    options: {
        input_height: "200px",
        ace: {
            minLines: 5,
        },
        onNotify: function (val, isManualChange) {
            // 验证代码是否正确
            if (isManualChange && val) {
                WUI.evalOptions(val, {});
            }
        }
    },
    description: "<a class='easyui-linkbutton btnExample' href='javascript:;'>查看示例</a>"
},

使用php编辑器。指定type: "php"。 下面mode中的inline:true配置,用于指定是纯php代码,不带<?php头,不支持html混合。

onInit: {
    $ref: "#/definitions/phpCode",
},
definitions: {
    phpCode: {
        type: "string",
        format: "php",
        options: {
            dependencies: {
                type: "default"
            },
            ace: {
                mode: {path: "ace/mode/php", inline: true},
                minLines: 5
            }
        },
    }
}

2.8 输入关联数组(map/dictionary)

使用patternProperties选项。示例:录入id => {kind, name}结构的关联数组。

https://github.com/json-editor/json-editor/issues/144

注意:文档里没有提及patternProperties用法

{
  "title": "pets",
  "type": "object",
  "patternProperties": {
    "": {
      "type": "object",
      "properties": {
        "kind": {
          "type": "string",
          "enum": [
            "cat",
            "dog",
            "bird",
            "reptile",
            "other"
          ]
        },
        "name": {
          "type": "string"
        }
      }
    }
  }
}

patternProperties中如果指定“abc”, 则添加含有abc的就是应用指定格式。也可以用“^abc”

3 JSONEditor扩展功能

3.1 监控变化 onNotify(val, isManualChange)

扩展的editor option,在属性变化时回调,常用于某属性变化后修改、隐藏、灰掉另一属性。还可以用于设置title(取代schema的headerTemplate属性,比它更灵活).

注意:由于JSONEditor自带的watch及on(‘change’)等机制不好用,一是难以监测动态添加的数组元素, 二是在新创建、加载初始化、调整数组元素顺序、删除等操作时也会多次调用,且不易区分这些场景,会造成误修改数据。

在实现A属性变化时修改B属性时,若B属性是可以手工修改的,则一定要加isManualChange判断条件,否则就会误修改数据,比如修改完关闭窗口后再重新打开窗口,B属性会又被改回默认值。

3.1.1 示例1:修改其它属性

示例:当type值变化后(是个下拉框),自动填写name值。

type: {
    title: "类型",
    ...
    options: {
        onNotify: function (val, isManualChange) {
            if (isManualChange) {
                var ed = this.parent.editors["name"];
                ed.setValue(val);
            }
        }
    }
},

示例:当uiType下拉框变化时,若值为空,则隐藏opt属性。同时,若值修改为“subobj”,则自动将type属性也修改为“subobj” 注意:JSONEditor自带的dependencies机制,但只能根据下拉框(enum)中的具体某个值来隐藏其它属性,不够灵活。

opt: {
  type: "textarea",
  options: {
    dependencies: {
      uiType: "subobj"
    }
  }
}

实现:使用onNotify回调:

...
    type: "object",
    properties: {
        type: {
            title: "类型",
            type: "string",
            enum: [
                "s",
                "i",
                "subobj"
            ],
            default: "s",
            options: {
                enum_titles: [
                    "s-字符串",
                    "i-整数",
                    "subobj-子对象"
                ]
            }
        },
        uiType: {
            title: "uiType/UI类型",
            type: "string",
            enum: [
                "combo",
                "upload",
                "subobj"
            ],
            default: null,
            options: {
                enum_titles: [
                    "combo:下拉列表-值映射",
                    "upload:图片或文件",
                    "subobj:子对象"
                ],
                onNotify: function (val, isManualChange) {
                    // 获取同级其它editor
                    var edOpt = this.parent.editors["opt"];
                    // editor可能不存在,比如从properties对话框中没有选择它
                    if (edOpt) {
                        // 当isManualChange参数为false时,由于其它editor此时可能尚未初始化完,所以对它的操作放在setTimeout中避免出错
                        setTimeout(function () {
                            // 显示或隐藏元素,用ed.container取到其DOM元素
                            $(edOpt.container).toggle(!!val);
                            // edOpt.disable();
                        });
                    }
                    // 加isManualChange判断,意味着如果初始化值uiType为subobj但type是其它值,就不会去处理它
                    if (isManualChange && val == "subobj") {
                        var edType = this.parent.editors["type"];
                        edType.setValue("subobj");
                    }
                }
            }
        },
        opt: {
            type: "string",
            format: "textarea"
        }
    }
  1. 在name.options.onNotify中写逻辑,函数中,this是当前属性(即name属性)的editor,参数val是当前值。
  2. 通过this.parent.editors[属性名]可以取到同级其它属性的editor。 取全局editor可以用this.jsoneditor.getEditor("root.0.type")
  3. 通过ed.setValue(val)设置值,通过ed.container取对应DOM元素。
  4. 由于其它属性可能尚未初始化完(操作顺序在当前editor后面的其它editor时),用setTimeout来操作避免出错。

3.1.2 示例2:修改其它属性的默认值

示例:当name修改时,自动根据name填写type默认值。

{
    type: "array",
    items: {
        title: "字段",
        type: "object",
        properties: {
            name: {
                title: "名称",
                type: "string",
                options: {
                    // !!! 在name属性下的options下 !!!
                    onNotify: function (val, isManualChange) {
                        if (! isManualChange)
                            return;
                        var typeVal = UiMeta.guessType(val);
                        var edType = this.parent.editors["type"];
                        edType.setValue(typeVal);
                    }
                }
            },
            type: {
                title: "类型",
                type: "string",
            }
        }
    }
}

由于只是填写默认值,人工可以再修改它。所以必须通过isManualChange判断后再修改,避免初始化、数组移动等操作导致人工修改的值被重置为默认。

3.1.3 示例3:修改标签标题

以tab页签方式显示数组,页签标题为name和type属性拼合而成:

{
    title: "字段配置",
    type: "array",
    format: "tabs", // tab页签式显示
    items: {
        title: "字段",
        type: "object",
        // headerTemplate: "{{self.name}}({{self.type}})",
        options: {
            // 与上面注释掉的headerTemplate作用相同;函数式处理更灵活,可用于无法简单属性拼接等复杂情形
            onNotify: function (val, isManualChange) {
                return val.name + '(' + val.type + ')';
            },
        },
        properties: {
            name: {
                title: "名称",
                type: "string",
            },
            type: {
                title: "名称",
                type: "string",
            }
        }
    }
}

此例中设置标题逻辑比较简单,一般建议直接使用JSONEditor默认支持的headerTemplate。

以下是个复杂示例,它根据res数组及join属性,自动生成title,逻辑为:

3.2 onClick(ev) 添加自定义按钮并处理事件

示例:为opt属性框添加“查看示例”按钮,点击则根据同级uiType属性的值,自动将示例填入opt属性框

实现:schema/uicols.js中定义如下:

        title: "字段",
        type: "object",
        properties: {
            ...
            uiType: {
                title: "uiType/UI类型",
                type: "string",
                enum: [
                    "combo",
                    "subobj"
                ],
                default: null,
            },
            opt: {
                title: "opt/配置代码",
                type: "string",
                format: "textarea",
                options: {
                    input_height: "200px",
                    onClick: function (ev) {
                        if ($(ev.target).is(".btnExample")) {
                            var field = this.parent.getValue();
                            this.setValue(examples[field.uiType]);
                        }
                    }
                },
                description: "<a class='easyui-linkbutton btnExample' href='javascript:;'>查看示例</a>"
            }
        }

3.3 其它扩展功能

3.4 页面调整

3.4.1 隐藏Tab下多余标题

({
    schema: schema,
    disable_array_delete_all_rows: true,
    disable_array_delete_last_row: true,
    disable_collapse: true,
    disable_edit_json: true,
    disable_properties: true,
    onReady: function () {
        // this: jsoneditor对象
        // this.root_container: DOM对象
        var jo = $(jsonEditor.root_container);
        jo.find(".card-title.je-object__title").not(":has(':checkbox')").hide()
    }
})

3.4.2 带滚动条的table来显示array元素下字段很多的情况

比如将root.alarmRuleList下的表格字段很多,为避免字段宽度过小,将它放大3倍,并显示横向滚动条。在onReady中:

var jo = $(jsonEditor.root_container);
var j1 = jo.find("[data-schemapath='root.alarmRuleList']");
j1.find(".card:first").css("overflow", "auto");
j1.find(".card:first table:first").css("width", "300%");

该字段定义示例为:

alarmRuleList: {
    type: "array",
    format: "table",
    items: {
        type: "object",
        properties: { ... }
    }
}