Визуальный редактор страниц с перетаскиванием 1
В-четвертых, меню левого компонента
Установите библиотеку компонентов Element-plus и используйте ее в проекте
yarn add element-plus
import ElementPlus from "element-plus";
import "element-plus/lib/theme-chalk/index.css";
const app = createApp(App);
app.use(ElementPlus);
app.mount("#app");
информация о конфигурации
- существует
visual-editor.utils.tsСоздайте функцию createVisualEditorConfig, чтобы создать конфигурацию редактора.
// 组件结构
export interface VisualEditorComponent {
key: string;
label: string;
preview: () => JSX.Element;
render: () => JSX.Element;
}
// 创建编辑器配置
export function createVisualEditorConfig() {
const componentList: VisualEditorComponent[] = [];
const componentMap: Record<string, VisualEditorComponent> = {};
return {
componentList,
componentMap,
registry: (key: string, component: Omit<VisualEditorComponent, "key">) => {
const comp = { ...component, key };
componentList.push(comp);
componentMap[key] = comp;
},
};
}
// 配置类型
export type VisualEditorConfig = ReturnType<typeof createVisualEditorConfig>;
- Новый файл конфигурации редактора
visual-editor.tsx - Создайте конфигурацию и зарегистрируйте три компонента: текст, кнопку и поле ввода в объекте конфигурации.
- Зарегистрированный объект компонента помещается в
componentList(для рендеринга меню компонентов) иcomponentMap(легко найти). - имя компонента метки, содержимое предварительного просмотра отображается в меню компонента, компонент рендеринга перетаскивается в контейнер для отображения содержимого
import { createVisualEditorConfig } from "./visual-editor.utils";
import { ElButton, ElInput } from "element-plus";
const visualConfig = createVisualEditorConfig();
visualConfig.registry("text", {
label: "文本",
preview: () => "预览文本",
render: () => "渲染文本",
});
visualConfig.registry("button", {
label: "按钮",
preview: () => <ElButton>按钮</ElButton>,
render: () => <ElButton>渲染按钮</ElButton>,
});
visualConfig.registry("input", {
label: "输入框",
preview: () => <ElInput />,
render: () => <ElInput />,
});
export default visualConfig;
- импорт конфигурации
visual-editorи визуализировать меню компонента
<div class="menu">
{props.config?.componentList.map((component) => (
<div class="menu-item">
<span class="menu-item-label">{component.label}</span>
{component.preview()}
</div>
))}
</div>
код фиксации
5. Перетащите компонент в контейнер и отрендерите его.
Перетащите меню в контейнер
- добавить в контейнер
containerRef - Добавление компонентов к левому боковому рендерингу
draggableсвойство (эффект перетаскивания) и контролироватьonDragstart,onDragendмероприятие
<div class="visual-editor">
<div class="menu">
{props.config?.componentList.map((component) => (
<div
class="menu-item"
draggable
onDragend={menuDragger.dragend}
onDragstart={(e) => menuDragger.dragstart(e, component)}
>
<span class="menu-item-label">{component.label}</span>
{component.preview()}
</div>
))}
</div>
<div class="head">head</div>
<div class="operator">operator</div>
<div class="body">
<div class="content">
<div
class="container"
ref={containerRef}
style={containerStyles.value}
>
{(dataModel.value?.blocks || []).map((block, index: number) => (
<VisualEditorBlock block={block} key={index} />
))}
</div>
</div>
</div>
</div>
- Источник данных редактора dataModel с помощью метода useModel для достижения двусторонней привязки
-
containerRefСсылка dom контейнера холста -
menuDraggerСлушатель событий перетаскивания мышью- При нажатии мыши компонент срабатывает.
dragstart, Слушайте контейнер в DragStartcontainerRefизdragenterdragoverdragleaveа такжеdropмероприятие - Запускается, когда компонент перетаскивается в контейнер
dragenterсобытие, состояние мышиdropEffectУстановить какmove(Размещаемый эффект) - Запускается, когда компонент перетаскивается из контейнера
dragleaveсобытие, состояние мышиdropEffectУстановить какnone(неустранимый эффект) - Когда компонент перетаскивается на холст и отпускается мышь, активируется контейнер.
dropсобытия и компонентыdragendмероприятие - существует
dropПолучить информацию о местоположении в событии и добавить новый блок в источник данных - существует
dragendУдалить событие перетаскивания, отслеживаемое контейнером в событии
- При нажатии мыши компонент срабатывает.
// 编辑器数据源
const dataModel = useModel(
() => props.modelValue,
(val) => ctx.emit("update:modelValue", val)
);
const containerStyles = computed(() => ({
width: `${props.modelValue?.container.width}px`,
height: `${props.modelValue?.container.height}px`,
}));
const containerRef = ref({} as HTMLElement);
const menuDragger = {
current: {
component: null as null | VisualEditorComponent,
},
dragstart: (e: DragEvent, component: VisualEditorComponent) => {
containerRef.value.addEventListener("dragenter", menuDragger.dragenter);
containerRef.value.addEventListener("dragover", menuDragger.dragover);
containerRef.value.addEventListener("dragleave", menuDragger.dragleave);
containerRef.value.addEventListener("drop", menuDragger.drop);
menuDragger.current.component = component;
},
dragenter: (e: DragEvent) => {
e.dataTransfer!.dropEffect = "move";
},
dragover: (e: DragEvent) => {
e.preventDefault();
},
dragleave: (e: DragEvent) => {
e.dataTransfer!.dropEffect = "none";
},
dragend: (e: DragEvent) => {
containerRef.value.removeEventListener(
"dragenter",
menuDragger.dragenter
);
containerRef.value.removeEventListener(
"dragover",
menuDragger.dragover
);
containerRef.value.removeEventListener(
"dragleave",
menuDragger.dragleave
);
containerRef.value.removeEventListener("drop", menuDragger.drop);
menuDragger.current.component = null;
},
drop: (e: DragEvent) => {
console.log("drop", menuDragger.current.component);
const blocks = dataModel.value?.blocks || [];
blocks.push({
top: e.offsetY,
left: e.offsetX,
});
console.log("x", e.offsetX);
console.log("y", e.offsetY);
dataModel.value = {
...dataModel.value,
blocks,
} as VisualEditorModelValue;
},
};
Компоненты рендерятся внутри контейнера
- Входящий блок в редакторе конфигурации
- в соответствии с блоком
componentKeyсобственность вcomponentMapЗарегистрированный компонент находится в , и метод рендеринга вызывается для рендеринга.
import { computed, defineComponent, PropType } from "vue";
import {
VisualEditorBlockData,
VisualEditorConfig,
} from "./visual-editor.utils";
export const VisualEditorBlock = defineComponent({
props: {
block: {
type: Object as PropType<VisualEditorBlockData>,
},
config: {
type: Object as PropType<VisualEditorConfig>,
},
},
setup(props) {
const styles = computed(() => ({
top: `${props.block?.top}px`,
left: `${props.block?.left}px`,
}));
return () => {
const component = props.config?.componentMap[props.block!.componentKey];
const Render = component?.render();
return (
<div class="visual-editor-block" style={styles.value}>
{Render}
</div>
);
};
},
});
добиться эффекта
код фиксации
6. Выбор компонента, перетаскивание и перемещение
Выбранный компонент
- Компонент будет иметь поведение по умолчанию после рендеринга контейнера, добавлять псевдоэлементы в компонент, запрещать ответ компонента и добавлять пунктирную рамку к элементу в выбранном состоянии.
.visual-editor-block {
position: absolute;
&::after {
$space: -3px;
position: absolute;
top: $space;
left: $space;
right: $space;
bottom: $space;
content: "";
}
&-focus {
&::after {
// 边框显示在伪元素上面
border: 1px dashed $primary;
}
}
}
- Используйте свойство фокуса, определенное в блоке, для управления выбранным состоянием компонента.
- существует
setupобъявлено в функцииfocusDataа такжеfocusHandlerДля обработки подбор компонентов
// 计算选中与未选中的block数据
const focusData = computed(() => {
const focus: VisualEditorBlockData[] =
dataModel.value?.blocks.filter((v) => v.focus) || [];
const unfocus: VisualEditorBlockData[] =
dataModel.value?.blocks.filter((v) => !v.focus) || [];
return {
focus, // 此时选中的数据
unfocus, // 此时未选中的数据
};
});
// 对外暴露的一些方法
const methods = {
clearFocus: (block?: VisualEditorBlockData) => {
let blocks = dataModel.value?.blocks || [];
if (blocks.length === 0) return;
if (block) {
blocks = blocks.filter((v) => v !== block);
}
blocks.forEach((block) => (block.focus = false));
},
};
// 处理组件的选中状态
const focusHandler = (() => {
return {
container: {
onMousedown: (e: MouseEvent) => {
e.stopPropagation();
methods.clearFocus();
},
},
block: {
onMousedown: (e: MouseEvent, block: VisualEditorBlockData) => {
e.stopPropagation();
e.preventDefault();
// 只有元素未选中状态下, 才去处理
if (!block.focus) {
if (!e.shiftKey) {
block.focus = !block.focus;
methods.clearFocus(block);
} else {
block.focus = true;
}
}
// 处理组件的选中移动
blockDragger.mousedown(e);
},
},
};
})();
- Добавить в контейнер и блочные компоненты
onMousedownпрослушиватель событий - Срабатывает при нажатии на компонент, блок
onMousedownСобытие, текущий компонент выбран, выбор других компонентов отменен - если
e.shiftKeyЕсли есть значение, значит, это множественный выбор, и снимать галочки с ранее выделенных компонентов не нужно. - Когда вы нажимаете контейнер, снимите флажок
<div class="content">
<div
class="container"
ref={containerRef}
style={containerStyles.value}
{...focusHandler.container}
>
{(dataModel.value?.blocks || []).map((block, index: number) => (
<VisualEditorBlock
block={block}
key={index}
config={props.config}
{...{
onMousedown: (e: MouseEvent) =>
focusHandler.block.onMousedown(e, block),
}}
/>
))}
</div>
</div>
Перетаскивание компонента
-
blockDraggerдля обработки перетаскивания компонентов - существует
focusHandlerблокonMousedownметод, и, наконец, мы передаем событие вblockDraggerизmousedownметод - существует
mousedownв, черезdragStateСначала запишите информацию о начальной позиции нажатия мыши и перетащите прослушиватель компонентаmousemoveа такжеmouseupмероприятие - существует
mousemoveВычисляя разницу между позицией при нажатии мыши и текущей позицией, вычисляются координаты последнего выбранного компонента.
// 处理组件在画布上他拖拽
const blockDragger = (() => {
let dragState = {
startX: 0,
startY: 0,
startPos: [] as { left: number; top: number }[],
};
const mousemove = (e: MouseEvent) => {
const durX = e.clientX - dragState.startX;
const durY = e.clientY - dragState.startY;
focusData.value.focus.forEach((block, i) => {
block.top = dragState.startPos[i].top + durY;
block.left = dragState.startPos[i].left + durX;
});
};
const mouseup = (e: MouseEvent) => {
document.removeEventListener("mousemove", mousemove);
document.removeEventListener("mouseup", mouseup);
};
const mousedown = (e: MouseEvent) => {
dragState = {
startX: e.clientX,
startY: e.clientY,
startPos: focusData.value.focus.map(({ top, left }) => ({
top,
left,
})),
};
document.addEventListener("mousemove", mousemove);
document.addEventListener("mouseup", mouseup);
};
return { mousedown };
})();
добиться эффекта