Skip to content

Commit e603c67

Browse files
authored
fix: reenable support for checkboxes in the flyout (#43)
* fix: reenable support for checkboxes in the flyout * refactor: use a map instead of an object for storing checkboxes * chore: remove debugging code * refactor: improve variable names for checkbox position * chore: fix line wrapping indentation * refactor: don't store checkbox wrapper objects on blocks
1 parent 4f97982 commit e603c67

File tree

7 files changed

+221
-15
lines changed

7 files changed

+221
-15
lines changed

blocks_vertical/data.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@ Blockly.Blocks['data_variable'] = {
4242
}
4343
],
4444
"category": Categories.data,
45-
"checkboxInFlyout": true,
4645
"extensions": ["contextMenu_getVariableBlock", "colours_data", "output_string"]
4746
});
47+
this.checkboxInFlyout = true;
4848
}
4949
};
5050

@@ -158,8 +158,8 @@ Blockly.Blocks['data_listcontents'] = {
158158
],
159159
"category": Categories.dataLists,
160160
"extensions": ["contextMenu_getListBlock", "colours_data_lists", "output_string"],
161-
"checkboxInFlyout": true
162161
});
162+
this.checkboxInFlyout = true;
163163
}
164164
};
165165

blocks_vertical/looks.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -282,9 +282,9 @@ Blockly.Blocks['looks_size'] = {
282282
this.jsonInit({
283283
"message0": Blockly.Msg.LOOKS_SIZE,
284284
"category": Categories.looks,
285-
"checkboxInFlyout": true,
286285
"extensions": ["colours_looks", "output_number"]
287286
});
287+
this.checkboxInFlyout = true;
288288
}
289289
};
290290

@@ -510,9 +510,9 @@ Blockly.Blocks['looks_backdropnumbername'] = {
510510
}
511511
],
512512
"category": Categories.looks,
513-
"checkboxInFlyout": true,
514513
"extensions": ["colours_looks", "output_number"]
515514
});
515+
this.checkboxInFlyout = true;
516516
}
517517
};
518518

@@ -535,9 +535,9 @@ Blockly.Blocks['looks_costumenumbername'] = {
535535
}
536536
],
537537
"category": Categories.looks,
538-
"checkboxInFlyout": true,
539538
"extensions": ["colours_looks", "output_number"]
540539
});
540+
this.checkboxInFlyout = true;
541541
}
542542
};
543543

blocks_vertical/motion.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -429,9 +429,9 @@ Blockly.Blocks['motion_xposition'] = {
429429
this.jsonInit({
430430
"message0": Blockly.Msg.MOTION_XPOSITION,
431431
"category": Categories.motion,
432-
"checkboxInFlyout": true,
433432
"extensions": ["colours_motion", "output_number"]
434433
});
434+
this.checkboxInFlyout = true;
435435
}
436436
};
437437

@@ -444,9 +444,9 @@ Blockly.Blocks['motion_yposition'] = {
444444
this.jsonInit({
445445
"message0": Blockly.Msg.MOTION_YPOSITION,
446446
"category": Categories.motion,
447-
"checkboxInFlyout": true,
448447
"extensions": ["colours_motion", "output_number"]
449448
});
449+
this.checkboxInFlyout = true;
450450
}
451451
};
452452

@@ -459,9 +459,9 @@ Blockly.Blocks['motion_direction'] = {
459459
this.jsonInit({
460460
"message0": Blockly.Msg.MOTION_DIRECTION,
461461
"category": Categories.motion,
462-
"checkboxInFlyout": true,
463462
"extensions": ["colours_motion", "output_number"]
464463
});
464+
this.checkboxInFlyout = true;
465465
}
466466
};
467467

blocks_vertical/sensing.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -179,9 +179,9 @@ Blockly.Blocks['sensing_answer'] = {
179179
this.jsonInit({
180180
"message0": Blockly.Msg.SENSING_ANSWER,
181181
"category": Categories.sensing,
182-
"checkboxInFlyout": true,
183182
"extensions": ["colours_sensing", "output_number"]
184183
});
184+
this.checkboxInFlyout = true;
185185
}
186186
};
187187

