Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 85 additions & 11 deletions src/cm/touchSelectionMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,33 @@ export function filterSelectionMenuItems(items, options) {
});
}

/**
* Detect which edge(s) should trigger drag auto-scroll.
* @param {{
* x:number,
* y:number,
* rect:{left:number,right:number,top:number,bottom:number},
* allowHorizontal?:boolean,
* gap?:number,
* }} options
* @returns {{horizontal:number, vertical:number}}
*/
export function getEdgeScrollDirections(options) {
const { x, y, rect, allowHorizontal = true, gap = EDGE_SCROLL_GAP } = options;
let horizontal = 0;
let vertical = 0;

if (allowHorizontal) {
if (x < rect.left + gap) horizontal = -1;
else if (x > rect.right - gap) horizontal = 1;
}

if (y < rect.top + gap) vertical = -1;
else if (y > rect.bottom - gap) vertical = 1;

return { horizontal, vertical };
}

function clamp(value, min, max) {
return Math.max(min, Math.min(max, value));
}
Expand Down Expand Up @@ -1014,7 +1041,8 @@ class TouchSelectionMenuController {
startX: x,
startY: y,
moved: false,
direction: 0,
scrollX: 0,
scrollY: 0,
fixedPos:
type === "start" ? range.to : type === "end" ? range.from : null,
};
Expand Down Expand Up @@ -1098,28 +1126,73 @@ class TouchSelectionMenuController {
this.#view.focus();
}

#getAutoScrollDelta(x, y) {
const scroller = this.#view.scrollDOM;
const rect = scroller.getBoundingClientRect();
const { horizontal, vertical } = getEdgeScrollDirections({
x,
y,
rect,
allowHorizontal: !this.#view.lineWrapping,
});
const maxScrollLeft = Math.max(
0,
scroller.scrollWidth - scroller.clientWidth,
);
const maxScrollTop = Math.max(
0,
scroller.scrollHeight - scroller.clientHeight,
);
let scrollX = horizontal * EDGE_SCROLL_STEP;
let scrollY = vertical * EDGE_SCROLL_STEP;

if (
(scrollX < 0 && scroller.scrollLeft <= 0) ||
(scrollX > 0 && scroller.scrollLeft >= maxScrollLeft)
) {
scrollX = 0;
}

if (
(scrollY < 0 && scroller.scrollTop <= 0) ||
(scrollY > 0 && scroller.scrollTop >= maxScrollTop)
) {
scrollY = 0;
}

return { scrollX, scrollY };
}

#startAutoScrollIfNeeded(x, y) {
const rect = this.#view.scrollDOM.getBoundingClientRect();
let direction = 0;
if (y < rect.top + EDGE_SCROLL_GAP) direction = -1;
if (y > rect.bottom - EDGE_SCROLL_GAP) direction = 1;
const { scrollX, scrollY } = this.#getAutoScrollDelta(x, y);
if (this.#dragState) {
this.#dragState.scrollX = scrollX;
this.#dragState.scrollY = scrollY;
}

if (!direction) {
if (!scrollX && !scrollY) {
this.#stopAutoScroll();
return;
}

this.#dragState.direction = direction;
if (this.#autoScrollRaf) return;

const tick = () => {
if (!this.#dragState?.direction) {
if (!this.#dragState) {
this.#autoScrollRaf = 0;
return;
}

const delta = this.#getAutoScrollDelta(this.#pointer.x, this.#pointer.y);
this.#dragState.scrollX = delta.scrollX;
this.#dragState.scrollY = delta.scrollY;
if (!delta.scrollX && !delta.scrollY) {
this.#autoScrollRaf = 0;
return;
}

this.#view.scrollDOM.scrollTop +=
this.#dragState.direction * EDGE_SCROLL_STEP;
this.#view.scrollDOM.scrollLeft += delta.scrollX;
this.#view.scrollDOM.scrollTop += delta.scrollY;
this.#dragTo(this.#pointer.x, this.#pointer.y);
this.#autoScrollRaf = requestAnimationFrame(tick);
};
Expand All @@ -1131,7 +1204,8 @@ class TouchSelectionMenuController {
cancelAnimationFrame(this.#autoScrollRaf);
this.#autoScrollRaf = 0;
if (this.#dragState) {
this.#dragState.direction = 0;
this.#dragState.scrollX = 0;
this.#dragState.scrollY = 0;
}
}

Expand Down
37 changes: 37 additions & 0 deletions src/test/editor.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { highlightSelectionMatches, searchKeymap } from "@codemirror/search";
import { EditorSelection, EditorState } from "@codemirror/state";
import { EditorView } from "@codemirror/view";
import createBaseExtensions from "cm/baseExtensions";
import { getEdgeScrollDirections } from "cm/touchSelectionMenu";
import { TestRunner } from "./tester";

export async function runCodeMirrorTests(writeOutput) {
Expand Down Expand Up @@ -700,6 +701,42 @@ export async function runCodeMirrorTests(writeOutput) {
});
});

runner.test("Edge scroll direction helper", async (test) => {
const rect = {
left: 100,
right: 300,
top: 200,
bottom: 400,
};

const leftTop = getEdgeScrollDirections({
x: 110,
y: 210,
rect,
allowHorizontal: true,
});
test.assertEqual(leftTop.horizontal, -1);
test.assertEqual(leftTop.vertical, -1);

const rightBottom = getEdgeScrollDirections({
x: 295,
y: 395,
rect,
allowHorizontal: true,
});
test.assertEqual(rightBottom.horizontal, 1);
test.assertEqual(rightBottom.vertical, 1);

const noHorizontal = getEdgeScrollDirections({
x: 110,
y: 395,
rect,
allowHorizontal: false,
});
test.assertEqual(noHorizontal.horizontal, 0);
test.assertEqual(noHorizontal.vertical, 1);
});

runner.test("lineBlockAt", async (test) => {
await withEditor(test, async (view) => {
view.dispatch({
Expand Down