mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2025-06-22 18:05:17 +08:00
refactor(editor): floatbox 使用公共组件
This commit is contained in:
parent
8d1ba220c1
commit
cda5413fd1
@ -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,
|
||||
};
|
||||
};
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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";
|
||||
|
Loading…
x
Reference in New Issue
Block a user