Skip to content

Commit cedde35

Browse files
feat: improved insertion flow (#455)
* refactor: create findInsertStartPoint * refactor: pass Navigation into the keyboard drag strategy * refactor: pass block into startMove * refactor: set selected and curNode in startDrag * feat: wire isNewBlock into keyboard drag strategy * feat: monkeypatch dragger as needed to delete on aborted insertion * feat: pass insertStartPoint into the drag strategy after all * feat: make insertion wrap a block immediately * fix: undo deletes the newly insertedblock * fix: reference to getCurrentBlock * chore: fix review feedback * chore: remove unnecessary unpatch method
1 parent 47d67fb commit cedde35

File tree

5 files changed

+195
-99
lines changed

5 files changed

+195
-99
lines changed

src/actions/enter.ts

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -128,30 +128,28 @@ export class EnterAction {
128128
*/
129129
private insertFromFlyout(workspace: WorkspaceSvg) {
130130
workspace.setResizesEnabled(false);
131-
Events.setGroup(true);
131+
// Create a new event group or append to the existing group.
132+
const existingGroup = Events.getGroup();
133+
if (!existingGroup) {
134+
Events.setGroup(true);
135+
}
132136

133137
const stationaryNode = this.navigation.getStationaryNode(workspace);
134138
const newBlock = this.createNewBlock(workspace);
135139
if (!newBlock) return;
136-
if (stationaryNode) {
137-
if (!this.navigation.tryToConnectBlock(stationaryNode, newBlock)) {
138-
console.warn(
139-
'Something went wrong while inserting a block from the flyout.',
140-
);
141-
}
142-
}
143-
140+
const insertStartPoint = stationaryNode
141+
? this.navigation.findInsertStartPoint(stationaryNode, newBlock)
142+
: null;
144143
if (workspace.getTopBlocks().includes(newBlock)) {
145144
this.positionNewTopLevelBlock(workspace, newBlock);
146145
}
147146

148-
Events.setGroup(false);
149147
workspace.setResizesEnabled(true);
150148

151149
this.navigation.focusWorkspace(workspace);
152150
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
153151
workspace.getCursor()?.setCurNode(ASTNode.createBlockNode(newBlock)!);
154-
this.mover.startMove(workspace);
152+
this.mover.startMove(workspace, newBlock, insertStartPoint);
155153

156154
const isStartBlock =
157155
!newBlock.outputConnection &&

src/actions/move.ts

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66

77
import {
8+
BlockSvg,
89
ContextMenuRegistry,
910
ShortcutRegistry,
1011
utils,
@@ -28,8 +29,16 @@ export class MoveActions {
2829
// Begin and end move.
2930
{
3031
name: 'Start move',
31-
preconditionFn: (workspace) => this.mover.canMove(workspace),
32-
callback: (workspace) => this.mover.startMove(workspace),
32+
preconditionFn: (workspace) => {
33+
const startBlock = this.getCurrentBlock(workspace);
34+
return !!startBlock && this.mover.canMove(workspace, startBlock);
35+
},
36+
callback: (workspace) => {
37+
const startBlock = this.getCurrentBlock(workspace);
38+
return (
39+
!!startBlock && this.mover.startMove(workspace, startBlock, null)
40+
);
41+
},
3342
keyCodes: [KeyCodes.M],
3443
},
3544
{
@@ -131,12 +140,19 @@ export class MoveActions {
131140
const workspace = scope.block?.workspace as WorkspaceSvg | null;
132141
if (!workspace || menuOpenEvent instanceof PointerEvent)
133142
return 'hidden';
134-
return this.mover.canMove(workspace) ? 'enabled' : 'disabled';
143+
144+
const startBlock = this.getCurrentBlock(workspace);
145+
return !!startBlock && this.mover.canMove(workspace, startBlock)
146+
? 'enabled'
147+
: 'disabled';
135148
},
136149
callback: (scope) => {
137150
const workspace = scope.block?.workspace as WorkspaceSvg | null;
138151
if (!workspace) return false;
139-
this.mover.startMove(workspace);
152+
const startBlock = this.getCurrentBlock(workspace);
153+
return (
154+
!!startBlock && this.mover.startMove(workspace, startBlock, null)
155+
);
140156
},
141157
scopeType: ContextMenuRegistry.ScopeType.BLOCK,
142158
id: 'move',
@@ -168,4 +184,30 @@ export class MoveActions {
168184
ContextMenuRegistry.registry.unregister(menuItem.id);
169185
}
170186
}
187+
188+
/**
189+
* Get the source block for the cursor location, or undefined if no
190+
* source block can be found.
191+
* If the cursor is on a shadow block, walks up the tree until it finds
192+
* a non-shadow block to drag.
193+
*
194+
* @param workspace The workspace to inspect for a cursor.
195+
* @returns The source block, or undefined if no appropriate block
196+
* could be found.
197+
*/
198+
getCurrentBlock(workspace: WorkspaceSvg): BlockSvg | undefined {
199+
const curNode = workspace?.getCursor()?.getCurNode();
200+
let block = curNode?.getSourceBlock();
201+
if (!block) return undefined;
202+
while (block?.isShadow()) {
203+
block = block.getParent();
204+
if (!block) {
205+
throw new Error(
206+
'Tried to drag a shadow block with no parent. ' +
207+
'Shadow blocks should always have parents.',
208+
);
209+
}
210+
}
211+
return block as BlockSvg;
212+
}
171213
}

src/actions/mover.ts

Lines changed: 70 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,17 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7-
import type {BlockSvg, IDragger, IDragStrategy, Gesture} from 'blockly';
7+
import type {
8+
BlockSvg,
9+
IDragger,
10+
IDragStrategy,
11+
Gesture,
12+
RenderedConnection,
13+
} from 'blockly';
814
import {
915
ASTNode,
10-
common,
1116
Connection,
17+
dragging,
1218
registry,
1319
utils,
1420
WorkspaceSvg,
@@ -63,11 +69,10 @@ export class Mover {
6369
* currently has focus on the given workspace.
6470
*
6571
* @param workspace The workspace to move on.
72+
* @param block The block to try to drag.
6673
* @returns True iff we can begin a move.
6774
*/
68-
canMove(workspace: WorkspaceSvg) {
69-
const block = this.getCurrentBlock(workspace);
70-
75+
canMove(workspace: WorkspaceSvg, block: BlockSvg) {
7176
return !!(
7277
this.navigation.getState(workspace) === Constants.STATE.WORKSPACE &&
7378
this.navigation.canCurrentlyEdit(workspace) &&
@@ -96,19 +101,18 @@ export class Mover {
96101
* Should only be called if canMove has returned true.
97102
*
98103
* @param workspace The workspace we might be moving on.
104+
* @param block The block to start dragging.
105+
* @param insertStartPoint Where to insert the block, or null if the block
106+
* already existed.
99107
* @returns True iff a move has successfully begun.
100108
*/
101-
startMove(workspace: WorkspaceSvg) {
102-
const cursor = workspace?.getCursor();
103-
const block = this.getCurrentBlock(workspace);
104-
if (!cursor || !block) throw new Error('precondition failure');
105-
106-
// Select and focus block.
107-
common.setSelected(block);
108-
cursor.setCurNode(ASTNode.createBlockNode(block));
109-
109+
startMove(
110+
workspace: WorkspaceSvg,
111+
block: BlockSvg,
112+
insertStartPoint: RenderedConnection | null,
113+
) {
110114
this.patchWorkspace(workspace);
111-
this.patchDragStrategy(block);
115+
this.patchDragStrategy(block, insertStartPoint);
112116
// Begin dragging block.
113117
const DraggerClass = registry.getClassFromOptions(
114118
registry.Type.BLOCK_DRAGGER,
@@ -167,19 +171,34 @@ export class Mover {
167171
const info = this.moves.get(workspace);
168172
if (!info) throw new Error('no move info for workspace');
169173

170-
// Monkey patch dragger to trigger call to draggable.revertDrag.
171-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
172-
(info.dragger as any).shouldReturnToStart = () => true;
173-
const blockSvg = info.block;
174+
const dragStrategy = info.block.getDragStrategy() as KeyboardDragStrategy;
175+
this.patchDragger(
176+
info.dragger as dragging.Dragger,
177+
dragStrategy.isNewBlock,
178+
);
174179

175180
// Explicitly call `hidePreview` because it is not called in revertDrag.
176-
// @ts-expect-error Access to private property dragStrategy.
177-
blockSvg.dragStrategy.connectionPreviewer.hidePreview();
181+
// @ts-expect-error Access to private property connectionPreviewer.
182+
dragStrategy.connectionPreviewer.hidePreview();
183+
184+
// Save the position so we can put the cursor in a reasonable spot.
185+
// @ts-expect-error Access to private property connectionCandidate.
186+
const target = dragStrategy.connectionCandidate?.neighbour;
187+
188+
// Prevent the stragegy connecting the block so we just delete one block.
189+
// @ts-expect-error Access to private property connectionCandidate.
190+
dragStrategy.connectionCandidate = null;
191+
178192
info.dragger.onDragEnd(
179193
info.fakePointerEvent('pointerup'),
180-
new utils.Coordinate(0, 0),
194+
info.startLocation,
181195
);
182196

197+
if (dragStrategy.isNewBlock && target) {
198+
const newNode = ASTNode.createConnectionNode(target);
199+
if (newNode) workspace.getCursor()?.setCurNode(newNode);
200+
}
201+
183202
this.unpatchWorkspace(workspace);
184203
this.unpatchDragStrategy(info.block);
185204
this.moves.delete(workspace);
@@ -233,32 +252,6 @@ export class Mover {
233252
return true;
234253
}
235254

236-
/**
237-
* Get the source block for the cursor location, or undefined if no
238-
* source block can be found.
239-
* If the cursor is on a shadow block, walks up the tree until it finds
240-
* a non-shadow block to drag.
241-
*
242-
* @param workspace The workspace to inspect for a cursor.
243-
* @returns The source block, or undefined if no appropriate block
244-
* could be found.
245-
*/
246-
protected getCurrentBlock(workspace: WorkspaceSvg): BlockSvg | undefined {
247-
const curNode = workspace?.getCursor()?.getCurNode();
248-
let block = curNode?.getSourceBlock();
249-
if (!block) return undefined;
250-
while (block && block.isShadow()) {
251-
block = block.getParent();
252-
if (!block) {
253-
throw new Error(
254-
'Tried to drag a shadow block with no parent. ' +
255-
'Shadow blocks should always have parents.',
256-
);
257-
}
258-
}
259-
return block as BlockSvg;
260-
}
261-
262255
/**
263256
* Monkeypatch over workspace functions to consider keyboard drags as
264257
* well as mouse/pointer drags.
@@ -297,15 +290,21 @@ export class Mover {
297290
(workspace as any).getGesture = this.oldGetGesture;
298291
}
299292
}
293+
300294
/**
301295
* Monkeypatch: replace the block's drag strategy and cache the old value.
302296
*
303297
* @param block The block to patch.
298+
* @param insertStartPoint Where to insert the block, or null if the block
299+
* already existed.
304300
*/
305-
private patchDragStrategy(block: BlockSvg) {
301+
private patchDragStrategy(
302+
block: BlockSvg,
303+
insertStartPoint: RenderedConnection | null,
304+
) {
306305
// @ts-expect-error block.dragStrategy is private.
307306
this.oldDragStrategy = block.dragStrategy;
308-
block.setDragStrategy(new KeyboardDragStrategy(block));
307+
block.setDragStrategy(new KeyboardDragStrategy(block, insertStartPoint));
309308
}
310309

311310
/**
@@ -328,14 +327,34 @@ export class Mover {
328327
* the workspace's viewport.
329328
*/
330329
private scrollCurrentBlockIntoView(workspace: WorkspaceSvg, padding = 10) {
331-
const blockToView = this.getCurrentBlock(workspace);
330+
const blockToView = this.moves.get(workspace)?.block;
332331
if (blockToView) {
333332
workspace.scrollBoundsIntoView(
334333
blockToView.getBoundingRectangleWithoutChildren(),
335334
padding,
336335
);
337336
}
338337
}
338+
339+
/**
340+
* Monkeypatch: override either wouldDeleteDraggable or shouldReturnToStart,
341+
* based on whether this was an insertion of a new block or a movement of
342+
* an existing block.
343+
*
344+
* @param dragger The dragger to patch.
345+
* @param isNewBlock Whether the moving block was created for this action.
346+
*/
347+
private patchDragger(dragger: dragging.Dragger, isNewBlock: boolean) {
348+
if (isNewBlock) {
349+
// Monkey patch dragger to trigger delete.
350+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
351+
(dragger as any).wouldDeleteDraggable = () => true;
352+
} else {
353+
// Monkey patch dragger to trigger call to draggable.revertDrag.
354+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
355+
(dragger as any).shouldReturnToStart = () => true;
356+
}
357+
}
339358
}
340359

341360
/**

0 commit comments

Comments
 (0)