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.parent = null
|
||||
Object.assign(selectState, initialRectState)
|
||||
// 临时借用 remote 事件出发 currentSchema 更新
|
||||
canvasState?.emit?.('remove')
|
||||
}
|
||||
|
||||
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 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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,8 +28,11 @@
|
|||
"@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",
|
||||
"postcss": "^8.4.31",
|
||||
"vue": "3.2.45"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
@save="save(CSS_TYPE.Style, $event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<class-names-container></class-names-container>
|
||||
<tiny-collapse v-model="activeNames">
|
||||
<tiny-collapse-item title="布局" name="layout">
|
||||
<layout-group :display="state.style.display" @update="updateStyle" />
|
||||
|
@ -58,23 +58,26 @@
|
|||
<script>
|
||||
import { ref } from '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 SizeGroup from './components/size/SizeGroup.vue'
|
||||
import LayoutGroup from './components/layout/LayoutGroup.vue'
|
||||
import FlexBox from './components/layout/FlexBox.vue'
|
||||
import GridBox from './components/layout/GridBox.vue'
|
||||
import PositionGroup from './components/position/PositionGroup.vue'
|
||||
import BorderGroup from './components/border/BorderGroup.vue'
|
||||
import SpacingGroup from './components/spacing/SpacingGroup.vue'
|
||||
import BackgroundGroup from './components/background/BackgroundGroup.vue'
|
||||
import EffectGroup from './components/effects/EffectGroup.vue'
|
||||
// import BoxShadowGroup from './components/shadow/BoxShadowGroup.vue'
|
||||
import TypographyGroup from './components/typography/TypographyGroup.vue'
|
||||
import { formatString } from '@opentiny/tiny-engine-common/js/ast'
|
||||
import {
|
||||
SizeGroup,
|
||||
LayoutGroup,
|
||||
FlexBox,
|
||||
GridBox,
|
||||
PositionGroup,
|
||||
BorderGroup,
|
||||
SpacingGroup,
|
||||
BackgroundGroup,
|
||||
EffectGroup,
|
||||
TypographyGroup,
|
||||
ClassNamesContainer
|
||||
} from './components'
|
||||
import { CSS_TYPE } from './js/cssType'
|
||||
import useStyle from './js/useStyle'
|
||||
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 {
|
||||
components: {
|
||||
|
@ -87,9 +90,9 @@ export default {
|
|||
BorderGroup,
|
||||
SpacingGroup,
|
||||
BackgroundGroup,
|
||||
// BoxShadowGroup,
|
||||
TypographyGroup,
|
||||
EffectGroup,
|
||||
ClassNamesContainer,
|
||||
TinyCollapse: Collapse,
|
||||
TinyCollapseItem: CollapseItem
|
||||
},
|
||||
|
@ -106,7 +109,7 @@ export default {
|
|||
])
|
||||
const { getCurrentSchema, getPageSchema } = useCanvas()
|
||||
// 获取当前节点 style 对象
|
||||
const { state, updateStyle, setStyle } = useStyle() // updateStyle
|
||||
const { state, updateStyle } = useStyle() // updateStyle
|
||||
const { addHistory } = useHistory()
|
||||
const { getSchema } = useProperties()
|
||||
|
||||
|
@ -117,7 +120,7 @@ export default {
|
|||
if (type === CSS_TYPE.Style) {
|
||||
const pageSchema = getCanvasPageSchema()
|
||||
const schema = getSchema() || pageSchema
|
||||
const styleString = styleStrRemoveRoot(content)
|
||||
const styleString = formatString(styleStrRemoveRoot(content), 'css')
|
||||
const currentSchema = getCurrentSchema() || pageSchema
|
||||
|
||||
state.styleContent = content
|
||||
|
@ -132,13 +135,13 @@ export default {
|
|||
delete currentSchema.props.style
|
||||
}
|
||||
|
||||
setStyle(styleString)
|
||||
addHistory()
|
||||
} else if (type === CSS_TYPE.Css) {
|
||||
const cssString = content.replace(/"/g, "'")
|
||||
const cssString = formatString(content.replace(/"/g, "'"), 'css')
|
||||
getPageSchema().css = cssString
|
||||
getCanvasPageSchema().css = cssString
|
||||
setPageCss(cssString)
|
||||
state.schemaUpdateKey++
|
||||
addHistory()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -144,7 +144,6 @@ export default {
|
|||
})
|
||||
|
||||
const { getProperty, getSettingFlag } = useProperties({
|
||||
props,
|
||||
names: Object.values(BACKGROUND_PROPERTY),
|
||||
parseNumber: true
|
||||
})
|
||||
|
|
|
@ -235,7 +235,6 @@ import useEvent from '../../js/useEvent'
|
|||
import { useProperties } from '../../js/useStyle'
|
||||
import { RADIUS_SETTING, BORDER_SETTING, BORDER_STYLE_TYPE } from '../../js/cssType'
|
||||
import { BORDER_PROPERTY, BORDER_RADIUS_PROPERTY } from '../../js/styleProperty'
|
||||
// import { hyphenate } from '@opentiny/tiny-engine-controller/utils'
|
||||
|
||||
const BORDER_STYLE = {
|
||||
[BORDER_SETTING.All]: BORDER_PROPERTY.BorderStyle,
|
||||
|
@ -299,7 +298,6 @@ export default {
|
|||
const { setPosition } = useModal()
|
||||
|
||||
const { getProperty, getSettingFlag, getPropertyValue } = useProperties({
|
||||
props,
|
||||
names: Object.values({ ...BORDER_RADIUS_PROPERTY, ...BORDER_PROPERTY }),
|
||||
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 { getSettingFlag, getProperty } = useProperties({
|
||||
props,
|
||||
names: Object.values(EFFECTS_PROPERTY),
|
||||
parseNumber: true
|
||||
})
|
||||
|
|
|
@ -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 ClassNamesContainer } from './classNamesContainer/index.vue'
|
||||
|
|
|
@ -176,7 +176,6 @@ export default {
|
|||
const showModal = ref(false)
|
||||
|
||||
const { getSettingFlag } = useProperties({
|
||||
props,
|
||||
names: Object.values(FLEX_PROPERTY),
|
||||
parseNumber: true
|
||||
})
|
||||
|
|
|
@ -361,7 +361,6 @@ export default {
|
|||
})
|
||||
|
||||
const { getProperty, getSettingFlag } = useProperties({
|
||||
props,
|
||||
names: Object.values(GRID_PROPERTY),
|
||||
parseNumber: true
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -339,7 +339,6 @@ export default {
|
|||
})
|
||||
|
||||
const { getProperty, getSettingFlag, getPropertyValue } = useProperties({
|
||||
props,
|
||||
names: Object.values(SIZE_PROPERTY),
|
||||
parseNumber: true
|
||||
})
|
||||
|
|
|
@ -397,7 +397,6 @@
|
|||
|
||||
<script>
|
||||
import { computed, reactive } from 'vue'
|
||||
import { camelize } from '@opentiny/tiny-engine-controller/utils'
|
||||
import SpacingSetting from './SpacingSetting.vue'
|
||||
import ModalMask, { useModal } from '../inputs/ModalMask.vue'
|
||||
import useEvent from '../../js/useEvent'
|
||||
|
@ -433,8 +432,7 @@ export default {
|
|||
const spacing = computed(() => {
|
||||
const properties = {}
|
||||
|
||||
Object.values(SPACING_PROPERTY).forEach((str) => {
|
||||
const name = camelize(str)
|
||||
Object.values(SPACING_PROPERTY).forEach((name) => {
|
||||
const value = props.style[name]
|
||||
|
||||
properties[name] = {
|
||||
|
@ -447,6 +445,21 @@ export default {
|
|||
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) => {
|
||||
state.className = styleName
|
||||
state.show = true
|
||||
|
@ -463,28 +476,11 @@ export default {
|
|||
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 = () => {
|
||||
state.show = 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 对象
|
||||
const update = (property) => {
|
||||
// 更新属性设置弹窗的属性值
|
||||
|
|
|
@ -239,7 +239,6 @@ export default {
|
|||
let activedName = ''
|
||||
const showModal = ref(false)
|
||||
const { getProperty, getSettingFlag, getPropertyValue } = useProperties({
|
||||
props,
|
||||
names: Object.values(TYPO_PROPERTY),
|
||||
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 { 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
|
||||
}
|
||||
|
||||
// 根据当前选中的组件,随机生成一个 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 () => {
|
||||
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 {
|
||||
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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue