forked from forkphorus/cat-plus
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathbetter-context-menus.user.js
132 lines (114 loc) · 4.37 KB
/
better-context-menus.user.js
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
// ==UserScript==
// @name Better Context Menus for Scratch 3
// @version 0.2
// @namespace https://github.com/forkphorus/cat-plus
// @match https://scratch.mit.edu/projects/*
// @run-at document-idle
// ==/UserScript==
'use strict';
window.addEventListener('load', function() {
let workspace = Blockly.getMainWorkspace();
let hooked = false;
// Maps block opcodes to our custom context menu handler, if any.
// Handlers are passed the menu options list and the Blockly SVG block
// Handlers are expected to modify the options list in place in any way they choose (push, sort, pop, etc.)
const menuLibrary = {
// Add a way to rename variables from their reference
data_variable: variableRenameFactory(3, 'variable'),
// Add a way to rename lists from their reference
data_listcontents: variableRenameFactory(3, 'list'),
// This doesn't work. Seems to be an editor and scratch-vm desync.
// event_broadcast: variableRenameFactory(3, 'broadcast'),
// TODO: consider rename options on var sets, var changes, list gets, etc.
// Add a way to jump to a block's definition on procedure call blocks
procedures_call(options, block) {
options.push({
enabled: true,
text: "Go To Definition",
callback() {
const procCode = block.procCode_;
for (const block of workspace.getAllBlocks()) {
if (block.type === 'procedures_prototype' && block.procCode_ === procCode) {
const parent = block.parentBlock_;
scrollToBlock(parent);
break;
}
}
},
});
},
};
function variableRenameFactory(insertIndex, type) {
return function(options, block) {
options.splice(insertIndex, 0, {
enabled: true,
text: `Rename ${type}`,
callback() {
const target = block.childBlocks_.length > 0 ? block.childBlocks_[0] : block;
const inputList = target.inputList;
const fieldRow = inputList[0].fieldRow;
const variable = fieldRow[0].variable_;
const variableId = variable.id_;
const oldName = variable.name;
// prompt() isn't perfect but works well enough for this use case
const newName = prompt(`Rename '${oldName}' ${type} to:`, oldName);
if (typeof newName !== 'string') {
return;
}
workspace.renameVariableById(variableId, newName);
},
});
};
}
function scrollToBlock(block) {
const MARGIN = 20;
const position = block.getRelativeToSurfaceXY();
const metrics = workspace.getMetrics();
const x = position.x * workspace.scale - metrics.contentLeft;
const y = position.y * workspace.scale - metrics.contentTop;
workspace.scrollbar.set(x - MARGIN, y - MARGIN);
}
function hook() {
hooked = true;
// We add our own change listener to react to all block creations
workspace.addChangeListener(function(change) {
if (change.type !== 'create') {
return;
}
// change.ids is a list of blocks that have been created.
for (const id of change.ids) {
const block = workspace.getBlockById(id);
if (!block) continue;
const type = block.type;
if (type in menuLibrary) {
// customContextMenu will be called when a block's context menu is accessed.
// It is passed a list it is expected to modify it in place.
// Retain the original customContextMenu() (if any) to avoid breaking behavior.
const nativeCustomContextMenu = block.customContextMenu;
block.customContextMenu = function(options) {
if (nativeCustomContextMenu) {
nativeCustomContextMenu.call(this, options);
}
menuLibrary[type](options, this);
}
}
}
});
}
// If the workspace is already available, then we hook right away.
if (workspace) {
hook();
} else {
// Probably a project page outside of the editor.
// Wait until the user sees inside before hooking since the editor object won't exist yet.
document.body.addEventListener('click', function(e) {
if (!hooked && e.target.closest('.see-inside-button')) {
// The editor will be defined after Scratch's handler runs.
setImmediate(function() {
workspace = Blockly.getMainWorkspace();
hook();
});
}
});
}
});