-
Notifications
You must be signed in to change notification settings - Fork 3.8k
Expand file tree
/
Copy pathcommon.ts
More file actions
347 lines (319 loc) · 9.85 KB
/
common.ts
File metadata and controls
347 lines (319 loc) · 9.85 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
/**
* @license
* Copyright 2021 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
// Former goog.module ID: Blockly.common
import type {Block} from './block.js';
import {BlockDefinition, Blocks} from './blocks.js';
import * as browserEvents from './browser_events.js';
import type {Connection} from './connection.js';
import {EventType} from './events/type.js';
import * as eventUtils from './events/utils.js';
import {getFocusManager} from './focus_manager.js';
import {ISelectable, isSelectable} from './interfaces/i_selectable.js';
import {ShortcutRegistry} from './shortcut_registry.js';
import type {Workspace} from './workspace.js';
import type {WorkspaceSvg} from './workspace_svg.js';
/** Database of all workspaces. */
const WorkspaceDB_ = Object.create(null);
/**
* Find the workspace with the specified ID.
*
* @param id ID of workspace to find.
* @returns The sought after workspace or null if not found.
*/
export function getWorkspaceById(id: string): Workspace | null {
return WorkspaceDB_[id] || null;
}
/**
* Find all workspaces.
*
* @returns Array of workspaces.
*/
export function getAllWorkspaces(): Workspace[] {
const workspaces = [];
for (const workspaceId in WorkspaceDB_) {
workspaces.push(WorkspaceDB_[workspaceId]);
}
return workspaces;
}
/**
* Register a workspace in the workspace db.
*
* @param workspace
*/
export function registerWorkspace(workspace: Workspace) {
WorkspaceDB_[workspace.id] = workspace;
}
/**
* Unregister a workspace from the workspace db.
*
* @param workspace
*/
export function unregisterWorkspace(workspace: Workspace) {
delete WorkspaceDB_[workspace.id];
}
/**
* The main workspace most recently used.
* Set by Blockly.WorkspaceSvg.prototype.markFocused
*/
let mainWorkspace: Workspace;
/**
* Returns the last used top level workspace (based on focus). Try not to use
* this function, particularly if there are multiple Blockly instances on a
* page.
*
* @returns The main workspace.
*/
export function getMainWorkspace(): Workspace {
return mainWorkspace;
}
/**
* Sets last used main workspace.
*
* @param workspace The most recently used top level workspace.
*/
export function setMainWorkspace(workspace: Workspace) {
mainWorkspace = workspace;
}
/**
* Returns the current selection.
*/
export function getSelected(): ISelectable | null {
const focused = getFocusManager().getFocusedNode();
if (focused && isSelectable(focused)) return focused;
return null;
}
/**
* Sets the current selection.
*
* To clear the current selection, select another ISelectable or focus a
* non-selectable (like the workspace root node).
*
* @param newSelection The new selection to make.
* @internal
*/
export function setSelected(newSelection: ISelectable) {
getFocusManager().focusNode(newSelection);
}
/**
* Fires a selection change event based on the new selection.
*
* This is only expected to be called by ISelectable implementations and should
* always be called before updating the current selection state. It does not
* change focus or selection state.
*
* @param newSelection The new selection.
* @internal
*/
export function fireSelectedEvent(newSelection: ISelectable | null) {
const selected = getSelected();
const event = new (eventUtils.get(EventType.SELECTED))(
selected?.id ?? null,
newSelection?.id ?? null,
newSelection?.workspace.id ?? selected?.workspace.id ?? '',
);
eventUtils.fire(event);
}
/**
* Container element in which to render the WidgetDiv, DropDownDiv and Tooltip.
*/
let parentContainer: Element | null;
/**
* Get the container element in which to render the WidgetDiv, DropDownDiv and
* Tooltip.
*
* @returns The parent container.
*/
export function getParentContainer(): Element | null {
return parentContainer;
}
/**
* Set the parent container. This is the container element that the WidgetDiv,
* DropDownDiv, and Tooltip are rendered into the first time `Blockly.inject`
* is called.
* This method is a NOP if called after the first `Blockly.inject`.
*
* @param newParent The container element.
*/
export function setParentContainer(newParent: Element) {
parentContainer = newParent;
}
/**
* Size the SVG image to completely fill its container. Call this when the view
* actually changes sizes (e.g. on a window resize/device orientation change).
* See workspace.resizeContents to resize the workspace when the contents
* change (e.g. when a block is added or removed).
* Record the height/width of the SVG image.
*
* @param workspace Any workspace in the SVG.
*/
export function svgResize(workspace: WorkspaceSvg) {
let mainWorkspace = workspace;
while (mainWorkspace.options.parentWorkspace) {
mainWorkspace = mainWorkspace.options.parentWorkspace;
}
const svg = mainWorkspace.getParentSvg();
const cachedSize = mainWorkspace.getCachedParentSvgSize();
const div = svg.parentElement;
if (!(div instanceof HTMLElement)) {
// Workspace deleted, or something.
return;
}
const width = div.offsetWidth;
const height = div.offsetHeight;
if (cachedSize.width !== width) {
svg.setAttribute('width', width + 'px');
mainWorkspace.setCachedParentSvgSize(width, null);
}
if (cachedSize.height !== height) {
svg.setAttribute('height', height + 'px');
mainWorkspace.setCachedParentSvgSize(null, height);
}
mainWorkspace.resize();
}
/**
* All of the connections on blocks that are currently being dragged.
*/
export const draggingConnections: Connection[] = [];
/**
* Get a map of all the block's descendants mapping their type to the number of
* children with that type.
*
* @param block The block to map.
* @param opt_stripFollowing Optionally ignore all following
* statements (blocks that are not inside a value or statement input
* of the block).
* @returns Map of types to type counts for descendants of the bock.
*/
export function getBlockTypeCounts(
block: Block,
opt_stripFollowing?: boolean,
): {[key: string]: number} {
const typeCountsMap = Object.create(null);
const descendants = block.getDescendants(true);
if (opt_stripFollowing) {
const nextBlock = block.getNextBlock();
if (nextBlock) {
const index = descendants.indexOf(nextBlock);
descendants.splice(index, descendants.length - index);
}
}
for (let i = 0, checkBlock; (checkBlock = descendants[i]); i++) {
if (typeCountsMap[checkBlock.type]) {
typeCountsMap[checkBlock.type]++;
} else {
typeCountsMap[checkBlock.type] = 1;
}
}
return typeCountsMap;
}
/**
* Helper function for defining a block from JSON. The resulting function has
* the correct value of jsonDef at the point in code where jsonInit is called.
*
* @param jsonDef The JSON definition of a block.
* @returns A function that calls jsonInit with the correct value
* of jsonDef.
*/
function jsonInitFactory(jsonDef: AnyDuringMigration): () => void {
return function (this: Block) {
this.jsonInit(jsonDef);
};
}
/**
* Define blocks from an array of JSON block definitions, as might be generated
* by the Blockly Developer Tools.
*
* @param jsonArray An array of JSON block definitions.
*/
export function defineBlocksWithJsonArray(jsonArray: AnyDuringMigration[]) {
TEST_ONLY.defineBlocksWithJsonArrayInternal(jsonArray);
}
/**
* Private version of defineBlocksWithJsonArray for stubbing in tests.
*/
function defineBlocksWithJsonArrayInternal(jsonArray: AnyDuringMigration[]) {
defineBlocks(createBlockDefinitionsFromJsonArray(jsonArray));
}
/**
* Define blocks from an array of JSON block definitions, as might be generated
* by the Blockly Developer Tools.
*
* @param jsonArray An array of JSON block definitions.
* @returns A map of the block
* definitions created.
*/
export function createBlockDefinitionsFromJsonArray(
jsonArray: AnyDuringMigration[],
): {[key: string]: BlockDefinition} {
const blocks: {[key: string]: BlockDefinition} = {};
for (let i = 0; i < jsonArray.length; i++) {
const elem = jsonArray[i];
if (!elem) {
console.warn(`Block definition #${i} in JSON array is ${elem}. Skipping`);
continue;
}
const type = elem['type'];
if (!type) {
console.warn(
`Block definition #${i} in JSON array is missing a type attribute. ` +
'Skipping.',
);
continue;
}
blocks[type] = {init: jsonInitFactory(elem)};
}
return blocks;
}
/**
* Add the specified block definitions to the block definitions
* dictionary (Blockly.Blocks).
*
* @param blocks A map of block
* type names to block definitions.
*/
export function defineBlocks(blocks: {[key: string]: BlockDefinition}) {
// Iterate over own enumerable properties.
for (const type of Object.keys(blocks)) {
const definition = blocks[type];
if (type in Blocks) {
console.warn(
`Block definition "${type}" overwrites previous definition.`,
);
}
Blocks[type] = definition;
}
}
/**
* Handle a key-down on SVG drawing surface. Does nothing if the main workspace
* is not visible.
*
* @internal
* @param e Key down event.
*/
export function globalShortcutHandler(e: KeyboardEvent) {
// This would ideally just be a `focusedTree instanceof WorkspaceSvg`, but
// importing `WorkspaceSvg` (as opposed to just its type) causes cycles.
let workspace: WorkspaceSvg = getMainWorkspace() as WorkspaceSvg;
const focusedTree = getFocusManager().getFocusedTree();
for (const ws of getAllWorkspaces()) {
if (focusedTree === (ws as WorkspaceSvg)) {
workspace = ws as WorkspaceSvg;
break;
}
}
if (
browserEvents.isTargetInput(e) ||
!workspace ||
(workspace.rendered && !workspace.isFlyout && !workspace.isVisible())
) {
// When focused on an HTML text input widget, don't trap any keys.
// Ignore keypresses on rendered workspaces that have been explicitly
// hidden.
return;
}
ShortcutRegistry.registry.onKeyDown(workspace, e);
}
export const TEST_ONLY = {defineBlocksWithJsonArrayInternal};