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 { computed, ComputedRef, ref, watch } from 'vue';
|
||||||
import Moveable from 'moveable';
|
|
||||||
|
|
||||||
import { type Services } from '@editor/type';
|
|
||||||
|
|
||||||
export const useFloatBox = (slideKeys: ComputedRef<string[]>) => {
|
export const useFloatBox = (slideKeys: ComputedRef<string[]>) => {
|
||||||
const services = inject<Services>('services');
|
const floatBoxStates = ref<{
|
||||||
const moveable = ref<Moveable>();
|
[key in (typeof slideKeys.value)[number]]: {
|
||||||
const floatBox = ref<HTMLElement[]>();
|
status: boolean;
|
||||||
|
top: number;
|
||||||
const floatBoxStates = computed(() => services?.uiService.get('floatBox'));
|
left: number;
|
||||||
|
};
|
||||||
const curKey = ref('');
|
}>(
|
||||||
const target = computed(() =>
|
slideKeys.value.reduce(
|
||||||
floatBox.value
|
(total, cur) => ({
|
||||||
? floatBox.value.find((item) => item.classList.contains(`m-editor-float-box-${curKey.value}`))
|
...total,
|
||||||
: undefined,
|
[cur]: {
|
||||||
|
status: false,
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const showingBoxKeys = computed(() =>
|
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 isDraging = ref(false);
|
||||||
|
|
||||||
const showFloatBox = async (key: string) => {
|
const dragstartHandler = () => (isDraging.value = true);
|
||||||
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 dragendHandler = (key: string, e: DragEvent) => {
|
const dragendHandler = (key: string, e: DragEvent) => {
|
||||||
setSlideState(key, {
|
floatBoxStates.value[key] = {
|
||||||
left: e.clientX,
|
left: e.clientX,
|
||||||
top: e.clientY,
|
top: e.clientY,
|
||||||
});
|
status: true,
|
||||||
showFloatBox(key);
|
};
|
||||||
isDraging.value = false;
|
isDraging.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -127,13 +41,17 @@ export const useFloatBox = (slideKeys: ComputedRef<string[]>) => {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
const dragstartHandler = () => (isDraging.value = true);
|
|
||||||
|
|
||||||
// 监听 slide 长度变化,更新 ui serice map
|
|
||||||
watch(
|
watch(
|
||||||
() => slideKeys.value,
|
() => 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,
|
deep: true,
|
||||||
@ -142,12 +60,9 @@ export const useFloatBox = (slideKeys: ComputedRef<string[]>) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
showFloatBox,
|
|
||||||
closeFloatBox,
|
|
||||||
dragstartHandler,
|
dragstartHandler,
|
||||||
dragendHandler,
|
dragendHandler,
|
||||||
floatBoxStates,
|
floatBoxStates,
|
||||||
floatBox,
|
|
||||||
showingBoxKeys,
|
showingBoxKeys,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<div
|
<div
|
||||||
class="m-editor-sidebar-header-item"
|
class="m-editor-sidebar-header-item"
|
||||||
v-for="(config, index) in sideBarItems"
|
v-for="(config, index) in sideBarItems"
|
||||||
v-show="!floatBoxStates?.get(config.$key)?.status"
|
v-show="!floatBoxStates[config.$key]?.status"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
:key="config.$key ?? index"
|
:key="config.$key ?? index"
|
||||||
:class="{ 'is-active': activeTabName === config.text }"
|
:class="{ 'is-active': activeTabName === config.text }"
|
||||||
@ -23,7 +23,7 @@
|
|||||||
v-show="activeTabName === config.text"
|
v-show="activeTabName === config.text"
|
||||||
>
|
>
|
||||||
<component
|
<component
|
||||||
v-if="config && !floatBoxStates?.get(config.$key)?.status"
|
v-if="config && !floatBoxStates[config.$key]?.status"
|
||||||
:is="config.component"
|
:is="config.component"
|
||||||
v-bind="config.props || {}"
|
v-bind="config.props || {}"
|
||||||
v-on="config?.listeners || {}"
|
v-on="config?.listeners || {}"
|
||||||
@ -96,43 +96,37 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
<div class="m-editor-float-box-list">
|
<template v-for="(config, index) in sideBarItems">
|
||||||
<div
|
<FloatingBox
|
||||||
v-for="(config, index) in sideBarItems"
|
|
||||||
:key="config.$key ?? index"
|
:key="config.$key ?? index"
|
||||||
ref="floatBox"
|
v-if="floatBoxStates[config.$key]?.status"
|
||||||
:class="['m-editor-float-box', `m-editor-float-box-${config.$key}`]"
|
v-model:visible="floatBoxStates[config.$key].status"
|
||||||
:style="{
|
:title="config.text"
|
||||||
left: `${floatBoxStates?.get(config.$key)?.left}px`,
|
:position="{
|
||||||
top: `${floatBoxStates?.get(config.$key)?.top}px`,
|
left: floatBoxStates[config.$key].left,
|
||||||
zIndex: floatBoxStates?.get(config.$key)?.zIndex,
|
top: floatBoxStates[config.$key].top,
|
||||||
}"
|
}"
|
||||||
v-show="floatBoxStates?.get(config.$key)?.status"
|
|
||||||
>
|
>
|
||||||
<div
|
<template #body>
|
||||||
:class="['m-editor-float-box-header', `m-editor-float-box-header-${config.$key}`]"
|
<div class="m-editor-slide-list-box">
|
||||||
@click="showFloatBox(config.$key)"
|
<component
|
||||||
>
|
v-if="config && floatBoxStates[config.$key].status"
|
||||||
<div>{{ config.text }}</div>
|
:is="config.boxComponentConfig?.component || config.component"
|
||||||
<MIcon class="m-editor-float-box-close" :icon="Close" @click.stop="closeFloatBox(config.$key)"></MIcon>
|
v-bind="config.boxComponentConfig?.props || config.props || {}"
|
||||||
</div>
|
v-on="config?.listeners || {}"
|
||||||
<div class="m-editor-float-box-body">
|
/>
|
||||||
<component
|
</div>
|
||||||
v-if="config && floatBoxStates?.get(config.$key)?.status"
|
</template>
|
||||||
:is="config.boxComponentConfig?.component || config.component"
|
</FloatingBox>
|
||||||
v-bind="config.boxComponentConfig?.props || config.props || {}"
|
</template>
|
||||||
v-on="config?.listeners || {}"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Teleport>
|
</Teleport>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, inject, ref, watch } from 'vue';
|
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 MIcon from '@editor/components/Icon.vue';
|
||||||
import { useFloatBox } from '@editor/hooks/use-float-box';
|
import { useFloatBox } from '@editor/hooks/use-float-box';
|
||||||
import type {
|
import type {
|
||||||
@ -200,11 +194,6 @@ const getItemConfig = (data: SideItem): SideComponent => {
|
|||||||
text: '代码编辑',
|
text: '代码编辑',
|
||||||
component: CodeBlockListPanel,
|
component: CodeBlockListPanel,
|
||||||
slots: {},
|
slots: {},
|
||||||
boxComponentConfig: {
|
|
||||||
props: {
|
|
||||||
slideType: 'box',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
'data-source': {
|
'data-source': {
|
||||||
$key: 'data-source',
|
$key: 'data-source',
|
||||||
@ -213,11 +202,6 @@ const getItemConfig = (data: SideItem): SideComponent => {
|
|||||||
text: '数据源',
|
text: '数据源',
|
||||||
component: DataSourceListPanel,
|
component: DataSourceListPanel,
|
||||||
slots: {},
|
slots: {},
|
||||||
boxComponentConfig: {
|
|
||||||
props: {
|
|
||||||
slideType: 'box',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -235,8 +219,7 @@ watch(
|
|||||||
|
|
||||||
const slideKeys = computed(() => sideBarItems.value.map((sideBarItem) => sideBarItem.$key));
|
const slideKeys = computed(() => sideBarItems.value.map((sideBarItem) => sideBarItem.$key));
|
||||||
|
|
||||||
const { showFloatBox, closeFloatBox, dragstartHandler, dragendHandler, floatBoxStates, floatBox, showingBoxKeys } =
|
const { dragstartHandler, dragendHandler, floatBoxStates, showingBoxKeys } = useFloatBox(slideKeys);
|
||||||
useFloatBox(slideKeys);
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => showingBoxKeys.value.length,
|
() => 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-methods.scss";
|
||||||
@import "./data-source-input.scss";
|
@import "./data-source-input.scss";
|
||||||
@import "./key-value.scss";
|
@import "./key-value.scss";
|
||||||
@import "./floatbox.scss";
|
|
||||||
@import "./tree.scss";
|
@import "./tree.scss";
|
||||||
@import "./floating-box.scss";
|
@import "./floating-box.scss";
|
||||||
@import "./page-fragment-select.scss";
|
@import "./page-fragment-select.scss";
|
||||||
|
Loading…
x
Reference in New Issue
Block a user