refactor(editor): floatbox 使用公共组件

This commit is contained in:
moonszhang 2024-02-22 19:54:53 +08:00 committed by roymondchen
parent 8d1ba220c1
commit cda5413fd1
5 changed files with 73 additions and 208 deletions

View File

@ -1,124 +1,38 @@
import { computed, ComputedRef, inject, nextTick, ref, watch } from 'vue';
import Moveable from 'moveable';
import { type Services } from '@editor/type';
import { computed, ComputedRef, ref, watch } from 'vue';
export const useFloatBox = (slideKeys: ComputedRef<string[]>) => {
const services = inject<Services>('services');
const moveable = ref<Moveable>();
const floatBox = ref<HTMLElement[]>();
const floatBoxStates = computed(() => services?.uiService.get('floatBox'));
const curKey = ref('');
const target = computed(() =>
floatBox.value
? floatBox.value.find((item) => item.classList.contains(`m-editor-float-box-${curKey.value}`))
: undefined,
const floatBoxStates = ref<{
[key in (typeof slideKeys.value)[number]]: {
status: boolean;
top: number;
left: number;
};
}>(
slideKeys.value.reduce(
(total, cur) => ({
...total,
[cur]: {
status: false,
top: 0,
left: 0,
},
}),
{},
),
);
const showingBoxKeys = computed(() =>
[...(floatBoxStates.value?.keys() ?? [])].filter((key) => floatBoxStates.value?.get(key)?.status),
Object.keys(floatBoxStates.value).filter((key) => floatBoxStates.value[key].status),
);
const isDraging = ref(false);
const showFloatBox = async (key: string) => {
const curBoxStatus = floatBoxStates.value?.get(curKey.value)?.status;
if (curKey.value === key && curBoxStatus) return;
curKey.value = key;
setSlideState(key, {
zIndex: getMaxZIndex() + 1,
status: true,
});
await nextTick();
if (moveable.value) {
moveable.value.target = target.value;
moveable.value.dragTarget = getDragTarget();
moveable.value.updateRect();
} else {
initFloatBoxMoveable();
}
};
const setSlideState = (key: string, data: Record<string, string | number | boolean>) => {
const slideState = floatBoxStates.value?.get(key);
if (!slideState) return;
floatBoxStates.value?.set(key, {
...slideState,
...data,
});
};
const getDragTarget = (key?: string) => `.m-editor-float-box-header-${key ?? curKey.value}`;
const closeFloatBox = (key: string) => {
setSlideState(key, {
status: false,
});
// 如果只有一个关掉后需要销毁moveable实例
if (!floatBoxStates.value?.values) return;
const keys = [...floatBoxStates.value?.keys()];
const values = [...floatBoxStates.value?.values()];
const lastFloatBoxLen = values.filter((state) => state.status).length;
if (lastFloatBoxLen === 0) {
moveable.value?.destroy();
moveable.value = undefined;
return;
}
// 如果关掉的 box 是最大的,需要选中下面的一层
if (key === curKey.value) {
// 查找显示的最大 zIndex 对应的 index
const zIndexList = values.filter((item) => item.status).map((item) => item.zIndex);
const maxZIndex = Math.max(...zIndexList);
const key = keys.find((key) => floatBoxStates.value?.get(key)?.zIndex === maxZIndex);
if (!key) return;
showFloatBox(key);
}
};
const getMaxZIndex = () => {
if (!floatBoxStates.value?.values()) return 0;
const list = [...floatBoxStates.value?.values()].map((state) => state.zIndex);
return Math.max(...list) ?? 0;
};
const initFloatBoxMoveable = () => {
const dragTarget = getDragTarget();
moveable.value = new Moveable(document.body, {
target: target.value,
draggable: true,
resizable: true,
edge: true,
keepRatio: false,
origin: false,
snappable: true,
dragTarget,
dragTargetSelf: false,
linePadding: 10,
controlPadding: 10,
elementGuidelines: [...(floatBoxStates.value?.keys() ?? [])].map((key) => getDragTarget(key)),
bounds: { left: 0, top: 0, right: 0, bottom: 0, position: 'css' },
});
moveable.value.on('drag', (e) => {
e.target.style.transform = e.transform;
});
moveable.value.on('resize', (e) => {
e.target.style.width = `${e.width}px`;
e.target.style.height = `${e.height}px`;
e.target.style.transform = e.drag.transform;
});
};
const dragstartHandler = () => (isDraging.value = true);
const dragendHandler = (key: string, e: DragEvent) => {
setSlideState(key, {
floatBoxStates.value[key] = {
left: e.clientX,
top: e.clientY,
});
showFloatBox(key);
status: true,
};
isDraging.value = false;
};
@ -127,13 +41,17 @@ export const useFloatBox = (slideKeys: ComputedRef<string[]>) => {
e.preventDefault();
});
const dragstartHandler = () => (isDraging.value = true);
// 监听 slide 长度变化,更新 ui serice map
watch(
() => slideKeys.value,
() => {
services?.uiService.setFloatBox(slideKeys.value);
for (const key in slideKeys.value) {
if (floatBoxStates.value[key]) continue;
floatBoxStates.value[key] = {
status: false,
top: 0,
left: 0,
};
}
},
{
deep: true,
@ -142,12 +60,9 @@ export const useFloatBox = (slideKeys: ComputedRef<string[]>) => {
);
return {
showFloatBox,
closeFloatBox,
dragstartHandler,
dragendHandler,
floatBoxStates,
floatBox,
showingBoxKeys,
};
};

View File

@ -4,7 +4,7 @@
<div
class="m-editor-sidebar-header-item"
v-for="(config, index) in sideBarItems"
v-show="!floatBoxStates?.get(config.$key)?.status"
v-show="!floatBoxStates[config.$key]?.status"
draggable="true"
:key="config.$key ?? index"
:class="{ 'is-active': activeTabName === config.text }"
@ -23,7 +23,7 @@
v-show="activeTabName === config.text"
>
<component
v-if="config && !floatBoxStates?.get(config.$key)?.status"
v-if="config && !floatBoxStates[config.$key]?.status"
:is="config.component"
v-bind="config.props || {}"
v-on="config?.listeners || {}"
@ -96,43 +96,37 @@
</div>
<Teleport to="body">
<div class="m-editor-float-box-list">
<div
v-for="(config, index) in sideBarItems"
<template v-for="(config, index) in sideBarItems">
<FloatingBox
:key="config.$key ?? index"
ref="floatBox"
:class="['m-editor-float-box', `m-editor-float-box-${config.$key}`]"
:style="{
left: `${floatBoxStates?.get(config.$key)?.left}px`,
top: `${floatBoxStates?.get(config.$key)?.top}px`,
zIndex: floatBoxStates?.get(config.$key)?.zIndex,
v-if="floatBoxStates[config.$key]?.status"
v-model:visible="floatBoxStates[config.$key].status"
:title="config.text"
:position="{
left: floatBoxStates[config.$key].left,
top: floatBoxStates[config.$key].top,
}"
v-show="floatBoxStates?.get(config.$key)?.status"
>
<div
:class="['m-editor-float-box-header', `m-editor-float-box-header-${config.$key}`]"
@click="showFloatBox(config.$key)"
>
<div>{{ config.text }}</div>
<MIcon class="m-editor-float-box-close" :icon="Close" @click.stop="closeFloatBox(config.$key)"></MIcon>
</div>
<div class="m-editor-float-box-body">
<template #body>
<div class="m-editor-slide-list-box">
<component
v-if="config && floatBoxStates?.get(config.$key)?.status"
v-if="config && floatBoxStates[config.$key].status"
:is="config.boxComponentConfig?.component || config.component"
v-bind="config.boxComponentConfig?.props || config.props || {}"
v-on="config?.listeners || {}"
/>
</div>
</div>
</div>
</template>
</FloatingBox>
</template>
</Teleport>
</template>
<script setup lang="ts">
import { computed, inject, ref, watch } from 'vue';
import { Close, Coin, EditPen, Goods, List } from '@element-plus/icons-vue';
import { Coin, EditPen, Goods, List } from '@element-plus/icons-vue';
import FloatingBox from '@editor/components/FloatingBox.vue';
import MIcon from '@editor/components/Icon.vue';
import { useFloatBox } from '@editor/hooks/use-float-box';
import type {
@ -200,11 +194,6 @@ const getItemConfig = (data: SideItem): SideComponent => {
text: '代码编辑',
component: CodeBlockListPanel,
slots: {},
boxComponentConfig: {
props: {
slideType: 'box',
},
},
},
'data-source': {
$key: 'data-source',
@ -213,11 +202,6 @@ const getItemConfig = (data: SideItem): SideComponent => {
text: '数据源',
component: DataSourceListPanel,
slots: {},
boxComponentConfig: {
props: {
slideType: 'box',
},
},
},
};
@ -235,8 +219,7 @@ watch(
const slideKeys = computed(() => sideBarItems.value.map((sideBarItem) => sideBarItem.$key));
const { showFloatBox, closeFloatBox, dragstartHandler, dragendHandler, floatBoxStates, floatBox, showingBoxKeys } =
useFloatBox(slideKeys);
const { dragstartHandler, dragendHandler, floatBoxStates, showingBoxKeys } = useFloatBox(slideKeys);
watch(
() => showingBoxKeys.value.length,

View File

@ -1,48 +0,0 @@
.m-editor-float-box-list {
.m-editor-float-box {
position: absolute;
display: flex;
flex-direction: column;
width: auto;
height: 966px;
top: 240px;
left: 240px;
background: #fff;
box-sizing: border-box;
z-index: 999;
max-width: auto;
min-width: auto;
max-height: auto;
min-height: auto;
border: 1px solid #eee;
box-shadow: 0 0 72px #ccc;
&-header {
display: flex;
align-items: center;
padding: 0 16px;
height: 44px;
border-bottom: 1px solid #d8dee8;
&:hover {
cursor: pointer;
}
}
&-body {
display: flex;
flex: 1;
width: 100%;
overflow: scroll;
> *:first-child {
min-width: 247px;
border-right: 1px solid #d8dee8;
}
}
&-close {
position: absolute;
right: 16px;
}
}
.moveable-resizable {
opacity: 0;
}
}

View File

@ -70,3 +70,19 @@
}
}
}
.m-editor-slide-list-box {
display: flex;
min-width: 270px;
min-height: 500px;
max-height: 1024px;
> div {
&:first-child {
width: 100%;
}
&:nth-of-type(2) {
width: 100%;
flex: 1;
}
}
}

View File

@ -21,7 +21,6 @@
@import "./data-source-methods.scss";
@import "./data-source-input.scss";
@import "./key-value.scss";
@import "./floatbox.scss";
@import "./tree.scss";
@import "./floating-box.scss";
@import "./page-fragment-select.scss";