Skip to content

Commit

Permalink
✨ feat: 新增 LevaPanel 属性面板
Browse files Browse the repository at this point in the history
  • Loading branch information
arvinxx committed Jun 14, 2023
1 parent 6e6c41e commit b7b0633
Show file tree
Hide file tree
Showing 5 changed files with 189 additions and 0 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
"handsontable": "^12",
"highlight.js": "~10.5.0",
"immer": "^9.0.7",
"leva": "^0.9.35",
"lodash.flatten": "^4.4.0",
"lodash.get": "^4.4.2",
"lodash.isempty": "^4.4.0",
Expand Down
63 changes: 63 additions & 0 deletions src/LevaPanel/Schema.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { JSONSchema } from '@/ComponentAsset';
import { getDefaultValueFromSchema } from '@c2d2c/utils';
import { useMemoizedFn } from 'ahooks';
import isEqual from 'fast-deep-equal';
import { useControls, useStoreContext } from 'leva';
import { DataInput } from 'leva/src/types';
import merge from 'lodash.merge';
import { ReactNode, memo, useEffect, useMemo } from 'react';

import { SchemaItem, toLevaSchema } from './utils/schema';

export interface SchemaProps<T> {
schema: JSONSchema<T>;
defaultValue?: T;
value?: T;
onChange?: (changedValue: Partial<T>, fullValue: T) => void;
}

const Schema: <T>(props: SchemaProps<T>) => ReactNode = memo(
({ value: outConfig, onChange, schema }) => {
const store = useStoreContext();

const getValue = useMemoizedFn(() => {
const data = store.getData();

return Object.fromEntries(
Object.values(data).map((item) => [item.key, (item as DataInput).value]),
);
});

const levaSchema = useMemo(() => {
return toLevaSchema(schema, (item, key) => {
const config = {
onChange: (value, path, context) => {
if (context.initial || !context.fromPanel) return;

const full = getValue();
onChange?.({ [context.key]: value } as any, full as any);
},
} as SchemaItem;
if (outConfig[key]) {
config['value'] = outConfig[key];
}
return config;
});
}, [schema]);

useEffect(() => {
const innerValue = getValue();
if (isEqual(innerValue, outConfig)) return;

const config = merge({}, getDefaultValueFromSchema(schema), outConfig);

store.set(config, false);
}, [outConfig]);

useControls(levaSchema, { store });

return null;
},
);

export default Schema;
74 changes: 74 additions & 0 deletions src/LevaPanel/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { createStyles } from 'antd-style';
import { LevaPanel, LevaStoreProvider, useCreateStore } from 'leva';
import { ReactNode, memo } from 'react';
import Schema, { SchemaProps } from './Schema';

const useStyles = createStyles(
({ css, token }) => css`
--leva-sizes-controlWidth: 66%;
--leva-colors-elevation1: ${token.colorFillSecondary};
--leva-colors-elevation2: transparent;
--leva-colors-elevation3: ${token.colorFillSecondary};
--leva-colors-accent1: ${token.colorPrimary};
--leva-colors-accent2: ${token.colorPrimaryHover};
--leva-colors-accent3: ${token.colorPrimaryActive};
--leva-colors-highlight1: ${token.colorTextTertiary};
--leva-colors-highlight2: ${token.colorTextSecondary};
--leva-colors-highlight3: ${token.colorText};
--leva-colors-vivid1: ${token.colorWarning};
--leva-shadows-level1: unset;
--leva-shadows-level2: unset;
--leva-fonts-mono: ${token.fontFamilyCode};
overflow: auto;
width: 100%;
height: 100%;
padding: 6px 0;
> div {
background: transparent;
> div {
background: transparent;
}
}
input:checked + label > svg {
stroke: ${token.colorBgLayout};
}
button {
--leva-colors-accent2: ${token.colorFillSecondary};
}
`,
);

export interface LevaPanelProps<T> extends SchemaProps<T> {
title?: ReactNode;
}

const Panel: <T>(props: LevaPanelProps<T>) => ReactNode = memo(
({ value, title, onChange, schema }) => {
const store = useCreateStore();
const { styles } = useStyles();

return (
<div className={styles}>
<LevaPanel
hideCopyButton
neverHide
titleBar={{ title, drag: false }}
fill
flat
store={store}
/>

<LevaStoreProvider store={store}>
<Schema schema={schema} value={value} onChange={onChange} />
</LevaStoreProvider>
</div>
);
},
);

export default Panel;
49 changes: 49 additions & 0 deletions src/LevaPanel/utils/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { JSONSchema } from '@/ComponentAsset';
import { LevaInputs, Schema } from 'leva/src/types';

export type SchemaItem = Schema[keyof Schema];

const getOptions = (schemaItem: JSONSchema) => {
if (schemaItem.enumOptions) {
return Object.fromEntries(schemaItem.enumOptions.map((i) => [i.label, i.value]));
} else if (schemaItem.enum) {
return Object.fromEntries(
schemaItem.enum.map((i, index) => {
const name = schemaItem.enumNames?.[index] || i;
return [name, i];
}),
);
}
};

const getRenderType = (schemaItem: JSONSchema): LevaInputs => {
switch (schemaItem.type) {
case 'boolean':
return LevaInputs.BOOLEAN;
case 'number':
return LevaInputs.NUMBER;
case 'string':
if (!!getOptions(schemaItem)) return LevaInputs.SELECT;
return LevaInputs.STRING;
}
};

export const toLevaSchema = (
schema: JSONSchema,
visitItem?: (item: JSONSchema, key: string) => Partial<SchemaItem>,
) =>
Object.fromEntries(
Object.entries(schema.properties).map(([key, schemaItem]) => {
const after = visitItem?.(schemaItem, key) || {};

const schema: SchemaItem = {
type: getRenderType(schemaItem),
label: schemaItem.title,
value: schemaItem.default,
options: getOptions(schemaItem),
...(after as object),
};

return [key, schema];
}),
);
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export * from './Highlight';
export * from './IconPicker';
export { default as InteractContainer } from './InteractContainer';
export type { CanvasInteractRule, InteractStatus, InteractStatusNode } from './InteractContainer';
export { default as LevaPanel } from './LevaPanel';
export type { LevaPanelProps } from './LevaPanel';
export * from './ProEditor';
export * from './SortableList';
export * from './SortableTree';
Expand Down

0 comments on commit b7b0633

Please sign in to comment.