mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-29 10:09:26 +02:00
editor: drop placeholder on first item on task list should not hide when moving the dragged item above it or above the task list header. it should safely drop as the first list item.
- Improve drag/drop reliability be increasing the hit slop area for starting the drag. - Ensure drag survives between rerenders - Add a solid background to dragged item so it doesn't conflict with items underneath it
This commit is contained in:
@@ -133,7 +133,10 @@ export function startItemDrag(
|
|||||||
if (!editor.isEditable || event.button !== 0 || !item) return;
|
if (!editor.isEditable || event.button !== 0 || !item) return;
|
||||||
|
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
if (event.pointerType === "mouse") event.preventDefault();
|
// the handle has no tap action of its own, so cancelling the default is
|
||||||
|
// safe — and on touch it is what stops the WebView from starting a text
|
||||||
|
// selection instead of the drag
|
||||||
|
if (event.cancelable) event.preventDefault();
|
||||||
|
|
||||||
const { view } = editor;
|
const { view } = editor;
|
||||||
let drag: Drag | undefined;
|
let drag: Drag | undefined;
|
||||||
@@ -185,7 +188,7 @@ export function startItemDrag(
|
|||||||
|
|
||||||
// undefined means "leave the gap as it is"; null clears it, so a drop
|
// undefined means "leave the gap as it is"; null clears it, so a drop
|
||||||
// outside any task list has no target and is cancelled
|
// outside any task list has no target and is cancelled
|
||||||
const target = findGap(view, state, e.clientX, top);
|
const target = findGap(view, state, e.clientX, e.clientY, top);
|
||||||
if (target !== undefined) {
|
if (target !== undefined) {
|
||||||
state.gap = target ?? undefined;
|
state.gap = target ?? undefined;
|
||||||
setGap(view, target);
|
setGap(view, target);
|
||||||
@@ -205,9 +208,12 @@ export function startItemDrag(
|
|||||||
|
|
||||||
const cleanup = () => {
|
const cleanup = () => {
|
||||||
clearTimeout(hold);
|
clearTimeout(hold);
|
||||||
handle.removeEventListener("pointermove", move);
|
// NOTE: on `window`, not the handle. The handle is re-rendered whenever
|
||||||
handle.removeEventListener("pointerup", end);
|
// the gap moves (a decoration change re-renders the node views), and
|
||||||
handle.removeEventListener("pointercancel", cleanup);
|
// listeners on the old element would be lost — the drag would freeze.
|
||||||
|
window.removeEventListener("pointermove", move);
|
||||||
|
window.removeEventListener("pointerup", end);
|
||||||
|
window.removeEventListener("pointercancel", cleanup);
|
||||||
if (!drag) return;
|
if (!drag) return;
|
||||||
cancelAnimationFrame(drag.frame ?? 0);
|
cancelAnimationFrame(drag.frame ?? 0);
|
||||||
drag.preview.remove();
|
drag.preview.remove();
|
||||||
@@ -217,14 +223,26 @@ export function startItemDrag(
|
|||||||
drag = undefined;
|
drag = undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
handle.setPointerCapture?.(event.pointerId);
|
window.addEventListener("pointermove", move, { passive: false });
|
||||||
handle.addEventListener("pointermove", move, { passive: false });
|
window.addEventListener("pointerup", end);
|
||||||
handle.addEventListener("pointerup", end);
|
window.addEventListener("pointercancel", cleanup);
|
||||||
handle.addEventListener("pointercancel", cleanup);
|
|
||||||
if (event.pointerType !== "mouse")
|
if (event.pointerType !== "mouse")
|
||||||
hold = setTimeout(start, HOLD_DELAY) as unknown as number;
|
hold = setTimeout(start, HOLD_DELAY) as unknown as number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** the nearest background colour that is not see-through */
|
||||||
|
function opaqueBackground(element: HTMLElement) {
|
||||||
|
for (
|
||||||
|
let node: HTMLElement | null = element;
|
||||||
|
node;
|
||||||
|
node = node.parentElement
|
||||||
|
) {
|
||||||
|
const bg = getComputedStyle(node).backgroundColor;
|
||||||
|
if (bg && bg !== "transparent" && !bg.startsWith("rgba(0, 0, 0, 0")) return bg; // prettier-ignore
|
||||||
|
}
|
||||||
|
return "var(--background, #fff)";
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A copy of the item that follows the pointer, with its nested items left
|
* A copy of the item that follows the pointer, with its nested items left
|
||||||
* out so that tall items stay easy to place.
|
* out so that tall items stay easy to place.
|
||||||
@@ -237,6 +255,12 @@ function createPreview(view: EditorView, item: HTMLElement, box: DOMRect) {
|
|||||||
preview.style.width = `${box.width}px`;
|
preview.style.width = `${box.width}px`;
|
||||||
preview.style.font = style.font;
|
preview.style.font = style.font;
|
||||||
preview.style.color = style.color;
|
preview.style.color = style.color;
|
||||||
|
// the preview lives on `document.body`, where the editor's theme
|
||||||
|
// variables are not defined, so the CSS `var(--background)` resolves to
|
||||||
|
// transparent — a WebKit issue in particular. Read a concrete colour
|
||||||
|
// while the item is still in the editor, or the checkboxes below show
|
||||||
|
// through it.
|
||||||
|
preview.style.backgroundColor = opaqueBackground(item);
|
||||||
|
|
||||||
const list = (item.parentElement ?? document.createElement("ul")).cloneNode(
|
const list = (item.parentElement ?? document.createElement("ul")).cloneNode(
|
||||||
false
|
false
|
||||||
@@ -273,20 +297,33 @@ function createPreview(view: EditorView, item: HTMLElement, box: DOMRect) {
|
|||||||
* Returns `undefined` to leave the gap where it is (the pointer is over the
|
* Returns `undefined` to leave the gap where it is (the pointer is over the
|
||||||
* item itself, or off the document for a frame), and `null` to clear it —
|
* item itself, or off the document for a frame), and `null` to clear it —
|
||||||
* a task item only drops into a task list, so anywhere else is cancelled.
|
* a task item only drops into a task list, so anywhere else is cancelled.
|
||||||
|
*
|
||||||
|
* The list is found under the pointer (`pointerY`), but the slot within it
|
||||||
|
* from the item's own top edge (`top`). The item's top rises above the list
|
||||||
|
* before the pointer does, so hit testing with the pointer is what lets the
|
||||||
|
* item reach the very first slot.
|
||||||
*/
|
*/
|
||||||
function findGap(
|
function findGap(
|
||||||
view: EditorView,
|
view: EditorView,
|
||||||
drag: Drag,
|
drag: Drag,
|
||||||
x: number,
|
x: number,
|
||||||
|
pointerY: number,
|
||||||
top: number
|
top: number
|
||||||
): DropGap | null | undefined {
|
): DropGap | null | undefined {
|
||||||
const element = document.elementFromPoint(
|
const element = document.elementFromPoint(
|
||||||
Math.max(x, view.dom.getBoundingClientRect().left + 1),
|
Math.max(x, view.dom.getBoundingClientRect().left + 1),
|
||||||
top
|
pointerY
|
||||||
);
|
);
|
||||||
if (!element || !view.dom.contains(element)) return undefined;
|
if (!element || !view.dom.contains(element)) return undefined;
|
||||||
|
|
||||||
const list = element.closest<HTMLElement>("ul.tasklist-content-wrapper");
|
// the list under the pointer, or — when the pointer is on a list's header
|
||||||
|
// (the tools bar sits above the first item, outside the `ul`) — that
|
||||||
|
// list, so the item can still be dropped into its first slot
|
||||||
|
const list =
|
||||||
|
element.closest<HTMLElement>("ul.tasklist-content-wrapper") ||
|
||||||
|
element
|
||||||
|
.closest(".taskList-view-content-wrap")
|
||||||
|
?.querySelector<HTMLElement>("ul.tasklist-content-wrapper");
|
||||||
if (!list || !view.dom.contains(list)) return null;
|
if (!list || !view.dom.contains(list)) return null;
|
||||||
|
|
||||||
let closest: number | null = null;
|
let closest: number | null = null;
|
||||||
|
|||||||
@@ -97,8 +97,20 @@ export function TaskItemComponent(
|
|||||||
// selection or the long press callout
|
// selection or the long press callout
|
||||||
touchAction: "none",
|
touchAction: "none",
|
||||||
userSelect: "none",
|
userSelect: "none",
|
||||||
|
WebkitUserSelect: "none",
|
||||||
WebkitTouchCallout: "none",
|
WebkitTouchCallout: "none",
|
||||||
svg: { pointerEvents: "none" }
|
svg: { pointerEvents: "none" },
|
||||||
|
// hit slop: an invisible box larger than the icon, so a finger
|
||||||
|
// landing near the handle still starts the drag instead of the
|
||||||
|
// browser selecting the text next to it.
|
||||||
|
position: "relative",
|
||||||
|
"::before": {
|
||||||
|
content: '""',
|
||||||
|
position: "absolute",
|
||||||
|
insetBlock: "-12px",
|
||||||
|
insetInlineStart: "-16px",
|
||||||
|
insetInlineEnd: "-2px"
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
size={isMobile ? "2.46ch" : "2.22ch"}
|
size={isMobile ? "2.46ch" : "2.22ch"}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user