/** * @file 生成一些通用的配置项 */ /** * 创建一个层级,比如 name 是 title,这里面的子组件 name 就相当于 title.name * @param name * @param label */ export const createHierarchy = (name: string, controls: any[]) => { return { name: name, type: 'combo', label: '', noBorder: true, multiLine: true, controls: controls }; }; /** * 方便生成颜色类的控件 * @param name * @param label */ export const color = ( name: string, label: string, defaultColor?: string, labelRemark?: string ) => { return { type: 'color', format: 'rgba', pipeIn: (value: any) => { if ( typeof value === 'undefined' && defaultColor && defaultColor !== 'null' ) { return defaultColor; } return value; }, labelRemark: labelRemark ? { type: 'remark', content: labelRemark } : undefined, name: name, label: label }; }; /** * 默认值是 true 的 switch,并且 inline * @param name * @param label */ export const trueSwitch = ( name: string, label: string, labelRemark?: string ) => { return { type: 'switch', name: name, mode: 'inline', className: 'w-full', label: label, labelRemark: labelRemark ? { type: 'remark', content: labelRemark } : undefined, pipeIn: (value: any) => { if (typeof value === 'undefined') { return true; } return value; } }; }; /** * 默认值是 false 的 switch,并且 inline * @param name * @param label */ export const falseSwitch = ( name: string, label: string, labelRemark?: string ) => { return { type: 'switch', name: name, mode: 'inline', className: 'w-full', labelRemark: labelRemark ? { type: 'remark', content: labelRemark } : undefined, label: label }; }; /** * 如果没数据就默认用第一个的 select * @param name * @param label * @param options */ export const select = ( name: string, label: string, options: any[], labelRemark?: string ) => { return { type: 'select', name: name, label: label, labelRemark: labelRemark ? { type: 'remark', content: labelRemark } : undefined, pipeIn: (value: any) => { if (typeof value === 'undefined') { return options[0]; } return value; }, options: options }; }; /** * fieldSet 辅助 * @param label * @param controls * @param collapsed */ export const fieldSet = ( label: string, controls: any[], collapsed: boolean = false ) => { return { type: 'fieldSet', title: label, collapsable: true, collapsed: collapsed, mountOnEnter: true, controls: controls }; }; /** * 控制一组控件的显隐 * @param visibleOn * @param controls */ export const visibleOn = (visibleOn: string, controls: any[]) => { return { type: 'container', visibleOn: visibleOn, controls: controls }; }; /** * 方便生成数字类的控件 * @param name * @param label * @param isInteger 是否是整数 */ export const number = ( name: string, label: string, labelRemark?: string, defaultNumber?: number, min: number = -1e4, max: number = 1e4, step: number = 1 ) => { const control: any = { type: 'number', name: name, label: label }; if (labelRemark) { control.labelRemark = { type: 'remark', content: labelRemark }; } if (typeof defaultNumber !== 'undefined') { control.pipeIn = (value: any) => { if (typeof value === 'undefined') { return defaultNumber; } return value; }; } control.min = min; control.max = max; control.step = step; return control; }; /** * 生成文本控件 * @param name * @param label * @param labelRemark * @param defaultText */ export const text = ( name: string, label: string, labelRemark?: string, defaultText?: string ) => { const control: any = { type: 'text', name: name, label: label }; if (labelRemark) { if (labelRemark) { control.labelRemark = { type: 'remark', content: labelRemark }; } } if (typeof defaultText !== 'undefined') { control.pipeIn = (value: any) => { if (typeof value === 'undefined') { return defaultText; } return value; }; } return control; }; /** * 用于 left/top 等,支持三种类型,一种是关键字,比如 auto,一种是数字,另一种是百分比 * @param name * @param label * @param labelForRadio 切换按钮的文字 * @param keywordList 关键字列表 */ export const viewportControl = ( name: string, label: string, labelForRadio: string, keywordList: string[] ) => { return [ { type: 'button-group', name: name, label: labelForRadio, options: [ { label: '关键字', value: keywordList[0] }, { label: '数字', value: 2 }, { label: '百分比', value: 3 } ], pipeIn: value => { if (typeof value === 'undefined') { return keywordList[0]; } if (typeof value === 'string') { if (value.indexOf('%') !== -1) { return 3; } else { return keywordList[0]; } } else if (typeof value === 'number') { return 2; } else { return keywordList[0]; } }, pipeOut: value => { if (value === 1) { return keywordList[0]; } else if (value === 2) { return 0; } else if (value === 3) { return '0%'; } else { return keywordList[0]; } } }, { type: 'select', name: name, label: label, options: keywordList, visibleOn: `(typeof this.${name} === "undefined") || ((typeof this.${name} === "string") && (this.${name}.indexOf("%") === -1))` }, { type: 'number', name: name, label: label, visibleOn: `(typeof this.${name} === "number")` }, { type: 'text', name: name, label: label, visibleOn: `(typeof this.${name} === "string") && (this.${name}.indexOf("%") !== -1)` } ]; }; /** * 设置某个组件在容器中的距离 * @param label 前缀 * @param label 组件名 */ export const viewport = (scope: string, label: string) => { return { type: 'fieldSet', title: '离容器边距', collapsable: true, collapsed: true, mountOnEnter: true, controls: [ ...viewportControl( `${scope}left`, `${label}离容器左侧的距离`, '左侧距离值类型', ['auto', 'left', 'center', 'right'] ), ...viewportControl( `${scope}top`, `${label}离容器上侧的距离`, '上侧距离值类型', ['auto', 'top', 'middle', 'bottom'] ), ...viewportControl( `${scope}right`, `${label}离容器右侧的距离`, '右侧距离值类型', ['auto'] ), ...viewportControl( `${scope}bottom`, `${label}离容器下侧的距离`, '下侧距离值类型', ['auto'] ) ] }; }; /** * 设置宽度的便捷方法 * @param label */ export const width = (label: string) => { return keywordOrNumber('width', `${label}的宽度`, '宽度的单位', ['auto']); }; /** * 设置高度的便捷方法 * @param label */ export const height = (label: string) => { return keywordOrNumber('height', `${label}的高度`, '高度的单位', ['auto']); }; /** * 设置 padding 的便捷方法 * @param label */ export const padding = (label: string) => { return numberOrArray('padding', label + '内边距', '单独设置每个内边距'); }; /** * 设置 orient 的便捷方法 * @param label */ export const origin = (label: string) => { return select('origin', `${label}列表的布局朝向`, ['horizontal', 'vertical']); }; /** * formatter 的简便方法,不过这里不支持函数 * @param label */ export const formatter = (label: string) => { return { type: 'text', name: 'formatter', label: `格式化${label}文本` }; }; /** * selectedMode 的简便写法 * @param label */ export const selectedMode = (label: string) => { return booleanOrKeyword('selectedMode', `${label}选择的模式`, [ {label: '单选', value: 'single'}, {label: '多选', value: 'multiple'} ]); }; /** * 生成动画相关的控件 * @param scope */ export const animation = (scope?: string, collapsed: boolean = true) => { let prefix = ''; if (scope) { prefix = `${scope}.`; } const easing = [ 'linear', 'quadraticIn', 'quadraticOut', 'quadraticInOut', 'cubicIn', 'cubicOut', 'cubicInOut', 'quarticIn', 'quarticOut', 'quarticInOut', 'quinticIn', 'quinticOut', 'quinticInOut', 'sinusoidalIn', 'sinusoidalOut', 'sinusoidalInOut', 'exponentialIn', 'exponentialOut', 'exponentialInOut', 'circularIn', 'circularOut', 'circularInOut', 'elasticIn', 'elasticOut', 'elasticInOut', 'backIn', 'backOut', 'backInOut', 'bounceIn', 'bounceOut', 'bounceInOut' ]; return fieldSet('动画', [ trueSwitch(`${prefix}animation`, '是否开启动画'), visibleOn(`this.${prefix}animation`, [ number( `${prefix}animationThreshold`, '是否开启动画的阈值', '当单个系列显示的图形数量大于这个阈值时会关闭动画', 2000 ), number( `${prefix}animationDuration`, '初始动画的时长', '支持回调函数,可以通过每个数据返回不同的时长实现更戏剧的初始动画效果,不过只能写代码', 1000 ), select(`${prefix}animationEasing`, '初始动画的缓动效果', easing), number( `${prefix}animationDelay`, '初始动画的延迟', '初始动画的延迟,支持回调函数,可以通过每个数据返回不同的 delay 时间实现更戏剧的初始动画效果', 0 ) ]), number( `${prefix}animationDurationUpdate`, '数据更新动画的时长', '支持回调函数,可以通过每个数据返回不同的时长实现更戏剧的更新动画效果', 200 ), select(`${prefix}animationEasingUpdate`, '数据更新动画的缓动效果', easing), number( `${prefix}animationDelayUpdate`, '数据更新动画的延迟', '支持回调函数,可以通过每个数据返回不同的 delay 时间实现更戏剧的更新动画效果' ) ]); }; /** * icon 的简版写法 */ export const icon = (label: string) => { return keywordOrString( 'icon', `${label}的 icon`, '切换类型为 url', [ 'circle', 'rect', 'roundRect', 'triangle', 'diamond', 'pin', 'arrow', 'none' ], 'image://http://', `可以通过 'image://url' 设置为图片,其中 URL 为图片的链接,或者 dataURI。 URL 为图片链接例如: 'image://http://xxx.xxx.xxx/a/b.png' URL 为 dataURI 例如: 'image://data:image/gif;base64,R0lGODlhEAAQAMQAAORHHOVSKudfOulrSOp3WOyDZu6QdvCchPGolfO0o/XBs/fNwfjZ0frl3/zy7////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAABAALAAAAAAQABAAAAVVICSOZGlCQAosJ6mu7fiyZeKqNKToQGDsM8hBADgUXoGAiqhSvp5QAnQKGIgUhwFUYLCVDFCrKUE1lBavAViFIDlTImbKC5Gm2hB0SlBCBMQiB0UjIQA7' 可以通过 'path://' 将图标设置为任意的矢量路径。这种方式相比于使用图片的方式,不用担心因为缩放而产生锯齿或模糊,而且可以设置为任意颜色。路径图形会自适应调整为合适的大小。路径的格式参见 SVG PathData。可以从 Adobe Illustrator 等工具编辑导出。` ); }; /** * 用于生成类似 padding 那种可以是数字或数组的控件 * @param name * @param label * @param labelForSwitch 切换按钮的文字 * @param defaultNumber 默认数字 * @param defaultArray 默认数组 */ export const numberOrArray = ( name: string, label: string, labelForSwitch: string, defaultNumber: number = 5, defaultArray = [0, 0, 0, 0] ) => { return [ { type: 'group', controls: [ { type: 'number', name: name, hiddenOn: `Array.isArray(this.${name})`, label: label }, { type: 'switch', name: name, label: labelForSwitch, pipeIn: (value: any) => { return Array.isArray(value); }, pipeOut: (value: any, oldValue: any) => { return value ? defaultArray : defaultNumber; } } ] }, { type: 'array', name: name, label: label, labelRemark: '设置两个值将分别是上下、左右;设置四个值则分别是上、右、下、左', visibleOn: `Array.isArray(this.${name})`, minLength: 2, maxLength: 4, items: { type: 'number' } } ]; }; /** * 用于生成类似 padding 那种可以是数字或数组的控件 * @param name * @param label * @param labelForSwitch 切换按钮的文字 * @param labelRemark remark * @param defaultNumber 默认数字 * @param defaultArray 默认数组 */ export const vector = ( name: string, label: string, labelForSwitch: string, labelRemark?: string, defaultNumber: number = 0, defaultArray = [0, 0, 0, 0] ) => { return [ { type: 'array', name: name, label: label, visibleOn: `Array.isArray(this.${name})`, minLength: 2, maxLength: 4, items: { type: 'number' } }, { type: 'group', controls: [ { type: 'number', name: name, hiddenOn: `Array.isArray(this.${name})`, label: label }, { type: 'switch', name: name, label: labelForSwitch, pipeIn: (value: any) => { return Array.isArray(value); }, pipeOut: (value: any) => { return value ? defaultArray : defaultNumber; }, labelRemark: labelRemark ? { type: 'remark', content: labelRemark } : undefined } ] } ]; }; /** * 阴影相关的控件 */ export const shadowControls = (scope: string) => { return { type: 'fieldSet', title: '阴影', collapsable: true, collapsed: true, mountOnEnter: true, controls: [ color(`${scope}shadowColor`, '阴影颜色'), number(`${scope}shadowBlur`, '阴影模糊大小'), number(`${scope}shadowOffsetX`, '阴影水平方向上的偏移距离'), number(`${scope}shadowOffsetY`, '阴影垂直方向上的偏移距离') ] }; }; /** * 关键字或数字,比如 fontWeight,可以是 normal,或者 100 * @param name * @param label * @param labelForSwitch * @param keywordList */ export const keywordOrNumber = ( name: string, label: string, labelForSwitch: string, keywordList: string[], defaultNumber: number = 100 ) => { return { type: 'group', controls: [ { type: 'switch', label: labelForSwitch, name: name, pipeIn: (value: any, data) => { if (typeof data[name] === 'undefined') { return false; } return typeof data[name] !== 'string'; }, pipeOut: (value: any, oldValue: any, data: any) => { if (value) { return defaultNumber; } else { return keywordList[0]; } } }, { type: 'number', name: name, visibleOn: `typeof(this.${name}) === 'number'`, label: label }, { type: 'select', name: name, label: label, visibleOn: `typeof(this.${name}) === 'undefined' || typeof(this.${name}) === 'string'`, options: keywordList } ] }; }; /** * 可以是关键字或者是字符串 * @param name * @param label * @param labelForSwitch * @param keywordList * @param remark */ export const keywordOrString = ( name: string, label: string, labelForSwitch: string, keywordList: string[], defaultString: string = '', remark?: string ) => { return { type: 'group', controls: [ { type: 'switch', label: labelForSwitch, name: name, pipeIn: (value: any) => { if (typeof value === 'undefined') { return false; } return keywordList.indexOf(value) === -1; }, pipeOut: (value: any, oldValue: any, data: any) => { if (value) { return defaultString; } else { return keywordList[0]; } }, labelRemark: remark }, { type: 'text', name: name, visibleOn: `this.${name} && ${JSON.stringify( keywordList )}.indexOf(this.${name}) === -1`, label: label }, { type: 'select', name: name, label: label, visibleOn: `typeof(this.${name}) === 'undefined' || ${JSON.stringify( keywordList )}.indexOf(this.${name}) !== -1`, options: keywordList } ] }; }; /** * 对象或者对象数组 * TODO: 这样会造成配置项 double,是否有更好的方法? * @param name * @param label * @param labelForSwitch * @param keywordList * @param controls */ export const objectOrArray = ( name: string, labelForSwitch: string, controls: any[] ) => { return [ { type: 'switch', label: labelForSwitch, mode: 'inline', name: name, pipeIn: (value: any) => { if (typeof value === 'undefined') { return false; } return Array.isArray(value); }, pipeOut: (value: any, oldValue: any, data: any) => { if (value) { if (data[name]) { return [data[name]]; } return [{}]; } else { if (data[name] && Array.isArray(data[name])) { return data[name][0]; } return {}; } } }, { type: 'combo', name: name, label: '', noBorder: true, multiLine: true, visibleOn: `typeof(this.${name}) === 'undefined' || !Array.isArray(this.${name})`, multiple: false, controls: controls }, { type: 'combo', name: name, label: '', noBorder: true, multiLine: true, visibleOn: `Array.isArray(this.${name})`, multiple: true, controls: controls } ]; }; /** * 常见样式设置 * @param label */ export const commonStyle = (scope: string, label: string) => { return [ color(`${scope}backgroundColor`, `${label}背景色,默认透明`), color(`${scope}borderColor`, `${label}的边框颜色`), number(`${scope}borderWidth`, `${label}的边框线宽`), ...numberOrArray( `${scope}borderRadius`, '圆角半径', '单独设置每个圆角半径' ), shadowControls(scope) ]; }; /** * 布尔类型或关键字,优先布尔类型 * @param name * @param label * @param keywordList */ export const booleanOrKeyword = ( name: string, label: string, keywordList: any[], defaultBoolean: boolean = true ) => { return { type: 'select', name: name, label: label, pipeIn: (value: any) => { if (typeof value === 'undefined') { return defaultBoolean ? 'true' : 'false'; } return value; }, pipeOut: (value: any) => { if (value === 'true') { return true; } else if (value === 'false') { return false; } else { return value; } }, options: [ {label: '开启', value: 'true'}, {label: '关闭', value: 'false'}, ...keywordList ] }; }; /** * enum 辅助方法 * @param name * @param label * @param keywordList */ export const enumControl = ( name: string, label: string, keywordList: any[], defaultValue: string, labelRemark?: string ) => { return { type: 'select', name: name, label: label, labelRemark: labelRemark ? { type: 'remark', content: labelRemark } : undefined, pipeIn: (value: any) => { if (typeof value === 'undefined') { defaultValue; } if (value === true) { return 'true'; } if (value === false) { return 'false'; } return value; }, pipeOut: (value: any) => { if (value === 'true') { return true; } else if (value === 'false') { return false; } else { return value; } }, options: keywordList }; }; /** * 数字或百分比 * @param name * @param label * @param labelForSwitch * @param keywordList */ export const numberOrPercentage = ( name: string, label: string, labelForSwitch: string, labelRemark?: string, defaultPercent: string = '1%', defaultNumber: number = 100 ) => { return [ { type: 'group', controls: [ { type: 'switch', label: labelForSwitch, name: name, pipeIn: (value: any) => { if (typeof value === 'undefined') { return false; } return typeof value !== 'string'; }, pipeOut: (value: any) => { if (value) { return defaultNumber; } else { return defaultPercent; } }, labelRemark: labelRemark ? { type: 'remark', content: labelRemark } : undefined }, { type: 'number', name: name, visibleOn: `typeof(this.${name}) === 'number'`, label: label }, { type: 'text', name: name, label: label, visibleOn: `typeof(this.${name}) === 'undefined' || typeof(this.${name}) === 'string'` } ] } ]; }; /** * 生成文本样式的控件 * @param scope */ export const textStyleControls = (scope: string, label: string) => { return createHierarchy(scope, [ { type: 'fieldSet', title: label, collapsable: true, controls: [ color('color', `${label}文字的颜色`), select('fontStyle', `${label}文字字体的风格`, [ 'normal', 'italic', 'oblique' ]), keywordOrNumber( 'fontWeight', `${label}文字字体的粗细`, '字体粗细格式使用数字', ['normal', 'bold', 'bolder', 'lighter'] ), { type: 'text', name: 'fontFamily', label: `${label}文字的字体`, options: [ 'Arial', 'Bookman', 'Candara', 'Courier New', 'Courier', 'Garamond', 'Georgia', 'Helvetica', 'Impact', 'Microsoft YaHei', 'Monaco', 'monospace', 'Palatino', 'Roboto', 'sans-serif', 'serif', 'Times New Roman', 'Verdana' ] }, number('fontSize', `${label}文字的字体大小`), number('lineHeight', `${label}行高`), // TODO:用处不大,要不别支持了 numberOrPercentage('width', '文字块宽度', '宽度使用数字'), numberOrPercentage('height', '文字块高度', '高度使用数字'), color('textBorderColor', '文字本身的描边颜色'), number('textBorderWidth', '文字本身的描边宽度'), color('textShadowColor', '文字本身的阴影颜色'), number('textShadowBlur', '文字本身的阴影长度'), number('textShadowOffsetX', '文字本身的阴影 X 偏移'), number('textShadowOffsetY', '文字本身的阴影 Y 偏移') // TODO: rich 暂时不支持 ] } ]); }; // 对于一些没正确识别的 label 手动处理 const FIX_LABEL = { symbolKeepAspect: '是否在缩放时保持该图形的长宽比', pageIcons: '图例控制块的图标', pageIconColor: '翻页按钮的颜色', pageIconInactiveColor: '翻页按钮不激活时(即翻页到头时)的颜色', pageIconSize: '翻页按钮的大小', scrollDataIndex: '图例当前最左上显示项的 dataIndex', pageButtonItemGap: '图例控制块中,按钮和页信息之间的间隔', pageButtonGap: '图例控制块和图例项之间的间隔', pageButtonPosition: '图例控制块的位置', pageFormatter: '图例控制块中,页信息的显示格式', appendToBody: '是否添加为 HTML 的
的子节点' }; /** * 构建某个基于文档的控件 * @param name * @param option */ const buildOneOption = (scope: string, name: string, option: any) => { if (name.indexOf('<') !== -1) { return; } if (name.indexOf('.0') != -1) { return; } if (name.indexOf('.1') != -1) { return; } const desc = option.desc.trim(); const uiControl = option.uiControl; if (!desc) { console.warn('must have desc', name); return false; } const descSplit = desc.split('。'); let label = descSplit[0] .trim() .replace('', '') .replace(/</g, '<') .replace(/>/g, '>') .replace(/'/g, '') .replace(/<\/?[^>]+(>|$)/g, ''); let remark = descSplit.length > 1 ? descSplit[1].trim().replace('
', '') : ''; // 有些描述太长了,再通过逗号拆分一下 const labelSplitComma = label.split(','); if (labelSplitComma.length > 1) { label = labelSplitComma[0]; remark = labelSplitComma[1] + remark; } if (name in FIX_LABEL) { label = FIX_LABEL[name]; } remark = `「${name}」${remark}`; name = scope + name; if (!uiControl || !uiControl.type) { // 这种可能只有 desc return text(name, label, remark); } const uiControlType = uiControl.type; if (uiControlType === 'boolean') { if (uiControl.default) { return trueSwitch(name, label, remark); } else { return falseSwitch(name, label, remark); } } else if (uiControlType === 'color') { return color(name, label, uiControl.default, remark); } else if (uiControlType === 'number' || uiControlType === 'angle') { const defaultValue = typeof uiControl.default === 'undefined' ? 0 : +uiControl.default; const min = typeof uiControl.min === 'undefined' ? -1e4 : +uiControl.min; const max = typeof uiControl.max === 'undefined' ? 1e4 : +uiControl.max; const step = typeof uiControl.step === 'undefined' ? 1 : +uiControl.step; return number(name, label, remark, defaultValue, min, max, step); } else if (uiControlType === 'percent') { return numberOrPercentage(name, label, '使用绝对值', remark); } else if (uiControlType === 'enum') { if (uiControl.options) { return enumControl( name, label, uiControl.options.split(',').map((item: string) => item.trim()), uiControl.default, remark ); } else { console.warn('enum do not have options, fallback to text', name); return text(name, label, remark); } } else if (uiControlType === 'vector') { return vector(name, label, '单独设置', remark); } else if (uiControlType === 'percentvector') { // TODO: 可能需要特殊处理 // return vector(name, label, '单独设置', remark); } else if (uiControlType === 'text') { // return text(name, label, remark, uiControl.default); } else { console.warn('unknow type', name, uiControlType); } }; export const buildGroupOptions = ( scope: string, parentName: string, options: any ) => { let controls = []; for (const name in options) { if (name.startsWith(parentName + '.')) { const control = buildOneOption(scope, name, options[name]); if (control) { if (Array.isArray(control)) { controls = controls.concat(control); } else { controls.push(control); } } } } return controls; }; /** * 基于 ECharts 文档的数据构建控件,对常见元素做分组 * @param scope 前缀,虽然可以用 combo,但加前缀可以方便 debug,空字符串就是没前缀 * @param options */ export const buildOptions = (scope: string, options: any) => { const commonStyleKeys = new Set([ 'backgroundColor', 'borderColor', 'borderWidth', 'borderRadius' ]); const viewportKeys = new Set(['left', 'top', 'right', 'bottom']); // 没啥用的 const uselessKeys = new Set(['id', 'z', 'zlevel']); let controls = []; // 有些属性有深层结构,对它们进行特殊处理,使用 fieldSet 来自动折叠 const groupKeys = new Set(); for (const name in options) { if (name.indexOf('.') !== -1) { groupKeys.add(name.split('.')[0]); } } for (const name in options) { // if (!name.startsWith('label.padding')) { // continue; // 用于开发时单独测试某个属性 // } // 这些样式单独处理或忽略 if ( commonStyleKeys.has(name) || viewportKeys.has(name) || uselessKeys.has(name) ) { continue; } if ( name.startsWith('textStyle') || name.startsWith('subtextStyle') || name.startsWith('pageTextStyle') || name.startsWith('animation') ) { continue; } // TODO: 后续单独处理 if (name === 'data' || name === 'tooltip') { continue; } if (groupKeys.has(name)) { controls.push( fieldSet(name, buildGroupOptions(scope, name, options), true) ); } if (name.indexOf('.') !== -1) { // 前面已经处理了 continue; } const control = buildOneOption(scope, name, options[name]); if (control) { if (Array.isArray(control)) { controls = controls.concat(control); } else { controls.push(control); } } else { console.warn('build control error', name); } } return controls; };