Визуальный редактор страниц с перетаскиванием 2

Vue.js
Визуальный редактор страниц с перетаскиванием 2

Визуальный редактор страниц с перетаскиванием 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изdragenter dragover dragleaveа также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 };
    })();

добиться эффекта

код фиксации

Полный код GitHub

Следующий раздел: Функция удаления компонента, горизонтальное и вертикальное перетаскивание, растяжение и изменение размера