Spaces:
Runtime error
Runtime error
Create userscript.js
Browse files
src/addons/addons/multi-tab-code/userscript.js
ADDED
@@ -0,0 +1,555 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/* eslint-disable */ // FUCK OFF FOR THE LOVE OF CHRIST
|
2 |
+
export default async function ({ addon, msg, console }) {
|
3 |
+
// make migrating code between tabs possible at all
|
4 |
+
let selectedTab = -1;
|
5 |
+
let hoveredTab = -1;
|
6 |
+
let dragging = false;
|
7 |
+
const tabs = [];
|
8 |
+
window.tabs = tabs;
|
9 |
+
let tabTarget = null;
|
10 |
+
const commentId = '// multi-tab configuration entry\n';
|
11 |
+
let scroll = 0;
|
12 |
+
let scrollSelected = false;
|
13 |
+
let selectStartX = 0;
|
14 |
+
let selectStartScroll = 0;
|
15 |
+
const soup_ = '!#%()*+,-./:;=?@[]^_`{|}~ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
16 |
+
function uid() {
|
17 |
+
const length = 20;
|
18 |
+
const soupLength = soup_.length;
|
19 |
+
const id = [];
|
20 |
+
for (let i = 0; i < length; i++) {
|
21 |
+
id[i] = soup_.charAt(Math.random() * soupLength);
|
22 |
+
}
|
23 |
+
return id.join('');
|
24 |
+
}
|
25 |
+
|
26 |
+
const Blockly = await addon.tab.traps.getBlockly();
|
27 |
+
// goofy i know, but it makes vscode understand so
|
28 |
+
/** @type {import('../../../../../scratch-vm/src/index')} */
|
29 |
+
const vm = addon.tab.traps.vm;
|
30 |
+
const { Blocks, Variable, RenderedTarget, Comment } = vm.exports;
|
31 |
+
const ogEmitUpdate = vm.emitWorkspaceUpdate;
|
32 |
+
vm.emitWorkspaceUpdate = function() {
|
33 |
+
if (!tabs[selectedTab]) return ogEmitUpdate.call(this, []);
|
34 |
+
// Create a list of broadcast message Ids according to the stage variables
|
35 |
+
const stageVariables = this.runtime.getTargetForStage().variables;
|
36 |
+
let messageIds = [];
|
37 |
+
for (const varId in stageVariables) {
|
38 |
+
if (stageVariables[varId].type === Variable.BROADCAST_MESSAGE_TYPE) {
|
39 |
+
messageIds.push(varId);
|
40 |
+
}
|
41 |
+
}
|
42 |
+
// Go through all blocks on all targets, removing referenced
|
43 |
+
// broadcast ids from the list.
|
44 |
+
for (let i = 0; i < this.runtime.targets.length; i++) {
|
45 |
+
const currTarget = this.runtime.targets[i];
|
46 |
+
const currBlocks = currTarget.blocks._blocks;
|
47 |
+
for (const blockId in currBlocks) {
|
48 |
+
if (currBlocks[blockId].fields.BROADCAST_OPTION) {
|
49 |
+
const id = currBlocks[blockId].fields.BROADCAST_OPTION.id;
|
50 |
+
const index = messageIds.indexOf(id);
|
51 |
+
if (index !== -1) {
|
52 |
+
messageIds = messageIds.slice(0, index)
|
53 |
+
.concat(messageIds.slice(index + 1));
|
54 |
+
}
|
55 |
+
}
|
56 |
+
}
|
57 |
+
}
|
58 |
+
// Anything left in messageIds is not referenced by a block, so delete it.
|
59 |
+
for (let i = 0; i < messageIds.length; i++) {
|
60 |
+
const id = messageIds[i];
|
61 |
+
delete this.runtime.getTargetForStage().variables[id];
|
62 |
+
}
|
63 |
+
const globalVarMap = Object.assign({}, this.runtime.getTargetForStage().variables);
|
64 |
+
const localVarMap = this.editingTarget.isStage ?
|
65 |
+
Object.create(null) :
|
66 |
+
Object.assign({}, this.editingTarget.variables);
|
67 |
+
|
68 |
+
// ensure that all scripts belong to some tab
|
69 |
+
for (const script of vm.editingTarget.blocks._scripts) {
|
70 |
+
const owner = tabs.find(tab => tab.blocks._scripts.includes(script));
|
71 |
+
if (!owner)
|
72 |
+
copyScript(script, tabs[selectedTab].blocks);
|
73 |
+
}
|
74 |
+
|
75 |
+
const globalVariables = Object.keys(globalVarMap).map(k => globalVarMap[k]);
|
76 |
+
const localVariables = Object.keys(localVarMap).map(k => localVarMap[k]);
|
77 |
+
const workspaceComments = Object.keys(this.editingTarget.comments)
|
78 |
+
.map(k => this.editingTarget.comments[k])
|
79 |
+
.filter(c => c.blockId === null && c.tab == selectedTab && !c.text.startsWith(commentId));
|
80 |
+
|
81 |
+
const xmlString = `<xml xmlns="http://www.w3.org/1999/xhtml">
|
82 |
+
<variables>
|
83 |
+
${globalVariables.map(v => v.toXML()).join()}
|
84 |
+
${localVariables.map(v => v.toXML(true)).join()}
|
85 |
+
</variables>
|
86 |
+
${workspaceComments.map(c => c.toXML()).join()}
|
87 |
+
${tabs[selectedTab].blocks.toXML(this.editingTarget.comments)}
|
88 |
+
</xml>`;
|
89 |
+
|
90 |
+
this.emit('workspaceUpdate', {xml: xmlString});
|
91 |
+
}
|
92 |
+
const ogBlockListener = vm.blockListener;
|
93 |
+
vm.blockListener = function(e) {
|
94 |
+
// skip operations on comments that arnt real
|
95 |
+
if (e.type === 'comment_delete' && tabTarget.comments[e.commentId]?.tab !== selectedTab) return;
|
96 |
+
// do not delete blocks from other tabs, the main sprite must be a pool of all tabs
|
97 |
+
if (e.type === 'delete' && !tabs[selectedTab].blocks._blocks[e.blockId]) return;
|
98 |
+
if (selectedTab !== -1 && e.type !== 'ui' && !e.varId)
|
99 |
+
tabs[selectedTab].blocks.blocklyListen(e);
|
100 |
+
ogBlockListener(e);
|
101 |
+
if (!e.isOutside) {
|
102 |
+
switch (e.type) {
|
103 |
+
case 'endDrag':
|
104 |
+
dragging = false;
|
105 |
+
if (hoveredTab === -1) break;
|
106 |
+
const blocks = vm.editingTarget.blocks.XMLToBlock(e);
|
107 |
+
for (const block of blocks) {
|
108 |
+
const oldId = block.id;
|
109 |
+
const newId = block.id = uid();
|
110 |
+
// replace all instances of the old id with the new one
|
111 |
+
blocks.forEach(block => {
|
112 |
+
if (block.next === oldId) block.next = newId;
|
113 |
+
if (block.parent === oldId) block.parent = newId;
|
114 |
+
for (const name in block.inputs) {
|
115 |
+
const input = block.inputs[name];
|
116 |
+
if (input.block === oldId) input.block = newId;
|
117 |
+
if (input.shadow === oldId) input.shadow = newId;
|
118 |
+
}
|
119 |
+
});
|
120 |
+
tabs[hoveredTab].blocks.createBlock(block);
|
121 |
+
}
|
122 |
+
}
|
123 |
+
}
|
124 |
+
}
|
125 |
+
const workspace = Blockly.getMainWorkspace();
|
126 |
+
// remove old, bound, function
|
127 |
+
const func = workspace.listeners_.findIndex(func => func.name.includes(ogBlockListener.name));
|
128 |
+
workspace.listeners_.splice(func, 1);
|
129 |
+
const vmSaveJSON = vm.toJSON;
|
130 |
+
vm.toJSON = function(optTargetId, serializationOptions) {
|
131 |
+
saveTabs();
|
132 |
+
serializationOptions ??= {};
|
133 |
+
// id compression breaks comment loading
|
134 |
+
serializationOptions.allowOptimization = false;
|
135 |
+
return vmSaveJSON.call(this, optTargetId, serializationOptions);
|
136 |
+
}
|
137 |
+
vm.runtime._updateGlows = function(optExtraThreads) {
|
138 |
+
const searchThreads = [];
|
139 |
+
searchThreads.push.apply(searchThreads, this.threads);
|
140 |
+
if (optExtraThreads) {
|
141 |
+
searchThreads.push.apply(searchThreads, optExtraThreads);
|
142 |
+
}
|
143 |
+
// Set of scripts that request a glow this frame.
|
144 |
+
const requestedGlowsThisFrame = [];
|
145 |
+
// Final set of scripts glowing during this frame.
|
146 |
+
const finalScriptGlows = [];
|
147 |
+
// Find all scripts that should be glowing.
|
148 |
+
for (let i = 0; i < searchThreads.length; i++) {
|
149 |
+
const thread = searchThreads[i];
|
150 |
+
const target = thread.target;
|
151 |
+
if (target === this._editingTarget) {
|
152 |
+
const blockForThread = thread.blockGlowInFrame;
|
153 |
+
if (thread.requestScriptGlowInFrame || thread.stackClick) {
|
154 |
+
let script = tabs[selectedTab].blocks.getTopLevelScript(blockForThread);
|
155 |
+
if (!script) {
|
156 |
+
// Attempt to find in flyout blocks.
|
157 |
+
script = this.flyoutBlocks.getTopLevelScript(
|
158 |
+
blockForThread
|
159 |
+
);
|
160 |
+
}
|
161 |
+
if (script) {
|
162 |
+
requestedGlowsThisFrame.push(script);
|
163 |
+
}
|
164 |
+
}
|
165 |
+
}
|
166 |
+
}
|
167 |
+
// Compare to previous frame.
|
168 |
+
for (let j = 0; j < this._scriptGlowsPreviousFrame.length; j++) {
|
169 |
+
const previousFrameGlow = this._scriptGlowsPreviousFrame[j];
|
170 |
+
if (requestedGlowsThisFrame.indexOf(previousFrameGlow) < 0) {
|
171 |
+
this.glowScript(previousFrameGlow, false);
|
172 |
+
} else {
|
173 |
+
// Still glowing.
|
174 |
+
finalScriptGlows.push(previousFrameGlow);
|
175 |
+
}
|
176 |
+
}
|
177 |
+
for (let k = 0; k < requestedGlowsThisFrame.length; k++) {
|
178 |
+
const currentFrameGlow = requestedGlowsThisFrame[k];
|
179 |
+
if (this._scriptGlowsPreviousFrame.indexOf(currentFrameGlow) < 0) {
|
180 |
+
// Glow turned on.
|
181 |
+
this.glowScript(currentFrameGlow, true);
|
182 |
+
finalScriptGlows.push(currentFrameGlow);
|
183 |
+
}
|
184 |
+
}
|
185 |
+
this._scriptGlowsPreviousFrame = finalScriptGlows;
|
186 |
+
}
|
187 |
+
RenderedTarget.prototype.createComment = function(id, blockId, text, x, y, width, height, minimized) {
|
188 |
+
if (!this.comments.hasOwnProperty(id)) {
|
189 |
+
const newComment = new Comment(id, text, x, y, width, height, minimized);
|
190 |
+
newComment.tab = selectedTab;
|
191 |
+
if (blockId) {
|
192 |
+
newComment.blockId = blockId;
|
193 |
+
const blockWithComment = this.blocks.getBlock(blockId);
|
194 |
+
if (blockWithComment) {
|
195 |
+
blockWithComment.comment = id;
|
196 |
+
tabs[selectedTab].blocks._blocks[blockId].comment = id;
|
197 |
+
} else {
|
198 |
+
log.warn(`Could not find block with id ${blockId} associated with commentId: ${id}`);
|
199 |
+
}
|
200 |
+
}
|
201 |
+
this.comments[id] = newComment;
|
202 |
+
}
|
203 |
+
}
|
204 |
+
class MutatorWrapper {
|
205 |
+
mutation = {}
|
206 |
+
blockId = null;
|
207 |
+
constructor(mute, blockId) { this.mutation = mute; this.blockId = blockId; }
|
208 |
+
getInput() { return null }
|
209 |
+
getProcCode() { return this.mutation.proccode }
|
210 |
+
get workspace() { return ScratchBlocks.getMainWorkspace() }
|
211 |
+
mutationToDom() {
|
212 |
+
const blocks = vm.editingTarget.blocks;
|
213 |
+
const str = blocks.mutationToXML(this.mutation);
|
214 |
+
const parser = new DOMParser();
|
215 |
+
const dom = parser.parseFromString(`<xml>${str}</xml>`, 'text/xml');
|
216 |
+
return dom.firstChild.firstChild;
|
217 |
+
}
|
218 |
+
domToMutation(dom) {
|
219 |
+
const blocks = vm.editingTarget.blocks;
|
220 |
+
this.mutation = blocks.XMLToMutation(dom);
|
221 |
+
// event isnt ever fired for whatever reason????????
|
222 |
+
blocks._blocks[this.blockId].mutation = this.mutation;
|
223 |
+
for (const tab of tabs) {
|
224 |
+
const block = tab.blocks._blocks[this.blockId];
|
225 |
+
if (block)
|
226 |
+
block.mutation = this.mutation;
|
227 |
+
}
|
228 |
+
}
|
229 |
+
}
|
230 |
+
// Make procedures get sourced from the target, rather then the workspace
|
231 |
+
const oldProcedureMutations = ScratchBlocks.Procedures.allProcedureMutations;
|
232 |
+
ScratchBlocks.Procedures.allProcedureMutations = function(root) {
|
233 |
+
let blockMutes = oldProcedureMutations.call(this, root);
|
234 |
+
if (!vm.editingTarget) return blockMutes;
|
235 |
+
blockMutes = Object.fromEntries(blockMutes.map(mutation => [mutation.getAttribute('proccode'), mutation]));
|
236 |
+
const blocks = vm.editingTarget.blocks;
|
237 |
+
for (const id in blocks._blocks) {
|
238 |
+
const block = blocks._blocks[id];
|
239 |
+
if (block.opcode === 'procedures_prototype' && !blockMutes[block.mutation.proccode]) {
|
240 |
+
const wrapper = new MutatorWrapper(block.mutation, id);
|
241 |
+
blockMutes[block.mutation.proccode] = wrapper.mutationToDom();
|
242 |
+
}
|
243 |
+
}
|
244 |
+
return Object.values(blockMutes);
|
245 |
+
}
|
246 |
+
ScratchBlocks.Procedures.getPrototypeBlock = function(procCode, workspace) {
|
247 |
+
var defineBlock = Blockly.Procedures.getDefineBlock(procCode, workspace);
|
248 |
+
if (defineBlock instanceof MutatorWrapper) {
|
249 |
+
const blocks = vm.editingTarget.blocks;
|
250 |
+
for (const id in blocks._blocks) {
|
251 |
+
const block = blocks._blocks[id];
|
252 |
+
if (block.opcode === 'procedures_prototype' && block.mutation.proccode === procCode) {
|
253 |
+
return new MutatorWrapper(block.mutation, id);
|
254 |
+
}
|
255 |
+
}
|
256 |
+
}
|
257 |
+
if (defineBlock) {
|
258 |
+
return defineBlock.getInput('custom_block').connection.targetBlock();
|
259 |
+
}
|
260 |
+
return null;
|
261 |
+
}
|
262 |
+
const oldDefineBlock = ScratchBlocks.Procedures.getDefineBlock;
|
263 |
+
ScratchBlocks.Procedures.getDefineBlock = function(procCode, workspace) {
|
264 |
+
const prototype = oldDefineBlock.call(this, procCode, workspace);
|
265 |
+
if (!prototype) {
|
266 |
+
const blocks = vm.editingTarget.blocks;
|
267 |
+
for (const id in blocks._blocks) {
|
268 |
+
const block = blocks._blocks[id];
|
269 |
+
if (block.opcode === 'procedures_prototype' && block.mutation.proccode === procCode) {
|
270 |
+
const parent = blocks._blocks[block.parent];
|
271 |
+
return new MutatorWrapper(parent.mutation, id);
|
272 |
+
}
|
273 |
+
}
|
274 |
+
}
|
275 |
+
return prototype;
|
276 |
+
}
|
277 |
+
const oldCallers = ScratchBlocks.Procedures.getCallers;
|
278 |
+
function checkBlocks(entry, blocks, check) {
|
279 |
+
let block;
|
280 |
+
do {
|
281 |
+
block = blocks.getBlock(entry);
|
282 |
+
entry = block.next;
|
283 |
+
check(block);
|
284 |
+
for (const name in block.inputs) {
|
285 |
+
const input = block.inputs[name];
|
286 |
+
checkBlocks(input.block, blocks, check);
|
287 |
+
}
|
288 |
+
} while (blocks.getBlock(block.next));
|
289 |
+
}
|
290 |
+
ScratchBlocks.Procedures.getCallers = function(name, ws, definitionRoot, allowRecursive) {
|
291 |
+
let callers = oldCallers.call(this, name, ws, definitionRoot, allowRecursive);
|
292 |
+
if (!vm.editingTarget) return callers;
|
293 |
+
callers = Object.fromEntries(callers.map(item => [item.getProcCode(), item]));
|
294 |
+
const blocks = vm.editingTarget.blocks;
|
295 |
+
for (const id of blocks._scripts) {
|
296 |
+
const block = blocks._blocks[id];
|
297 |
+
if (!allowRecursive && (block.opcode === 'procedures_definition' || block.opcode === 'procedures_definition_return')) continue;
|
298 |
+
checkBlocks(id, blocks, block => {
|
299 |
+
if (block.opcode === 'procedures_call')
|
300 |
+
callers[block.mutation.proccode] ??= new MutatorWrapper(block.mutation. block.id);
|
301 |
+
});
|
302 |
+
}
|
303 |
+
return Object.values(callers);
|
304 |
+
}
|
305 |
+
// listen to when we start dragging blocks around
|
306 |
+
const oldStartBlockDrag = Blockly.BlockDragger.prototype.startBlockDrag;
|
307 |
+
Blockly.BlockDragger.prototype.startBlockDrag = function(...args) {
|
308 |
+
dragging = true;
|
309 |
+
return oldStartBlockDrag.call(this, ...args);
|
310 |
+
}
|
311 |
+
Blockly.BlockDragger.prototype.fireEndDragEvent_ = function(isOutside) {
|
312 |
+
// make sure that xml is actually generated
|
313 |
+
var event = new Blockly.Events.EndBlockDrag(this.draggingBlock_, true);
|
314 |
+
// do apply the real value
|
315 |
+
event.isOutside = isOutside;
|
316 |
+
Blockly.Events.fire(event);
|
317 |
+
};
|
318 |
+
|
319 |
+
const codeTab = document.getElementById('react-tabs-1');
|
320 |
+
const blockSpace = codeTab.getElementsByClassName('injectionDiv')[0];
|
321 |
+
const flyout = blockSpace.getElementsByClassName('blocklyFlyout')[0];
|
322 |
+
const toolbox = blockSpace.getElementsByClassName('blocklyToolboxDiv')[0];
|
323 |
+
const scrollBar = document.createElement('div');
|
324 |
+
scrollBar.classList.add('tab-scrollbar');
|
325 |
+
const tabScroller = document.createElement('div');
|
326 |
+
tabScroller.classList.add('tab-scroller');
|
327 |
+
tabScroller.onmouseleave = () => hoveredTab = -1;
|
328 |
+
const tabWrapper = document.createElement('div');
|
329 |
+
tabWrapper.classList.add('tab-wrapper');
|
330 |
+
tabWrapper.appendChild(tabScroller);
|
331 |
+
tabWrapper.appendChild(scrollBar);
|
332 |
+
(function animateBar() {
|
333 |
+
const size = flyout.getBoundingClientRect();
|
334 |
+
const altSize = toolbox.getBoundingClientRect();
|
335 |
+
const boundSize = blockSpace.getBoundingClientRect();
|
336 |
+
tabWrapper.style.width = `calc(100% - ${(Math.max(size.right, altSize.right) - boundSize.left)}px)`;
|
337 |
+
computeScrollbar();
|
338 |
+
requestAnimationFrame(animateBar);
|
339 |
+
})();
|
340 |
+
blockSpace.appendChild(tabWrapper);
|
341 |
+
const addButton = document.createElement('img');
|
342 |
+
addButton.src = addon.self.getResource('/add.svg');
|
343 |
+
addButton.textContent = '+';
|
344 |
+
addButton.classList.add('tab-adder-button');
|
345 |
+
tabScroller.appendChild(addButton);
|
346 |
+
|
347 |
+
function copyScript(id, blocks) {
|
348 |
+
let block;
|
349 |
+
do {
|
350 |
+
block = vm.editingTarget.blocks.getBlock(id);
|
351 |
+
if (!block) break;
|
352 |
+
blocks.createBlock(block);
|
353 |
+
for (const name in block.inputs) {
|
354 |
+
copyScript(block.inputs[name].block, blocks);
|
355 |
+
copyScript(block.inputs[name].shadow, blocks);
|
356 |
+
}
|
357 |
+
id = block.next;
|
358 |
+
} while (block.next);
|
359 |
+
}
|
360 |
+
function selectTab(idx) {
|
361 |
+
const { element: tab } = tabs[idx];
|
362 |
+
selectedTab = idx;
|
363 |
+
for (const meta of tabs) {
|
364 |
+
if (!meta) continue;
|
365 |
+
const { element: tab } = meta;
|
366 |
+
tab.classList.remove('selected');
|
367 |
+
tab.classList.remove('unselected');
|
368 |
+
tab.classList.add('unselected');
|
369 |
+
}
|
370 |
+
tab.classList.toggle('unselected');
|
371 |
+
tab.classList.toggle('selected');
|
372 |
+
vm.emitWorkspaceUpdate();
|
373 |
+
// clear glows to prevent glowOff throwing errors
|
374 |
+
vm.runtime._scriptGlowsPreviousFrame = [];
|
375 |
+
vm.runtime._updateGlows();
|
376 |
+
}
|
377 |
+
function addTab(enabled, name, scripts) {
|
378 |
+
const meta = { name: name, element: null, idx: -1, blocks: new Blocks(vm.runtime) };
|
379 |
+
if (scripts) {
|
380 |
+
meta.blocks._scripts = [...scripts];
|
381 |
+
for (const scriptId of scripts)
|
382 |
+
copyScript(scriptId, meta.blocks);
|
383 |
+
}
|
384 |
+
meta.idx = tabs.push(meta) -1;
|
385 |
+
meta.name ??= `Tab ${meta.idx +1}`;
|
386 |
+
const tabOuter = document.createElement('div');
|
387 |
+
tabOuter.classList.add('tab-bounds');
|
388 |
+
const tab = document.createElement('div');
|
389 |
+
meta.element = tab;
|
390 |
+
tab.classList.add('tab');
|
391 |
+
tab.classList.add('unselected');
|
392 |
+
if (enabled) selectTab(meta.idx);
|
393 |
+
tab.textContent = meta.name;
|
394 |
+
tabOuter.onclick = () => {
|
395 |
+
if (meta.idx === selectedTab) {
|
396 |
+
const res = prompt(`New name for ${meta.name}?`, meta.name);
|
397 |
+
if (!res) return;
|
398 |
+
meta.name = res;
|
399 |
+
tab.textContent = meta.name;
|
400 |
+
return;
|
401 |
+
}
|
402 |
+
selectTab(meta.idx);
|
403 |
+
}
|
404 |
+
tab.onmouseenter = () => {
|
405 |
+
hoveredTab = meta.idx;
|
406 |
+
if (dragging) {
|
407 |
+
tab.classList.add('copying');
|
408 |
+
tabOuter.classList.add('copying');
|
409 |
+
tabWrapper.classList.add('copying');
|
410 |
+
return;
|
411 |
+
}
|
412 |
+
tab.classList.add('hover');
|
413 |
+
}
|
414 |
+
tab.onmouseleave = () => {
|
415 |
+
tab.classList.remove('copying');
|
416 |
+
tabOuter.classList.remove('copying');
|
417 |
+
tabWrapper.classList.remove('copying');
|
418 |
+
tab.classList.remove('hover');
|
419 |
+
}
|
420 |
+
tabOuter.appendChild(tab);
|
421 |
+
addButton.before(tabOuter);
|
422 |
+
computeScrollbar();
|
423 |
+
}
|
424 |
+
function removeTab(idx) {
|
425 |
+
if (tabs.length <= 1) return;
|
426 |
+
const tab = tabs[idx];
|
427 |
+
tab.element.parentElement.remove();
|
428 |
+
tabs.splice(idx, 1);
|
429 |
+
if (!tabs[selectedTab]) selectedTab--;
|
430 |
+
const shouldntDelete = addon.settings.get('shouldDelete');
|
431 |
+
for (const script of tab.blocks._scripts) {
|
432 |
+
if (shouldntDelete == 'true')
|
433 |
+
copyScript(script, tabs[selectedTab].blocks);
|
434 |
+
tabTarget.blocks.deleteBlock(script);
|
435 |
+
}
|
436 |
+
// offset indecies
|
437 |
+
for (const meta of tabs)
|
438 |
+
if (meta.idx > idx)
|
439 |
+
meta.idx--;
|
440 |
+
selectTab(selectedTab);
|
441 |
+
computeScrollbar();
|
442 |
+
}
|
443 |
+
addButton.onclick = () => addTab(true);
|
444 |
+
function computeScrollbar() {
|
445 |
+
scrollBar.hidden = true;
|
446 |
+
const bodySize = tabScroller.getBoundingClientRect();
|
447 |
+
const wrapperSize = tabWrapper.getBoundingClientRect();
|
448 |
+
const diff = bodySize.width - wrapperSize.width;
|
449 |
+
const len = wrapperSize.width - diff;
|
450 |
+
if (len > wrapperSize.width) return;
|
451 |
+
scrollBar.hidden = false;
|
452 |
+
scrollBar.style.width = `${len}px`;
|
453 |
+
// clamp scroll into the viewbox
|
454 |
+
scroll = Math.max(Math.min(scroll, diff), 0);
|
455 |
+
scrollBar.style.left = `${scroll}px`;
|
456 |
+
tabScroller.style.left = `-${scroll}px`;
|
457 |
+
}
|
458 |
+
tabWrapper.onwheel = e => {
|
459 |
+
const bodySize = tabScroller.getBoundingClientRect();
|
460 |
+
const wrapperSize = tabWrapper.getBoundingClientRect();
|
461 |
+
const diff = bodySize.width - wrapperSize.width;
|
462 |
+
// prefer deltaX, otherwise use deltaY
|
463 |
+
scroll = Math.max(Math.min((e.deltaX || e.deltaY) + scroll, diff), 0);
|
464 |
+
scrollBar.style.left = `${scroll}px`;
|
465 |
+
tabScroller.style.left = `-${scroll}px`;
|
466 |
+
}
|
467 |
+
scrollBar.onmousedown = e => {
|
468 |
+
scrollSelected = true;
|
469 |
+
selectStartScroll = scroll;
|
470 |
+
selectStartX = e.x;
|
471 |
+
}
|
472 |
+
document.onmouseup = () => scrollSelected = false;
|
473 |
+
document.onmousemove = e => {
|
474 |
+
if (!scrollSelected) return;
|
475 |
+
const bodySize = tabScroller.getBoundingClientRect();
|
476 |
+
const wrapperSize = tabWrapper.getBoundingClientRect();
|
477 |
+
const diff = bodySize.width - wrapperSize.width;
|
478 |
+
scroll = Math.max(Math.min((e.x - selectStartX) + selectStartScroll, diff), 0);
|
479 |
+
scrollBar.style.left = `${scroll}px`;
|
480 |
+
tabScroller.style.left = `-${scroll}px`;
|
481 |
+
}
|
482 |
+
function loadTabs() {
|
483 |
+
for (const comment of Object.values(tabTarget.comments))
|
484 |
+
if (comment.text.startsWith(commentId))
|
485 |
+
return JSON.parse(comment.text.slice(commentId.length));
|
486 |
+
}
|
487 |
+
function saveTabs() {
|
488 |
+
const serial = tabs
|
489 |
+
.filter(tab => tab.blocks._scripts.length > 0)
|
490 |
+
.map((tab, idx) => ({
|
491 |
+
name: tab.name,
|
492 |
+
scripts: tab.blocks._scripts,
|
493 |
+
selected: selectedTab === idx,
|
494 |
+
comments: Object.values(tabTarget.comments)
|
495 |
+
.filter(c => !c.text.startsWith(commentId) && c.tab == idx)
|
496 |
+
.map(c => c.id)
|
497 |
+
}));
|
498 |
+
for (const comment of Object.values(tabTarget.comments))
|
499 |
+
if (comment.text.startsWith(commentId))
|
500 |
+
return comment.text = commentId + JSON.stringify(serial);
|
501 |
+
tabTarget.createComment(null, null, commentId + JSON.stringify(serial), 10,10, -100000,-100000, true);
|
502 |
+
}
|
503 |
+
|
504 |
+
vm.on('targetsUpdate', () => {
|
505 |
+
// if editingTarget doesnt exist, tabTarget cant either
|
506 |
+
if (!vm.editingTarget) return tabTarget = null;
|
507 |
+
if (tabTarget && vm.editingTarget.id === tabTarget.id) return;
|
508 |
+
if (tabTarget) saveTabs();
|
509 |
+
while (tabs.length) tabs.shift();
|
510 |
+
while (tabScroller.children.length > 1)
|
511 |
+
tabScroller.children[0].remove();
|
512 |
+
tabTarget = vm.editingTarget;
|
513 |
+
try {
|
514 |
+
const savedTabs = loadTabs();
|
515 |
+
if (!savedTabs?.length) throw new Error('No saved tabs');
|
516 |
+
const scripts = tabTarget.blocks._scripts;
|
517 |
+
for (const tabIdx in savedTabs) {
|
518 |
+
const tab = savedTabs[tabIdx];
|
519 |
+
for (const cid of tab.comments)
|
520 |
+
tabTarget.comments[cid].tab = tabIdx;
|
521 |
+
for (const script of tab.scripts) {
|
522 |
+
const idx = scripts.indexOf(script);
|
523 |
+
scripts.splice(idx, 1);
|
524 |
+
}
|
525 |
+
addTab(tab.selected, tab.name, tab.scripts);
|
526 |
+
}
|
527 |
+
for (const script of scripts)
|
528 |
+
copyScript(script, tabs[selectedTab].blocks);
|
529 |
+
} catch (err) {
|
530 |
+
console.warn('Couldnt read the serialized tabs', err);
|
531 |
+
addTab(true, null, vm.editingTarget.blocks._scripts);
|
532 |
+
}
|
533 |
+
});
|
534 |
+
|
535 |
+
const keysPressed = {};
|
536 |
+
document.addEventListener('keydown', e => {
|
537 |
+
keysPressed[e.key] = true;
|
538 |
+
if (keysPressed['{']) {
|
539 |
+
if (selectedTab > 0)
|
540 |
+
selectTab(--selectedTab);
|
541 |
+
}
|
542 |
+
if (keysPressed['}']) {
|
543 |
+
if ((selectedTab +1) < tabs.length)
|
544 |
+
selectTab(++selectedTab);
|
545 |
+
}
|
546 |
+
if (keysPressed['_']) {
|
547 |
+
if (tabs.length > 1)
|
548 |
+
removeTab(selectedTab);
|
549 |
+
}
|
550 |
+
if (keysPressed['+']) {
|
551 |
+
addTab(true);
|
552 |
+
}
|
553 |
+
});
|
554 |
+
document.addEventListener('keyup', e => delete keysPressed[e.key]);
|
555 |
+
}
|