Skip to content

Commit

Permalink
[WEB-2166] chore: smoother drag experience in the document editor (ma…
Browse files Browse the repository at this point in the history
…keplane#5296)

* chore: update drag and drop behaviour

* chore: update drag and drop behaviour

* chore: disable pwa updates on development mode
  • Loading branch information
aaryan610 authored Aug 5, 2024
1 parent c99f2fc commit f9e7a58
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 83 deletions.
100 changes: 45 additions & 55 deletions packages/editor/src/core/extensions/drag-drop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,18 @@ export const DragAndDrop = (setHideDragHandle?: (hideDragHandlerFromDragDrop: ()
},
});

function createDragHandleElement(): HTMLElement {
const dragHandleElement = document.createElement("div");
const createDragHandleElement = (): HTMLElement => {
const dragHandleElement = document.createElement("button");
dragHandleElement.type = "button";
dragHandleElement.draggable = true;
dragHandleElement.dataset.dragHandle = "";
dragHandleElement.classList.add("drag-handle");

const dragHandleContainer = document.createElement("div");
const dragHandleContainer = document.createElement("span");
dragHandleContainer.classList.add("drag-handle-container");
dragHandleElement.appendChild(dragHandleContainer);

const dotsContainer = document.createElement("div");
const dotsContainer = document.createElement("span");
dotsContainer.classList.add("drag-handle-dots");

for (let i = 0; i < 6; i++) {
Expand All @@ -50,19 +51,19 @@ function createDragHandleElement(): HTMLElement {
dragHandleContainer.appendChild(dotsContainer);

return dragHandleElement;
}
};

function absoluteRect(node: Element) {
const absoluteRect = (node: Element) => {
const data = node.getBoundingClientRect();

return {
top: data.top,
left: data.left,
width: data.width,
};
}
};

function nodeDOMAtCoords(coords: { x: number; y: number }) {
const nodeDOMAtCoords = (coords: { x: number; y: number }) => {
const elements = document.elementsFromPoint(coords.x, coords.y);
const generalSelectors = [
"li",
Expand All @@ -73,6 +74,7 @@ function nodeDOMAtCoords(coords: { x: number; y: number }) {
"h1, h2, h3, h4, h5, h6",
"[data-type=horizontalRule]",
".table-wrapper",
".issue-embed",
].join(", ");

for (const elem of elements) {
Expand All @@ -94,27 +96,27 @@ function nodeDOMAtCoords(coords: { x: number; y: number }) {
}
}
return null;
}
};

function nodePosAtDOM(node: Element, view: EditorView, options: DragHandleOptions) {
const nodePosAtDOM = (node: Element, view: EditorView, options: DragHandleOptions) => {
const boundingRect = node.getBoundingClientRect();

return view.posAtCoords({
left: boundingRect.left + 50 + options.dragHandleWidth,
top: boundingRect.top + 1,
})?.inside;
}
};

function nodePosAtDOMForBlockquotes(node: Element, view: EditorView) {
const nodePosAtDOMForBlockQuotes = (node: Element, view: EditorView) => {
const boundingRect = node.getBoundingClientRect();

return view.posAtCoords({
left: boundingRect.left + 1,
top: boundingRect.top + 1,
})?.inside;
}
};

function calcNodePos(pos: number, view: EditorView, node: Element) {
const calcNodePos = (pos: number, view: EditorView, node: Element) => {
const maxPos = view.state.doc.content.size;
const safePos = Math.max(0, Math.min(pos, maxPos));
const $pos = view.state.doc.resolve(safePos);
Expand All @@ -128,11 +130,11 @@ function calcNodePos(pos: number, view: EditorView, node: Element) {
}

return safePos;
}
};

function DragHandle(options: DragHandleOptions) {
const DragHandle = (options: DragHandleOptions) => {
let listType = "";
function handleDragStart(event: DragEvent, view: EditorView) {
const handleDragStart = (event: DragEvent, view: EditorView) => {
view.focus();

if (!event.dataTransfer) return;
Expand All @@ -159,6 +161,7 @@ function DragHandle(options: DragHandleOptions) {
// Check if nodePos points to the top level node
if (nodePos.node().type.name === "doc") differentNodeSelected = true;
else {
// TODO FIX ERROR
const nodeSelection = NodeSelection.create(view.state.doc, nodePos.before());
// Check if the node where the drag event started is part of the current selection
differentNodeSelected = !(
Expand All @@ -171,6 +174,7 @@ function DragHandle(options: DragHandleOptions) {
const multiNodeSelection = TextSelection.create(view.state.doc, draggedNodePos, endSelection.$to.pos);
view.dispatch(view.state.tr.setSelection(multiNodeSelection));
} else {
// TODO FIX ERROR
const nodeSelection = NodeSelection.create(view.state.doc, draggedNodePos);
view.dispatch(view.state.tr.setSelection(nodeSelection));
}
Expand All @@ -181,14 +185,15 @@ function DragHandle(options: DragHandleOptions) {
}

if (node.matches("blockquote")) {
let nodePosForBlockquotes = nodePosAtDOMForBlockquotes(node, view);
if (nodePosForBlockquotes === null || nodePosForBlockquotes === undefined) return;
let nodePosForBlockQuotes = nodePosAtDOMForBlockQuotes(node, view);
if (nodePosForBlockQuotes === null || nodePosForBlockQuotes === undefined) return;

const docSize = view.state.doc.content.size;
nodePosForBlockquotes = Math.max(0, Math.min(nodePosForBlockquotes, docSize));
nodePosForBlockQuotes = Math.max(0, Math.min(nodePosForBlockQuotes, docSize));

if (nodePosForBlockquotes >= 0 && nodePosForBlockquotes <= docSize) {
const nodeSelection = NodeSelection.create(view.state.doc, nodePosForBlockquotes);
if (nodePosForBlockQuotes >= 0 && nodePosForBlockQuotes <= docSize) {
// TODO FIX ERROR
const nodeSelection = NodeSelection.create(view.state.doc, nodePosForBlockQuotes);
view.dispatch(view.state.tr.setSelection(nodeSelection));
}
}
Expand All @@ -204,9 +209,9 @@ function DragHandle(options: DragHandleOptions) {
event.dataTransfer.setDragImage(node, 0, 0);

view.dragging = { slice, move: event.ctrlKey };
}
};

function handleClick(event: MouseEvent, view: EditorView) {
const handleClick = (event: MouseEvent, view: EditorView) => {
view.focus();

const node = nodeDOMAtCoords({
Expand All @@ -217,13 +222,14 @@ function DragHandle(options: DragHandleOptions) {
if (!(node instanceof Element)) return;

if (node.matches("blockquote")) {
let nodePosForBlockquotes = nodePosAtDOMForBlockquotes(node, view);
let nodePosForBlockquotes = nodePosAtDOMForBlockQuotes(node, view);
if (nodePosForBlockquotes === null || nodePosForBlockquotes === undefined) return;

const docSize = view.state.doc.content.size;
nodePosForBlockquotes = Math.max(0, Math.min(nodePosForBlockquotes, docSize));

if (nodePosForBlockquotes >= 0 && nodePosForBlockquotes <= docSize) {
// TODO FIX ERROR
const nodeSelection = NodeSelection.create(view.state.doc, nodePosForBlockquotes);
view.dispatch(view.state.tr.setSelection(nodeSelection));
}
Expand All @@ -237,51 +243,37 @@ function DragHandle(options: DragHandleOptions) {
// Adjust the nodePos to point to the start of the node, ensuring NodeSelection can be applied
nodePos = calcNodePos(nodePos, view, node);

// TODO FIX ERROR
// Use NodeSelection to select the node at the calculated position
const nodeSelection = NodeSelection.create(view.state.doc, nodePos);

// Dispatch the transaction to update the selection
view.dispatch(view.state.tr.setSelection(nodeSelection));
}
};

let dragHandleElement: HTMLElement | null = null;

function hideDragHandle() {
if (dragHandleElement) {
dragHandleElement.classList.add("hidden");
}
}

function showDragHandle() {
if (dragHandleElement) {
dragHandleElement.classList.remove("hidden");
}
}
// drag handle view actions
const hideDragHandle = () => dragHandleElement?.classList.add("drag-handle-hidden");
const showDragHandle = () => dragHandleElement?.classList.remove("drag-handle-hidden");

options.setHideDragHandle?.(hideDragHandle);

return new Plugin({
key: new PluginKey("dragHandle"),
view: (view) => {
dragHandleElement = createDragHandleElement();
dragHandleElement.addEventListener("dragstart", (e) => {
handleDragStart(e, view);
});
dragHandleElement.addEventListener("click", (e) => {
handleClick(e, view);
});
dragHandleElement.addEventListener("contextmenu", (e) => {
handleClick(e, view);
});
dragHandleElement.addEventListener("dragstart", (e) => handleDragStart(e, view));
dragHandleElement.addEventListener("click", (e) => handleClick(e, view));
dragHandleElement.addEventListener("contextmenu", (e) => handleClick(e, view));

dragHandleElement.addEventListener("drag", (e) => {
hideDragHandle();
const a = document.querySelector(".frame-renderer");
if (!a) return;
const frameRenderer = document.querySelector(".frame-renderer");
if (!frameRenderer) return;
if (e.clientY < options.scrollThreshold.up) {
a.scrollBy({ top: -70, behavior: "smooth" });
frameRenderer.scrollBy({ top: -70, behavior: "smooth" });
} else if (window.innerHeight - e.clientY < options.scrollThreshold.down) {
a.scrollBy({ top: 70, behavior: "smooth" });
frameRenderer.scrollBy({ top: 70, behavior: "smooth" });
}
});

Expand All @@ -299,9 +291,7 @@ function DragHandle(options: DragHandleOptions) {
props: {
handleDOMEvents: {
mousemove: (view, event) => {
if (!view.editable) {
return;
}
if (!view.editable) return;

const node = nodeDOMAtCoords({
x: event.clientX + 50 + options.dragHandleWidth,
Expand Down Expand Up @@ -411,4 +401,4 @@ function DragHandle(options: DragHandleOptions) {
},
},
});
}
};
3 changes: 1 addition & 2 deletions packages/editor/src/core/extensions/extensions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,7 @@ export const CoreEditorExtensions = ({
horizontalRule: false,
blockquote: false,
dropcursor: {
color: "rgba(var(--color-text-100))",
width: 1,
class: "text-custom-text-300",
},
...(enableHistory ? {} : { history: false }),
}),
Expand Down
57 changes: 32 additions & 25 deletions packages/editor/src/styles/drag-drop.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,20 @@
.drag-handle {
position: fixed;
opacity: 1;
transition: opacity ease-in 0.2s;
height: 20px;
width: 15px;
width: 20px;
aspect-ratio: 1 / 1;
display: grid;
place-items: center;
z-index: 5;
cursor: grab;
border-radius: 2px;
transition: background-color 0.2s;
outline: none !important;
transition:
opacity 0.2s ease 0.2s,
background-color 0.2s ease,
top 0.2s ease,
left 0.2s ease;

&:hover {
background-color: rgba(var(--color-background-80));
Expand All @@ -21,7 +26,7 @@
cursor: grabbing;
}

&.hidden {
&.drag-handle-hidden {
opacity: 0;
pointer-events: none;
}
Expand Down Expand Up @@ -62,25 +67,33 @@
cursor: grab;
outline: none !important;
box-shadow: none;
}

.ProseMirror:not(.dragging) .ProseMirror-selectednode::after {
content: "";
position: absolute;
top: 0;
left: -5px;
height: 100%;
width: 100%;
background-color: rgba(var(--color-primary-100), 0.2);
border-radius: 4px;
--horizontal-offset: 5px;

&:has(.issue-embed),
&.table-wrapper {
--horizontal-offset: 0px;
}

&::after {
content: "";
position: absolute;
top: 0;
left: calc(-1 * var(--horizontal-offset));
height: 100%;
width: calc(100% + (var(--horizontal-offset) * 2));
background-color: rgba(var(--color-primary-100), 0.2);
border-radius: 4px;
pointer-events: none;
}
}

/* for targetting the taks list items */
/* for targeting the task list items */
li.ProseMirror-selectednode:not(.dragging)[data-checked]::after {
margin-left: -5px;
}

/* for targetting the unordered list items */
/* for targeting the unordered list items */
ul > li.ProseMirror-selectednode:not(.dragging)::after {
margin-left: -10px; /* Adjust as needed */
}
Expand All @@ -90,18 +103,18 @@ ol {
counter-reset: item;
}

/* for targetting the ordered list items */
/* for targeting the ordered list items */
ol > li.ProseMirror-selectednode:not(.dragging)::after {
counter-increment: item;
margin-left: -18px;
}

/* for targetting the ordered list items after the 9th item */
/* for targeting the ordered list items after the 9th item */
ol > li:nth-child(n + 10).ProseMirror-selectednode:not(.dragging)::after {
margin-left: -25px;
}

/* for targetting the ordered list items after the 99th item */
/* for targeting the ordered list items after the 99th item */
ol > li:nth-child(n + 100).ProseMirror-selectednode:not(.dragging)::after {
margin-left: -35px;
}
Expand All @@ -118,9 +131,3 @@ ol > li:nth-child(n + 100).ProseMirror-selectednode:not(.dragging)::after {
filter: brightness(90%);
}
}

:not(.dragging) .ProseMirror-selectednode.table-wrapper {
padding: 4px 2px;
background-color: rgba(var(--color-primary-300), 0.1) !important;
box-shadow: rgba(var(--color-primary-100)) 0px 0px 0px 2px inset !important;
}
1 change: 1 addition & 0 deletions web/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const path = require("path");
const { withSentryConfig } = require("@sentry/nextjs");
const withPWA = require("next-pwa")({
dest: "public",
disable: process.env.NODE_ENV === "development",
});

const nextConfig = {
Expand Down
Loading

0 comments on commit f9e7a58

Please sign in to comment.