soiz1 commited on
Commit
b6a3ea0
·
verified ·
1 Parent(s): 851f4bc

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
+ }