@@ -343,9 +343,9 @@ Blockly.Blocks['sensing_loudness'] = {
343343
this.jsonInit({
344344
"message0": Blockly.Msg.SENSING_LOUDNESS,
345345
"category": Categories.sensing,
346-
"checkboxInFlyout": true,
347346
"extensions": ["colours_sensing", "output_number"]
348347
});
348+
this.checkboxInFlyout = true;
349349
}
350350
};
351351

@@ -374,9 +374,9 @@ Blockly.Blocks['sensing_timer'] = {
374374
this.jsonInit({
375375
"message0": Blockly.Msg.SENSING_TIMER,
376376
"category": Categories.sensing,
377-
"checkboxInFlyout": true,
378377
"extensions": ["colours_sensing", "output_number"]
379378
});
379+
this.checkboxInFlyout = true;
380380
}
381381
};
382382

@@ -480,9 +480,9 @@ Blockly.Blocks['sensing_current'] = {
480480
}
481481
],
482482
"category": Categories.sensing,
483-
"checkboxInFlyout": true,
484483
"extensions": ["colours_sensing", "output_number"]
485484
});
485+
this.checkboxInFlyout = true;
486486
}
487487
};
488488

@@ -509,9 +509,9 @@ Blockly.Blocks['sensing_username'] = {
509509
this.jsonInit({
510510
"message0": Blockly.Msg.SENSING_USERNAME,
511511
"category": Categories.sensing,
512-
"checkboxInFlyout": true,
513512
"extensions": ["colours_sensing", "output_number"]
514513
});
514+
this.checkboxInFlyout = true;
515515
}
516516
};
517517

blocks_vertical/sound.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,5 +205,6 @@ Blockly.Blocks['sound_volume'] = {
205205
"checkboxInFlyout": true,
206206
"extensions": ["colours_sounds", "output_number"]
207207
});
208+
this.checkboxInFlyout = true;
208209
}
209210
};

