[WEB-2166] chore: smoother drag experience in the document editor (#5296)

* chore: update drag and drop behaviour

* chore: update drag and drop behaviour

* chore: disable pwa updates on development mode
This commit is contained in:
Aaryan Khandelwal 2024-08-05 13:59:14 +05:30 committed by GitHub
parent c99f2fcdbb
commit f9e7a5826b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 112 additions and 84 deletions

View file

@ -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++) {
@ -50,9 +51,9 @@ function createDragHandleElement(): HTMLElement {
dragHandleContainer.appendChild(dotsContainer);
return dragHandleElement;
}
};
function absoluteRect(node: Element) {
const absoluteRect = (node: Element) => {
const data = node.getBoundingClientRect();
return {
@ -60,9 +61,9 @@ function absoluteRect(node: Element) {
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",
@ -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) {
@ -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);
@ -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;
@ -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 = !(
@ -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));
}
@ -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));
}
}
@ -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({
@ -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));
}
@ -237,26 +243,18 @@ 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);
@ -264,24 +262,18 @@ function DragHandle(options: DragHandleOptions) {
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" });
}
});
@ -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,
@ -411,4 +401,4 @@ function DragHandle(options: DragHandleOptions) {
},
},
});
}
};

View file

@ -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 }),
}),

View file

@ -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));
@ -21,7 +26,7 @@
cursor: grabbing;
}
&.hidden {
&.drag-handle-hidden {
opacity: 0;
pointer-events: none;
}
@ -62,25 +67,33 @@
cursor: grab;
outline: none !important;
box-shadow: none;
--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;
}
}
.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;
}
/* 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 */
}
@ -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;
}
@ -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;
}