mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-29 01:58:51 +02:00
editor: only drop a task item into a task list & fix crash
This commit is contained in:
@@ -79,6 +79,7 @@ function dropGapPlugin() {
|
||||
}
|
||||
|
||||
function createGap({ height, indent }: DropGap) {
|
||||
// a task list is made of list items, so the gap is one too
|
||||
const element = document.createElement("li");
|
||||
element.className = DROP_GAP_CLASS;
|
||||
element.contentEditable = "false";
|
||||
@@ -182,8 +183,13 @@ export function startItemDrag(
|
||||
const top = e.clientY + state.offsetY;
|
||||
state.preview.style.transform = `translate3d(0, ${top}px, 0)`;
|
||||
|
||||
// undefined means "leave the gap as it is"; null clears it, so a drop
|
||||
// outside any task list has no target and is cancelled
|
||||
const target = findGap(view, state, e.clientX, top);
|
||||
if (target) setGap(view, (state.gap = target));
|
||||
if (target !== undefined) {
|
||||
state.gap = target ?? undefined;
|
||||
setGap(view, target);
|
||||
}
|
||||
fitPreview(view, state);
|
||||
autoScroll(state, e.clientY);
|
||||
};
|
||||
@@ -259,18 +265,29 @@ function createPreview(view: EditorView, item: HTMLElement, box: DOMRect) {
|
||||
|
||||
/**
|
||||
* Where the item would land: the sibling top edge nearest to the top edge
|
||||
* of the item being dragged. The gap counts as one of those edges, which
|
||||
* is what keeps it in place while the item is over it — moving it would
|
||||
* move everything below it, putting a different edge under the item, and
|
||||
* it would flicker between the two.
|
||||
* of the item being dragged, within the task list under the pointer. The
|
||||
* gap counts as one of those edges, which is what keeps it in place while
|
||||
* the item is over it — moving it would move everything below it, putting a
|
||||
* different edge under the item, and it would flicker between the two.
|
||||
*
|
||||
* 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 —
|
||||
* a task item only drops into a task list, so anywhere else is cancelled.
|
||||
*/
|
||||
function findGap(view: EditorView, drag: Drag, x: number, top: number) {
|
||||
function findGap(
|
||||
view: EditorView,
|
||||
drag: Drag,
|
||||
x: number,
|
||||
top: number
|
||||
): DropGap | null | undefined {
|
||||
const element = document.elementFromPoint(
|
||||
Math.max(x, view.dom.getBoundingClientRect().left + 1),
|
||||
top
|
||||
);
|
||||
const list = element?.closest("ul");
|
||||
if (!list || !view.dom.contains(list)) return drag.gap ?? null;
|
||||
if (!element || !view.dom.contains(element)) return undefined;
|
||||
|
||||
const list = element.closest<HTMLElement>("ul.tasklist-content-wrapper");
|
||||
if (!list || !view.dom.contains(list)) return null;
|
||||
|
||||
let closest: number | null = null;
|
||||
let distance = Infinity;
|
||||
@@ -295,8 +312,9 @@ function findGap(view: EditorView, drag: Drag, x: number, top: number) {
|
||||
if (child === children.at(-1)) consider(box.bottom, pos.after);
|
||||
}
|
||||
|
||||
if (closest === null || (closest > drag.pos && closest < drag.end))
|
||||
return drag.gap ?? null;
|
||||
if (closest === null) return null;
|
||||
// dropping the item into itself is a no-op: leave the gap alone
|
||||
if (closest > drag.pos && closest < drag.end) return undefined;
|
||||
|
||||
const nest = x - drag.startX > NEST_THRESHOLD && canNest(view, closest, drag);
|
||||
return { pos: closest, height: drag.height, indent: nest ? NEST_INDENT : 0 };
|
||||
@@ -340,18 +358,34 @@ function canNest(view: EditorView, pos: number, drag: Drag) {
|
||||
|
||||
/** moves the item at `from` to `to`, returning where it ended up */
|
||||
function moveItem(view: EditorView, from: number, to: number) {
|
||||
const item = view.state.doc.nodeAt(from);
|
||||
const { state } = view;
|
||||
const item = state.doc.nodeAt(from);
|
||||
if (!item) return null;
|
||||
|
||||
const tr = view.state.tr.deleteRange(from, from + item.nodeSize);
|
||||
const at = tr.mapping.map(to);
|
||||
// NOTE: `deleteRange`, not `delete`: taking the only child out of a
|
||||
// nested list leaves the list empty, and an empty list is not valid
|
||||
// content, so it would be filled with a blank item. This takes the list
|
||||
// itself away instead.
|
||||
const tr = state.tr.deleteRange(from, from + item.nodeSize);
|
||||
const at = Math.min(tr.mapping.map(to), tr.doc.content.size);
|
||||
const deleted = tr.doc;
|
||||
|
||||
tr.replaceRangeWith(at, at, item);
|
||||
if (tr.doc.eq(deleted)) return null;
|
||||
if (tr.doc.eq(view.state.doc)) return at;
|
||||
// the target is always a task list, but guard anyway: dropping the item
|
||||
// where it does not fit would put it somewhere unexpected
|
||||
const $at = tr.doc.resolve(at);
|
||||
if (!$at.parent.canReplaceWith($at.index(), $at.index(), item.type))
|
||||
return null;
|
||||
|
||||
tr.replaceRangeWith(at, at, item);
|
||||
// nowhere it fits, or already exactly there
|
||||
if (tr.doc.eq(deleted)) return null;
|
||||
if (tr.doc.eq(state.doc)) return at;
|
||||
|
||||
// select the item, but only if it really landed where we think it did:
|
||||
// NodeSelection throws if there is no node right after `at`
|
||||
const node = tr.doc.resolve(at).nodeAfter;
|
||||
if (node?.type === item.type) tr.setSelection(NodeSelection.create(tr.doc, at)); // prettier-ignore
|
||||
|
||||
tr.setSelection(NodeSelection.create(tr.doc, at));
|
||||
view.dispatch(tr.setMeta("uiEvent", "drop"));
|
||||
return at;
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ export function TaskItemComponent(
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
maxWidth: "95%",
|
||||
maxWidth: "100%",
|
||||
flexGrow: 1
|
||||
}}
|
||||
>
|
||||
@@ -159,7 +159,9 @@ export function TaskItemComponent(
|
||||
sx={{
|
||||
bg: "background",
|
||||
opacity: 0,
|
||||
alignSelf: "flex-start",
|
||||
position: "absolute",
|
||||
insetInlineEnd: 0,
|
||||
top: 0,
|
||||
marginTop: "calc((1lh - 14px) / 2)"
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -245,7 +245,10 @@ export function TaskListComponent(
|
||||
paddingInlineStart: 0,
|
||||
marginBlockStart: isNested ? 10 : 0,
|
||||
marginBlockEnd: 0,
|
||||
marginLeft: isNested ? (editor.isEditable ? -35 : -10) : 0,
|
||||
// NOTE: inline-start, not left: the indent has to be on the
|
||||
// side the text flows from, so it stays on the right in RTL
|
||||
// instead of showing up on the wrong side
|
||||
marginInlineStart: isNested ? (editor.isEditable ? -35 : -10) : 0,
|
||||
padding: 0
|
||||
},
|
||||
li: {
|
||||
|
||||
Reference in New Issue
Block a user