src/checkable_continuous_flyout.js

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
import * as Blockly from 'blockly';
2+
import {ContinuousFlyout} from '@blockly/continuous-toolbox';
3+
4+
export class CheckableContinuousFlyout extends ContinuousFlyout {
5+
/**
6+
* Size of a checkbox next to a variable reporter.
7+
* @type {number}
8+
* @const
9+
*/
10+
static CHECKBOX_SIZE = 25;
11+
12+
/**
13+
* Amount of touchable padding around reporter checkboxes.
14+
* @type {number}
15+
* @const
16+
*/
17+
static CHECKBOX_TOUCH_PADDING = 12;
18+
19+
/**
20+
* SVG path data for checkmark in checkbox.
21+
* @type {string}
22+
* @const
23+
*/
24+
static CHECKMARK_PATH =
25+
'M' + CheckableContinuousFlyout.CHECKBOX_SIZE / 4 +
26+
' ' + CheckableContinuousFlyout.CHECKBOX_SIZE / 2 +
27+
'L' + 5 * CheckableContinuousFlyout.CHECKBOX_SIZE / 12 +
28+
' ' + 2 * CheckableContinuousFlyout.CHECKBOX_SIZE / 3 +
29+
'L' + 3 * CheckableContinuousFlyout.CHECKBOX_SIZE / 4 +
30+
' ' + CheckableContinuousFlyout.CHECKBOX_SIZE / 3;
31+
32+
/**
33+
* Size of the checkbox corner radius
34+
* @type {number}
35+
* @const
36+
*/
37+
static CHECKBOX_CORNER_RADIUS = 5;
38+
39+
/**
40+
* @type {number}
41+
* @const
42+
*/
43+
static CHECKBOX_MARGIN = ContinuousFlyout.prototype.MARGIN;
44+
45+
/**
46+
* Total additional width of a row that contains a checkbox.
47+
* @type {number}
48+
* @const
49+
*/
50+
static CHECKBOX_SPACE_X =
51+
CheckableContinuousFlyout.CHECKBOX_SIZE +
52+
2 * CheckableContinuousFlyout.CHECKBOX_MARGIN;
53+
54+
55+
constructor(workspaceOptions) {
56+
super(workspaceOptions);
57+
CheckableContinuousFlyout.CHECKBOX_MARGIN = this.MARGIN;
58+
59+
/**
60+
* Map of checkboxes that correspond to monitored blocks.
61+
* Each element is an object containing the SVG for the checkbox, a boolean
62+
* for its checked state, and the block the checkbox is associated with.
63+
* @type {!Object.<string, !Object>}
64+
* @private
65+
*/
66+
this.checkboxes_ = new Map();
67+
}
68+
69+
show(flyoutDef) {
70+
this.clearOldCheckboxes();
71+
super.show(flyoutDef);
72+
}
73+
74+
clearOldCheckboxes() {
75+
for (const checkbox of this.checkboxes_.values()) {
76+
checkbox.svgRoot.remove();
77+
}
78+
this.checkboxes_.clear();
79+
}
80+
81+
addBlockListeners_(root, block, rect) {
82+
if (block.checkboxInFlyout) {
83+
const coordinates = block.getRelativeToSurfaceXY();
84+
const checkbox = this.createCheckbox_(
85+
block, coordinates.x, coordinates.y, block.getHeightWidth());
86+
let moveX = coordinates.x;
87+
if (this.RTL) {
88+
moveX -= (CheckableContinuousFlyout.CHECKBOX_SIZE + CheckableContinuousFlyout.CHECKBOX_MARGIN);
89+
} else {
90+
moveX += CheckableContinuousFlyout.CHECKBOX_SIZE + CheckableContinuousFlyout.CHECKBOX_MARGIN;
91+
}
92+
block.moveBy(moveX, 0);
93+
this.listeners.push(Blockly.browserEvents.bind(checkbox.svgRoot,
94+
'mousedown', null, this.checkboxClicked_(checkbox)));
95+
}
96+
super.addBlockListeners_(root, block, rect);
97+
}
98+
99+
/**
100+
* Respond to a click on a checkbox in the flyout.
101+
* @param {!Object} checkboxObj An object containing the svg element of the
102+
* checkbox, a boolean for the state of the checkbox, and the block the
103+
* checkbox is associated with.
104+
* @return {!Function} Function to call when checkbox is clicked.
105+
* @private
106+
*/
107+
checkboxClicked_(checkboxObj) {
108+
return function(e) {
109+
this.setCheckboxState(checkboxObj.block.id, !checkboxObj.clicked);
110+
// This event has been handled. No need to bubble up to the document.
111+
e.stopPropagation();
112+
e.preventDefault();
113+
}.bind(this);
114+
}
115+
116+
/**
117+
* Create and place a checkbox corresponding to the given block.
118+
* @param {!Blockly.Block} block The block to associate the checkbox to.
119+
* @param {number} cursorX The x position of the cursor during this layout pass.
120+
* @param {number} cursorY The y position of the cursor during this layout pass.
121+
* @param {!{height: number, width: number}} blockHW The height and width of the
122+
* block.
123+
* @private
124+
*/
125+
createCheckbox_(block, cursorX, cursorY, blockHW) {
126+
var checkboxState = this.getCheckboxState(block.id);
127+
var svgRoot = block.getSvgRoot();
128+
var extraSpace = CheckableContinuousFlyout.CHECKBOX_SIZE + CheckableContinuousFlyout.CHECKBOX_MARGIN;
129+
var xOffset = this.RTL ? this.getWidth() / this.workspace_.scale - extraSpace : cursorX;
130+
var yOffset = cursorY + blockHW.height / 2 - CheckableContinuousFlyout.CHECKBOX_SIZE / 2;
131+
var touchMargin = CheckableContinuousFlyout.CHECKBOX_TOUCH_PADDING;
132+
var checkboxGroup = Blockly.utils.dom.createSvgElement('g',
133+
{
134+
'transform': `translate(${xOffset}, ${yOffset})`,
135+
'fill': 'transparent',
136+
}, null);
137+
Blockly.utils.dom.createSvgElement('rect',
138+
{
139+
'class': 'blocklyFlyoutCheckbox',
140+
'height': CheckableContinuousFlyout.CHECKBOX_SIZE,
141+
'width': CheckableContinuousFlyout.CHECKBOX_SIZE,
142+
'rx': CheckableContinuousFlyout.CHECKBOX_CORNER_RADIUS,
143+
'ry': CheckableContinuousFlyout.CHECKBOX_CORNER_RADIUS
144+
}, checkboxGroup);
145+
Blockly.utils.dom.createSvgElement('path',
146+
{
147+
'class': 'blocklyFlyoutCheckboxPath',
148+
'd': CheckableContinuousFlyout.CHECKMARK_PATH
149+
}, checkboxGroup);
150+
Blockly.utils.dom.createSvgElement('rect',
151+
{
152+
'class': 'blocklyTouchTargetBackground',
153+
'x': -touchMargin + 'px',
154+
'y': -touchMargin + 'px',
155+
'height': CheckableContinuousFlyout.CHECKBOX_SIZE + 2 * touchMargin,
156+
'width': CheckableContinuousFlyout.CHECKBOX_SIZE + 2 * touchMargin,
157+
}, checkboxGroup);
158+
var checkboxObj = {svgRoot: checkboxGroup, clicked: checkboxState, block: block};
159+
160+
if (checkboxState) {
161+
Blockly.utils.dom.addClass((checkboxObj.svgRoot), 'checked');
162+
}
163+
164+
this.workspace_.getCanvas().insertBefore(checkboxGroup, svgRoot);
165+
this.checkboxes_.set(block.id, checkboxObj);
166+
return checkboxObj;
167+
}
168+
169+
/**
170+
* Set the state of a checkbox by block ID.
171+
* @param {string} blockId ID of the block whose checkbox should be set
172+
* @param {boolean} value Value to set the checkbox to.
173+
* @public
174+
*/
175+
setCheckboxState(blockId, value) {
176+
var checkboxObj = this.checkboxes_.get(blockId);
177+
if (!checkboxObj || checkboxObj.clicked === value) {
178+
return;
179+
}
180+
181+
var oldValue = checkboxObj.clicked;
182+
checkboxObj.clicked = value;
183+
184+
if (checkboxObj.clicked) {
185+
Blockly.utils.dom.addClass(checkboxObj.svgRoot, 'checked');
186+
} else {
187+
Blockly.utils.dom.removeClass(checkboxObj.svgRoot, 'checked');
188+
}
189+
190+
Blockly.Events.fire(new Blockly.Events.BlockChange(
191+
checkboxObj.block, 'checkbox', null, oldValue, value));
192+
}
193+
194+
/**
195+
* Gets the checkbox state for a block
196+
* @param {string} blockId The ID of the block in question.
197+
* @return {boolean} Whether the block is checked.
198+
* @public
199+
*/
200+
getCheckboxState() {
201+
// Patched by scratch-gui in src/lib/blocks.js.
202+
return false;
203+
}
204+
}

src/index.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {
2525
ContinuousFlyout,
2626
ContinuousMetrics,
2727
} from '@blockly/continuous-toolbox';
28-
28+
import {CheckableContinuousFlyout} from './checkable_continuous_flyout.js';
2929
import './scratch_continuous_category.js';
3030

3131
export * from 'blockly';
@@ -35,12 +35,13 @@ export * from '../core/colours.js';
3535
export * from '../core/field_colour_slider.js';
3636
export * from '../msg/scratch_msgs.js';
3737
export {scratchBlocksUtils};
38+
export {CheckableContinuousFlyout};
3839

3940
export function inject(container, options) {
4041
Object.assign(options, {
4142
plugins: {
4243
toolbox: ContinuousToolbox,
43-
flyoutsVerticalToolbox: ContinuousFlyout,
44+
flyoutsVerticalToolbox: CheckableContinuousFlyout,
4445
metricsManager: ContinuousMetrics,
4546
},
4647
});

0 commit comments

Comments
 (0)