feat: Make navigation looping configurable (#9511)

* feat: Make navigation looping configurable

* chore: Add TODO to clean up API
This commit is contained in:
Aaron Dodson
2025-12-05 14:25:44 -08:00
committed by GitHub
parent f35f4d8aec
commit 8522bfff61
2 changed files with 60 additions and 13 deletions

View File

@@ -181,7 +181,8 @@ function getBlockNavigationCandidates(
* `delta` relative to the current element's stack when navigating backwards.
*/
export function navigateStacks(current: ISelectable, delta: number) {
const stacks: IFocusableNode[] = (current.workspace as WorkspaceSvg)
const workspace = current.workspace as WorkspaceSvg;
const stacks: IFocusableNode[] = workspace
.getTopBoundedElements(true)
.filter((element: IBoundedElement) => isFocusableNode(element));
const currentIndex = stacks.indexOf(
@@ -189,12 +190,15 @@ export function navigateStacks(current: ISelectable, delta: number) {
);
const targetIndex = currentIndex + delta;
let result: IFocusableNode | null = null;
const loop = workspace.getCursor().getNavigationLoops();
if (targetIndex >= 0 && targetIndex < stacks.length) {
result = stacks[targetIndex];
} else if (targetIndex < 0) {
} else if (loop && targetIndex < 0) {
result = stacks[stacks.length - 1];
} else if (targetIndex >= stacks.length) {
} else if (loop && targetIndex >= stacks.length) {
result = stacks[0];
} else {
return null;
}
// When navigating to a previous block stack, our previous sibling is the last

View File

@@ -42,6 +42,9 @@ export class LineCursor extends Marker {
/** Locations to try moving the cursor to after a deletion. */
private potentialNodes: IFocusableNode[] | null = null;
/** Whether or not navigation loops around when reaching the end. */
private navigationLoops = true;
/**
* @param workspace The workspace this cursor belongs to.
*/
@@ -64,7 +67,7 @@ export class LineCursor extends Marker {
const newNode = this.getNextNode(
curNode,
this.getValidationFunction(NavigationDirection.NEXT),
true,
this.getNavigationLoops(),
);
if (newNode) {
@@ -89,7 +92,7 @@ export class LineCursor extends Marker {
const newNode = this.getNextNode(
curNode,
this.getValidationFunction(NavigationDirection.IN),
true,
this.getNavigationLoops(),
);
if (newNode) {
@@ -112,7 +115,7 @@ export class LineCursor extends Marker {
const newNode = this.getPreviousNode(
curNode,
this.getValidationFunction(NavigationDirection.PREVIOUS),
true,
this.getNavigationLoops(),
);
if (newNode) {
@@ -137,7 +140,7 @@ export class LineCursor extends Marker {
const newNode = this.getPreviousNode(
curNode,
this.getValidationFunction(NavigationDirection.OUT),
true,
this.getNavigationLoops(),
);
if (newNode) {
@@ -158,12 +161,12 @@ export class LineCursor extends Marker {
const inNode = this.getNextNode(
curNode,
this.getValidationFunction(NavigationDirection.IN),
true,
this.getNavigationLoops(),
);
const nextNode = this.getNextNode(
curNode,
this.getValidationFunction(NavigationDirection.NEXT),
true,
this.getNavigationLoops(),
);
return inNode === nextNode;
@@ -219,11 +222,22 @@ export class LineCursor extends Marker {
getNextNode(
node: IFocusableNode | null,
isValid: (p1: IFocusableNode | null) => boolean,
// TODO: Consider deprecating and removing this argument.
loop: boolean,
): IFocusableNode | null {
if (!node || (!loop && this.getLastNode() === node)) return null;
const originalLoop = this.getNavigationLoops();
this.setNavigationLoops(loop);
return this.getNextNodeImpl(node, isValid);
let result: IFocusableNode | null;
if (!node || (!loop && this.getLastNode() === node)) {
result = null;
} else {
result = this.getNextNodeImpl(node, isValid);
}
this.setNavigationLoops(originalLoop);
return result;
}
/**
@@ -273,11 +287,22 @@ export class LineCursor extends Marker {
getPreviousNode(
node: IFocusableNode | null,
isValid: (p1: IFocusableNode | null) => boolean,
// TODO: Consider deprecating and removing this argument.
loop: boolean,
): IFocusableNode | null {
if (!node || (!loop && this.getFirstNode() === node)) return null;
const originalLoop = this.getNavigationLoops();
this.setNavigationLoops(loop);
return this.getPreviousNodeImpl(node, isValid);
let result: IFocusableNode | null;
if (!node || (!loop && this.getFirstNode() === node)) {
result = null;
} else {
result = this.getPreviousNodeImpl(node, isValid);
}
this.setNavigationLoops(originalLoop);
return result;
}
/**
@@ -538,6 +563,24 @@ export class LineCursor extends Marker {
const first = this.getFirstNode();
return this.getPreviousNode(first, () => true, true);
}
/**
* Sets whether or not navigation should loop around when reaching the end
* of the workspace.
*
* @param loops True if navigation should loop around, otherwise false.
*/
setNavigationLoops(loops: boolean) {
this.navigationLoops = loops;
}
/**
* Returns whether or not navigation loops around when reaching the end of
* the workspace.
*/
getNavigationLoops(): boolean {
return this.navigationLoops;
}
}
registry.register(registry.Type.CURSOR, registry.DEFAULT, LineCursor);