From 605e649158e01ea3927929edcf8bef399f391ede Mon Sep 17 00:00:00 2001 From: chilingling Date: Fri, 13 Oct 2023 03:09:21 -0700 Subject: [PATCH 1/4] feat(style): stylePanel add style selector, write css to global styles --- .../src/components/container/container.js | 2 + packages/controller/src/useProperties.js | 15 +- packages/settings/styles/package.json | 1 + packages/settings/styles/src/Main.vue | 42 ++-- .../components/background/BackgroundGroup.vue | 1 - .../src/components/border/BorderGroup.vue | 2 - .../components/classNamesContainer/index.vue | 108 ++++++++++ .../src/components/effects/EffectGroup.vue | 1 - .../settings/styles/src/components/index.js | 65 +++--- .../styles/src/components/layout/FlexBox.vue | 1 - .../styles/src/components/layout/GridBox.vue | 1 - .../src/components/position/PositionGroup.vue | 3 - .../styles/src/components/size/SizeGroup.vue | 1 - .../src/components/spacing/SpacingGroup.vue | 36 ++-- .../components/typography/TypographyGroup.vue | 1 - packages/settings/styles/src/js/parser.js | 187 ++++++++++++++++++ 16 files changed, 375 insertions(+), 92 deletions(-) create mode 100644 packages/settings/styles/src/components/classNamesContainer/index.vue create mode 100644 packages/settings/styles/src/js/parser.js diff --git a/packages/canvas/src/components/container/container.js b/packages/canvas/src/components/container/container.js index 1bea1dd83b..d27a686ea8 100644 --- a/packages/canvas/src/components/container/container.js +++ b/packages/canvas/src/components/container/container.js @@ -403,6 +403,8 @@ export const clearSelect = () => { canvasState.current = null canvasState.parent = null Object.assign(selectState, initialRectState) + // 临时借用 remote 事件出发 currentSchema 更新 + canvasState?.emit?.('remove') } export const querySelectById = (id, type = '') => { diff --git a/packages/controller/src/useProperties.js b/packages/controller/src/useProperties.js index dad95daf3d..81e132aed7 100644 --- a/packages/controller/src/useProperties.js +++ b/packages/controller/src/useProperties.js @@ -10,11 +10,13 @@ * */ -import { toRaw, nextTick, shallowReactive } from 'vue' +import { toRaw, nextTick, shallowReactive, ref } from 'vue' +import { getNode, setState, updateRect } from '@opentiny/tiny-engine-canvas' import useCanvas from './useCanvas' import useResource from './useResource' import useTranslate from './useTranslate' -import { getNode, setState, updateRect } from '@opentiny/tiny-engine-canvas' + +const propsUpdateKey = ref(0) const otherBaseKey = { className: { @@ -168,6 +170,10 @@ const getProps = (schema, parent) => { } const setProp = (name, value) => { + if (!properties.schema) { + return + } + properties.schema.props = properties.schema.props || {} if (value === '' || value === undefined || value === null) { @@ -178,6 +184,7 @@ const setProp = (name, value) => { // 没有父级,或者不在节点上面,要更新内容。就用setState getNode(properties.schema.id, true).parent || setState(useCanvas().getPageSchema().state) + propsUpdateKey.value++ nextTick(updateRect) } @@ -188,6 +195,7 @@ const getProp = (key) => { const delProp = (name) => { const props = properties.schema.props || {} delete props[name] + propsUpdateKey.value++ } const setProps = (schema) => { @@ -205,6 +213,7 @@ export default function () { translateProp, getSchema(parent) { return parent ? properties : properties.schema - } + }, + propsUpdateKey } } diff --git a/packages/settings/styles/package.json b/packages/settings/styles/package.json index 179e94d15f..1e407eaac9 100644 --- a/packages/settings/styles/package.json +++ b/packages/settings/styles/package.json @@ -19,6 +19,7 @@ "@opentiny/tiny-engine-http": "workspace:^1.0.0", "@opentiny/vue": "~3.10.0", "@opentiny/vue-renderless": "~3.10.0", + "postcss": "^8.4.31", "vue": "3.2.45" }, "devDependencies": { diff --git a/packages/settings/styles/src/Main.vue b/packages/settings/styles/src/Main.vue index 396b45b8be..fb5a6daabc 100644 --- a/packages/settings/styles/src/Main.vue +++ b/packages/settings/styles/src/Main.vue @@ -17,7 +17,7 @@ @save="save(CSS_TYPE.Style, $event)" /> - + @@ -58,23 +58,27 @@ + + \ No newline at end of file diff --git a/packages/settings/styles/src/components/effects/EffectGroup.vue b/packages/settings/styles/src/components/effects/EffectGroup.vue index 7d46ea1b53..95c66a02a4 100644 --- a/packages/settings/styles/src/components/effects/EffectGroup.vue +++ b/packages/settings/styles/src/components/effects/EffectGroup.vue @@ -388,7 +388,6 @@ export default { const { setPosition } = useModal() const { getSettingFlag, getProperty } = useProperties({ - props, names: Object.values(EFFECTS_PROPERTY), parseNumber: true }) diff --git a/packages/settings/styles/src/components/index.js b/packages/settings/styles/src/components/index.js index 31d6d66b65..65e388788d 100644 --- a/packages/settings/styles/src/components/index.js +++ b/packages/settings/styles/src/components/index.js @@ -1,41 +1,28 @@ /** -* Copyright (c) 2023 - present TinyEngine Authors. -* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. -* -* Use of this source code is governed by an MIT-style license. -* -* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, -* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR -* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. -* -*/ + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ -import BackgroundGroup from './background/BackgroundGroup.vue' -import BorderGroup from './border/BorderGroup.vue' -import EffectGroup from './effects/EffectGroup.vue' -import ImageSelect from './inputs/ImageSelect.vue' -import ResetButton from './inputs/ResetButton.vue' -import LayoutGroup from './layout/LayoutGroup.vue' -import PositionGroup from './position/PositionGroup.vue' -import BoxShadowGroup from './shadow/BoxShadowGroup.vue' -import SizeGroup from './size/SizeGroup.vue' -import SpacingGroup from './spacing/SpacingGroup.vue' -import SpacingSetting from './spacing/SpacingSetting.vue' -import TypographyGroup from './typography/TypographyGroup.vue' -import TypographyMore from './typography/TypographyMore.vue' - -export default { - BackgroundGroup, - BorderGroup, - EffectGroup, - ImageSelect, - ResetButton, - LayoutGroup, - PositionGroup, - BoxShadowGroup, - SizeGroup, - SpacingGroup, - SpacingSetting, - TypographyGroup, - TypographyMore -} +export { default as BackgroundGroup } from './background/BackgroundGroup.vue' +export { default as BorderGroup } from './border/BorderGroup.vue' +export { default as EffectGroup } from './effects/EffectGroup.vue' +export { default as ImageSelect } from './inputs/ImageSelect.vue' +export { default as ResetButton } from './inputs/ResetButton.vue' +export { default as LayoutGroup } from './layout/LayoutGroup.vue' +export { default as PositionGroup } from './position/PositionGroup.vue' +export { default as BoxShadowGroup } from './shadow/BoxShadowGroup.vue' +export { default as SizeGroup } from './size/SizeGroup.vue' +export { default as SpacingGroup } from './spacing/SpacingGroup.vue' +export { default as SpacingSetting } from './spacing/SpacingSetting.vue' +export { default as TypographyGroup } from './typography/TypographyGroup.vue' +export { default as TypographyMore } from './typography/TypographyMore.vue' +export { default as FlexBox } from './layout/FlexBox.vue' +export { default as GridBox } from './layout/GridBox.vue' +export { default as ClassNameContainer } from './classNamesContainer' diff --git a/packages/settings/styles/src/components/layout/FlexBox.vue b/packages/settings/styles/src/components/layout/FlexBox.vue index a92beacec8..7aa410a205 100644 --- a/packages/settings/styles/src/components/layout/FlexBox.vue +++ b/packages/settings/styles/src/components/layout/FlexBox.vue @@ -176,7 +176,6 @@ export default { const showModal = ref(false) const { getSettingFlag } = useProperties({ - props, names: Object.values(FLEX_PROPERTY), parseNumber: true }) diff --git a/packages/settings/styles/src/components/layout/GridBox.vue b/packages/settings/styles/src/components/layout/GridBox.vue index ae377015b4..5f07cefcf8 100644 --- a/packages/settings/styles/src/components/layout/GridBox.vue +++ b/packages/settings/styles/src/components/layout/GridBox.vue @@ -361,7 +361,6 @@ export default { }) const { getProperty, getSettingFlag } = useProperties({ - props, names: Object.values(GRID_PROPERTY), parseNumber: true }) diff --git a/packages/settings/styles/src/components/position/PositionGroup.vue b/packages/settings/styles/src/components/position/PositionGroup.vue index a8e1664204..f103ff021e 100644 --- a/packages/settings/styles/src/components/position/PositionGroup.vue +++ b/packages/settings/styles/src/components/position/PositionGroup.vue @@ -221,7 +221,6 @@ import { reactive, watchEffect } from 'vue' import { Tooltip } from '@opentiny/vue' import { MetaSelect } from '@opentiny/tiny-engine-common' -import { camelize } from '@opentiny/tiny-engine-controller/utils' import { push } from '@opentiny/vue-renderless/common/array' import ModalMask, { useModal } from '../inputs/ModalMask.vue' import SpacingSetting from '../spacing/SpacingSetting.vue' @@ -368,7 +367,6 @@ export default { } const { getProperty, getSettingFlag, getPropertyValue, getPropertyText } = useProperties({ - props, names: Object.values(POSITION_PROPERTY), parseNumber: true }) @@ -413,7 +411,6 @@ export default { } const openDirectionSetting = (type, styleName) => { - styleName = camelize(styleName) state.property = { type, diff --git a/packages/settings/styles/src/components/size/SizeGroup.vue b/packages/settings/styles/src/components/size/SizeGroup.vue index b440f0765c..84165a5b15 100644 --- a/packages/settings/styles/src/components/size/SizeGroup.vue +++ b/packages/settings/styles/src/components/size/SizeGroup.vue @@ -339,7 +339,6 @@ export default { }) const { getProperty, getSettingFlag, getPropertyValue } = useProperties({ - props, names: Object.values(SIZE_PROPERTY), parseNumber: true }) diff --git a/packages/settings/styles/src/components/spacing/SpacingGroup.vue b/packages/settings/styles/src/components/spacing/SpacingGroup.vue index bb45f860b6..57dc55861d 100644 --- a/packages/settings/styles/src/components/spacing/SpacingGroup.vue +++ b/packages/settings/styles/src/components/spacing/SpacingGroup.vue @@ -397,7 +397,6 @@ + \ No newline at end of file + diff --git a/packages/settings/styles/src/components/index.js b/packages/settings/styles/src/components/index.js index 65e388788d..6128af9223 100644 --- a/packages/settings/styles/src/components/index.js +++ b/packages/settings/styles/src/components/index.js @@ -10,19 +10,19 @@ * */ -export { default as BackgroundGroup } from './background/BackgroundGroup.vue' -export { default as BorderGroup } from './border/BorderGroup.vue' -export { default as EffectGroup } from './effects/EffectGroup.vue' -export { default as ImageSelect } from './inputs/ImageSelect.vue' -export { default as ResetButton } from './inputs/ResetButton.vue' -export { default as LayoutGroup } from './layout/LayoutGroup.vue' -export { default as PositionGroup } from './position/PositionGroup.vue' -export { default as BoxShadowGroup } from './shadow/BoxShadowGroup.vue' -export { default as SizeGroup } from './size/SizeGroup.vue' -export { default as SpacingGroup } from './spacing/SpacingGroup.vue' -export { default as SpacingSetting } from './spacing/SpacingSetting.vue' -export { default as TypographyGroup } from './typography/TypographyGroup.vue' -export { default as TypographyMore } from './typography/TypographyMore.vue' +export { default as BackgroundGroup } from './background/BackgroundGroup.vue' +export { default as BorderGroup } from './border/BorderGroup.vue' +export { default as EffectGroup } from './effects/EffectGroup.vue' +export { default as ImageSelect } from './inputs/ImageSelect.vue' +export { default as ResetButton } from './inputs/ResetButton.vue' +export { default as LayoutGroup } from './layout/LayoutGroup.vue' +export { default as PositionGroup } from './position/PositionGroup.vue' +export { default as BoxShadowGroup } from './shadow/BoxShadowGroup.vue' +export { default as SizeGroup } from './size/SizeGroup.vue' +export { default as SpacingGroup } from './spacing/SpacingGroup.vue' +export { default as SpacingSetting } from './spacing/SpacingSetting.vue' +export { default as TypographyGroup } from './typography/TypographyGroup.vue' +export { default as TypographyMore } from './typography/TypographyMore.vue' export { default as FlexBox } from './layout/FlexBox.vue' export { default as GridBox } from './layout/GridBox.vue' -export { default as ClassNameContainer } from './classNamesContainer' +export { default as ClassNamesContainer } from './classNamesContainer' diff --git a/packages/settings/styles/src/js/parser.js b/packages/settings/styles/src/js/parser.js index 7ae4148a10..d08bcd6179 100644 --- a/packages/settings/styles/src/js/parser.js +++ b/packages/settings/styles/src/js/parser.js @@ -1,187 +1,278 @@ import postcss from 'postcss' const handleRules = (node) => { - const declarations = node.nodes || [] - const style = {} - let selectors = node.selectors || '' - let commentIndex = 0 - - if (Array.isArray(selectors)) { - selectors = selectors.join(',') + const declarations = node.nodes || [] + const style = {} + let selectors = node.selectors || '' + let commentIndex = 0 + + if (Array.isArray(selectors)) { + selectors = selectors.join(',') + } + + declarations.forEach(({ prop, value, important, type, text }) => { + if (type === 'decl') { + style[prop] = { + type, + value: `${value}${important ? '!important' : ''}` + } + } else if (type === 'comment') { + style[`comment${commentIndex}`] = { + type, + value: `/*${text}*/` + } + commentIndex++ } + }) - declarations.forEach(({ prop, value, important, type, text }) => { - if (type === 'decl') { - style[prop] = { - type, - value: `${value}${important ? '!important' : ''}` - } - } else if (type === 'comment') { - style[`comment${commentIndex}`] = { - type, - value: `/*${text}*/` - } - commentIndex++ - } - }) - - return { - selectors, - style - } + return { + selectors, + style + } } const handleAtRules = (node) => { - // 这里我们不处理 at rules, 直接转换成字符串 - const { source = {}, type } = node - const { start, end, input } = source + // 这里我们不处理 at rules(如 @media、@keyframe 等规则), 直接转换成字符串 + const { source = {}, type } = node + const { start, end, input } = source - const rawString = input.css.slice(start.offset, end.offset) + const rawString = input.css.slice(start.offset, end.offset) - return { - type, - style: { - type, - value: rawString - } + return { + type, + style: { + type, + value: rawString } + } } const handleComments = (node) => { - const { type, text } = node - - // comment 需要存起来 + const { type, text } = node - return { - type, - style: { - type, - value: `/*${text}*/` - } + return { + type, + style: { + type, + value: `/*${text}*/` } + } } const nodeHandlerMap = { - rule: handleRules, - atrule: handleAtRules, - comment: handleComments + rule: handleRules, + atrule: handleAtRules, + comment: handleComments } /** - * + * 将 css 字符串解析成 css 对象 + * @param {string} css css 字符串 + * @returns */ export const parser = (css) => { - const parseList = [] - const selectors = [] - const styleObject = {} + const parseList = [] + const selectors = [] + const styleObject = {} - const ast = postcss().process(css).sync().root + if (!css) { + return { + parseList, + selectors, + styleObject + } + } - ast.nodes.forEach((node) => { - const { type } = node - const result = nodeHandlerMap[type](node) + const ast = postcss().process(css).sync().root - parseList.push(result) - }) + ast.nodes.forEach((node) => { + const { type } = node + const result = nodeHandlerMap[type](node) - parseList.forEach((item) => { - if (!item.selectors) { - return - } + parseList.push(result) + }) - // 不支持属性选择器,以及组合选择器 - if (/[,[\]>~+]/.test(item.selectors)) { - return - } + parseList.forEach((item) => { + if (!item.selectors) { + return + } - let selector = item.selectors - let mouseState = '' + // 不支持属性选择器,以及组合选择器 + if (/[,[\]>~+]/.test(item.selectors)) { + return + } - if (selector.includes(':')) { - const [pureSelector, innerMouseState] = selector.split(':') - // 仅支持部分伪类选择器 - if(!['hover', 'pressed', 'focused', 'disabled'].includes(innerMouseState)) { - return - } + let selector = item.selectors + let mouseState = '' - selector = pureSelector - mouseState = innerMouseState - } + if (selector.includes(':')) { + const [pureSelector, innerMouseState] = selector.split(':') + // 仅支持部分伪类选择器 + if (!['hover', 'pressed', 'focused', 'disabled'].includes(innerMouseState)) { + return + } - selectors.push(selector) + selector = pureSelector + mouseState = innerMouseState + } - styleObject[item.selectors] = { - mouseState, - pureSelector: selector - } - const rules = {} + selectors.push(selector) - Object.entries(item.style).forEach(([key, value]) => { - if (value.type !== 'decl') { - return - } - rules[key] = value.value - }) + styleObject[item.selectors] = { + mouseState, + pureSelector: selector + } + const rules = {} - styleObject[item.selectors].rules = rules + Object.entries(item.style).forEach(([key, value]) => { + if (value.type !== 'decl') { + return + } + rules[key] = value.value }) - return { - parseList, - selectors, - styleObject - } + styleObject[item.selectors].rules = rules + }) + + return { + parseList, + selectors, + styleObject + } } +/** + * 拿到组合选择器的数组,比如 .test1.test2 得到 ['.test1', '.test2'] + * @param {string} selector + * @returns + */ +export const getSelectorArr = (selector) => { + const res = [] -export const stringify = (originParseList, styleObject) => { - let str = '' - const originSelectors = [] + if (!selector || typeof selector !== 'string') { + return res + } - originParseList.forEach((item) => { - if (['comment', 'atrule'].includes(item.type) || !item.selectors) { - str += `\n${item.style.value}\n` + const separator = ['.', '#'] - return - } + for (let i = 0; i < selector.length; i++) { + if (!separator.includes(selector[i])) { + continue + } - originSelectors.push(item.selectors) + let str = selector[i] - str += `${item.selectors} {\n` + i++ - if (!styleObject[item.selectors]) { - for (const [key, value] of Object.entries(item.style)) { - if (key.includes('comment')) { - str += `${value.value}\n` - } else { - str += `${key}: ${value.value};\n` - } - } - } else { - // 在 styleObject 的,可能有改动,所以需要用 styleObject 拼接 - for (const [key, value] of Object.entries(styleObject[item.selectors].rules)) { - str += `${key}: ${value};\n` - } + while (!separator.includes(selector[i]) && i < selector.length) { + str += selector[i] + i++ + } - } - str += '}\n' + res.push(str) + + i-- + } + + return res +} + +// 根据配置替换选择器 +const getFinalSelector = (config = {}) => { + const { selectorStr, originSelector, newSelector } = config + + if (!originSelector || !newSelector) { + return selectorStr + } + + const { pureSelector, mouseState } = config + + const selectorArr = getSelectorArr(pureSelector) + + let finalSelector = selectorArr + .map((item) => { + if (item === originSelector) { + return newSelector + } + + return item }) + .join('') - // 需要找出 styleObject 新增的选择器,然后写入到 str 中 - Object.entries(styleObject).forEach(([selector, value]) => { - if (originSelectors.includes(selector)) { - return - } + if (mouseState) { + finalSelector += `:${mouseState}` + } - // 这里是新增的选择器,需要写入 - str += `${selector} {\n` + return finalSelector +} - for (const [declKey, declValue] of Object.entries(value.rules)) { - str += `${declKey}: ${declValue};\n` +/** + * 序列化对象成 css 字符串 + * @param {object} originParseList 原解析对象 + * @param {object} styleObject 可能被编辑过的 styleobject + * @param {object} config 配置,可以配置替换制定选择器 + * @returns string + */ +export const stringify = (originParseList, styleObject, config = {}) => { + let str = '' + const originSelectors = [] + // 配置需要替换的选择器 + const { originSelector, newSelector } = config + + originParseList.forEach((item) => { + if (['comment', 'atrule'].includes(item.type) || !item.selectors) { + str += `\n${item.style.value}\n` + + return + } + + originSelectors.push(item.selectors) + + if (!styleObject[item.selectors]) { + str += `${item.selectors} {\n` + + for (const [key, value] of Object.entries(item.style)) { + if (key.includes('comment')) { + str += `${value.value}\n` + } else { + str += `${key}: ${value.value};\n` } + } + } else { + const { mouseState, pureSelector } = styleObject[item.selectors] + const sel = getFinalSelector({ + selectorStr: item.selectors, + originSelector, + newSelector, + pureSelector, + mouseState + }) + + str += `${sel} {\n` + + // 在 styleObject 的,可能有改动,所以需要用 styleObject 拼接 + for (const [key, value] of Object.entries(styleObject[item.selectors].rules)) { + str += `${key}: ${value};\n` + } + } + str += '}\n' + }) - str += '}\n' - }) + // 需要找出 styleObject 新增的选择器,然后写入到 str 中 + Object.entries(styleObject).forEach(([selector, value]) => { + if (originSelectors.includes(selector)) { + return + } + + // 这里是新增的选择器,需要写入 + str += `${selector} {\n` + + for (const [declKey, declValue] of Object.entries(value.rules)) { + str += `${declKey}: ${declValue};\n` + } + + str += '}\n' + }) - return str + return str } diff --git a/packages/settings/styles/src/js/useStyle.js b/packages/settings/styles/src/js/useStyle.js index 595315ab01..2a2cb15f78 100644 --- a/packages/settings/styles/src/js/useStyle.js +++ b/packages/settings/styles/src/js/useStyle.js @@ -11,91 +11,341 @@ */ import { computed, reactive, watch } from 'vue' +import { useBroadcastChannel } from '@vueuse/core' +import { getSchema as getCanvasPageSchema, updateRect, setPageCss } from '@opentiny/tiny-engine-canvas' import { useCanvas, useHistory, useProperties as useProps } from '@opentiny/tiny-engine-controller' -import { camelize } from '@opentiny/tiny-engine-controller/utils' -import { obj2StyleStr } from '@opentiny/tiny-engine-common/js/css' -import { styleStr2Obj, styleStrRemoveRoot } from './cssConvert' -import { updateRect, getSchema as getCanvasPageSchema } from '@opentiny/tiny-engine-canvas' +import { formatString } from '@opentiny/tiny-engine-common/js/ast' +import { constants, utils } from '@opentiny/tiny-engine-utils' +import { parser, stringify, getSelectorArr } from './parser' -const getStyleObj = (styleStr) => { - let obj = {} +const { BROADCAST_CHANNEL, EXPRESSION_TYPE } = constants +const { generateRandomLetters, parseExpression } = utils - if (typeof styleStr === 'string') { - obj = styleStr2Obj(styleStr) +const { data: schemaLength } = useBroadcastChannel({ name: BROADCAST_CHANNEL.SchemaLength }) + +const state = reactive({ + // 当前选中节点的 style,解析成对象返回 + style: {}, + // 编辑器显示的行内样式字符串 + styleContent: '', + // 编辑器显示的全局样式字符串 + cssContent: '', + pageCssObject: {}, + currentClassSelector: '', + existClassSelectors: [], + className: { + classNameList: '', + mouseState: '' + }, + cssParseList: [], + selectors: [], + styleObject: {}, + currentClassNameList: [], + currentIdList: [], + selectorOptionLists: [], + schemaUpdateKey: 0 +}) + +const getCurrentClassSelector = () => { + let res = `${state.className.classNameList}` + const mouseState = state.className.mouseState + + if (mouseState) { + res += `:${mouseState}` } - return obj + return res } -export default () => { - const { getPageSchema, getCurrentSchema } = useCanvas() - const { getSchema } = useProps() - const { addHistory } = useHistory() - - const state = reactive({ - // 当前选中节点的 style,解析成对象返回 - style: {}, - // 编辑器显示的行内样式字符串 - styleContent: '', - // 编辑器显示的全局样式字符串 - cssContent: '' - }) +// 根据当前选中的组件,随机生成一个 css 类名 +export const genRandomClassNames = (componentName) => { + return `.${componentName}-${generateRandomLetters(5)}`.toLowerCase() +} + +const getPropsFromExpression = (propValue) => { + let res = [] - watch( - () => getPageSchema()?.css, - (value) => { - state.cssContent = value || '' + try { + const expressRes = parseExpression(propValue?.value) + + if (Array.isArray(expressRes)) { + res = expressRes + .map((item) => { + if (typeof item === 'string') { + return item + } + + if (typeof item === 'object') { + return Object.keys(item) + } + + return null + }) + .flat() + .filter(Boolean) + } else if (typeof expressRes === 'string' && expressRes) { + res = [expressRes] } - ) - - const setStyle = (styleString) => { - state.style = styleStr2Obj(styleString) - } - - watch( - [() => getCurrentSchema(), () => getCanvasPageSchema()], - () => { - const schema = getCurrentSchema() || getCanvasPageSchema() - const styleString = schema?.props?.style - state.styleContent = obj2StyleStr(getStyleObj(styleString)) - setStyle(styleString) - }, - { - immediate: true + } catch (e) { + // 不做处理 + } + + return res +} + +const parseClassOrIdProps = (propValue) => { + if (typeof propValue === 'string' && propValue) { + return propValue.split(' ').filter(Boolean) + } + + let res = [] + + if (propValue?.type === EXPRESSION_TYPE.JS_EXPRESSION) { + return getPropsFromExpression(propValue) + } + + return res +} + +const getClassNameAndIdList = (schema) => { + let classNameList = [] + let idList = [] + + if (!schema) { + return { + classNameList, + idList } - ) + } - // 更新 style 对象到 schema - const updateStyle = (properties) => { - const schema = getSchema() || getCanvasPageSchema() - schema.props = schema.props || {} + const classNameStr = schema?.props?.className + const idStr = schema?.props?.id - if (properties) { - Object.entries(properties).forEach(([key, value]) => { - state.style[camelize(key)] = value - }) + classNameList = parseClassOrIdProps(classNameStr) + idList = parseClassOrIdProps(idStr) + + return { + classNameList, + idList + } +} + +const { getPageSchema, getCurrentSchema } = useCanvas() +const { getSchema, propsUpdateKey } = useProps() +const { addHistory } = useHistory() + +watch( + () => [getCurrentSchema(), state.schemaUpdateKey, propsUpdateKey.value, getCanvasPageSchema(), schemaLength], + ([curSchema], [oldCurSchema] = []) => { + let schema = getCurrentSchema() + + if (!schema || Object.keys(schema).length === 0) { + schema = getCanvasPageSchema() + } + + if (!schema) { + return } - state.styleContent = obj2StyleStr(state.style) - const newStyleStr = styleStrRemoveRoot(state.styleContent) + // 获取当前选中组件的类名以及 id 列表 + const { classNameList, idList } = getClassNameAndIdList(schema) + + state.currentClassNameList = classNameList.map((item) => `.${item}`) + state.currentIdList = idList.map((item) => `#${item}`) - if (newStyleStr) { - schema.props.style = styleStrRemoveRoot(state.styleContent) - } else { - delete schema.props.style + // 变化了相当于重新选中了,需要重置当前选中的 className 以及样式面板的样式 + if (curSchema !== oldCurSchema) { + state.className = { + classNameList: '', + mouseState: '' + } + state.style = {} } - addHistory() - updateRect() + state.styleContent = `:root {\n ${schema?.props?.style || ''}\n}` + }, + { + immediate: true, + deep: true + } +) + +// 监听全局样式的变化,重新解析 +watch( + () => getPageSchema()?.css, + (value) => { + state.cssContent = value || '' + + // 解析css + const { parseList, selectors, styleObject } = parser(value) + + state.cssParseList = parseList + state.selectors = selectors + state.styleObject = styleObject } +) + +// 计算当前类名下拉列表 +watch( + () => [state.currentClassNameList, state.currentIdList, state.styleObject], + () => { + let list = [] + + const classNameListOptions = state.currentClassNameList.map((item) => ({ label: item, value: item })) + const idListOptions = state.currentIdList.map((item) => ({ label: item, value: item })) + + list = list.concat(classNameListOptions, idListOptions) + Object.values(state.styleObject).forEach((value) => { + const selectorArr = getSelectorArr(value.pureSelector) + + if (selectorArr.length <= 1) { + return + } + + const isComboSelector = selectorArr.every( + (item) => state.currentClassNameList.includes(item) || state.currentIdList.includes(item) + ) + + if (isComboSelector) { + list.push({ label: value.pureSelector, value: value.pureSelector }) + } + }) + + // 默认选择的类 + let defaultSelector = '' + let defaultMouseState = '' + const curClassName = state.className.classNameList + + if (list.find(({ value }) => value === curClassName)) { + defaultSelector = curClassName + defaultMouseState = state.className.mouseState + } else if (list.length) { + defaultSelector = list.at(-1).value + } + + state.selectorOptionLists = list + + state.className = { + classNameList: defaultSelector, + mouseState: defaultMouseState + } + } +) + +// 计算当前样式面板展示的样式 +watch( + () => state.className, + () => { + const { classNameList, mouseState } = state.className + + if (!classNameList) { + return + } + + const matchStyles = Object.values(state.styleObject).filter( + (value) => value.pureSelector === classNameList && value.mouseState === mouseState + ) + const style = matchStyles.length ? matchStyles[0].rules : {} + state.style = style + }, + { + deep: true + } +) + +export const updateGlobalStyleStr = (styleStr) => { + const pageSchema = getPageSchema() + + pageSchema.css = styleStr + getCanvasPageSchema().css = styleStr + setPageCss(styleStr) + state.schemaUpdateKey++ +} + +const updateGlobalStyle = (newSelector) => { + let currentSelector = getCurrentClassSelector() + + const mouseState = state.className.mouseState + + if (newSelector) { + currentSelector = newSelector + + if (mouseState) { + currentSelector += `:${mouseState}` + } + } + + state.styleObject[currentSelector] = { + ...(state.styleObject[currentSelector] || {}), + rules: state.style + } + + if (!Object.keys(state.style).length) { + delete state.styleObject[currentSelector] + } + + const styleStr = formatString(stringify(state.cssParseList, state.styleObject), 'css') + + updateGlobalStyleStr(styleStr) +} + +// 更新 style 对象到 schema +const updateStyle = (properties) => { + const schema = getSchema() || getCanvasPageSchema() + schema.props = schema.props || {} + + if (properties) { + Object.entries(properties).forEach(([key, value]) => { + state.style[key] = value + }) + } + + const currentSelector = getCurrentClassSelector() + let randomClassName = '' + + const classNames = schema.props.className || '' + + // 不存在选择器,需要生成一个随机类名,添加到当前选中组件中,然后写入到全局样式 + if (!currentSelector && typeof classNames === 'string') { + randomClassName = genRandomClassNames(schema?.componentName || 'component') + let newClassNames = randomClassName.slice(1) + + if (classNames) { + newClassNames = `${classNames} ${newClassNames}` + } + + schema.props.className = newClassNames + state.className.classNameList = randomClassName + } + + // 更新到全局样式 + updateGlobalStyle(randomClassName) + + addHistory() + updateRect() +} + +export default () => { return { state, - setStyle, updateStyle } } +const getTextOfValue = (value) => { + const basicValueMap = { + auto: 'auto', + none: 'none' + } + + if (basicValueMap[value] || /^\d+(\.\d+)?%$/.test(value)) { + return value + } + + return String(Number.parseInt(value) || '') +} + /** * 根据 style 对象生成样式属性对象 properties * styleName: { @@ -105,28 +355,20 @@ export default () => { * setting // 属性是否已设置值 * } */ -export const useProperties = ({ props, names, parseNumber }) => { +export const useProperties = ({ names, parseNumber }) => { const properties = computed(() => { - const properties = {} - if (Array.isArray(names) && props.style) { + const newProperties = {} + + if (Array.isArray(names) && state.style) { names.forEach((name) => { - name = camelize(name) - const value = props.style[name] + const value = state.style[name] let text = value || '' if (parseNumber) { - if (value === 'auto') { - text = 'auto' - } else if (value === 'none') { - text = 'none' - } else if (/^\d+(\.\d+)?%$/.test(value)) { - text = value - } else { - text = String(Number.parseInt(value) || '') - } + text = getTextOfValue(value) } - properties[name] = { + newProperties[name] = { name, // 属性名 text, // 界面显示的值 value, // 属性原始值 @@ -135,13 +377,13 @@ export const useProperties = ({ props, names, parseNumber }) => { }) } - return reactive(properties) + return newProperties }) - const getProperty = (styleName) => properties.value[camelize(styleName)] - const getSettingFlag = (styleName) => Boolean(properties.value[camelize(styleName)]?.setting) - const getPropertyText = (styleName) => properties.value[camelize(styleName)]?.text - const getPropertyValue = (styleName) => properties.value[camelize(styleName)]?.value + const getProperty = (styleName) => properties.value[styleName] + const getSettingFlag = (styleName) => Boolean(properties.value[styleName]?.setting) + const getPropertyText = (styleName) => properties.value[styleName]?.text + const getPropertyValue = (styleName) => properties.value[styleName]?.value return { properties, diff --git a/packages/theme/dark/settings.less b/packages/theme/dark/settings.less index 81abc1f7a2..67b47de136 100644 --- a/packages/theme/dark/settings.less +++ b/packages/theme/dark/settings.less @@ -22,3 +22,24 @@ --ti-lowcode-block-link-field-link-icon-color: var(--ti-lowcode-base-gray-0); --ti-lowcode-block-link-field-link-icon-bg-color: var(--ti-lowcode-base-success-color); } + +.className-container { + --ti-lowcode-className-selector-container-color: var(--ti-lowcode-base-text-color); + --ti-lowcode-className-selector-container-error-border-color: var(--ti-lowcode-base-error-color); + --ti-lowcode-className-selector-container-error-bg-color: rgba(242, 48, 48, 0.1); + --ti-lowcode-className-selector-error-tips-color: var(--ti-lowcode-base-error-color); + --ti-lowcode-className-selector-container-border-color: var(--ti-lowcode-base-gray-40); + --ti-lowcode-className-selector-container-hover-border-color: var(--ti-lowcode-base-primary-color-2); + --ti-lowcode-className-selector-container-empty-tips-color: var(--ti-lowcode-base-text-color-1); + --ti-lowcode-className-selector-container-label-bg-color: var(--ti-lowcode-base-blue-6); + --ti-lowcode-className-selector-container-option-btn-color: var(--ti-lowcode-base-gray-0); + --ti-lowcode-className-selector-dropdown-list-bg-color: #202020; + --ti-lowcode-className-selector-dropdown-list-item-color: var(--ti-lowcode-base-text-color); + --ti-lowcode-className-selector-dropdown-list-item-active-bg-color: var(--ti-lowcode-base-bg-2); + --ti-lowcode-className-selector-title-color: var(--ti-lowcode-base-text-color); +} + +:root { + --ti-lowcode-className-selector-del-popover-bg-color: var(--ti-lowcode-base-bg-5); + --ti-lowcode-className-selector-del-popover-title-color: var(--ti-lowcode-base-text-color); +} diff --git a/packages/theme/light/settings.less b/packages/theme/light/settings.less index 14f2c9de6b..a23edb9480 100644 --- a/packages/theme/light/settings.less +++ b/packages/theme/light/settings.less @@ -22,3 +22,24 @@ --ti-lowcode-block-link-field-link-icon-color: var(--ti-lowcode-base-gray-0); --ti-lowcode-block-link-field-link-icon-bg-color: var(--ti-lowcode-base-success-color); } + +.className-container { + --ti-lowcode-className-selector-container-color: var(--ti-lowcode-base-text-color); + --ti-lowcode-className-selector-container-error-border-color: var(--ti-lowcode-base-error-color); + --ti-lowcode-className-selector-container-error-bg-color: rgba(242, 48, 48, 0.1); + --ti-lowcode-className-selector-error-tips-color: var(--ti-lowcode-base-error-color); + --ti-lowcode-className-selector-container-border-color: var(--ti-lowcode-base-gray-40); + --ti-lowcode-className-selector-container-hover-border-color: var(--ti-lowcode-base-gray-90); + --ti-lowcode-className-selector-container-empty-tips-color: var(--ti-lowcode-base-text-color-1); + --ti-lowcode-className-selector-container-label-bg-color: var(--ti-lowcode-base-blue-6); + --ti-lowcode-className-selector-container-option-btn-color: var(--ti-lowcode-base-gray-0); + --ti-lowcode-className-selector-dropdown-list-bg-color: var(--ti-lowcode-base-gray-0); + --ti-lowcode-className-selector-dropdown-list-item-color: var(--ti-lowcode-base-text-color); + --ti-lowcode-className-selector-dropdown-list-item-active-bg-color: var(--ti-lowcode-base-bg-2); + --ti-lowcode-className-selector-title-color: var(--ti-lowcode-base-text-color); +} + +:root { + --ti-lowcode-className-selector-del-popover-bg-color: var(--ti-lowcode-base-bg-5); + --ti-lowcode-className-selector-del-popover-title-color: var(--ti-lowcode-base-text-color); +} From 454ed12d8ac81ee993a3a92453420ae8a8121803 Mon Sep 17 00:00:00 2001 From: chilingling Date: Wed, 1 Nov 2023 19:07:41 -0700 Subject: [PATCH 3/4] fix(build): fix setting-style plugin build error --- packages/settings/styles/package.json | 1 + packages/settings/styles/src/components/index.js | 2 +- packages/settings/styles/src/js/parser.js | 4 ---- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/settings/styles/package.json b/packages/settings/styles/package.json index 8193d972d9..9b81820376 100644 --- a/packages/settings/styles/package.json +++ b/packages/settings/styles/package.json @@ -17,6 +17,7 @@ "@opentiny/tiny-engine-common": "workspace:^1.0.0", "@opentiny/tiny-engine-controller": "workspace:^1.0.0", "@opentiny/tiny-engine-http": "workspace:^1.0.0", + "@opentiny/tiny-engine-utils": "workspace:^1.0.0", "@opentiny/vue": "~3.10.0", "@opentiny/vue-renderless": "~3.10.0", "@vueuse/core": "^9.6.0", diff --git a/packages/settings/styles/src/components/index.js b/packages/settings/styles/src/components/index.js index 6128af9223..ec2f4c1c78 100644 --- a/packages/settings/styles/src/components/index.js +++ b/packages/settings/styles/src/components/index.js @@ -25,4 +25,4 @@ export { default as TypographyGroup } from './typography/TypographyGroup.vue' export { default as TypographyMore } from './typography/TypographyMore.vue' export { default as FlexBox } from './layout/FlexBox.vue' export { default as GridBox } from './layout/GridBox.vue' -export { default as ClassNamesContainer } from './classNamesContainer' +export { default as ClassNamesContainer } from './classNamesContainer/index.vue' diff --git a/packages/settings/styles/src/js/parser.js b/packages/settings/styles/src/js/parser.js index d08bcd6179..c8712c72e5 100644 --- a/packages/settings/styles/src/js/parser.js +++ b/packages/settings/styles/src/js/parser.js @@ -156,10 +156,6 @@ export const getSelectorArr = (selector) => { const separator = ['.', '#'] for (let i = 0; i < selector.length; i++) { - if (!separator.includes(selector[i])) { - continue - } - let str = selector[i] i++ From 82522ba6ed9223ac35b1475354af456bd869afc8 Mon Sep 17 00:00:00 2001 From: chilingling Date: Thu, 7 Dec 2023 22:47:13 -0800 Subject: [PATCH 4/4] fix(chore): fix review comment --- .../styles/src/components/classNamesContainer/index.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/settings/styles/src/components/classNamesContainer/index.vue b/packages/settings/styles/src/components/classNamesContainer/index.vue index e13cafed8a..012fde5488 100644 --- a/packages/settings/styles/src/components/classNamesContainer/index.vue +++ b/packages/settings/styles/src/components/classNamesContainer/index.vue @@ -278,7 +278,7 @@ const selectorValidator = (selector) => { } // 不能包含空格 - if (sel.includes(' ') || sel.includes('>') || sel.includes('~') || sel.includes('+')) { + if (/[\s>~+]/.test(sel)) { classNameState.selectorHasError = "不能包含空格 '>' '~' '+' 等符号" return false