* 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 { reactive, nextTick } from 'vue'
import { useStorage } from '@vueuse/core'
import { constants } from '@opentiny/tiny-engine-utils'
import { META_APP as PLUGIN_NAME, getMetaApi, getMergeMeta, getAllMergeMeta } from '@opentiny/tiny-engine-meta-register'
import defaultLayout from '../defaultLayout'
import { utils } from '@opentiny/tiny-engine-utils'
const { PAGE_STATUS, STORAGE_KEY_LEFT_FIXED_PANELS, STORAGE_KEY_RIGHT_FIXED_PANELS, PLUGIN_DEFAULT_WIDTH } = constants
const { deepClone } = utils
export interface IMetaApi {
[key: string]: any
}
export interface IPluginPosition {
leftTop: string
leftBottom: string
independence: string
rightTop: string
rightBottom: string
fixed: string
}
export interface IPluginState {
pluginEvent: 'all' | string
}
export interface IDimension {
deviceType: 'desktop' | 'tablet' | 'mobile'
width: string
maxWidth: string
minWidth: string
scale: number
height: string
}
export interface IPluginBase {
isShow: boolean
fixedPanels: string[]
render: string
activating: boolean
showDesignSettings: boolean
}
export interface IPlugins extends IPluginBase, IPluginState {}
export interface ISettings extends IPluginBase {
api: IMetaApi | null
}
export interface ILayoutState {
isMoveDragBar: boolean
dimension: IDimension
plugins: IPlugins
settings: ISettings
toolbars: {
visiblePopover: boolean
render: string
}
pageStatus: any
}
export interface IPluginStorageItem {
width?: number | undefined
offset?: number
align?: string
index: number
isShow?: boolean
widthResizable?: boolean
}
export interface IPluginStorage {
[key: string]: IPluginStorageItem
}
export interface IPlugin {
id: string
componentName?: string
name?: string
label?: string
[key: string]: any
}
const PLUGIN_POSITION: IPluginPosition = {
leftTop: 'leftTop',
leftBottom: 'leftBottom',
independence: 'independence',
rightTop: 'rightTop',
rightBottom: 'rightBottom',
fixed: 'fixed'
}
const pluginState = reactive<IPluginState>({
pluginEvent: 'all'
})
const layoutState = reactive<ILayoutState>({
isMoveDragBar: false,
dimension: {
deviceType: 'desktop',
width: '',
maxWidth: '',
minWidth: '',
scale: 1,
height: '100%'
},
plugins: {
isShow: true,
fixedPanels: [PLUGIN_NAME.Materials],
render: PLUGIN_NAME.Materials,
pluginEvent: 'all',
activating: false,
showDesignSettings: true
},
settings: {
isShow: true,
fixedPanels: [PLUGIN_NAME.Props, PLUGIN_NAME.Styles, PLUGIN_NAME.Event],
render: PLUGIN_NAME.Props,
api: null,
activating: false,
showDesignSettings: true
},
toolbars: {
visiblePopover: false,
render: ''
},
pageStatus: {
state: '',
data: {}
}
})
const getMoveDragBarState = (): boolean => {
return layoutState.isMoveDragBar
}
const changeMoveDragBarState = (state: boolean): void => {
layoutState.isMoveDragBar = state
}
const leftMenuShownStorage = useStorage<boolean>('leftMenuShown', layoutState.plugins.isShow)
const rightMenuShownStorage = useStorage<boolean>('rightMenuShown', layoutState.settings.isShow)
const changeMenuShown = (menuName: 'left' | 'right'): void => {
switch (menuName) {
case 'left': {
leftMenuShownStorage.value = !leftMenuShownStorage.value
break
}
case 'right': {
rightMenuShownStorage.value = !rightMenuShownStorage.value
break
}
}
}
const leftFixedPanelsStorage = useStorage<string[]>(STORAGE_KEY_LEFT_FIXED_PANELS, layoutState.plugins.fixedPanels)
const rightFixedPanelsStorage = useStorage<string[]>(STORAGE_KEY_RIGHT_FIXED_PANELS, layoutState.settings.fixedPanels)
const changeLeftFixedPanels = (pluginName: string): void => {
leftFixedPanelsStorage.value = leftFixedPanelsStorage.value?.includes(pluginName)
? leftFixedPanelsStorage.value?.filter((item) => item !== pluginName)
: [...leftFixedPanelsStorage.value, pluginName]
}
const changeRightFixedPanels = (pluginName: string): void => {
rightFixedPanelsStorage.value = rightFixedPanelsStorage.value?.includes(pluginName)
? rightFixedPanelsStorage.value?.filter((item) => item !== pluginName)
: [...rightFixedPanelsStorage.value, pluginName]
}
const getScale = (): number => layoutState.dimension.scale
const getPluginState = (): IPlugins => layoutState.plugins
const getSettingState = (): ISettings => layoutState.settings
const getDimension = (): IDimension => layoutState.dimension
const setDimension = (data: Partial<IDimension>): void => {
Object.assign(layoutState.dimension, data)
}
const activeSetting = (name: string): void => {
const { settings } = layoutState
settings.render = name
nextTick(() => {
settings.activating = true
setTimeout(() => {
settings.activating = false
}, 1000)
})
}
* 两侧面板的固定状态
*/
const getFixedPanelsStatus = () => {
const leftPanelFixed = leftFixedPanelsStorage.value.includes(layoutState.plugins.render)
const rightPanelFixed = rightFixedPanelsStorage.value.includes(layoutState.settings.render)
return { leftPanelFixed, rightPanelFixed }
}
const closeSetting = (forceClose?: boolean) => {
const { settings } = layoutState
if (!rightFixedPanelsStorage.value.includes(settings.render) || forceClose) {
settings.render = ''
}
}
const activePlugin = (name: string, noActiveRender?: boolean) => {
const { plugins } = layoutState
if (!noActiveRender) {
plugins.render = name
}
return new Promise<IMetaApi>((resolve) => {
nextTick(() => resolve(getMetaApi(name)))
})
}
const closePlugin = (forceClose?: boolean) => {
const { plugins } = layoutState
if (!leftFixedPanelsStorage.value.includes(plugins.render) || forceClose) {
plugins.render = ''
}
}
const isEmptyPage = () => layoutState.pageStatus?.state === PAGE_STATUS.Empty
export default () => {
let plugin: IPluginStorage = {}
try {
const storedPlugin = localStorage.getItem('plugin')
if (storedPlugin) {
plugin = JSON.parse(storedPlugin) as IPluginStorage
}
} catch (error) {
if (error instanceof Error) {
throw new Error(error.message)
}
throw new Error('Failed to parse plugin storage')
}
const pluginStorageReactive = useStorage<IPluginStorage>('plugin', plugin)
const getPluginWidth = (name: string) => pluginStorageReactive.value[name]?.width || PLUGIN_DEFAULT_WIDTH
const changePluginWidth = (name: string, width: number, offset?: number): void => {
if (Object.prototype.hasOwnProperty.call(pluginStorageReactive.value, name)) {
pluginStorageReactive.value[name].width = width
if (typeof offset === 'number') {
pluginStorageReactive.value[name].offset = offset
}
} else {
pluginStorageReactive.value[name] = { width, index: 0, isShow: true }
}
}
const getPluginByLayout = (name: string): string => pluginStorageReactive.value[name]?.align || 'leftTop'
const getPluginsByLayout = (layout: string = 'all'): string[] => {
const pluginNames = Object.keys(pluginStorageReactive.value).filter(
(key) => pluginStorageReactive.value[key].align === layout || layout === 'all'
)
pluginNames.sort((a, b) => pluginStorageReactive.value[a].index - pluginStorageReactive.value[b].index)
return pluginNames
}
const getPluginById = (pluginList: IPlugin[], pluginId: string): IPlugin | undefined => {
return pluginList.find((item) => item.id === pluginId)
}
const getPluginsByPosition = (position: string, pluginList: IPlugin[]): IPlugin[] => {
const res = getPluginsByLayout(position)
.map((pluginId) => getPluginById(pluginList, pluginId))
.filter((plugin): plugin is IPlugin => Boolean(plugin))
return res
}
const changePluginLayout = (name: string, layout: string): void => {
if (pluginStorageReactive.value[name]) {
pluginStorageReactive.value[name].align = layout
}
}
* 拖拽后改变插件位置
* @param {*} from 插件的起始位置
* @param {*} to 插件的结束位置
* @param {*} oldIndex 插件的起始索引
* @param {*} newIndex 插件的结束索引
* @returns
*/
const dragPluginLayout = (from: string, to: string, oldIndex: number, newIndex: number): void => {
if (from === to && oldIndex === newIndex) return
const items = Object.values(pluginStorageReactive.value)
const movedItem = items.find((item) => item.align === from && item.index === oldIndex)
if (from === to) {
if (oldIndex < newIndex) {
items.forEach((item) => {
if (item !== movedItem && item.align === from && item.index > oldIndex && item.index <= newIndex) {
item.index -= 1
}
})
} else {
items.forEach((item) => {
if (item !== movedItem && item.align === from && item.index >= newIndex && item.index < oldIndex) {
item.index += 1
}
})
}
} else {
items.forEach((item) => {
if (item !== movedItem && item.align === from && item.index > oldIndex) {
item.index -= 1
}
if (item !== movedItem && item.align === to && item.index >= newIndex) {
item.index += 1
}
})
}
if (movedItem) {
movedItem.align = to
movedItem.index = newIndex
}
}
const isSameSide = (from: string, to: string): boolean => {
const leftSide = [PLUGIN_POSITION.leftTop, PLUGIN_POSITION.leftBottom]
const rightSide = [PLUGIN_POSITION.rightTop, PLUGIN_POSITION.rightBottom]
const isLeft = leftSide.includes(from) && leftSide.includes(to)
const isRight = rightSide.includes(from) && rightSide.includes(to)
return isLeft || isRight
}
const getPluginShown = (name: string): boolean => pluginStorageReactive.value[name]?.isShow || false
const changePluginShown = (name: string): void => {
if (!pluginStorageReactive.value[name]) {
pluginStorageReactive.value[name] = { isShow: true, index: 0 }
}
pluginStorageReactive.value[name].isShow = !pluginStorageReactive.value[name].isShow
}
* 返回面板是否宽度可调
* @param {string} name 插件名称
* @returns
*/
const isPanelWidthResizable = (name: string): boolean => pluginStorageReactive.value[name]?.widthResizable || false
const initPluginStorageReactive = (pluginList: IPluginStorage): void => {
pluginStorageReactive.value = pluginList
}
const removeUndefineLayoutId = (layout) => {
if (Array.isArray(layout)) {
layout.forEach((item, index) => {
if (Array.isArray(item)) {
removeUndefineLayoutId(item)
}
else if (Object.prototype.toString.call(item) === '[object Object]') {
removeUndefineLayoutId(item)
}
else if (typeof item === 'string' && !getMergeMeta(item)) {
layout.splice(index, 1)
}
})
}
if (Object.prototype.toString.call(layout) === '[object Object]') {
Object.values(layout).forEach((value) => {
removeUndefineLayoutId(value)
})
}
}
const removeById = (layout, id) => {
if (Array.isArray(layout)) {
layout.forEach((item, index) => {
if (Array.isArray(item)) {
removeById(item, id)
} else if (Object.prototype.toString.call(item) === '[object Object]') {
removeById(item, id)
} else if (item === id) {
layout.splice(index, 1)
}
})
}
if (Object.prototype.toString.call(layout) === '[object Object]') {
Object.values(layout).forEach((value) => {
removeById(value, id)
})
}
}
const replaceByPosition = (layout, originId, targetId, position) => {
if (Array.isArray(layout)) {
for (let i = 0; i < layout.length; i++) {
const item = layout[i]
if (Array.isArray(item)) {
replaceByPosition(item, originId, targetId, position)
} else if (Object.prototype.toString.call(item) === '[object Object]') {
replaceByPosition(item, originId, targetId, position)
} else if (item === targetId) {
const insertIndex = position === 'before' ? i : i + 1
layout.splice(insertIndex, 0, originId)
return
}
}
return
}
if (Object.prototype.toString.call(layout) === '[object Object]') {
Object.values(layout).forEach((value) => {
replaceByPosition(value, originId, targetId, position)
})
}
}
const computeFinalLayoutConfig = (layout, relativeLayoutConfig) => {
const finalLayoutConfig = deepClone(layout)
Object.entries(relativeLayoutConfig).forEach(([key, value]) => {
if (value.insertBefore) {
removeById(finalLayoutConfig, key)
replaceByPosition(finalLayoutConfig, key, value.insertBefore, 'before')
} else if (value.insertAfter) {
removeById(finalLayoutConfig, key)
replaceByPosition(finalLayoutConfig, key, value.insertAfter, 'after')
}
})
removeUndefineLayoutId(finalLayoutConfig)
return finalLayoutConfig
}
let finalLayoutConfig = null
const getFinalLayoutConfig = () => {
if (finalLayoutConfig) {
return finalLayoutConfig
}
const userCustomLayout = getMergeMeta('engine.layout')?.options?.layoutConfig
if (userCustomLayout) {
return userCustomLayout
}
const relativeLayoutConfig = getMergeMeta('engine.layout')?.options?.relativeLayoutConfig || {}
finalLayoutConfig = computeFinalLayoutConfig(deepClone(defaultLayout), relativeLayoutConfig)
return finalLayoutConfig
}
const getAllPlugins = () => {
return getAllMergeMeta()
.filter((item) => item.type === 'plugins')
.map((item) => {
return {
id: item.id,
title: item.title,
type: item.type
}
})
}
return {
isPanelWidthResizable,
getFixedPanelsStatus,
initPluginStorageReactive,
PLUGIN_NAME,
PLUGIN_POSITION,
activeSetting,
closeSetting,
activePlugin,
closePlugin,
layoutState,
getScale,
setDimension,
getDimension,
getPluginById,
pluginState,
getPluginState,
getSettingState,
isEmptyPage,
getPluginWidth,
changePluginWidth,
leftFixedPanelsStorage,
rightFixedPanelsStorage,
leftMenuShownStorage,
rightMenuShownStorage,
changeLeftFixedPanels,
changeRightFixedPanels,
getPluginsByLayout,
changePluginLayout,
getPluginByLayout,
dragPluginLayout,
isSameSide,
getPluginShown,
changePluginShown,
changeMenuShown,
getMoveDragBarState,
changeMoveDragBarState,
getPluginsByPosition,
getFinalLayoutConfig,
getAllPlugins
}
}