diff --git a/packages/amis-editor/src/plugin/Button.tsx b/packages/amis-editor/src/plugin/Button.tsx index 08a8c3388..b075734d0 100644 --- a/packages/amis-editor/src/plugin/Button.tsx +++ b/packages/amis-editor/src/plugin/Button.tsx @@ -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'] + } + ] + }) ]) }, { diff --git a/packages/amis-editor/src/plugin/Form/InputNumber.tsx b/packages/amis-editor/src/plugin/Form/InputNumber.tsx index 2e3cf8739..759d7ac47 100644 --- a/packages/amis-editor/src/plugin/Form/InputNumber.tsx +++ b/packages/amis-editor/src/plugin/Form/InputNumber.tsx @@ -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'} ) diff --git a/packages/amis-editor/src/plugin/Form/InputText.tsx b/packages/amis-editor/src/plugin/Form/InputText.tsx index dd96535c9..74e8d0002 100644 --- a/packages/amis-editor/src/plugin/Form/InputText.tsx +++ b/packages/amis-editor/src/plugin/Form/InputText.tsx @@ -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 }) ], diff --git a/packages/amis-editor/src/plugin/Page.tsx b/packages/amis-editor/src/plugin/Page.tsx index 7496a4f6c..36c2fd344 100644 --- a/packages/amis-editor/src/plugin/Page.tsx +++ b/packages/amis-editor/src/plugin/Page.tsx @@ -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']) ]) ] }, diff --git a/packages/amis-editor/src/renderer/style-control/ThemeCssCode.tsx b/packages/amis-editor/src/renderer/style-control/ThemeCssCode.tsx index 80097681e..088619169 100644 --- a/packages/amis-editor/src/renderer/style-control/ThemeCssCode.tsx +++ b/packages/amis-editor/src/renderer/style-control/ThemeCssCode.tsx @@ -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([]); - const [value, setValue] = useState(''); - const [select, setSelect] = useState(0); - function getCssAndSetValue(themeClass: string[]) { + const [cssNodes, setCssNodes] = useState([]); + 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 (
@@ -179,12 +221,125 @@ function AmisStyleCodeEditor(props: FormControlProps) {
+
+
+ {cssNodes.map((node, index) => { + return ( +
setTabId(index)} + className={cx( + 'ThemeCssCode-editor-content-header-title', + index === tabId && + 'ThemeCssCode-editor-content-header-title--active' + )} + > + {node.name} +
+ ); + })} +
+
+ {cssNodes.map((node, i) => { + const children = node.children; + return ( +
+ {children.map((css, j) => { + return ( +
+ {children.length > 1 ? ( +
+ {formateTitle(css.selector)} +
+ ) : null} +
+ + handleChange(value, i, j) + }} + /> +
+
+ ); + })} +
+ ); + })} +
+
+ + ); +} +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 ( +
+
编辑样式源码
+
+ +
@@ -193,7 +348,6 @@ function AmisStyleCodeEditor(props: FormControlProps) { function ThemeCssCode(props: FormControlProps) { const ref = useRef(null); - const {value} = props; const [showEditor, setShowEditor] = useState(false); function handleShowEditor() { setShowEditor(true); @@ -213,7 +367,17 @@ function ThemeCssCode(props: FormControlProps) { rootClose={false} > setShowEditor(false)}> - setShowEditor(false)} /> + {props.isLayout ? ( + setShowEditor(false)} + /> + ) : ( + setShowEditor(false)} + /> + )} diff --git a/packages/amis-editor/src/renderer/style-control/themeLanguage.ts b/packages/amis-editor/src/renderer/style-control/themeLanguage.ts index f7f14ac06..1372c50ee 100644 --- a/packages/amis-editor/src/renderer/style-control/themeLanguage.ts +++ b/packages/amis-editor/src/renderer/style-control/themeLanguage.ts @@ -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; } diff --git a/packages/amis-editor/src/tpl/style.tsx b/packages/amis-editor/src/tpl/style.tsx index b66cb131a..1e8314a27 100644 --- a/packages/amis-editor/src/tpl/style.tsx +++ b/packages/amis-editor/src/tpl/style.tsx @@ -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)