amis-saas-10220 外观源码编辑器

Change-Id: I36dcfaa22c811dda6d970f0c508abdb792daf1fa
This commit is contained in:
qkiroc 2023-03-16 19:37:48 +08:00
parent b47c56eee2
commit acd3dd078f
7 changed files with 382 additions and 205 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,12 +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 {cloneDeep, 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',
@ -53,39 +54,62 @@ interface CssNode {
selector: string;
}
function AmisStyleCodeEditor(props: FormControlProps) {
interface CssNodeTab {
name: string;
children: CssNode[];
}
function AmisThemeCssCodeEditor(props: FormControlProps) {
const {themeClass, data} = props;
const id = data.id.replace('u:', '');
const [cssNodes, setCssNodes] = useState<CssNode[]>([]);
const [value, setValue] = useState('');
const [select, setSelect] = useState(0);
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 style = node.nodes.map((n: any) => `${n.prop}: ${n.value};`);
return {
selector: node.selector,
value: style.join('\n')
};
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};`);
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
});
});
setValue(css[select].value);
setCssNodes(css);
setCssNodes(newCssNodes);
} catch (error) {
console.error(error);
}
@ -95,61 +119,70 @@ function AmisStyleCodeEditor(props: FormControlProps) {
getCssAndSetValue(themeClass);
}, []);
const editorChange = debounce((nodes: CssNode[]) => {
const editorChange = debounce((nodeTabs: CssNodeTab[]) => {
try {
const {data, onBulkChange} = props;
const sourceCss = data.themeCss || data.css || {};
const newCss: any = {};
nodes.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(':hover: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(' ');
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({
@ -163,13 +196,22 @@ function AmisStyleCodeEditor(props: FormControlProps) {
}
});
function handleChange(value: string) {
const newCssNodes = cloneDeep(cssNodes);
newCssNodes[select].value = value;
setCssNodes(newCssNodes);
setValue(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 (
<div className="ThemeCssCode-editor">
@ -179,12 +221,125 @@ 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={{
onChange: handleChange
}}
/>
</div>
</div>
@ -193,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);
@ -213,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>
</>

View File

@ -1,7 +1,7 @@
import {PlainObject} from 'amis-core';
import {parse as cssParse} from 'amis-postcss';
import {isEmpty} from 'lodash';
const conf = {
const conf: any = {
ws: '[ \t\n\r\f]*',
identifier:
'-?-?([a-zA-Z]|(\\\\(([0-9a-fA-F]{1,6}\\s?)|[^[0-9a-fA-F])))([\\w\\-]|(\\\\(([0-9a-fA-F]{1,6}\\s?)|[^[0-9a-fA-F])))*',
@ -3005,7 +3005,8 @@ const keywords: PlainObject = {
}
};
function validate(model: any, monaco: any) {
function validate(editor: any, monaco: any) {
const model = editor.getModel();
const markers = [];
const lineLen = model.getLineCount();
for (let i = 1; i < lineLen + 1; i++) {
@ -3060,81 +3061,90 @@ export default function editorFactory(
monaco: any,
options: any
) {
// 注册语言
monaco.languages.register({id: 'amisTheme'});
// 设置主题
monaco.editor.defineTheme('amisTheme', {base: 'vs'});
// 设置高亮
monaco.languages.setMonarchTokensProvider('amisTheme', conf);
// 设置提示
monaco.languages.registerCompletionItemProvider('amisTheme', {
provideCompletionItems: (model: any, position: any) => {
const {lineNumber, column} = position;
// 获取输入前的字符
const textBeforePointer = model.getValueInRange({
startLineNumber: lineNumber,
startColumn: 0,
endLineNumber: lineNumber,
endColumn: column
});
// 如果已经配置了key和value就不给建议了
if (/(.*):(.*);/.test(textBeforePointer)) {
return {suggestions: []};
}
const token = /(.*):/.exec(textBeforePointer) || [];
const valueTip = keywords[token[1]];
let suggestions;
if (!monaco.languages.getEncodedLanguageId('amisTheme')) {
// 注册语言
monaco.languages.register({id: 'amisTheme'});
// 设置高亮
monaco.languages.setMonarchTokensProvider('amisTheme', conf);
// 设置提示
monaco.languages.registerCompletionItemProvider('amisTheme', {
provideCompletionItems: (model: any, position: any) => {
const {lineNumber, column} = position;
// 获取输入前的字符
const textBeforePointer = model.getValueInRange({
startLineNumber: lineNumber,
startColumn: 0,
endLineNumber: lineNumber,
endColumn: column
});
// 如果已经配置了key和value就不给建议了
if (/(.*):(.*);/.test(textBeforePointer)) {
return {suggestions: []};
}
const token = /(.*):/.exec(textBeforePointer) || [];
const valueTip = keywords[token[1]];
let suggestions;
// 判断是需要提示key还是value
if (valueTip) {
suggestions = [
...valueTip.values.map((k: string) => ({
label: k,
kind: monaco.languages.CompletionItemKind.Enum,
insertText: k + ';'
}))
];
} else {
suggestions = [
...Object.keys(keywords).map(k => ({
label: k,
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: k + ': '
}))
];
}
// 判断是需要提示key还是value
if (!isEmpty(valueTip)) {
suggestions = [
...valueTip.values.map((k: string) => ({
label: k,
kind: monaco.languages.CompletionItemKind.Enum,
insertText: k + ';'
}))
];
} else {
suggestions = [
...Object.keys(keywords).map(k => ({
label: k,
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: k + ': '
}))
];
}
return {
suggestions: suggestions
};
},
triggerCharacters: [' ']
});
const model = monaco.editor.createModel('', 'amisTheme');
model.onDidChangeContent(() => {
validate(model, monaco);
});
return monaco.editor.create(containerElement, {
model,
language: 'amisTheme',
options: {
automaticLayout: true,
lineNumbers: 'off',
glyphMargin: false,
tabSize: 2,
wordWrap: 'on',
lineDecorationsWidth: 0,
lineNumbersMinChars: 0,
selectOnLineNumbers: true,
scrollBeyondLastLine: false,
folding: true,
minimap: {
enabled: false
return {
suggestions: suggestions
};
},
...options
}
triggerCharacters: [' ']
});
}
// const uri = monaco.Uri.parse(options.uri.replace(/:/g, '-'));
// let model: any = null;
// try {
// model = monaco.editor.createModel(options.value, 'amisTheme', uri);
// } catch (error) {
// model = monaco.editor.getModel(uri);
// }
const editor = monaco.editor.create(containerElement, {
...options,
'language': 'amisTheme',
'autoIndent': true,
'formatOnType': true,
'formatOnPaste': true,
'selectOnLineNumbers': true,
'scrollBeyondLastLine': false,
'folding': true,
'minimap': {
enabled: false
},
'scrollbar': {
alwaysConsumeMouseWheel: false
},
'bracketPairColorization.enabled': true,
'automaticLayout': true,
'lineNumbers': 'off',
'glyphMargin': false,
'wordWrap': 'on',
'lineDecorationsWidth': 0,
'lineNumbersMinChars': 0
});
editor.onDidChangeModelContent(() => {
validate(editor, monaco);
options.onChange && options.onChange(editor.getValue());
});
return editor;
}

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)