Merge changes I36dcfaa2,I0d1645bd into theme/editor-20230201

* changes:
  amis-saas-10220 外观源码编辑器
  amis-saas-10220 外观源码编辑器
This commit is contained in:
qinhaoyan 2023-03-16 19:41:24 +08:00 committed by iCode
commit 61ab64fd00
7 changed files with 3452 additions and 128 deletions

View File

@ -367,7 +367,14 @@ export class ButtonPlugin extends BasePlugin {
...buttonStateFunc("${editorState == 'active'}", 'active')
]
},
getSchemaTpl('theme:cssCode')
getSchemaTpl('theme:cssCode', {
themeClass: [
{
value: '',
state: ['default', 'hover', 'active']
}
]
})
])
},
{

View File

@ -282,7 +282,17 @@ export class NumberControlPlugin extends BasePlugin {
)
]
},
getSchemaTpl('theme:cssCode', {isFormItem: true})
getSchemaTpl('theme:cssCode', {
themeClass: [
{
name: '数字输入框',
value: '',
className: 'inputControlClassName',
state: ['default', 'hover', 'active']
}
],
isFormItem: true
})
],
{...context?.schema, configTitle: 'style'}
)

View File

@ -383,7 +383,19 @@ export class TextControlPlugin extends BasePlugin {
]
},
getSchemaTpl('theme:cssCode', {
themeClass: ['addOn'],
themeClass: [
{
name: '输入框',
value: '',
className: 'inputControlClassName',
state: ['default', 'hover', 'active']
},
{
name: 'addOn',
value: 'addOn',
className: 'addOnClassName'
}
],
isFormItem: true
})
],

View File

@ -274,30 +274,7 @@ export class PagePlugin extends BasePlugin {
className: 'p-none',
body: [
getSchemaTpl('collapseGroup', [
...getSchemaTpl('theme:common', ['layout']),
getSchemaTpl('style:classNames', {
isFormItem: false,
schema: [
getSchemaTpl('className', {
name: 'headerClassName',
label: '顶部'
}),
getSchemaTpl('className', {
name: 'bodyClassName',
label: '内容区'
}),
getSchemaTpl('className', {
name: 'asideClassName',
label: '边栏'
}),
getSchemaTpl('className', {
name: 'toolbarClassName',
label: '工具栏'
})
]
})
...getSchemaTpl('theme:common', ['layout'])
])
]
},

View File

