import { ref } from 'vue'
import { useCanvas, useMessage, useHistory, getOptions } from '@opentiny/tiny-engine-meta-register'
import { utils } from '@opentiny/tiny-engine-utils'
import { getRect, querySelectById, POSITION, insertNode, selectNode, canvasState } from '../container'
import type { Node } from '../../../types'
interface SelectionState {
id: string
top?: number
left?: number
width?: number
height?: number
schema?: any
parent?: {
id: string
children: Node[]
}
}
const multiSelectedStates = ref<SelectionState[]>([])
let selectionUpdateTimer: ReturnType<typeof setTimeout> | null = null
* 创建TinyPopover组件结构
* @param {Object} props 组件属性
* @param {Node | Node[]} content 内容节点
* @returns {Node} TinyPopover组件结构
*/
const createTinyPopoverSchema = (props: Record<string, any> = {}, content: Node | Node[]): Node => {
const children = Array.isArray(content) ? content : [content]
return {
componentName: 'TinyPopover',
id: utils.guid(),
props: {
width: 200,
title: '弹框标题',
trigger: 'manual',
modelValue: true,
...props
},
children: [
{
componentName: 'Template',
id: utils.guid(),
props: {
slot: 'reference'
},
children
},
{
componentName: 'Template',
id: utils.guid(),
props: {
slot: 'default'
},
children: [
{
componentName: 'div',
id: utils.guid(),
props: {
placeholder: '提示内容'
}
}
]
}
]
}
}
export const useMultiSelect = () => {
const isMouseDown = ref(false)
* 取消待执行的选择更新
*/
const cancelSelectionUpdate = (): void => {
if (selectionUpdateTimer !== null) {
clearTimeout(selectionUpdateTimer)
selectionUpdateTimer = null
}
}
* 添加state到多选列表
* @param {SelectionState} selectState
* @param {boolean} isMultiple 是否多选
* @returns {boolean} 添加成功返回true,否则返回false
*/
const toggleMultiSelection = (selectState: SelectionState, isMultiple: boolean = false): boolean => {
if (!selectState || typeof selectState !== 'object') {
return false
}
cancelSelectionUpdate()
if (isMultiple) {
const isExistNode = multiSelectedStates.value.some((state) => state.id === selectState.id)
if (isExistNode && !isMouseDown.value) {
multiSelectedStates.value = multiSelectedStates.value.filter((state) => state.id !== selectState.id)
} else {
multiSelectedStates.value = multiSelectedStates.value.concat(selectState)
}
return !isExistNode
}
multiSelectedStates.value = [selectState]
return true
}
const refreshSelectionState = (): SelectionState[] => {
multiSelectedStates.value = multiSelectedStates.value
.filter((state) => {
const element = querySelectById(state.id)
return !!element
})
.map((state) => {
const element = querySelectById(state.id)
const { top, left, width, height } = getRect(element!)
return {
...state,
top,
left,
width,
height
}
})
return multiSelectedStates.value
}
const clearMultiSelection = (): void => {
multiSelectedStates.value = []
}
* 获取选中节点在父节点children中的索引位置
* @param {children} children 父节点的children
* @param {string[]} selectedIds 选中的节点ID列表
* @returns {number[]} 排序后的索引数组
*/
const getSelectedNodeIndices = (children: Node[], selectedIds: string[]): number[] => {
return selectedIds
.map((id) => children.findIndex((child: Node) => child.id === id))
.filter((index) => index !== -1)
.sort((a, b) => a - b)
}
* 判断选中的节点是否都是兄弟节点且是连续的
* @returns {boolean} 如果所有选中节点都有相同的父节点且在父节点的children中是连续的,返回true;否则返回false
*/
const areSiblingNodes = (): boolean => {
if (multiSelectedStates.value.length <= 1) return false
const canvas = useCanvas()
const nodesWithParent = multiSelectedStates.value.map((node) => canvas.getNodeWithParentById(node.id) || {})
if (nodesWithParent.some((node) => !node.parent)) return false
const firstParent = nodesWithParent[0].parent
const parentId = firstParent.id
if (nodesWithParent.some((node) => node.parent.id !== parentId)) return false
const selectedIds = multiSelectedStates.value.map((state) => state.id)
const nodeIndices = getSelectedNodeIndices(firstParent.children, selectedIds)
return nodeIndices.every((value, index) => value === nodeIndices[0] + index)
}
* 更新添加父级节点后的选中状态
* @param {string[]} newParentIds 新创建的父容器ID数组
*/
const updateSelectionAfterAddParent = (newParentIds: string[]): void => {
useMessage().publish({ topic: 'schemaChange', data: {} })
clearMultiSelection()
cancelSelectionUpdate()
selectionUpdateTimer = setTimeout(() => {
if (newParentIds.length > 0) {
if (newParentIds.length === 1) {
const canvas = useCanvas()
const nodeId = newParentIds[0]
const nodeWithParent = canvas.getNodeWithParentById(nodeId)
if (nodeWithParent) {
canvas.setNode(nodeWithParent.node, nodeWithParent.parent)
}
selectNode(nodeId)
} else {
const validNodes: SelectionState[] = []
newParentIds.forEach((id) => {
const element = querySelectById(id)
if (!element) return
const canvas = useCanvas()
const nodeWithParent = canvas.getNodeWithParentById(id)
if (nodeWithParent) {
const { top, left, width, height } = getRect(element)
validNodes.push({
id,
top,
left,
width,
height,
schema: nodeWithParent.node,
parent: nodeWithParent.parent
})
}
})
if (validNodes.length > 0) {
toggleMultiSelection(validNodes[0], false)
for (let i = 1; i < validNodes.length; i++) {
toggleMultiSelection(validNodes[i], true)
}
}
}
}
}, 100)
}
* 获取组件基础样式的className
* @returns {string} 组件基础样式类名
*/
const getComponentBaseStyleClassName = () => {
const materialsOptions = getOptions('engine.plugins.materials') || {}
return materialsOptions.useBaseStyle && materialsOptions.componentBaseStyle?.className
? materialsOptions.componentBaseStyle.className
: ''
}
* 为一组兄弟节点添加共同的父级
* @param {string} componentName 父级组件名称
* @param {Object} props 父级组件属性
* @returns {boolean} 操作成功返回true,否则返回false
*/
const groupAddParent = (componentName: string, props: Record<string, any> = {}): boolean => {
if (!areSiblingNodes()) {
return false
}
const firstState = multiSelectedStates.value[0]
const { parent } = useCanvas().getNodeWithParentById(firstState.id) || {}
if (!parent) {
return false
}
const selectedIds = multiSelectedStates.value.map((state) => state.id)
const indices = getSelectedNodeIndices(parent.children, selectedIds)
if (indices.length === 0) return false
const firstIndex = indices[0]
const selectedNodes = indices.map((index) => parent.children[index])
let wrapSchema: Node = {
componentName,
id: utils.guid(),
props: {
...props,
...(componentName === 'div' && getComponentBaseStyleClassName()
? { className: getComponentBaseStyleClassName() }
: {})
},
children: selectedNodes
}
if (componentName === 'TinyPopover') {
wrapSchema = createTinyPopoverSchema(props, selectedNodes)
}
for (let i = indices.length - 1; i >= 0; i--) {
parent.children.splice(indices[i], 1)
}
parent.children.splice(firstIndex, 0, wrapSchema)
const canvas = useCanvas()
canvas.setNode(wrapSchema, parent)
selectedNodes.forEach((node) => {
canvas.setNode(node, wrapSchema)
})
Object.assign(canvasState, {
current: wrapSchema,
parent: parent
})
updateSelectionAfterAddParent([wrapSchema.id])
useHistory().addHistory()
return true
}
* 创建包装组件架构
* @param {string} componentName 组件名称
* @param {Object} props 组件属性
* @param {Schema} childSchema 子组件架构
* @returns {Schema} 包装组件架构
*/
const createWrapperSchema = (componentName: string, props: Record<string, any> = {}, childSchema: Node): Node => {
let wrapSchema: Node = {
componentName,
id: utils.guid(),
props: {
content: '提示信息',
...props,
...(componentName === 'div' && getComponentBaseStyleClassName()
? { className: getComponentBaseStyleClassName() }
: {})
},
children: [childSchema]
}
if (componentName === 'TinyPopover') {
wrapSchema = createTinyPopoverSchema(props, childSchema)
}
return wrapSchema
}
* 批量为多个节点添加相同的父级
* @param {string} componentName 父级组件名称
* @param {Object} props 父级组件属性
* @returns {boolean} 操作成功返回true,否则返回false
*/
const batchAddParent = (componentName: string, props: Record<string, any> = {}): boolean => {
if (multiSelectedStates.value.length === 0) {
return false
}
const newParentIds: string[] = []
multiSelectedStates.value.forEach(({ schema, parent }) => {
if (!schema || !parent) {
return
}
const index = parent.children.findIndex((child) => child.id === schema.id)
if (index === -1) {
return
}
const wrapSchema = createWrapperSchema(componentName, props, schema)
wrapSchema.id = utils.guid()
newParentIds.push(wrapSchema.id)
const originalNode = schema
insertNode(
{
node: originalNode,
parent,
data: wrapSchema
},
POSITION.REPLACE,
false
)
})
updateSelectionAfterAddParent(newParentIds)
useHistory().addHistory()
return true
}
return {
multiSelectedStates,
isMouseDown,
toggleMultiSelection,
refreshSelectionState,
clearMultiSelection,
areSiblingNodes,
batchAddParent,
groupAddParent
}
}