amis
Version:
一种MIS页面生成工具
856 lines (737 loc) • 23.4 kB
text/typescript
import isPlainObject = require('lodash/isPlainObject');
import transform = require('lodash/transform');
import isEqual = require('lodash/isEqual');
import lodashIsObject = require('lodash/isObject');
import uniq = require('lodash/uniq');
import {Schema, PlainObject, FunctionPropertyNames} from '../types';
import {evalExpression} from './tpl';
import {boundMethod} from 'autobind-decorator';
import qs from 'qs';
import {IIRendererStore} from '../store';
import {IFormStore} from '../store/form';
// 方便取值的时候能够把上层的取到,但是获取的时候不会全部把所有的数据获取到。
export function createObject(
superProps?: {[propName: string]: any},
props?: {[propName: string]: any},
properties?: any
): object {
if (superProps && Object.isFrozen(superProps)) {
superProps = cloneObject(superProps);
}
const obj = superProps
? Object.create(superProps, {
...properties,
__super: {
value: superProps,
writable: false,
enumerable: false
}
})
: Object.create(Object.prototype, properties);
props && Object.keys(props).forEach(key => (obj[key] = props[key]));
return obj;
}
export function cloneObject(from: any) {
const obj =
from && from.__super
? Object.create(from.__super, {
__super: {
value: from.__super,
writable: false,
enumerable: false
}
})
: Object.create(Object.prototype);
from && Object.keys(from).forEach(key => (obj[key] = from[key]));
return obj;
}
export function extendObject(to: any, from?: any) {
const obj = cloneObject(to);
from && Object.keys(from).forEach(key => (obj[key] = from[key]));
return obj;
}
export function syncDataFromSuper(
data: any,
superObject: any,
prevSuperObject: any,
force?: boolean,
store?: IIRendererStore
) {
const obj = {
...data
};
let keys = Object.keys(obj);
// 如果是 form store,则从父级同步 formItem 种东西。
if (store && store.storeType === 'FormStore') {
keys = uniq((store as IFormStore).items.map(item => item.name));
force = false;
}
if (superObject || prevSuperObject) {
keys.forEach(key => {
if (
((superObject && typeof superObject[key] !== 'undefined') ||
(prevSuperObject && typeof prevSuperObject[key] !== 'undefined')) &&
(force ||
(prevSuperObject && !superObject) ||
(!prevSuperObject && superObject) ||
prevSuperObject[key] !== superObject[key])
) {
obj[key] = superObject[key];
}
});
}
return obj;
}
/**
* 生成 8 位随机数字。
*
* @return {string} 8位随机数字
*/
export function guid() {
function s4() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}
return s4() + s4() + s4();
}
export function findIndex(arr: Array<any>, detect: (item?: any, index?: number) => boolean) {
for (let i = 0, len = arr.length; i < len; i++) {
if (detect(arr[i], i)) {
return i;
}
}
return -1;
}
export function getVariable(data: {[propName: string]: any}, key: string, canAccessSuper: boolean = true): any {
if (!data || !key) {
return undefined;
} else if (canAccessSuper ? key in data : data.hasOwnProperty(key)) {
return data[key];
}
return key
.split('.')
.reduce(
(obj, key) =>
obj && typeof obj === 'object' && (canAccessSuper ? key in obj : obj.hasOwnProperty(key))
? obj[key]
: undefined,
data
);
}
export function setVariable(data: {[propName: string]: any}, key: string, value: any) {
data = data || {};
if (key in data) {
data[key] = value;
return;
}
const parts = key.split('.');
const last = parts.pop() as string;
while (parts.length) {
let key = parts.shift() as string;
if (isPlainObject(data[key])) {
data = data[key] = {
...data[key]
};
} else if (data[key]) {
// throw new Error(`目标路径不是纯对象,不能覆盖`);
// 强行转成对象
data[key] = {};
data = data[key];
} else {
data[key] = {};
data = data[key];
}
}
data[last] = value;
}
export function deleteVariable(data: {[propName: string]: any}, key: string) {
if (!data) {
return;
} else if (data.hasOwnProperty(key)) {
delete data[key];
return;
}
const parts = key.split('.');
const last = parts.pop() as string;
while (parts.length) {
let key = parts.shift() as string;
if (isPlainObject(data[key])) {
data = data[key] = {
...data[key]
};
} else if (data[key]) {
throw new Error(`目标路径不是纯对象,不能修改`);
} else {
break;
}
}
if (data && data.hasOwnProperty && data.hasOwnProperty(last)) {
delete data[last];
}
}
export function hasOwnProperty(data: {[propName: string]: any}, key: string): boolean {
const parts = key.split('.');
while (parts.length) {
let key = parts.shift() as string;
if (!isObject(data) || !data.hasOwnProperty(key)) {
return false;
}
data = data[key];
}
return true;
}
export function noop() {}
export function anyChanged(
attrs: string | Array<string>,
from: {[propName: string]: any},
to: {[propName: string]: any},
strictMode: boolean = true
): boolean {
return (typeof attrs === 'string' ? attrs.split(/\s*,\s*/) : attrs).some(key =>
strictMode ? from[key] !== to[key] : from[key] != to[key]
);
}
export function rmUndefined(obj: PlainObject) {
const newObj: PlainObject = {};
if (typeof obj !== 'object') {
return obj;
}
const keys = Object.keys(obj);
keys.forEach(key => {
if (obj[key] !== undefined) {
newObj[key] = obj[key];
}
});
return newObj;
}
export function isObjectShallowModified(
prev: any,
next: any,
strictMode: boolean = true,
ignoreUndefined: boolean = false
) {
if (null == prev || null == next || typeof prev !== 'object' || typeof next !== 'object') {
return strictMode ? prev !== next : prev != next;
}
if (ignoreUndefined) {
prev = rmUndefined(prev);
next = rmUndefined(next);
}
const keys = Object.keys(prev);
const nextKeys = Object.keys(next);
if (keys.length !== nextKeys.length || keys.join(',') !== nextKeys.join(',')) {
return true;
}
for (let i: number = keys.length - 1; i >= 0; i--) {
let key = keys[i];
if (
strictMode ? next[key] !== prev[key] : isObjectShallowModified(next[key], prev[key], false, ignoreUndefined)
) {
return true;
}
}
return false;
}
export function isArrayChilrenModified(prev: Array<any>, next: Array<any>, strictMode: boolean = true) {
if (!Array.isArray(prev) || !Array.isArray(next)) {
return strictMode ? prev !== next : prev != next;
}
if (prev.length !== next.length) {
return true;
}
for (let i: number = prev.length - 1; i >= 0; i--) {
if (strictMode ? prev[i] !== next[i] : prev[i] != next[i]) {
return true;
}
}
return false;
}
// 即将抛弃
export function makeColumnClassBuild(steps: number, classNameTpl: string = 'col-sm-$value') {
let count = 12;
let step = Math.floor(count / steps);
return function(schema: Schema) {
if (schema.columnClassName && /\bcol-(?:xs|sm|md|lg)-(\d+)\b/.test(schema.columnClassName)) {
const flex = parseInt(RegExp.$1, 10);
count -= flex;
steps--;
step = Math.floor(count / steps);
return schema.columnClassName;
} else if (schema.columnClassName) {
count -= step;
steps--;
return schema.columnClassName;
}
count -= step;
steps--;
return classNameTpl.replace('$value', '' + step);
};
}
export function isVisible(
schema: {
visibleOn?: string;
hiddenOn?: string;
visible?: boolean;
hidden?: boolean;
},
data?: object
) {
return !(
schema.hidden ||
schema.visible === false ||
(schema.hiddenOn && evalExpression(schema.hiddenOn, data) === true) ||
(schema.visibleOn && evalExpression(schema.visibleOn, data) === false)
);
}
export function isDisabled(
schema: {
disabledOn?: string;
disabled?: string;
},
data?: object
) {
return schema.disabled || (schema.disabledOn && evalExpression(schema.disabledOn, data));
}
export function makeHorizontalDeeper(
horizontal: {
left: string;
right: string;
offset: string;
leftFixed?: any;
},
count: number
): {
left: string | number;
right: string | number;
offset: string | number;
leftFixed?: any;
} {
if (count > 1 && /\bcol-(xs|sm|md|lg)-(\d+)\b/.test(horizontal.left)) {
const flex = parseInt(RegExp.$2, 10) * count;
return {
leftFixed: horizontal.leftFixed,
left: flex,
right: 12 - flex,
offset: flex
};
} else if (count > 1 && typeof horizontal.left === 'number') {
const flex = horizontal.left * count;
return {
leftFixed: horizontal.leftFixed,
left: flex,
right: 12 - flex,
offset: flex
};
}
return horizontal;
}
export function promisify<T extends Function>(
fn: T
): (
...args: Array<any>
) => Promise<any> & {
raw: T;
} {
let promisified = function() {
try {
const ret = fn.apply(null, arguments);
if (ret && ret.then) {
return ret;
} else if (typeof ret === 'function') {
// thunk support
return new Promise((resolve, reject) =>
ret((error: boolean, value: any) => (error ? reject(error) : resolve(value)))
);
}
return Promise.resolve(ret);
} catch (e) {
Promise.reject(e);
}
};
(promisified as any).raw = fn;
return promisified;
}
export function getScrollParent(node: HTMLElement): HTMLElement | null {
if (node == null) {
return null;
}
const style = getComputedStyle(node);
const text =
style.getPropertyValue('overflow') +
style.getPropertyValue('overflow-x') +
style.getPropertyValue('overflow-y');
if (/auto|scroll/.test(text) || node.nodeName === 'BODY') {
return node;
}
return getScrollParent(node.parentNode as HTMLElement);
}
/**
* Deep diff between two object, using lodash
* @param {Object} object Object compared
* @param {Object} base Object to compare with
* @return {Object} Return a new object who represent the diff
*/
export function difference<T extends {[propName: string]: any}, U extends {[propName: string]: any}>(
object: T,
base: U
): {[propName: string]: any} {
function changes(object: T, base: U) {
const keys: Array<keyof T & keyof U> = uniq(Object.keys(object).concat(Object.keys(base)));
let result: any = {};
keys.forEach(key => {
const a: any = object[key as keyof T];
const b: any = base[key as keyof U];
if (isEqual(a, b)) {
return;
}
if (!object.hasOwnProperty(key)) {
result[key] = undefined;
} else if (Array.isArray(a) && Array.isArray(b)) {
// todo 数组要不要深入分析?我看先别了。
result[key] = a;
} else if (lodashIsObject(a) && lodashIsObject(b)) {
result[key] = changes(a as any, b as any);
} else {
result[key] = a;
}
});
return result;
}
return changes(object, base);
}
export const padArr = (arr: Array<any>, size = 4): Array<Array<any>> => {
const ret: Array<Array<any>> = [];
const pool: Array<any> = arr.concat();
let from = 0;
while (pool.length) {
let host: Array<any> = ret[from] || (ret[from] = []);
if (host.length >= size) {
from += 1;
continue;
}
host.push(pool.shift());
}
return ret;
};
export function __uri(id: string) {
return id;
}
export function isObject(obj: any) {
const typename = typeof obj;
return (
obj &&
typename !== 'string' &&
typename !== 'number' &&
typename !== 'boolean' &&
typename !== 'function' &&
!Array.isArray(obj)
);
}
// xs < 768px
// sm >= 768px
// md >= 992px
// lg >= 1200px
export function isBreakpoint(str: string): boolean {
if (typeof str !== 'string') {
return !!str;
}
const breaks = str.split(/\s*,\s*|\s+/);
if (window.matchMedia) {
return breaks.some(
item =>
item === '*' ||
(item === 'xs' && matchMedia(`screen and (max-width: 767px)`).matches) ||
(item === 'sm' && matchMedia(`screen and (min-width: 768px) and (max-width: 991px)`).matches) ||
(item === 'md' && matchMedia(`screen and (min-width: 992px) and (max-width: 1199px)`).matches) ||
(item === 'lg' && matchMedia(`screen and (min-width: 1200px)`).matches)
);
} else {
const width = window.innerWidth;
return breaks.some(
item =>
item === '*' ||
(item === 'xs' && width < 768) ||
(item === 'sm' && width >= 768 && width < 992) ||
(item === 'md' && width >= 992 && width < 1200) ||
(item === 'lg' && width >= 1200)
);
}
}
export function until(
fn: () => Promise<any>,
when: (ret: any) => boolean,
getCanceler: (fn: () => any) => void,
interval: number = 5000
) {
let timer: NodeJS.Timeout;
let stoped: boolean = false;
return new Promise((resolve, reject) => {
let cancel = () => {
clearTimeout(timer);
stoped = true;
};
let check = async () => {
try {
const ret = await fn();
if (stoped) {
return;
} else if (when(ret)) {
stoped = true;
resolve(ret);
} else {
timer = setTimeout(check, interval);
}
} catch (e) {
reject(e);
}
};
check();
getCanceler && getCanceler(cancel);
});
}
export function omitControls(controls: Array<any>, omitItems: Array<string>): Array<any> {
return controls.filter(control => !~omitItems.indexOf(control.name || control._name));
}
export function isEmpty(thing: any) {
if (isObject(thing) && Object.keys(thing).length) {
return false;
}
return true;
}
/**
* 基于时间戳的 uuid
*
* @returns uniqueId
*/
export const uuid = () => {
return (+new Date()).toString(36);
};
export interface TreeItem {
children?: TreeArray;
[propName: string]: any;
}
export interface TreeArray extends Array<TreeItem> {}
/**
* 类似于 arr.map 方法,此方法主要针对类似下面示例的树形结构。
* [
* {
* children: []
* },
* // 其他成员
* ]
*
* @param {Tree} tree 树形数据
* @param {Function} iterator 处理函数,返回的数据会被替换成新的。
* @return {Tree} 返回处理过的 tree
*/
export function mapTree<T extends TreeItem>(
tree: Array<T>,
iterator: (item: T, key: number, level: number, paths: Array<T>) => T,
level: number = 1,
depthFirst: boolean = false,
paths: Array<T> = []
) {
return tree.map((item: any, index) => {
if (depthFirst) {
let children: TreeArray | undefined = item.children
? mapTree(item.children, iterator, level + 1, depthFirst, paths.concat(item))
: undefined;
children && (item = {...item, children: children});
item = iterator(item, index, level, paths) || {...(item as object)};
return item;
}
item = iterator(item, index, level, paths) || {...(item as object)};
if (item.children && item.children.splice) {
item.children = mapTree(item.children, iterator, level + 1, depthFirst, paths.concat(item));
}
return item;
});
}
export function eachTree<T extends TreeItem>(
tree: Array<T>,
iterator: (item: T, key: number, level: number) => any,
level: number = 1
) {
tree.map((item, index) => {
iterator(item, index, level);
if (item.children && item.children.splice) {
eachTree(item.children, iterator, level + 1);
}
});
}
export function findTree<T extends TreeItem>(
tree: Array<T>,
iterator: (item: T, key: number, level: number) => any,
level: number = 1
): T | null {
let result: T | null = null;
everyTree(tree, (item, key, level) => {
if (iterator(item, key, level)) {
result = item;
return false;
}
return true;
});
return result;
}
export function filterTree<T extends TreeItem>(
tree: Array<T>,
iterator: (item: T, key: number, level: number) => boolean,
level: number = 1
) {
return tree.filter((item, index) => {
if (!iterator(item, index, level)) {
return false;
}
if (item.children && item.children.splice) {
item.children = filterTree(item.children, iterator, level + 1);
}
return true;
});
}
export function everyTree<T extends TreeItem>(
tree: Array<T>,
iterator: (item: T, key: number, level: number) => boolean,
level: number = 1
): boolean {
return tree.every((item, index) => {
const value: any = iterator(item, index, level);
if (value && item.children && item.children.splice) {
return everyTree(item.children, iterator, level + 1);
}
return value;
});
}
export function someTree<T extends TreeItem>(
tree: Array<T>,
iterator: (item: T, key: number, level: number) => boolean,
level: number = 1
): boolean {
return tree.some((item, index) => {
const value: any = iterator(item, index, level);
if (!value && item.children && item.children.splice) {
return someTree(item.children, iterator, level + 1);
}
return value;
});
}
export function flattenTree<T extends TreeItem>(tree: Array<T>): Array<T> {
let flattened: Array<T> = [];
eachTree(tree, item => flattened.push(item));
return flattened;
}
export function ucFirst(str?: string) {
return str ? str.substring(0, 1).toUpperCase() + str.substring(1) : '';
}
export function lcFirst(str?: string) {
return str ? str.substring(0, 1).toLowerCase() + str.substring(1) : '';
}
export function camel(str?: string) {
return str
? str
.split(/[\s_\-]/)
.map((item, index) => (index === 0 ? lcFirst(item) : ucFirst(item)))
.join('')
: '';
}
export function getWidthRate(value: any): number {
if (typeof value === 'string' && /\bcol\-\w+\-(\d+)\b/.test(value)) {
return parseInt(RegExp.$1, 10);
}
return value || 0;
}
export function getLevelFromClassName(value: string, defaultValue: string = 'default') {
if (/\b(?:btn|text)-(link|primary|secondary|info|success|warning|danger|light|dark)\b/.test(value)) {
return RegExp.$1;
}
return defaultValue;
}
export function pickEventsProps(props: any) {
const ret: any = {};
props && Object.keys(props).forEach(key => /^on/.test(key) && (ret[key] = props[key]));
return ret;
}
export const autobind = boundMethod;
export const bulkBindFunctions = function<
T extends {
[propName: string]: any;
}
>(context: T, funNames: Array<FunctionPropertyNames<T>>) {
funNames.forEach(key => (context[key] = context[key].bind(context)));
};
export function sortArray<T extends any>(items: Array<T>, field: string, dir: -1 | 1): Array<T> {
return items.sort((a, b) => {
let ret: number;
const a1 = a[field];
const b1 = b[field];
if (typeof a1 === 'number' && typeof b1 === 'number') {
ret = a1 < b1 ? -1 : a1 === b1 ? 0 : 1;
} else {
ret = String(a1).localeCompare(String(b1));
}
return ret * dir;
});
}
// 只判断一层, 如果层级很深,form-data 也不好表达。
export function hasFile(object: any): boolean {
return Object.keys(object).some(key => {
let value = object[key];
return value instanceof File || (Array.isArray(value) && value.length && value[0] instanceof File);
});
}
export function qsstringify(
data: any,
options: any = {
arrayFormat: 'indices',
encodeValuesOnly: true
}
) {
return qs.stringify(data, options);
}
export function object2formData(
data: any,
options: any = {
arrayFormat: 'indices',
encodeValuesOnly: true
},
fd: FormData = new FormData()
): any {
let others: any = {};
Object.keys(data).forEach(key => {
const value = data[key];
if (value instanceof File) {
fd.append(key, value, value.name);
} else if (Array.isArray(value) && value.length && value[0] instanceof File) {
value.forEach(value => fd.append(`${key}[]`, value, value.name));
} else {
others[key] = value;
}
});
// 因为 key 的格式太多了,偷个懒,用 qs 来处理吧。
qsstringify(others, options)
.split('&')
.forEach(item => {
let parts = item.split('=');
parts[0] && fd.append(parts[0], parts[1]);
});
return fd;
}
export function chainFunctions(...fns: Array<(...args: Array<any>) => void>): (...args: Array<any>) => void {
return (...args: Array<any>) => {
fns.forEach(fn => fn && fn(...args));
};
}
export function mapObject(value: any, fn: Function): any {
if (Array.isArray(value)) {
return value.map(item => mapObject(item, fn));
}
if (isObject(value)) {
let tmpValue = {...value};
Object.keys(tmpValue).forEach(key => {
(tmpValue as PlainObject)[key] = mapObject((tmpValue as PlainObject)[key], fn);
});
return tmpValue;
}
return fn(value);
}