@ -3,11 +3,13 @@
*/
import React, {useEffect, useRef, useState} from 'react';
import {Button, Editor, Overlay, PopOver} from 'amis-ui';
import {FormControlProps, FormItem} from 'amis-core';
import {FormControlProps, FormItem, uuid} from 'amis-core';
import {parse as cssParse} from 'amis-postcss';
import {PlainObject} from './types';
import {debounce} from 'lodash';
import {cloneDeep, debounce, isEmpty} from 'lodash';
import {Icon} from '../../icons/index';
import editorFactory from './themeLanguage';
import cx from 'classnames';
const valueMap: PlainObject = {
'margin-top': 'marginTop',
@ -47,35 +49,67 @@ const fontStyle = [
'line-height'
];
function AmisStyleCodeEditor(props: FormControlProps) {
interface CssNode {
value: string;
selector: string;
}
interface CssNodeTab {
name: string;
children: CssNode[];
}
function AmisThemeCssCodeEditor(props: FormControlProps) {
const {themeClass, data} = props;
const id = data.id.replace('u:', '');
const [value, setValue] = useState('');
function getCssAndSetValue(themeClass: string[]) {
const [cssNodes, setCssNodes] = useState<CssNodeTab[]>([]);
const [tabId, setTabId] = useState(0);
function getCssAndSetValue(themeClass: any[]) {
try {
const nodes: any[] = [];
const ids = themeClass.map(n => (n ? id + '-' + n : id));
ids?.forEach(id => {
const dom = document.getElementById(id || '') || null;
const newCssNodes: CssNodeTab[] = [];
themeClass?.forEach(n => {
const classId = n.value ? id + '-' + n.value : id;
const state = n.state || ['default'];
const className = n.className || 'className';
const dom = document.getElementById(classId || '') || null;
const content = dom?.innerHTML || '';
const ast = cssParse(content);
const nodes: any[] = [];
ast.nodes.forEach((node: any) => {
const selector = node.selector;
if (!selector.endsWith('.hover') && !selector.endsWith('.active')) {
nodes.push(node);
}
});
ast.nodes = nodes;
});
const css = nodes
.map(node => {
const css: {selector: string; value: string; state: string}[] = [];
state.forEach((s: string) => {
css.push({
selector: `.${className}-${id}${s === 'default' ? '' : ':' + s}`,
state: s,
value: ''
});
});
nodes.forEach(node => {
const style = node.nodes.map((n: any) => `${n.prop}: ${n.value};`);
return `${node.selector} {\n ${style.join('\n ')}\n}`;
})
.join('\n\n');
setValue(css);
const item = css.find(c => {
if (
c.selector === node.selector ||
node.selector.endsWith(`:${c.state}`)
) {
return c;
}
return false;
})!;
item.value = style.join('\n');
});
newCssNodes.push({
name: n.name || '自定义样式',
children: css
});
});
setCssNodes(newCssNodes);
} catch (error) {
console.error(error);
}
@ -85,53 +119,70 @@ function AmisStyleCodeEditor(props: FormControlProps) {
getCssAndSetValue(themeClass);
}, []);
const editorChange = debounce((value: string) => {
const editorChange = debounce((nodeTabs: CssNodeTab[]) => {
try {
const ast = cssParse(value);
const {data, onBulkChange} = props;
const sourceCss = data.themeCss || data.css || {};
const newCss: any = {};
ast.nodes.forEach((node: any) => {
const nodes = node.nodes;
const selector = node.selector;
const nameEtr = /\.(.*)\-/.exec(selector);
const cssCode: PlainObject = {};
let name = nameEtr ? nameEtr[1] : '';
let state = 'default';
if (!!~selector.indexOf(':hover:active')) {
state = 'active';
} else if (!!~selector.indexOf(':hover')) {
state = 'hover';
}
nodes.forEach((item: any) => {
const prop = item.prop;
const cssValue = item.value;
if (!!~prop.indexOf('radius')) {
const type = 'radius:' + state;
!cssCode[type] && (cssCode[type] = {});
const radius = cssValue.split(' ');
nodeTabs.forEach(tab => {
tab.children.forEach(node => {
const nodes = node.value
.replace(/\s/g, '')
.split(';')
.map(kv => {
const [prop, value] = kv.split(':');
return {
prop,
value
};
})
.filter(n => n.value);
const selector = node.selector;
const nameEtr = /\.(.*)\-/.exec(selector);
const cssCode: PlainObject = {};
let name = nameEtr ? nameEtr[1] : '';
let state = 'default';
if (!!~selector.indexOf(':active')) {
state = 'active';
} else if (!!~selector.indexOf(':hover')) {
state = 'hover';
}
nodes.forEach(item => {
const prop = item.prop;
const cssValue = item.value;
if (!!~prop.indexOf('radius')) {
const type = 'radius:' + state;
!cssCode[type] && (cssCode[type] = {});
const radius = cssValue.split(' ');
cssCode[type]['top-left-border-radius'] = radius[0];
cssCode[type]['top-right-border-radius'] = radius[1];
cssCode[type]['bottom-right-border-radius'] = radius[2];
cssCode[type]['bottom-left-border-radius'] = radius[3];
} else if (!!~prop.indexOf('border')) {
!cssCode['border:' + state] && (cssCode['border:' + state] = {});
cssCode['border:' + state][valueMap[prop] || prop] = cssValue;
} else if (!!~prop.indexOf('padding') || !!~prop.indexOf('margin')) {
!cssCode['padding-and-margin:' + state] &&
(cssCode['padding-and-margin:' + state] = {});
cssCode['padding-and-margin:' + state][valueMap[prop] || prop] =
cssValue;
} else if (fontStyle.includes(prop)) {
!cssCode['font:' + state] && (cssCode['font:' + state] = {});
cssCode['font:' + state][valueMap[prop] || prop] = cssValue;
cssCode[type]['top-left-border-radius'] = radius[0];
cssCode[type]['top-right-border-radius'] = radius[1];
cssCode[type]['bottom-right-border-radius'] = radius[2];
cssCode[type]['bottom-left-border-radius'] = radius[3];
} else if (!!~prop.indexOf('border')) {
!cssCode['border:' + state] && (cssCode['border:' + state] = {});
cssCode['border:' + state][valueMap[prop] || prop] = cssValue;
} else if (
!!~prop.indexOf('padding') ||
!!~prop.indexOf('margin')
) {
!cssCode['padding-and-margin:' + state] &&
(cssCode['padding-and-margin:' + state] = {});
cssCode['padding-and-margin:' + state][valueMap[prop] || prop] =
cssValue;
} else if (fontStyle.includes(prop)) {
!cssCode['font:' + state] && (cssCode['font:' + state] = {});
cssCode['font:' + state][valueMap[prop] || prop] = cssValue;
} else {
cssCode[(valueMap[prop] || prop) + ':' + state] = cssValue;
}
});
if (newCss[name]) {
newCss[name] = Object.assign(newCss[name], cssCode);
} else {
cssCode[(valueMap[prop] || prop) + ':' + state] = cssValue;
newCss[name] = cssCode;
}
});
newCss[name] = cssCode;
});
onBulkChange &&
onBulkChange({
@ -145,9 +196,21 @@ function AmisStyleCodeEditor(props: FormControlProps) {
}
});
function handleChange(value: string) {
setValue(value);
editorChange(value);
function handleChange(value: string, i: number, j: number) {
const newCssNodes = cssNodes;
newCssNodes[i].children[j].value = value;
setCssNodes(newCssNodes); // 好像不需要这个?
editorChange(newCssNodes);
}
function formateTitle(title: string) {
if (title.endsWith('hover')) {
return '悬浮态样式';
} else if (title.endsWith('active')) {
return '点击态样式';
} else if (title.endsWith('disabled')) {
return '禁用态样式';
}
return '常规态样式';
}
return (
@ -158,25 +221,124 @@ function AmisStyleCodeEditor(props: FormControlProps) {
<Icon icon="close" className="icon" />
</Button>
</div>
<div className="ThemeCssCode-editor-content">
<div className="ThemeCssCode-editor-content-header">
{cssNodes.map((node, index) => {
return (
<div
key={index}
onClick={() => setTabId(index)}
className={cx(
'ThemeCssCode-editor-content-header-title',
index === tabId &&
'ThemeCssCode-editor-content-header-title--active'
)}
>
{node.name}
</div>
);
})}
</div>
<div className="ThemeCssCode-editor-content-main">
{cssNodes.map((node, i) => {
const children = node.children;
return (
<div
key={i}
className={cx(
i !== tabId && 'ThemeCssCode-editor-content-body--hidden'
)}
>
{children.map((css, j) => {
return (
<div
className="ThemeCssCode-editor-content-body"
key={`${i}-${j}-${css.selector}`}
id={`${i}-${j}-${css.selector}`}
>
{children.length > 1 ? (
<div className="ThemeCssCode-editor-content-body-title">
{formateTitle(css.selector)}
</div>
) : null}
<div className="ThemeCssCode-editor-content-body-editor">
<Editor
value={css.value}
editorFactory={editorFactory}
options={{
onChange: (value: string) =>
handleChange(value, i, j)
}}
/>
</div>
</div>
);
})}
</div>
);
})}
</div>
</div>
</div>
);
}
function AmisStyleCodeEditor(props: FormControlProps) {
const {data, onBulkChange} = props;
const {style} = data;
const [value, setValue] = useState('');
function getCssAndSetValue(data: any) {
if (isEmpty(data)) {
return '';
}
let str = '';
for (let key in data) {
str += `${key}: ${data[key]};\n`;
}
return str;
}
useEffect(() => {
const res = getCssAndSetValue(style);
setValue(res);
}, []);
const editorChange = debounce((value: string) => {
const newStyle: PlainObject = {};
value
.replace(/\s/g, '')
.split(';')
.forEach(kv => {
const [prop, value] = kv.split(':');
if (value) {
newStyle[prop] = value;
}
});
onBulkChange &&
onBulkChange({
style: newStyle
});
});
function handleChange(value: string) {
editorChange(value);
setValue(value);
}
return (
<div className="ThemeCssCode-editor">
<div className="ThemeCssCode-editor-title"></div>
<div className="ThemeCssCode-editor-close">
<Button onClick={props.onHide} level="link">
<Icon icon="close" className="icon" />
</Button>
</div>
<div className="ThemeCssCode-editor-content">
<Editor
value={value}
onChange={handleChange}
editorFactory={editorFactory}
options={{
automaticLayout: true,
lineNumbers: 'off',
glyphMargin: false,
tabSize: 2,
wordWrap: 'on',
lineDecorationsWidth: 0,
lineNumbersMinChars: 0,
selectOnLineNumbers: true,
scrollBeyondLastLine: false,
folding: true,
minimap: {
enabled: false
}
onChange: handleChange
}}
/>
</div>
@ -186,7 +348,6 @@ function AmisStyleCodeEditor(props: FormControlProps) {
function ThemeCssCode(props: FormControlProps) {
const ref = useRef<HTMLDivElement>(null);
const {value} = props;
const [showEditor, setShowEditor] = useState(false);
function handleShowEditor() {
setShowEditor(true);
@ -206,7 +367,17 @@ function ThemeCssCode(props: FormControlProps) {
rootClose={false}
>
<PopOver overlay onHide={() => setShowEditor(false)}>
<AmisStyleCodeEditor {...props} onHide={() => setShowEditor(false)} />
{props.isLayout ? (
<AmisStyleCodeEditor
{...props}
onHide={() => setShowEditor(false)}
/>
) : (
<AmisThemeCssCodeEditor
{...props}
onHide={() => setShowEditor(false)}
/>
)}
</PopOver>
</Overlay>
</>

File diff suppressed because it is too large Load Diff

View File

@ -430,15 +430,23 @@ setSchemaTpl(
'theme:cssCode',
({
themeClass = [],
isFormItem
isFormItem,
isLayout
}: {
themeClass?: string[];
themeClass?: any[];
isFormItem?: boolean;
} = {}) => {
console.log(themeClass);
themeClass.push('');
if (isFormItem) {
themeClass.push(...['description', 'label']);
themeClass.push(
...[
{
name: 'description',
value: 'description',
className: 'descriptionClassName'
},
{name: 'label', value: 'label', className: 'labelClassName'}
]
);
}
return {
title: '样式源码',
@ -581,8 +589,7 @@ setSchemaTpl(
].filter(comp => !~exclude.indexOf(comp.type.replace(/^style-/i, '')))
},
{
header: '样式',
key: 'style',
title: '自定义样式',
body: [
getSchemaTpl('theme:border', {
name: 'style'
@ -606,24 +613,14 @@ setSchemaTpl(
]
},
{
header: '圆角',
key: 'radius',
body: []
},
{
header: '间距',
key: 'box-model',
body: []
},
{
header: '背景',
key: 'background',
body: []
},
{
header: '阴影',
key: 'box-shadow',
body: []
title: '样式源码',
body: [
{
type: 'theme-cssCode',
label: false,
isLayout: true
}
]
}
].filter(item =>
include.length ? ~include.indexOf(item.key) : !~exclude.indexOf(item.key)