Skip to content

Commit

Permalink
Merge pull request #21 from YousefED/draghandle-menu
Browse files Browse the repository at this point in the history
draghandle menu on click
  • Loading branch information
17Amir17 committed Mar 21, 2022
2 parents cc03566 + 938876d commit afed2c7
Show file tree
Hide file tree
Showing 21 changed files with 566 additions and 27 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { NodeSelection, Plugin, PluginKey } from "prosemirror-state";
import { EditorView, __serializeForClipboard } from "prosemirror-view";
import styles from "./draggableBlocks.module.css";
import ReactDOM from "react-dom";
import { DragHandle } from "./components/DragHandle";

import React from "react";

// code based on https://github.com/ueberdosis/tiptap/issues/323#issuecomment-506637799

Expand All @@ -26,12 +29,12 @@ function blockPosAtCoords(
coords: { left: number; top: number },
view: EditorView
) {
let node = getDraggableNodeFromCoords(coords, view);
let block = getDraggableBlockFromCoords(coords, view);

if (node && node.nodeType === 1) {
if (block && block.node.nodeType === 1) {
// TODO: this uses undocumented PM APIs? do we need this / let's add docs?
const docView = (view as any).docView;
let desc = docView.nearestDesc(node, true);
let desc = docView.nearestDesc(block.node, true);
if (!desc || desc === docView) {
return null;
}
Expand All @@ -40,7 +43,7 @@ function blockPosAtCoords(
return null;
}

function getDraggableNodeFromCoords(
function getDraggableBlockFromCoords(
coords: { left: number; top: number },
view: EditorView
) {
Expand All @@ -63,7 +66,10 @@ function getDraggableNodeFromCoords(
) {
node = node.parentNode as HTMLElement;
}
return node;
if (!node) {
return undefined;
}
return { node, id: node.getAttribute("data-id")! };
}

function dragStart(e: DragEvent, view: EditorView) {
Expand All @@ -88,8 +94,8 @@ function dragStart(e: DragEvent, view: EditorView) {
e.dataTransfer.setData("text/html", dom.innerHTML);
e.dataTransfer.setData("text/plain", text);
e.dataTransfer.effectAllowed = "move";
const node = getDraggableNodeFromCoords(coords, view);
e.dataTransfer.setDragImage(node as any, 0, 0);
const block = getDraggableBlockFromCoords(coords, view);
e.dataTransfer.setDragImage(block?.node as any, 0, 0);
view.dragging = { slice, move: true };
}
}
Expand All @@ -99,14 +105,22 @@ export const createDraggableBlocksPlugin = () => {

const WIDTH = 24;

let menuShown = false;

const onShow = () => {
menuShown = true;
};
const onHide = () => {
menuShown = false;
};

return new Plugin({
key: new PluginKey("DraggableBlocksPlugin"),
view(editorView) {
dropElement = document.createElement("div");
dropElement.setAttribute("draggable", "true");
dropElement.className = styles.dragHandle;
// dropElement.textContent = "⠿";
document.body.appendChild(dropElement);
dropElement.style.position = "absolute";
document.body.append(dropElement);

dropElement.addEventListener("dragstart", (e) =>
dragStart(e, editorView)
Expand Down Expand Up @@ -141,7 +155,8 @@ export const createDraggableBlocksPlugin = () => {
if (!dropElement) {
throw new Error("unexpected");
}
dropElement.classList.add(styles.hidden);
menuShown = false;
ReactDOM.render(<></>, dropElement);
return false;
},
handleDOMEvents: {
Expand All @@ -161,26 +176,40 @@ export const createDraggableBlocksPlugin = () => {
if (!dropElement) {
throw new Error("unexpected");
}
if (menuShown) {
// The submenu is open, don't move draghandle
return true;
}
let coords = {
left: view.dom.clientWidth / 2, // take middle of editor
top: event.clientY,
};
const node = getDraggableNodeFromCoords(coords, view);
const block = getDraggableBlockFromCoords(coords, view);

if (!node) {
dropElement.classList.add(styles.hidden);
if (!block) {
console.warn("Perhaps we should hide element?");
return true;
}

dropElement.classList.remove(styles.hidden);
let rect = absoluteRect(node);
let win = node.ownerDocument.defaultView!;
let rect = absoluteRect(block.node);
let win = block.node.ownerDocument.defaultView!;
rect.top += win.pageYOffset;
rect.left += win.pageXOffset;
// rect.width = WIDTH + "px";

dropElement.style.left = -WIDTH + rect.left + "px";
dropElement.style.top = rect.top + "px";

ReactDOM.render(
<DragHandle
onShow={onShow}
onHide={onHide}
key={block.id + ""}
view={view}
coords={coords}
/>,
dropElement
);
return true;
},
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.dragHandle {
position: absolute;
/* position: absolute; */
transition: opacity 300ms;
width: 20px;
height: 24px;
Expand All @@ -19,8 +19,3 @@
.dragHandle:hover {
background-color: #eee;
}

.dragHandle.hidden {
pointer-events: none;
opacity: 0;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import Tippy from "@tippyjs/react";
import DragHandleMenu from "./DragHandleMenu";
import styles from "./DragHandle.module.css";
import React, { useState } from "react";
import { EditorView } from "prosemirror-view";
import { TextSelection } from "prosemirror-state";
import { findBlock } from "../../Blocks/helpers/findBlock";

export const DragHandle = (props: {
view: EditorView;
coords: { left: number; top: number };
onShow?: () => void;
onHide?: () => void;
}) => {
const [deleted, setDeleted] = useState<boolean>(false);

const onDelete = () => {
const pos = props.view.posAtCoords(props.coords);
if (!pos) return;
const currentBlock = findBlock(
TextSelection.create(props.view.state.doc, pos.pos)
);

if (currentBlock) {
if (props.view.dispatch) {
props.view.dispatch(
props.view.state.tr.deleteRange(
currentBlock.pos,
currentBlock.pos + currentBlock.node.nodeSize
)
);
}
setDeleted(true);
}
};

if (deleted) return null;

return (
<Tippy
content={<DragHandleMenu onDelete={onDelete} />}
placement={"left"}
trigger={"click"}
duration={0}
interactiveBorder={100}
interactive={true}
onShow={props.onShow}
onHide={props.onHide}>
<div className={styles.dragHandle} />
</Tippy>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.menuList {
color: var(--N800);
background-color: white;
border: 1px solid var(--N40);
box-shadow: 0px 4px 8px rgba(9, 30, 66, 0.25),
0px 0px 1px rgba(9, 30, 66, 0.31);
border-radius: 4px;
max-width: 320;
margin: 16px auto;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import styles from "./DragHandleMenu.module.css";
import { MenuGroup, ButtonItem } from "@atlaskit/menu";
import React from "react";

type Props = {
onDelete: () => void;
};

const DragHandleMenu = (props: Props) => {
return (
<div className={styles.menuList}>
<MenuGroup>
<ButtonItem onClick={props.onDelete}>Delete</ButtonItem>
</MenuGroup>
</div>
);
};

export default DragHandleMenu;
96 changes: 96 additions & 0 deletions tests/end-to-end/draghandle/draghandle.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { test, expect, Page } from "@playwright/test";
import {
BASE_URL,
DRAGHANDLE,
H_ONE_BLOCK_SELECTOR,
H_THREE_BLOCK_SELECTOR,
H_TWO_BLOCK_SELECTOR,
TIPPY_MENU,
} from "../../utils/const";
import { compareDocToSnapshot, focusOnEditor } from "../../utils/editor";
import { executeSlashCommand } from "../../utils/slashmenu";

let page: Page;

test.describe("Check Draghandle functionality", () => {
test.beforeAll(async ({ browser }) => {
page = await browser.newPage();
});

test.beforeEach(async () => {
page.goto(BASE_URL, { waitUntil: "networkidle" });
await focusOnEditor(page);
});

test.afterAll(async () => {
await page.close();
});

test("Should show draghandle when hovering over block", async () => {
await executeSlashCommand(page, "h1");
await page.keyboard.type("Hover over this text");
await page.hover(H_ONE_BLOCK_SELECTOR);
await page.waitForSelector(DRAGHANDLE);
});

// TODO: add drag and drop test - playwright dragAndDrop function not working for some reason

test("Clicking draghandle should open menu", async () => {
await executeSlashCommand(page, "h1");
await page.keyboard.type("Hover over this text");
await page.hover(H_ONE_BLOCK_SELECTOR);
await page.click(DRAGHANDLE);
await page.waitForSelector(TIPPY_MENU);
await page.waitForTimeout(1000);
// Compare editor screenshot
expect(await page.screenshot()).toMatchSnapshot("draghandlemenu.png");
});

test("Clicking delete button should delete block", async () => {
await executeSlashCommand(page, "h1");
await page.keyboard.type("Hover over this text");
await page.hover(H_ONE_BLOCK_SELECTOR);
await page.click(DRAGHANDLE);
await page.waitForSelector(TIPPY_MENU);
await page.click("text=Delete");
await page.locator(H_ONE_BLOCK_SELECTOR).waitFor({ state: "detached" });
});

test("Delete button should delete correct block", async () => {
await executeSlashCommand(page, "h1");
await page.keyboard.type("This is h1");
await executeSlashCommand(page, "h2");
await page.keyboard.type("This is h2");
await executeSlashCommand(page, "h3");
await page.keyboard.type("This is h3");
await page.hover(H_TWO_BLOCK_SELECTOR);
await page.click(DRAGHANDLE);
await page.click("text=Delete");
await page.waitForSelector(H_ONE_BLOCK_SELECTOR);
await page.waitForSelector(H_TWO_BLOCK_SELECTOR, { state: "detached" });
await page.waitForSelector(H_THREE_BLOCK_SELECTOR);
// Compare doc object snapshot
await compareDocToSnapshot(page, "dragHandleDocStructure");
});

test("Deleting block with children should delete all children", async () => {
await page.goto(BASE_URL, { waitUntil: "networkidle" });
await focusOnEditor(page);
await executeSlashCommand(page, "h1");
await page.keyboard.type("This is h1");
await page.keyboard.press("Enter", { delay: 10 });
await page.keyboard.press("Tab", { delay: 10 });
await executeSlashCommand(page, "h2");
await page.keyboard.type("This is h2");
await page.keyboard.press("Enter", { delay: 10 });
await page.keyboard.press("Tab", { delay: 10 });
await executeSlashCommand(page, "h3");
await page.keyboard.type("This is h3");
await page.hover(H_TWO_BLOCK_SELECTOR);
await page.click(DRAGHANDLE);
await page.click("text=Delete");
await page.waitForSelector(H_ONE_BLOCK_SELECTOR);
await page.waitForSelector(H_TWO_BLOCK_SELECTOR, { state: "detached" });
await page.waitForSelector(H_THREE_BLOCK_SELECTOR, { state: "detached" });
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{
"type": "doc",
"content": [
{
"type": "blockGroup",
"content": [
{
"type": "tcblock",
"attrs": {
"headingType": 1
},
"content": [
{
"type": "tccontent",
"attrs": {},
"content": [
{
"type": "text",
"text": "This is h1"
}
]
}
]
},
{
"type": "tcblock",
"attrs": {
"headingType": 3
},
"content": [
{
"type": "tccontent",
"attrs": {},
"content": [
{
"type": "text",
"text": "This is h3"
}
]
}
]
},
{
"type": "tcblock",
"attrs": {},
"content": [
{
"type": "tccontent",
"attrs": {}
}
]
}
]
}
]
}

1 comment on commit afed2c7

@vercel
Copy link

@vercel vercel bot commented on afed2c7 Mar 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.