forked from opentiny/tiny-engine
feat(style): stylePanel add style selector, write css to global styles (#41)
* feat(style): stylePanel add style selector, write css to global styles * feat(style): classNameSelector support edit and delete * fix(build): fix setting-style plugin build error * fix(chore): fix review comment
This commit is contained in:
parent
0478258c87
commit
fd5baf1660
|
@ -410,6 +410,8 @@ export const clearSelect = () => {
|
||||||
canvasState.current = null
|
canvasState.current = null
|
||||||
canvasState.parent = null
|
canvasState.parent = null
|
||||||
Object.assign(selectState, initialRectState)
|
Object.assign(selectState, initialRectState)
|
||||||
|
// 临时借用 remote 事件出发 currentSchema 更新
|
||||||
|
canvasState?.emit?.('remove')
|
||||||
}
|
}
|
||||||
|
|
||||||
export const querySelectById = (id, type = '') => {
|
export const querySelectById = (id, type = '') => {
|
||||||
|
|
|
@ -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 useCanvas from './useCanvas'
|
||||||
import useResource from './useResource'
|
import useResource from './useResource'
|
||||||
import useTranslate from './useTranslate'
|
import useTranslate from './useTranslate'
|
||||||
import { getNode, setState, updateRect } from '@opentiny/tiny-engine-canvas'
|
|
||||||
|
const propsUpdateKey = ref(0)
|
||||||
|
|
||||||
const otherBaseKey = {
|
const otherBaseKey = {
|
||||||
className: {
|
className: {
|
||||||
|
@ -168,6 +170,10 @@ const getProps = (schema, parent) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const setProp = (name, value) => {
|
const setProp = (name, value) => {
|
||||||
|
if (!properties.schema) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
properties.schema.props = properties.schema.props || {}
|
properties.schema.props = properties.schema.props || {}
|
||||||
|
|
||||||
if (value === '' || value === undefined || value === null) {
|
if (value === '' || value === undefined || value === null) {
|
||||||
|
@ -178,6 +184,7 @@ const setProp = (name, value) => {
|
||||||
|
|
||||||
// 没有父级,或者不在节点上面,要更新内容。就用setState
|
// 没有父级,或者不在节点上面,要更新内容。就用setState
|
||||||
getNode(properties.schema.id, true).parent || setState(useCanvas().getPageSchema().state)
|
getNode(properties.schema.id, true).parent || setState(useCanvas().getPageSchema().state)
|
||||||
|
propsUpdateKey.value++
|
||||||
nextTick(updateRect)
|
nextTick(updateRect)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -188,6 +195,7 @@ const getProp = (key) => {
|
||||||
const delProp = (name) => {
|
const delProp = (name) => {
|
||||||
const props = properties.schema.props || {}
|
const props = properties.schema.props || {}
|
||||||
delete props[name]
|
delete props[name]
|
||||||
|
propsUpdateKey.value++
|
||||||
}
|
}
|
||||||
|
|
||||||
const setProps = (schema) => {
|
const setProps = (schema) => {
|
||||||
|
@ -205,6 +213,7 @@ export default function () {
|
||||||
translateProp,
|
translateProp,
|
||||||
getSchema(parent) {
|
getSchema(parent) {
|
||||||
return parent ? properties : properties.schema
|
return parent ? properties : properties.schema
|
||||||
}
|
},
|
||||||
|
propsUpdateKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,8 +28,11 @@
|
||||||
"@opentiny/tiny-engine-common": "workspace:^1.0.0",
|
"@opentiny/tiny-engine-common": "workspace:^1.0.0",
|
||||||
"@opentiny/tiny-engine-controller": "workspace:^1.0.0",
|
"@opentiny/tiny-engine-controller": "workspace:^1.0.0",
|
||||||
"@opentiny/tiny-engine-http": "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": "~3.10.0",
|
||||||
"@opentiny/vue-renderless": "~3.10.0",
|
"@opentiny/vue-renderless": "~3.10.0",
|
||||||
|
"@vueuse/core": "^9.6.0",
|
||||||
|
"postcss": "^8.4.31",
|
||||||
"vue": "3.2.45"
|
"vue": "3.2.45"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
@save="save(CSS_TYPE.Style, $event)"
|
@save="save(CSS_TYPE.Style, $event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<class-names-container></class-names-container>
|
||||||
<tiny-collapse v-model="activeNames">
|
<tiny-collapse v-model="activeNames">
|
||||||
<tiny-collapse-item title="布局" name="layout">
|
<tiny-collapse-item title="布局" name="layout">
|
||||||
<layout-group :display="state.style.display" @update="updateStyle" />
|
<layout-group :display="state.style.display" @update="updateStyle" />
|
||||||
|
@ -58,23 +58,26 @@
|
||||||
<script>
|
<script>
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { Collapse, CollapseItem } from '@opentiny/vue'
|
import { Collapse, CollapseItem } from '@opentiny/vue'
|
||||||
|
import { useHistory, useCanvas, useProperties } from '@opentiny/tiny-engine-controller'
|
||||||
|
import { setPageCss, getSchema as getCanvasPageSchema } from '@opentiny/tiny-engine-canvas'
|
||||||
import { MetaCodeEditor } from '@opentiny/tiny-engine-common'
|
import { MetaCodeEditor } from '@opentiny/tiny-engine-common'
|
||||||
import SizeGroup from './components/size/SizeGroup.vue'
|
import { formatString } from '@opentiny/tiny-engine-common/js/ast'
|
||||||
import LayoutGroup from './components/layout/LayoutGroup.vue'
|
import {
|
||||||
import FlexBox from './components/layout/FlexBox.vue'
|
SizeGroup,
|
||||||
import GridBox from './components/layout/GridBox.vue'
|
LayoutGroup,
|
||||||
import PositionGroup from './components/position/PositionGroup.vue'
|
FlexBox,
|
||||||
import BorderGroup from './components/border/BorderGroup.vue'
|
GridBox,
|
||||||
import SpacingGroup from './components/spacing/SpacingGroup.vue'
|
PositionGroup,
|
||||||
import BackgroundGroup from './components/background/BackgroundGroup.vue'
|
BorderGroup,
|
||||||
import EffectGroup from './components/effects/EffectGroup.vue'
|
SpacingGroup,
|
||||||
// import BoxShadowGroup from './components/shadow/BoxShadowGroup.vue'
|
BackgroundGroup,
|
||||||
import TypographyGroup from './components/typography/TypographyGroup.vue'
|
EffectGroup,
|
||||||
|
TypographyGroup,
|
||||||
|
ClassNamesContainer
|
||||||
|
} from './components'
|
||||||
import { CSS_TYPE } from './js/cssType'
|
import { CSS_TYPE } from './js/cssType'
|
||||||
import useStyle from './js/useStyle'
|
import useStyle from './js/useStyle'
|
||||||
import { styleStrRemoveRoot } from './js/cssConvert'
|
import { styleStrRemoveRoot } from './js/cssConvert'
|
||||||
import { useHistory, useCanvas, useProperties } from '@opentiny/tiny-engine-controller'
|
|
||||||
import { setPageCss, getSchema as getCanvasPageSchema } from '@opentiny/tiny-engine-canvas'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -87,9 +90,9 @@ export default {
|
||||||
BorderGroup,
|
BorderGroup,
|
||||||
SpacingGroup,
|
SpacingGroup,
|
||||||
BackgroundGroup,
|
BackgroundGroup,
|
||||||
// BoxShadowGroup,
|
|
||||||
TypographyGroup,
|
TypographyGroup,
|
||||||
EffectGroup,
|
EffectGroup,
|
||||||
|
ClassNamesContainer,
|
||||||
TinyCollapse: Collapse,
|
TinyCollapse: Collapse,
|
||||||
TinyCollapseItem: CollapseItem
|
TinyCollapseItem: CollapseItem
|
||||||
},
|
},
|
||||||
|
@ -106,7 +109,7 @@ export default {
|
||||||
])
|
])
|
||||||
const { getCurrentSchema, getPageSchema } = useCanvas()
|
const { getCurrentSchema, getPageSchema } = useCanvas()
|
||||||
// 获取当前节点 style 对象
|
// 获取当前节点 style 对象
|
||||||
const { state, updateStyle, setStyle } = useStyle() // updateStyle
|
const { state, updateStyle } = useStyle() // updateStyle
|
||||||
const { addHistory } = useHistory()
|
const { addHistory } = useHistory()
|
||||||
const { getSchema } = useProperties()
|
const { getSchema } = useProperties()
|
||||||
|
|
||||||
|
@ -117,7 +120,7 @@ export default {
|
||||||
if (type === CSS_TYPE.Style) {
|
if (type === CSS_TYPE.Style) {
|
||||||
const pageSchema = getCanvasPageSchema()
|
const pageSchema = getCanvasPageSchema()
|
||||||
const schema = getSchema() || pageSchema
|
const schema = getSchema() || pageSchema
|
||||||
const styleString = styleStrRemoveRoot(content)
|
const styleString = formatString(styleStrRemoveRoot(content), 'css')
|
||||||
const currentSchema = getCurrentSchema() || pageSchema
|
const currentSchema = getCurrentSchema() || pageSchema
|
||||||
|
|
||||||
state.styleContent = content
|
state.styleContent = content
|
||||||
|
@ -132,13 +135,13 @@ export default {
|
||||||
delete currentSchema.props.style
|
delete currentSchema.props.style
|
||||||
}
|
}
|
||||||
|
|
||||||
setStyle(styleString)
|
|
||||||
addHistory()
|
addHistory()
|
||||||
} else if (type === CSS_TYPE.Css) {
|
} else if (type === CSS_TYPE.Css) {
|
||||||
const cssString = content.replace(/"/g, "'")
|
const cssString = formatString(content.replace(/"/g, "'"), 'css')
|
||||||
getPageSchema().css = cssString
|
getPageSchema().css = cssString
|
||||||
getCanvasPageSchema().css = cssString
|
getCanvasPageSchema().css = cssString
|
||||||
setPageCss(cssString)
|
setPageCss(cssString)
|
||||||
|
state.schemaUpdateKey++
|
||||||
addHistory()
|
addHistory()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -144,7 +144,6 @@ export default {
|
||||||
})
|
})
|
||||||
|
|
||||||
const { getProperty, getSettingFlag } = useProperties({
|
const { getProperty, getSettingFlag } = useProperties({
|
||||||
props,
|
|
||||||
names: Object.values(BACKGROUND_PROPERTY),
|
names: Object.values(BACKGROUND_PROPERTY),
|
||||||
parseNumber: true
|
parseNumber: true
|
||||||
})
|
})
|
||||||
|
|
|
@ -235,7 +235,6 @@ import useEvent from '../../js/useEvent'
|
||||||
import { useProperties } from '../../js/useStyle'
|
import { useProperties } from '../../js/useStyle'
|
||||||
import { RADIUS_SETTING, BORDER_SETTING, BORDER_STYLE_TYPE } from '../../js/cssType'
|
import { RADIUS_SETTING, BORDER_SETTING, BORDER_STYLE_TYPE } from '../../js/cssType'
|
||||||
import { BORDER_PROPERTY, BORDER_RADIUS_PROPERTY } from '../../js/styleProperty'
|
import { BORDER_PROPERTY, BORDER_RADIUS_PROPERTY } from '../../js/styleProperty'
|
||||||
// import { hyphenate } from '@opentiny/tiny-engine-controller/utils'
|
|
||||||
|
|
||||||
const BORDER_STYLE = {
|
const BORDER_STYLE = {
|
||||||
[BORDER_SETTING.All]: BORDER_PROPERTY.BorderStyle,
|
[BORDER_SETTING.All]: BORDER_PROPERTY.BorderStyle,
|
||||||
|
@ -299,7 +298,6 @@ export default {
|
||||||
const { setPosition } = useModal()
|
const { setPosition } = useModal()
|
||||||
|
|
||||||
const { getProperty, getSettingFlag, getPropertyValue } = useProperties({
|
const { getProperty, getSettingFlag, getPropertyValue } = useProperties({
|
||||||
props,
|
|
||||||
names: Object.values({ ...BORDER_RADIUS_PROPERTY, ...BORDER_PROPERTY }),
|
names: Object.values({ ...BORDER_RADIUS_PROPERTY, ...BORDER_PROPERTY }),
|
||||||
parseNumber: true
|
parseNumber: true
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,636 @@
|
||||||
|
<template>
|
||||||
|
<div class="className-container">
|
||||||
|
<h6 class="title">样式选择器</h6>
|
||||||
|
<div class="selector-container">
|
||||||
|
<div class="className-selector-wrap">
|
||||||
|
<div
|
||||||
|
:class="['className-selector-container', { 'has-error': classNameState.selectorHasError }]"
|
||||||
|
@click="handleFocusInput"
|
||||||
|
>
|
||||||
|
<div v-if="classNameState.curSelector || classNameState.curSelectorIsEditing" class="current-selector">
|
||||||
|
<div class="current-selector-label">
|
||||||
|
<span
|
||||||
|
ref="selectorTextRef"
|
||||||
|
:contenteditable="classNameState.curSelectorIsEditing"
|
||||||
|
:class="['selector-label-text', { 'text-editing': classNameState.curSelectorIsEditing }]"
|
||||||
|
:key="classNameState.curSelectorIsEditing"
|
||||||
|
@click.stop="handleEditCurSelector"
|
||||||
|
@input="handleCurSelectorChange"
|
||||||
|
@blur="handleCompleteEditCurSelector"
|
||||||
|
@keyup.enter="handleCompleteEditCurSelector"
|
||||||
|
@keyup.esc="handleCompleteEditCurSelector"
|
||||||
|
>
|
||||||
|
{{ classNameState.curSelector }}
|
||||||
|
</span>
|
||||||
|
<div v-if="!classNameState.curSelectorIsEditing && classNameState.curSelectorEditable" class="edit-wrap">
|
||||||
|
<svg-icon name="edit" title="编辑" class="edit-btn" @click.stop="handleEditCurSelector"></svg-icon>
|
||||||
|
<tiny-popover
|
||||||
|
v-model="classNameState.showDelConfirm"
|
||||||
|
trigger="manual"
|
||||||
|
class="del-selector-popover"
|
||||||
|
popper-class="del-selector-popper-wrapper"
|
||||||
|
>
|
||||||
|
<div class="popper-confirm" @mousedown.stop="">
|
||||||
|
<div class="popper-confirm-header">
|
||||||
|
<svg-icon class="icon" name="warning"></svg-icon>
|
||||||
|
<span class="title">您确定删除该选择器吗?</span>
|
||||||
|
</div>
|
||||||
|
<div class="popper-confirm-footer">
|
||||||
|
<tiny-button class="confirm-btn" size="small" type="primary" @click="handleDelSelector">
|
||||||
|
确定
|
||||||
|
</tiny-button>
|
||||||
|
<tiny-button size="small" class="cancel-btn" @click="handleTriggerDelConfirm(false)">
|
||||||
|
取消
|
||||||
|
</tiny-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #reference>
|
||||||
|
<svg-icon
|
||||||
|
name="delete"
|
||||||
|
title="删除"
|
||||||
|
class="delete-btn"
|
||||||
|
@click.stop="handleTriggerDelConfirm(true)"
|
||||||
|
></svg-icon>
|
||||||
|
</template>
|
||||||
|
</tiny-popover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span v-else class="empty-tips">请选择或创建类名</span>
|
||||||
|
<input
|
||||||
|
ref="newSelectorInputRef"
|
||||||
|
type="text"
|
||||||
|
v-model="classNameState.newSelector"
|
||||||
|
class="selector-input"
|
||||||
|
@change="handleInputChange"
|
||||||
|
@blur="handleCreateNewClass"
|
||||||
|
@keyup.enter="handleCreateNewClass"
|
||||||
|
/>
|
||||||
|
<div v-if="classNameState.showDropdownList" class="selector-drop-down-list">
|
||||||
|
<span class="selector-dropdown-list-tips">输入并回车创建新选择器</span>
|
||||||
|
<span v-if="currentSelectorList.length" class="selector-dropdown-list-tips">选择已有选择器编辑</span>
|
||||||
|
<ul class="exist-class-list">
|
||||||
|
<li
|
||||||
|
v-for="item in currentSelectorList"
|
||||||
|
:key="item"
|
||||||
|
:title="item"
|
||||||
|
class="exist-class-item"
|
||||||
|
@mousedown="handleSelectExistingClass(item)"
|
||||||
|
>
|
||||||
|
<span>{{ item }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<span v-if="state.selectors.length" class="selector-dropdown-list-tips add-global-class-tips">
|
||||||
|
添加全局类到当前组件并编辑
|
||||||
|
</span>
|
||||||
|
<ul class="exist-class-list">
|
||||||
|
<li
|
||||||
|
v-for="item in state.selectors"
|
||||||
|
:key="item"
|
||||||
|
:title="item"
|
||||||
|
class="exist-class-item"
|
||||||
|
@mousedown="handleSelectExistingClass(item)"
|
||||||
|
>
|
||||||
|
<span>{{ item }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="classNameState.selectorHasError" class="error-tips">{{ classNameState.selectorHasError }}</div>
|
||||||
|
</div>
|
||||||
|
<tiny-select v-model="state.className.mouseState" :options="stateOptions" class="state-selector"> </tiny-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, reactive, ref, nextTick, watch, watchEffect } from 'vue'
|
||||||
|
import { Select as TinySelect, Popover as TinyPopover, Button as TinyButton } from '@opentiny/vue'
|
||||||
|
import { getSchema as getCanvasPageSchema } from '@opentiny/tiny-engine-canvas'
|
||||||
|
import { useProperties } from '@opentiny/tiny-engine-controller'
|
||||||
|
import useStyle, { updateGlobalStyleStr } from '../../js/useStyle'
|
||||||
|
import { stringify, getSelectorArr } from '../../js/parser'
|
||||||
|
|
||||||
|
const { getSchema, propsUpdateKey } = useProperties()
|
||||||
|
|
||||||
|
const stateOptions = [
|
||||||
|
{ label: 'None', value: '' },
|
||||||
|
{ label: 'hover', value: 'hover' },
|
||||||
|
{ label: 'pressed', value: 'pressed' },
|
||||||
|
{ label: 'focused', value: 'focused' },
|
||||||
|
{ label: 'disabled', value: 'disabled' }
|
||||||
|
]
|
||||||
|
const SELECTOR_TYPE = {
|
||||||
|
CLASS_NAME: 'className',
|
||||||
|
ID: 'id'
|
||||||
|
}
|
||||||
|
|
||||||
|
const OPTION_TYPE = {
|
||||||
|
ADD: 'add',
|
||||||
|
REMOVE: 'remove',
|
||||||
|
EDIT: 'edit'
|
||||||
|
}
|
||||||
|
|
||||||
|
const classNameState = reactive({
|
||||||
|
curSelector: '',
|
||||||
|
curSelectorEditable: false,
|
||||||
|
newSelector: '',
|
||||||
|
curSelectorIsEditing: false,
|
||||||
|
preSelector: '',
|
||||||
|
isSelectorValid: true,
|
||||||
|
showDropdownList: false,
|
||||||
|
showDelConfirm: false,
|
||||||
|
selectorHasError: ''
|
||||||
|
})
|
||||||
|
const selectorTextRef = ref(null)
|
||||||
|
const newSelectorInputRef = ref(null)
|
||||||
|
const state = useStyle().state
|
||||||
|
|
||||||
|
const getCurSelectorEditable = (selector) => {
|
||||||
|
const selArr = getSelectorArr(selector)
|
||||||
|
|
||||||
|
return selArr.length < 2
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => state.className.classNameList,
|
||||||
|
(className) => {
|
||||||
|
classNameState.showDelConfirm = false
|
||||||
|
classNameState.selectorHasError = ''
|
||||||
|
|
||||||
|
if (classNameState.curSelectorIsEditing) {
|
||||||
|
classNameState.curSelectorIsEditing = false
|
||||||
|
}
|
||||||
|
|
||||||
|
classNameState.curSelector = className
|
||||||
|
// 多类名的选择器的暂时不支持编辑,比如 .test1.test2
|
||||||
|
classNameState.curSelectorEditable = getCurSelectorEditable(className)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const setSelectorProps = (type, value) => {
|
||||||
|
const schema = getSchema() || getCanvasPageSchema()
|
||||||
|
|
||||||
|
if (!schema.props) {
|
||||||
|
schema.props = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
schema.props[type] = value
|
||||||
|
propsUpdateKey.value++
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑 className 新增、删除、或修改
|
||||||
|
const editClassName = (curClassName, optionType = OPTION_TYPE.ADD, oldSelector = '') => {
|
||||||
|
const schema = getSchema() || getCanvasPageSchema()
|
||||||
|
const type = curClassName.startsWith('.') ? SELECTOR_TYPE.CLASS_NAME : SELECTOR_TYPE.ID
|
||||||
|
const classNames = schema.props.className || ''
|
||||||
|
const ids = schema.props.id || ''
|
||||||
|
const typeMap = {
|
||||||
|
[SELECTOR_TYPE.CLASS_NAME]: classNames,
|
||||||
|
[SELECTOR_TYPE.ID]: ids
|
||||||
|
}
|
||||||
|
let newClassNames = curClassName.slice(1)
|
||||||
|
|
||||||
|
// 表达式类型,无法写入或删除
|
||||||
|
if (typeof typeMap[type] !== 'string') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const editSelectorHandler = () => {
|
||||||
|
const oldSelType = oldSelector.startsWith('.') ? SELECTOR_TYPE.CLASS_NAME : SELECTOR_TYPE.ID
|
||||||
|
let oldSelSymbol = oldSelector.slice(1)
|
||||||
|
let res = newClassNames
|
||||||
|
|
||||||
|
// 前后两种类型不一致,需要将原类型里面的删除,然后在新类型添加
|
||||||
|
if (oldSelType !== type) {
|
||||||
|
const selArr = typeMap[oldSelType].split(' ').filter((item) => item !== oldSelSymbol) || []
|
||||||
|
|
||||||
|
setSelectorProps(oldSelType, selArr.join(' '))
|
||||||
|
|
||||||
|
res = `${typeMap[type] ?? ''} ${newClassNames}`
|
||||||
|
} else {
|
||||||
|
// 前后两种类型一直,替换原类型就好
|
||||||
|
res = typeMap[type]
|
||||||
|
.split(' ')
|
||||||
|
.map((item) => {
|
||||||
|
if (item === oldSelSymbol) {
|
||||||
|
return newClassNames
|
||||||
|
}
|
||||||
|
|
||||||
|
return item
|
||||||
|
})
|
||||||
|
.join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
const addSelectorHandler = () => {
|
||||||
|
return `${typeMap[type] ?? ''} ${newClassNames}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeSelectorHandler = () => {
|
||||||
|
const leftSelectors = typeMap[type].split(' ').filter((item) => item !== newClassNames && Boolean(item)) || []
|
||||||
|
|
||||||
|
return leftSelectors.join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlersMap = {
|
||||||
|
[OPTION_TYPE.ADD]: addSelectorHandler,
|
||||||
|
[OPTION_TYPE.REMOVE]: removeSelectorHandler,
|
||||||
|
[OPTION_TYPE.EDIT]: editSelectorHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
newClassNames = handlersMap[optionType]?.()
|
||||||
|
|
||||||
|
setSelectorProps(type, newClassNames)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当前选中组件解析出来的选择器
|
||||||
|
const currentSelectorList = computed(() => [...state.currentClassNameList, ...state.currentIdList])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选择器简单校验规则
|
||||||
|
* 必须以下划线、连字符 - 或字符 a-z 开头,不能是数字
|
||||||
|
* @param {string} selector
|
||||||
|
*/
|
||||||
|
const selectorValidator = (selector) => {
|
||||||
|
let sel = selector.trim()
|
||||||
|
|
||||||
|
classNameState.selectorHasError = ''
|
||||||
|
|
||||||
|
if (sel.startsWith('.') || sel.startsWith('#')) {
|
||||||
|
sel = sel.slice(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开头不能是数字
|
||||||
|
if (/^[0-9]/.test(sel)) {
|
||||||
|
classNameState.selectorHasError = '开头不能是数字'
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 限制只添加一个类名
|
||||||
|
if (sel.includes('.') || sel.includes('#')) {
|
||||||
|
classNameState.selectorHasError = '单次只能添加一个类名'
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 不能包含空格
|
||||||
|
if (/[\s>~+]/.test(sel)) {
|
||||||
|
classNameState.selectorHasError = "不能包含空格 '>' '~' '+' 等符号"
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加现有 class 或者 id 到选中的组件中
|
||||||
|
const handleSelectExistingClass = (selector) => {
|
||||||
|
if (!state.selectorOptionLists.find(({ value }) => value === selector)) {
|
||||||
|
editClassName(selector, OPTION_TYPE.ADD)
|
||||||
|
}
|
||||||
|
|
||||||
|
state.className.classNameList = selector
|
||||||
|
state.className.mouseState = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 输入框失焦或输入回车即创建新类名
|
||||||
|
const handleCreateNewClass = () => {
|
||||||
|
// 收起下拉组件
|
||||||
|
classNameState.showDropdownList = false
|
||||||
|
let newSelector = classNameState.newSelector
|
||||||
|
const isValid = selectorValidator(newSelector)
|
||||||
|
classNameState.selectorHasError = ''
|
||||||
|
|
||||||
|
// 清空选择器
|
||||||
|
classNameState.newSelector = ''
|
||||||
|
newSelectorInputRef.value?.blur?.()
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newSelector.startsWith('.') && !newSelector.startsWith('#')) {
|
||||||
|
newSelector = `.${newSelector}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newSelector.length <= 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将类名添加到组件中
|
||||||
|
if (!state.selectorOptionLists.find(({ value }) => value === newSelector)) {
|
||||||
|
editClassName(newSelector, OPTION_TYPE.ADD)
|
||||||
|
}
|
||||||
|
|
||||||
|
state.className.classNameList = newSelector
|
||||||
|
state.className.mouseState = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑当前选择器
|
||||||
|
const handleEditCurSelector = async () => {
|
||||||
|
if (!classNameState.curSelectorEditable) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
classNameState.curSelectorIsEditing = true
|
||||||
|
classNameState.preSelector = classNameState.curSelector
|
||||||
|
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
if (!selectorTextRef.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑默认选中文本内容
|
||||||
|
const range = document.createRange()
|
||||||
|
range.setStart(selectorTextRef.value.childNodes[0], 0)
|
||||||
|
range.setEnd(selectorTextRef.value.childNodes[0], selectorTextRef.value.textContent.length)
|
||||||
|
|
||||||
|
const selection = window.getSelection()
|
||||||
|
selection.removeAllRanges()
|
||||||
|
selection.addRange(range)
|
||||||
|
|
||||||
|
selectorTextRef.value.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑当前选择器,回车、失焦、esc 键都代表确认
|
||||||
|
const handleCompleteEditCurSelector = () => {
|
||||||
|
if (!selectorTextRef.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const curValue = selectorTextRef.value.textContent
|
||||||
|
let textValue = curValue
|
||||||
|
|
||||||
|
if (textValue.startsWith('#') || textValue.startsWith('.')) {
|
||||||
|
textValue = textValue.slice(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
classNameState.curSelectorIsEditing = false
|
||||||
|
classNameState.showDropdownList = false
|
||||||
|
classNameState.selectorHasError = ''
|
||||||
|
|
||||||
|
// 修改的选择器不合法
|
||||||
|
if (!selectorValidator(textValue) || textValue.length < 1) {
|
||||||
|
classNameState.curSelector = classNameState.preSelector
|
||||||
|
classNameState.preSelector = ''
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
classNameState.curSelector = curValue
|
||||||
|
|
||||||
|
// 修改替换类名
|
||||||
|
editClassName(curValue, OPTION_TYPE.EDIT, classNameState.preSelector)
|
||||||
|
|
||||||
|
// 全局样式中包含该类名的,替换之(不包含写在复杂选择器中的类名)
|
||||||
|
const newStyleStr = stringify(state.cssParseList, state.styleObject, {
|
||||||
|
originSelector: classNameState.preSelector,
|
||||||
|
newSelector: curValue
|
||||||
|
})
|
||||||
|
|
||||||
|
updateGlobalStyleStr(newStyleStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
const listener = () => {
|
||||||
|
classNameState.showDelConfirm = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除确认弹窗
|
||||||
|
const handleTriggerDelConfirm = (visible) => {
|
||||||
|
classNameState.showDelConfirm = visible
|
||||||
|
|
||||||
|
if (visible) {
|
||||||
|
window.addEventListener('click', listener)
|
||||||
|
} else {
|
||||||
|
window.removeEventListener('click', listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelSelector = () => {
|
||||||
|
// 删除选择器,仅从当前选中组件中删除类名, 不删除全局 css 中的 css 类名和样式
|
||||||
|
// 后期需要可以拿到全局组件的类名,如果只有当前组件使用该类名,从全局样式中删除之
|
||||||
|
editClassName(classNameState.curSelector, OPTION_TYPE.REMOVE)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFocusInput = () => {
|
||||||
|
classNameState.showDropdownList = true
|
||||||
|
|
||||||
|
if (newSelectorInputRef.value) {
|
||||||
|
newSelectorInputRef.value.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验修改选择器是否合法
|
||||||
|
const handleCurSelectorChange = (event) => {
|
||||||
|
const newValue = event.target?.textContent
|
||||||
|
|
||||||
|
selectorValidator(newValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验新选择器是否合法
|
||||||
|
watchEffect(() => {
|
||||||
|
selectorValidator(classNameState.newSelector)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.className-container {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-container {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 10px;
|
||||||
|
color: var(--ti-lowcode-className-selector-container-color);
|
||||||
|
.className-selector-wrap {
|
||||||
|
.error-tips {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--ti-lowcode-className-selector-error-tips-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.className-selector-container {
|
||||||
|
flex: 7;
|
||||||
|
border: 1px solid var(--ti-lowcode-className-selector-container-border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 2px 10px;
|
||||||
|
display: flex;
|
||||||
|
max-width: 180px;
|
||||||
|
row-gap: 2px;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
overflow: visible;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--ti-lowcode-className-selector-container-hover-border-color);
|
||||||
|
}
|
||||||
|
&.has-error {
|
||||||
|
border-color: var(--ti-lowcode-className-selector-container-error-border-color);
|
||||||
|
background-color: var(--ti-lowcode-className-selector-container-error-bg-color);
|
||||||
|
.selector-drop-down-list {
|
||||||
|
top: calc(100% + 30px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:has(.selector-input:focus) {
|
||||||
|
.empty-tips {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.empty-tips {
|
||||||
|
position: absolute;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--ti-lowcode-className-selector-container-empty-tips-color);
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
.current-selector {
|
||||||
|
max-width: 100%;
|
||||||
|
.current-selector-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background-color: var(--ti-lowcode-className-selector-container-label-bg-color);
|
||||||
|
color: #fff;
|
||||||
|
padding: 1px 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
line-height: 30px;
|
||||||
|
.selector-label-text {
|
||||||
|
outline: none;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
&.text-editing {
|
||||||
|
text-overflow: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.edit-wrap {
|
||||||
|
display: flex;
|
||||||
|
.del-selector-popover {
|
||||||
|
display: inline-flex;
|
||||||
|
.svg-icon {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
:deep(.reference-wrapper) {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.edit-btn,
|
||||||
|
.delete-btn {
|
||||||
|
color: var(--ti-lowcode-className-selector-container-option-btn-color);
|
||||||
|
margin-left: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.selector-input {
|
||||||
|
color: var(--ti-lowcode-className-selector-container-color);
|
||||||
|
min-width: 0;
|
||||||
|
flex: 0 0 0;
|
||||||
|
line-height: 30px;
|
||||||
|
z-index: 1;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 0;
|
||||||
|
&:focus {
|
||||||
|
flex: 1 0 46px;
|
||||||
|
padding: 1px 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-drop-down-list {
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 200px;
|
||||||
|
top: calc(100% + 10px);
|
||||||
|
left: 0;
|
||||||
|
padding: 8px 0;
|
||||||
|
background-color: var(--ti-lowcode-className-selector-dropdown-list-bg-color);
|
||||||
|
|
||||||
|
border: 1px solid transparent;
|
||||||
|
z-index: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: scroll;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.08);
|
||||||
|
|
||||||
|
.selector-dropdown-list-tips {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-dropdown-list-tips + .selector-dropdown-list-tips {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.add-global-class-tips {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exist-class-item {
|
||||||
|
cursor: pointer;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 14px;
|
||||||
|
> span {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
&.active,
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--ti-lowcode-className-selector-dropdown-list-item-active-bg-color);
|
||||||
|
color: var(--ti-lowcode-className-selector-dropdown-list-item-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-selector {
|
||||||
|
flex: 4;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--ti-lowcode-className-selector-title-color);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style lang="less">
|
||||||
|
.tiny-popover.tiny-popper.del-selector-popper-wrapper {
|
||||||
|
width: 220px;
|
||||||
|
height: 108px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background-color: var(--ti-lowcode-className-selector-del-popover-bg-color);
|
||||||
|
padding: 6px;
|
||||||
|
|
||||||
|
.popper-confirm {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popper-confirm-header {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--ti-lowcode-className-selector-del-popover-title-color);
|
||||||
|
.icon {
|
||||||
|
color: var(--ti-lowcode-warning-color);
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.popper-confirm-footer {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 22px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -388,7 +388,6 @@ export default {
|
||||||
const { setPosition } = useModal()
|
const { setPosition } = useModal()
|
||||||
|
|
||||||
const { getSettingFlag, getProperty } = useProperties({
|
const { getSettingFlag, getProperty } = useProperties({
|
||||||
props,
|
|
||||||
names: Object.values(EFFECTS_PROPERTY),
|
names: Object.values(EFFECTS_PROPERTY),
|
||||||
parseNumber: true
|
parseNumber: true
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,41 +1,28 @@
|
||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 - present TinyEngine Authors.
|
* Copyright (c) 2023 - present TinyEngine Authors.
|
||||||
* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd.
|
* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd.
|
||||||
*
|
*
|
||||||
* Use of this source code is governed by an MIT-style license.
|
* 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,
|
* 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
|
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
|
||||||
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
|
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import BackgroundGroup from './background/BackgroundGroup.vue'
|
export { default as BackgroundGroup } from './background/BackgroundGroup.vue'
|
||||||
import BorderGroup from './border/BorderGroup.vue'
|
export { default as BorderGroup } from './border/BorderGroup.vue'
|
||||||
import EffectGroup from './effects/EffectGroup.vue'
|
export { default as EffectGroup } from './effects/EffectGroup.vue'
|
||||||
import ImageSelect from './inputs/ImageSelect.vue'
|
export { default as ImageSelect } from './inputs/ImageSelect.vue'
|
||||||
import ResetButton from './inputs/ResetButton.vue'
|
export { default as ResetButton } from './inputs/ResetButton.vue'
|
||||||
import LayoutGroup from './layout/LayoutGroup.vue'
|
export { default as LayoutGroup } from './layout/LayoutGroup.vue'
|
||||||
import PositionGroup from './position/PositionGroup.vue'
|
export { default as PositionGroup } from './position/PositionGroup.vue'
|
||||||
import BoxShadowGroup from './shadow/BoxShadowGroup.vue'
|
export { default as BoxShadowGroup } from './shadow/BoxShadowGroup.vue'
|
||||||
import SizeGroup from './size/SizeGroup.vue'
|
export { default as SizeGroup } from './size/SizeGroup.vue'
|
||||||
import SpacingGroup from './spacing/SpacingGroup.vue'
|
export { default as SpacingGroup } from './spacing/SpacingGroup.vue'
|
||||||
import SpacingSetting from './spacing/SpacingSetting.vue'
|
export { default as SpacingSetting } from './spacing/SpacingSetting.vue'
|
||||||
import TypographyGroup from './typography/TypographyGroup.vue'
|
export { default as TypographyGroup } from './typography/TypographyGroup.vue'
|
||||||
import TypographyMore from './typography/TypographyMore.vue'
|
export { default as TypographyMore } from './typography/TypographyMore.vue'
|
||||||
|
export { default as FlexBox } from './layout/FlexBox.vue'
|
||||||
export default {
|
export { default as GridBox } from './layout/GridBox.vue'
|
||||||
BackgroundGroup,
|
export { default as ClassNamesContainer } from './classNamesContainer/index.vue'
|
||||||
BorderGroup,
|
|
||||||
EffectGroup,
|
|
||||||
ImageSelect,
|
|
||||||
ResetButton,
|
|
||||||
LayoutGroup,
|
|
||||||
PositionGroup,
|
|
||||||
BoxShadowGroup,
|
|
||||||
SizeGroup,
|
|
||||||
SpacingGroup,
|
|
||||||
SpacingSetting,
|
|
||||||
TypographyGroup,
|
|
||||||
TypographyMore
|
|
||||||
}
|
|
||||||
|
|
|
@ -176,7 +176,6 @@ export default {
|
||||||
const showModal = ref(false)
|
const showModal = ref(false)
|
||||||
|
|
||||||
const { getSettingFlag } = useProperties({
|
const { getSettingFlag } = useProperties({
|
||||||
props,
|
|
||||||
names: Object.values(FLEX_PROPERTY),
|
names: Object.values(FLEX_PROPERTY),
|
||||||
parseNumber: true
|
parseNumber: true
|
||||||
})
|
})
|
||||||
|
|
|
@ -361,7 +361,6 @@ export default {
|
||||||
})
|
})
|
||||||
|
|
||||||
const { getProperty, getSettingFlag } = useProperties({
|
const { getProperty, getSettingFlag } = useProperties({
|
||||||
props,
|
|
||||||
names: Object.values(GRID_PROPERTY),
|
names: Object.values(GRID_PROPERTY),
|
||||||
parseNumber: true
|
parseNumber: true
|
||||||
})
|
})
|
||||||
|
|
|
@ -221,7 +221,6 @@
|
||||||
import { reactive, watchEffect } from 'vue'
|
import { reactive, watchEffect } from 'vue'
|
||||||
import { Tooltip } from '@opentiny/vue'
|
import { Tooltip } from '@opentiny/vue'
|
||||||
import { MetaSelect } from '@opentiny/tiny-engine-common'
|
import { MetaSelect } from '@opentiny/tiny-engine-common'
|
||||||
import { camelize } from '@opentiny/tiny-engine-controller/utils'
|
|
||||||
import { push } from '@opentiny/vue-renderless/common/array'
|
import { push } from '@opentiny/vue-renderless/common/array'
|
||||||
import ModalMask, { useModal } from '../inputs/ModalMask.vue'
|
import ModalMask, { useModal } from '../inputs/ModalMask.vue'
|
||||||
import SpacingSetting from '../spacing/SpacingSetting.vue'
|
import SpacingSetting from '../spacing/SpacingSetting.vue'
|
||||||
|
@ -368,7 +367,6 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
const { getProperty, getSettingFlag, getPropertyValue, getPropertyText } = useProperties({
|
const { getProperty, getSettingFlag, getPropertyValue, getPropertyText } = useProperties({
|
||||||
props,
|
|
||||||
names: Object.values(POSITION_PROPERTY),
|
names: Object.values(POSITION_PROPERTY),
|
||||||
parseNumber: true
|
parseNumber: true
|
||||||
})
|
})
|
||||||
|
@ -413,7 +411,6 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
const openDirectionSetting = (type, styleName) => {
|
const openDirectionSetting = (type, styleName) => {
|
||||||
styleName = camelize(styleName)
|
|
||||||
|
|
||||||
state.property = {
|
state.property = {
|
||||||
type,
|
type,
|
||||||
|
|
|
@ -339,7 +339,6 @@ export default {
|
||||||
})
|
})
|
||||||
|
|
||||||
const { getProperty, getSettingFlag, getPropertyValue } = useProperties({
|
const { getProperty, getSettingFlag, getPropertyValue } = useProperties({
|
||||||
props,
|
|
||||||
names: Object.values(SIZE_PROPERTY),
|
names: Object.values(SIZE_PROPERTY),
|
||||||
parseNumber: true
|
parseNumber: true
|
||||||
})
|
})
|
||||||
|
|
|
@ -397,7 +397,6 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { computed, reactive } from 'vue'
|
import { computed, reactive } from 'vue'
|
||||||
import { camelize } from '@opentiny/tiny-engine-controller/utils'
|
|
||||||
import SpacingSetting from './SpacingSetting.vue'
|
import SpacingSetting from './SpacingSetting.vue'
|
||||||
import ModalMask, { useModal } from '../inputs/ModalMask.vue'
|
import ModalMask, { useModal } from '../inputs/ModalMask.vue'
|
||||||
import useEvent from '../../js/useEvent'
|
import useEvent from '../../js/useEvent'
|
||||||
|
@ -433,8 +432,7 @@ export default {
|
||||||
const spacing = computed(() => {
|
const spacing = computed(() => {
|
||||||
const properties = {}
|
const properties = {}
|
||||||
|
|
||||||
Object.values(SPACING_PROPERTY).forEach((str) => {
|
Object.values(SPACING_PROPERTY).forEach((name) => {
|
||||||
const name = camelize(str)
|
|
||||||
const value = props.style[name]
|
const value = props.style[name]
|
||||||
|
|
||||||
properties[name] = {
|
properties[name] = {
|
||||||
|
@ -447,6 +445,21 @@ export default {
|
||||||
return reactive(properties)
|
return reactive(properties)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const getSettingFlag = (styleName) => Boolean(spacing.value[styleName]?.setting)
|
||||||
|
const getPropertyText = (styleName) => spacing.value[styleName]?.text || 0
|
||||||
|
const getPropertyValue = (styleName) => spacing.value[styleName]?.value
|
||||||
|
|
||||||
|
// 打开单个属性设置弹窗
|
||||||
|
const openSetting = (type, styleName) => {
|
||||||
|
state.property = {
|
||||||
|
type,
|
||||||
|
name: styleName,
|
||||||
|
value: getPropertyValue(styleName)
|
||||||
|
}
|
||||||
|
|
||||||
|
state.showModal = true
|
||||||
|
}
|
||||||
|
|
||||||
const clickMargin = (styleName, event) => {
|
const clickMargin = (styleName, event) => {
|
||||||
state.className = styleName
|
state.className = styleName
|
||||||
state.show = true
|
state.show = true
|
||||||
|
@ -463,28 +476,11 @@ export default {
|
||||||
openSetting(SPACING_PROPERTY.Padding, styleName)
|
openSetting(SPACING_PROPERTY.Padding, styleName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 打开单个属性设置弹窗
|
|
||||||
const openSetting = (type, styleName) => {
|
|
||||||
styleName = camelize(styleName)
|
|
||||||
|
|
||||||
state.property = {
|
|
||||||
type,
|
|
||||||
name: styleName,
|
|
||||||
value: getPropertyValue(styleName)
|
|
||||||
}
|
|
||||||
|
|
||||||
state.showModal = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const closeModal = () => {
|
const closeModal = () => {
|
||||||
state.show = false
|
state.show = false
|
||||||
state.showModal = false
|
state.showModal = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSettingFlag = (styleName) => Boolean(spacing.value[camelize(styleName)]?.setting)
|
|
||||||
const getPropertyText = (styleName) => spacing.value[camelize(styleName)]?.text || 0
|
|
||||||
const getPropertyValue = (styleName) => spacing.value[camelize(styleName)]?.value
|
|
||||||
|
|
||||||
// 向父级传递更新 style 对象
|
// 向父级传递更新 style 对象
|
||||||
const update = (property) => {
|
const update = (property) => {
|
||||||
// 更新属性设置弹窗的属性值
|
// 更新属性设置弹窗的属性值
|
||||||
|
|
|
@ -239,7 +239,6 @@ export default {
|
||||||
let activedName = ''
|
let activedName = ''
|
||||||
const showModal = ref(false)
|
const showModal = ref(false)
|
||||||
const { getProperty, getSettingFlag, getPropertyValue } = useProperties({
|
const { getProperty, getSettingFlag, getPropertyValue } = useProperties({
|
||||||
props,
|
|
||||||
names: Object.values(TYPO_PROPERTY),
|
names: Object.values(TYPO_PROPERTY),
|
||||||
parseNumber: true
|
parseNumber: true
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,274 @@
|
||||||
|
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(',')
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAtRules = (node) => {
|
||||||
|
// 这里我们不处理 at rules(如 @media、@keyframe 等规则), 直接转换成字符串
|
||||||
|
const { source = {}, type } = node
|
||||||
|
const { start, end, input } = source
|
||||||
|
|
||||||
|
const rawString = input.css.slice(start.offset, end.offset)
|
||||||
|
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
style: {
|
||||||
|
type,
|
||||||
|
value: rawString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleComments = (node) => {
|
||||||
|
const { type, text } = node
|
||||||
|
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
style: {
|
||||||
|
type,
|
||||||
|
value: `/*${text}*/`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeHandlerMap = {
|
||||||
|
rule: handleRules,
|
||||||
|
atrule: handleAtRules,
|
||||||
|
comment: handleComments
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 css 字符串解析成 css 对象
|
||||||
|
* @param {string} css css 字符串
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const parser = (css) => {
|
||||||
|
const parseList = []
|
||||||
|
const selectors = []
|
||||||
|
const styleObject = {}
|
||||||
|
|
||||||
|
if (!css) {
|
||||||
|
return {
|
||||||
|
parseList,
|
||||||
|
selectors,
|
||||||
|
styleObject
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ast = postcss().process(css).sync().root
|
||||||
|
|
||||||
|
ast.nodes.forEach((node) => {
|
||||||
|
const { type } = node
|
||||||
|
const result = nodeHandlerMap[type](node)
|
||||||
|
|
||||||
|
parseList.push(result)
|
||||||
|
})
|
||||||
|
|
||||||
|
parseList.forEach((item) => {
|
||||||
|
if (!item.selectors) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 不支持属性选择器,以及组合选择器
|
||||||
|
if (/[,[\]>~+]/.test(item.selectors)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let selector = item.selectors
|
||||||
|
let mouseState = ''
|
||||||
|
|
||||||
|
if (selector.includes(':')) {
|
||||||
|
const [pureSelector, innerMouseState] = selector.split(':')
|
||||||
|
// 仅支持部分伪类选择器
|
||||||
|
if (!['hover', 'pressed', 'focused', 'disabled'].includes(innerMouseState)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
selector = pureSelector
|
||||||
|
mouseState = innerMouseState
|
||||||
|
}
|
||||||
|
|
||||||
|
selectors.push(selector)
|
||||||
|
|
||||||
|
styleObject[item.selectors] = {
|
||||||
|
mouseState,
|
||||||
|
pureSelector: selector
|
||||||
|
}
|
||||||
|
const rules = {}
|
||||||
|
|
||||||
|
Object.entries(item.style).forEach(([key, value]) => {
|
||||||
|
if (value.type !== 'decl') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rules[key] = value.value
|
||||||
|
})
|
||||||
|
|
||||||
|
styleObject[item.selectors].rules = rules
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
parseList,
|
||||||
|
selectors,
|
||||||
|
styleObject
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拿到组合选择器的数组,比如 .test1.test2 得到 ['.test1', '.test2']
|
||||||
|
* @param {string} selector
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getSelectorArr = (selector) => {
|
||||||
|
const res = []
|
||||||
|
|
||||||
|
if (!selector || typeof selector !== 'string') {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
const separator = ['.', '#']
|
||||||
|
|
||||||
|
for (let i = 0; i < selector.length; i++) {
|
||||||
|
let str = selector[i]
|
||||||
|
|
||||||
|
i++
|
||||||
|
|
||||||
|
while (!separator.includes(selector[i]) && i < selector.length) {
|
||||||
|
str += selector[i]
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
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('')
|
||||||
|
|
||||||
|
if (mouseState) {
|
||||||
|
finalSelector += `:${mouseState}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalSelector
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 序列化对象成 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'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 需要找出 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
|
||||||
|
}
|
|
@ -11,91 +11,341 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { computed, reactive, watch } from 'vue'
|
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 { useCanvas, useHistory, useProperties as useProps } from '@opentiny/tiny-engine-controller'
|
||||||
import { camelize } from '@opentiny/tiny-engine-controller/utils'
|
import { formatString } from '@opentiny/tiny-engine-common/js/ast'
|
||||||
import { obj2StyleStr } from '@opentiny/tiny-engine-common/js/css'
|
import { constants, utils } from '@opentiny/tiny-engine-utils'
|
||||||
import { styleStr2Obj, styleStrRemoveRoot } from './cssConvert'
|
import { parser, stringify, getSelectorArr } from './parser'
|
||||||
import { updateRect, getSchema as getCanvasPageSchema } from '@opentiny/tiny-engine-canvas'
|
|
||||||
|
|
||||||
const getStyleObj = (styleStr) => {
|
const { BROADCAST_CHANNEL, EXPRESSION_TYPE } = constants
|
||||||
let obj = {}
|
const { generateRandomLetters, parseExpression } = utils
|
||||||
|
|
||||||
if (typeof styleStr === 'string') {
|
const { data: schemaLength } = useBroadcastChannel({ name: BROADCAST_CHANNEL.SchemaLength })
|
||||||
obj = styleStr2Obj(styleStr)
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据当前选中的组件,随机生成一个 css 类名
|
||||||
|
export const genRandomClassNames = (componentName) => {
|
||||||
|
return `.${componentName}-${generateRandomLetters(5)}`.toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPropsFromExpression = (propValue) => {
|
||||||
|
let res = []
|
||||||
|
|
||||||
|
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]
|
||||||
|
}
|
||||||
|
} 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const classNameStr = schema?.props?.className
|
||||||
|
const idStr = schema?.props?.id
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前选中组件的类名以及 id 列表
|
||||||
|
const { classNameList, idList } = getClassNameAndIdList(schema)
|
||||||
|
|
||||||
|
state.currentClassNameList = classNameList.map((item) => `.${item}`)
|
||||||
|
state.currentIdList = idList.map((item) => `#${item}`)
|
||||||
|
|
||||||
|
// 变化了相当于重新选中了,需要重置当前选中的 className 以及样式面板的样式
|
||||||
|
if (curSchema !== oldCurSchema) {
|
||||||
|
state.className = {
|
||||||
|
classNameList: '',
|
||||||
|
mouseState: ''
|
||||||
|
}
|
||||||
|
state.style = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 () => {
|
export default () => {
|
||||||
const { getPageSchema, getCurrentSchema } = useCanvas()
|
|
||||||
const { getSchema } = useProps()
|
|
||||||
const { addHistory } = useHistory()
|
|
||||||
|
|
||||||
const state = reactive({
|
|
||||||
// 当前选中节点的 style,解析成对象返回
|
|
||||||
style: {},
|
|
||||||
// 编辑器显示的行内样式字符串
|
|
||||||
styleContent: '',
|
|
||||||
// 编辑器显示的全局样式字符串
|
|
||||||
cssContent: ''
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => getPageSchema()?.css,
|
|
||||||
(value) => {
|
|
||||||
state.cssContent = value || ''
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// 更新 style 对象到 schema
|
|
||||||
const updateStyle = (properties) => {
|
|
||||||
const schema = getSchema() || getCanvasPageSchema()
|
|
||||||
schema.props = schema.props || {}
|
|
||||||
|
|
||||||
if (properties) {
|
|
||||||
Object.entries(properties).forEach(([key, value]) => {
|
|
||||||
state.style[camelize(key)] = value
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
state.styleContent = obj2StyleStr(state.style)
|
|
||||||
const newStyleStr = styleStrRemoveRoot(state.styleContent)
|
|
||||||
|
|
||||||
if (newStyleStr) {
|
|
||||||
schema.props.style = styleStrRemoveRoot(state.styleContent)
|
|
||||||
} else {
|
|
||||||
delete schema.props.style
|
|
||||||
}
|
|
||||||
|
|
||||||
addHistory()
|
|
||||||
updateRect()
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
state,
|
state,
|
||||||
setStyle,
|
|
||||||
updateStyle
|
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
|
* 根据 style 对象生成样式属性对象 properties
|
||||||
* styleName: {
|
* styleName: {
|
||||||
|
@ -105,28 +355,20 @@ export default () => {
|
||||||
* setting // 属性是否已设置值
|
* setting // 属性是否已设置值
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
export const useProperties = ({ props, names, parseNumber }) => {
|
export const useProperties = ({ names, parseNumber }) => {
|
||||||
const properties = computed(() => {
|
const properties = computed(() => {
|
||||||
const properties = {}
|
const newProperties = {}
|
||||||
if (Array.isArray(names) && props.style) {
|
|
||||||
|
if (Array.isArray(names) && state.style) {
|
||||||
names.forEach((name) => {
|
names.forEach((name) => {
|
||||||
name = camelize(name)
|
const value = state.style[name]
|
||||||
const value = props.style[name]
|
|
||||||
let text = value || ''
|
let text = value || ''
|
||||||
|
|
||||||
if (parseNumber) {
|
if (parseNumber) {
|
||||||
if (value === 'auto') {
|
text = getTextOfValue(value)
|
||||||
text = 'auto'
|
|
||||||
} else if (value === 'none') {
|
|
||||||
text = 'none'
|
|
||||||
} else if (/^\d+(\.\d+)?%$/.test(value)) {
|
|
||||||
text = value
|
|
||||||
} else {
|
|
||||||
text = String(Number.parseInt(value) || '')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
properties[name] = {
|
newProperties[name] = {
|
||||||
name, // 属性名
|
name, // 属性名
|
||||||
text, // 界面显示的值
|
text, // 界面显示的值
|
||||||
value, // 属性原始值
|
value, // 属性原始值
|
||||||
|
@ -135,13 +377,13 @@ export const useProperties = ({ props, names, parseNumber }) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return reactive(properties)
|
return newProperties
|
||||||
})
|
})
|
||||||
|
|
||||||
const getProperty = (styleName) => properties.value[camelize(styleName)]
|
const getProperty = (styleName) => properties.value[styleName]
|
||||||
const getSettingFlag = (styleName) => Boolean(properties.value[camelize(styleName)]?.setting)
|
const getSettingFlag = (styleName) => Boolean(properties.value[styleName]?.setting)
|
||||||
const getPropertyText = (styleName) => properties.value[camelize(styleName)]?.text
|
const getPropertyText = (styleName) => properties.value[styleName]?.text
|
||||||
const getPropertyValue = (styleName) => properties.value[camelize(styleName)]?.value
|
const getPropertyValue = (styleName) => properties.value[styleName]?.value
|
||||||
|
|
||||||
return {
|
return {
|
||||||
properties,
|
properties,
|
||||||
|
|
|
@ -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-color: var(--ti-lowcode-base-gray-0);
|
||||||
--ti-lowcode-block-link-field-link-icon-bg-color: var(--ti-lowcode-base-success-color);
|
--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);
|
||||||
|
}
|
||||||
|
|
|
@ -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-color: var(--ti-lowcode-base-gray-0);
|
||||||
--ti-lowcode-block-link-field-link-icon-bg-color: var(--ti-lowcode-base-success-color);
|
--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);
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue