Skip to content

Instantly share code, notes, and snippets.

@andrienko
Last active April 30, 2024 18:00
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save andrienko/a754de7e615fa01435a526c8b6d238cd to your computer and use it in GitHub Desktop.
Save andrienko/a754de7e615fa01435a526c8b6d238cd to your computer and use it in GitHub Desktop.
Monaco drag and drop provider
import React from 'react';
import { editor } from 'monaco-editor';
import IMouseTarget = editor.IMouseTarget;
import IStandaloneCodeEditor = editor.IStandaloneCodeEditor;
import IContentWidget = editor.IContentWidget;
const { ContentWidgetPositionPreference } = editor;
export type TDropHandler = (e: React.DragEvent, target: IMouseTarget, instance: IStandaloneCodeEditor) => void;
export type TInstanceGetter = () => IStandaloneCodeEditor;
export class MonacoDragNDropProvider {
onDrop = (e: React.DragEvent<HTMLDivElement>) => {
this.onDropHandler && this.onDropHandler(e, this.dragTarget, this.getInstance());
this.removeMouseDownWidget();
};
onDragOver = (e: React.DragEvent<HTMLDivElement>) => {
const instance = this.getInstance();
instance && this.displayMouseDropPosition(instance, instance.getTargetAtClientPoint(e.clientX, e.clientY));
e.preventDefault();
};
removeMouseDownWidget = () => {
const instance = this.getInstance();
if (instance && this.mouseDropWidget && this.domNode) {
instance.removeContentWidget(this.mouseDropWidget);
this.mouseDropWidget = null;
}
};
props: React.HTMLAttributes<HTMLDivElement> = {
onDragOver: this.onDragOver,
onDropCapture: this.onDrop,
onDragLeaveCapture: this.removeMouseDownWidget
};
domNode: HTMLDivElement = null;
mouseDropWidget: IContentWidget = null;
dragTarget: IMouseTarget;
buildMouseDropWidget = () => {
if (!this.domNode) {
this.domNode = document.createElement('div');
this.domNode.className = this.dropClassName;
this.domNode.style.pointerEvents = 'none';
this.domNode.style.borderLeft = '2px solid #ccc';
this.domNode.innerHTML = '&nbsp;';
}
return {
getId: () => 'drag',
getDomNode: () => this.domNode,
getPosition: () => ({
position: this.dragTarget.position,
preference: [ContentWidgetPositionPreference.EXACT, ContentWidgetPositionPreference.EXACT]
})
};
};
displayMouseDropPosition = (instance: IStandaloneCodeEditor, target: IMouseTarget) => {
this.dragTarget = target;
if (this.mouseDropWidget) {
instance.layoutContentWidget(this.mouseDropWidget);
} else {
this.mouseDropWidget = this.buildMouseDropWidget();
instance.addContentWidget(this.mouseDropWidget);
}
};
getInstance: TInstanceGetter;
dropClassName: string;
onDropHandler: TDropHandler;
constructor(onDrop: TDropHandler, getInstance: TInstanceGetter, dropClassName: string = 'drop') {
this.dropClassName = dropClassName;
this.onDropHandler = onDrop;
this.getInstance = getInstance;
}
}
@sidwebworks
Copy link

Thanks, Inspired by this I also wrote a functional version of this

export type TDropHandler = (
  e: React.DragEvent<HTMLDivElement>,
  target: editor.IMouseTarget,
  instance: Instance
) => void;

export type TInstanceRef = MutableRefObject<Instance | undefined>;

type DndControllerState = {
  target: editor.IMouseTarget | null;
  node: HTMLDivElement | null;
  widget: editor.IContentWidget | null;
};

export function createDndController(
  instanceRef: TInstanceRef,
  handler: TDropHandler
) {
  const state: DndControllerState = {
    target: null,
    node: null,
    widget: null,
  };

  const onDragOver = (ev: DragEvent<HTMLDivElement>) => {
    if (!instanceRef.current) return;
    const instance = instanceRef.current;

    const target = instance.getTargetAtClientPoint(ev.clientX, ev.clientY);

    showDropPosition(instance, target!);
    ev.preventDefault();
  };

  const onDrop = (ev: DragEvent<HTMLDivElement>) => {
    if (!instanceRef.current || !state.target) return;
    handler && handler(ev, state.target, instanceRef.current);
    onCleanup();
  };

  const createMouseDropWidget = () => {
    if (!state.node) {
      state.node = document.createElement("div");
      state.node.className = "drop";
      state.node.style.pointerEvents = "none";
      state.node.style.borderLeft = "2px solid #ccc";
      state.node.innerHTML = "&nbsp;";
    }

    return {
      getId: () => "drag",
      getDomNode: () => state.node as HTMLDivElement,
      getPosition: () => ({
        position: state.target!.position,
        preference: [0, 0],
      }),
    };
  };

  const showDropPosition = (
    instance: Instance,
    target: editor.IMouseTarget
  ) => {
    state.target = target;

    if (state.widget) {
      instance.layoutContentWidget(state.widget);
    } else {
      state.widget = createMouseDropWidget();
      instance.addContentWidget(state.widget);
    }
  };

  const onCleanup = () => {
    const instance = instanceRef.current;
    if (instance && state.widget && state.node) {
      instance.removeContentWidget(state.widget);
      state.widget = null;
    }
  };

  const getContainerProps = () => ({
    onDragOver,
    onDropCapture: onDrop,
    onDragLeaveCapture: onCleanup,
  });

  return {
    getContainerProps,
  };
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment