Skip to content

Commit eb6747f

Browse files
The2AndOnlySamq64DNin01
authored
New addon: Fix dragging costumes and sounds (fixed PR) (ScratchAddons#8511)
* Added fix costume drag addon * Format with Prettier and add scroll speed options * Format code * Formatted using Prettier * Added module for sortableHOC functions, fixed getting settings every frame * Support dynamic disable; Improved code; Edit patch SortableHOC module; Fixed reading settings every frame * Fixed scroll settings being default on page load; Improved code; Removed export on unneeded function in module * Rebranded addon * Format * Added fix costume drag addon * Format with Prettier and add scroll speed options * Format code * Formatted using Prettier * Added module for sortableHOC functions, fixed getting settings every frame * Support dynamic disable; Improved code; Edit patch SortableHOC module; Fixed reading settings every frame * Fixed scroll settings being default on page load; Improved code; Removed export on unneeded function in module * Rebranded addon * Format * Tweaked title and description * move module to libraries/common/cs with name patch-SortableHOC.js * Fixed interfering with sprites list * improved module/improved code * test * Fix accidentally adding "fix-sprite-drag" folder * recommended tag and enable by default * didn't release in update 1.44 * fix equal sign * fix equal sign again * Revise addon description --------- Co-authored-by: The2AndOnly <no email> Co-authored-by: The2AndOnly <The2AndOnly@users.noreply.github.com> Co-authored-by: Samq64 <81489795+Samq64@users.noreply.github.com> Co-authored-by: DNin01 <106490990+DNin01@users.noreply.github.com>
1 parent f5bfb9b commit eb6747f

6 files changed

Lines changed: 289 additions & 55 deletions

File tree

addons/addons.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@
160160
"no-sprite-confirm",
161161
"costume-editor-shortcuts",
162162
"live-read-topics",
163+
"fix-costume-drag",
163164
"recolor-custom-blocks",
164165
"totally-normal-modes",
165166

addons/fix-costume-drag/addon.json

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
{
2+
"name": "Improved dragging of costumes and sounds",
3+
"description": "When dragging a costume or sound, automatically scrolls the list and fixes a Scratch bug where the dragged item isn't placed at the right position after scrolling.",
4+
"credits": [
5+
{
6+
"name": "mysinginmonsters",
7+
"link": "https://scratch.mit.edu/users/mysinginmonsters/"
8+
}
9+
],
10+
"settings": [
11+
{
12+
"name": "Auto-scroll speed",
13+
"id": "scroll-speed",
14+
"type": "select",
15+
"description": "Drag and hold a costume or sound at the top or bottom of the list to auto scroll.",
16+
"potentialValues": [
17+
{
18+
"id": "none",
19+
"name": "Off"
20+
},
21+
{
22+
"id": "slow",
23+
"name": "Slow"
24+
},
25+
{
26+
"id": "default",
27+
"name": "Medium"
28+
},
29+
{
30+
"id": "fast",
31+
"name": "Fast"
32+
}
33+
],
34+
"default": "default"
35+
}
36+
],
37+
"userscripts": [
38+
{
39+
"url": "userscript.js",
40+
"matches": ["projects"]
41+
}
42+
],
43+
"userstyles": [
44+
{
45+
"url": "userstyle.css",
46+
"matches": ["projects"]
47+
}
48+
],
49+
"tags": ["editor", "costumeEditor", "recommended"],
50+
"enabledByDefault": true,
51+
"versionAdded": "1.45.0",
52+
"dynamicEnable": true,
53+
"dynamicDisable": true
54+
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
// Helper functions for patching SortableHOC taken from the folders addon by GarboMuffin
2+
import {
3+
getSortableHOCFromElement,
4+
verifySortableHOC,
5+
setReactInternalKey,
6+
} from "../../libraries/common/cs/patch-SortableHOC.js";
7+
8+
export default async function ({ addon, console }) {
9+
// Related to settings
10+
const SPEED_PRESETS = {
11+
none: 0,
12+
slow: 3,
13+
default: 6,
14+
fast: 12,
15+
};
16+
let scrollSpeed = SPEED_PRESETS[addon.settings.get("scroll-speed")];
17+
18+
// indexForPositionOnList taken from https://github.com/scratchfoundation/scratch-gui/blob/develop/src/lib/drag-utils.js
19+
const indexForPositionOnList = ({ x, y }, boxes, isRtl) => {
20+
if (boxes.length === 0) return null;
21+
let index = null;
22+
const leftEdge = Math.min.apply(
23+
null,
24+
boxes.map((b) => b.left)
25+
);
26+
const rightEdge = Math.max.apply(
27+
null,
28+
boxes.map((b) => b.right)
29+
);
30+
const topEdge = Math.min.apply(
31+
null,
32+
boxes.map((b) => b.top)
33+
);
34+
const bottomEdge = Math.max.apply(
35+
null,
36+
boxes.map((b) => b.bottom)
37+
);
38+
for (let n = 0; n < boxes.length; n++) {
39+
const box = boxes[n];
40+
// Construct an "extended" box for each, extending out to infinity if
41+
// the box is along a boundary.
42+
let minX = box.left === leftEdge ? -Infinity : box.left;
43+
let maxX = box.right === rightEdge ? Infinity : box.right;
44+
const minY = box.top === topEdge ? -Infinity : box.top;
45+
const maxY = box.bottom === bottomEdge ? Infinity : box.bottom;
46+
// The last item in the wrapped list gets a right edge at infinity, even
47+
// if it isn't the farthest right, in RTL mode. In LTR mode, it gets a
48+
// left edge at infinity.
49+
if (n === boxes.length - 1) {
50+
if (isRtl) {
51+
minX = -Infinity;
52+
} else {
53+
maxX = Infinity;
54+
}
55+
}
56+
57+
// Check if the point is in the bounds.
58+
if (x >= minX && x <= maxX && y >= minY && y <= maxY) {
59+
index = n;
60+
break; // No need to keep looking.
61+
}
62+
}
63+
return index;
64+
};
65+
66+
// Here is the original: https://github.com/scratchfoundation/scratch-gui/blob/develop/src/lib/sortable-hoc.jsx
67+
const patchSortableHOC = (SortableHOC) => {
68+
// Save original functions
69+
const originalCWRP = SortableHOC.prototype.componentWillReceiveProps;
70+
const originalGetMouseOverIndex = SortableHOC.prototype.getMouseOverIndex;
71+
72+
SortableHOC.prototype.componentWillReceiveProps = function (newProps) {
73+
originalCWRP.call(this, newProps);
74+
75+
// Just call original function if disabled or is a sprite
76+
if (addon.self.disabled || this.props.dragInfo.dragType === "SPRITE") {
77+
return;
78+
}
79+
80+
const scrollContainer = this.ref.querySelector('[class*="selector_list-area"]');
81+
82+
if (newProps.dragInfo.dragging && !this.props.dragInfo.dragging) {
83+
// When started dragging
84+
this.initialScrollTop = scrollContainer.scrollTop;
85+
86+
// Add scroll listener
87+
const onScroll = () => {
88+
if (this.props.dragInfo.dragging) {
89+
this.forceUpdate();
90+
}
91+
};
92+
93+
scrollContainer.addEventListener("scroll", onScroll);
94+
95+
// Save listener so it can be removed later
96+
this._scrollListener = onScroll;
97+
} else if (!newProps.dragInfo.dragging && this.props.dragInfo.dragging) {
98+
// When stopped dragging
99+
if (this._scrollListener) {
100+
scrollContainer.removeEventListener("scroll", this._scrollListener);
101+
this._scrollListener = null;
102+
}
103+
}
104+
};
105+
106+
// While dragging
107+
SortableHOC.prototype.getMouseOverIndex = function () {
108+
// Just call original function if disabled or is a sprite
109+
if (addon.self.disabled || this.props.dragInfo.dragType === "SPRITE") {
110+
return originalGetMouseOverIndex.call(this);
111+
}
112+
113+
let index = null;
114+
if (this.props.dragInfo.currentOffset) {
115+
const scrollContainer = this.ref.querySelector('[class*="selector_list-area"]');
116+
const containerRect = scrollContainer.getBoundingClientRect();
117+
const x = this.props.dragInfo.currentOffset.x; // x isn't affected
118+
const y =
119+
this.props.dragInfo.currentOffset.y +
120+
scrollContainer.scrollTop -
121+
containerRect.top -
122+
this.initialScrollTop +
123+
96;
124+
125+
if (this.boxes.length === 0) {
126+
index = 0;
127+
} else {
128+
index = indexForPositionOnList({ x, y }, this.boxes, this.props.isRtl);
129+
}
130+
131+
// Auto scroll
132+
const edgeSize = 30; // Distance from the top/bottom to trigger scroll
133+
if (this.props.dragInfo.currentOffset.y < containerRect.top + edgeSize) {
134+
scrollContainer.scrollTop -= scrollSpeed;
135+
} else if (this.props.dragInfo.currentOffset.y > containerRect.bottom - edgeSize) {
136+
scrollContainer.scrollTop += scrollSpeed;
137+
}
138+
}
139+
return index;
140+
};
141+
};
142+
143+
// When settings changed
144+
addon.settings.addEventListener("change", function () {
145+
scrollSpeed = SPEED_PRESETS[addon.settings.get("scroll-speed")];
146+
});
147+
148+
// Taken from folders addon by GarboMuffin
149+
const selectorListItem = await addon.tab.waitForElement("[class*='selector_list-area']", {
150+
reduxCondition: (state) => state.scratchGui.editorTab.activeTabIndex !== 0 && !state.scratchGui.mode.isPlayerOnly,
151+
});
152+
setReactInternalKey(addon.tab.traps.getInternalKey(selectorListItem));
153+
const sortableHOCInstance = getSortableHOCFromElement(selectorListItem);
154+
verifySortableHOC(sortableHOCInstance, true);
155+
patchSortableHOC(sortableHOCInstance.constructor);
156+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[class*="selector_list-area"] {
2+
scroll-behavior: auto !important;
3+
overflow-anchor: none;
4+
overflow-x: hidden; /* This addon also fixes the horizontal bar appearing while dragging */
5+
}

addons/folders/userscript.js

Lines changed: 12 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
import { escapeHTML } from "../../libraries/common/cs/autoescaper.js";
2+
import {
3+
getSortableHOCFromElement,
4+
verifySortableHOC,
5+
setReactInternalKey,
6+
getReactInternalKey,
7+
} from "../../libraries/common/cs/patch-SortableHOC.js";
28

39
const DIVIDER = "//";
410

@@ -92,8 +98,6 @@ export default async function ({ addon, console, msg }) {
9298
// We run too early, will be set later
9399
let vm;
94100

95-
let reactInternalKey;
96-
97101
let currentSpriteItems;
98102
let currentAssetItems;
99103

@@ -113,37 +117,18 @@ export default async function ({ addon, console, msg }) {
113117
});
114118
};
115119

116-
const getSortableHOCFromElement = (el) => {
117-
let reactInternalInstance;
118-
const nearestSpriteSelector = el.closest("[class*='sprite-selector_sprite-selector']");
119-
if (nearestSpriteSelector) {
120-
reactInternalInstance = nearestSpriteSelector[reactInternalKey].child.sibling;
121-
}
122-
const nearestAssetPanelWrapper = el.closest('[class*="asset-panel_wrapper"]');
123-
if (nearestAssetPanelWrapper) {
124-
reactInternalInstance = nearestAssetPanelWrapper[reactInternalKey];
125-
}
126-
if (reactInternalInstance) {
127-
while (!isSortableHOC(reactInternalInstance.stateNode)) {
128-
reactInternalInstance = reactInternalInstance.child;
129-
}
130-
return reactInternalInstance.stateNode;
131-
}
132-
throw new Error("cannot find SortableHOC");
133-
};
134-
135120
const getBackpackFromElement = (el) => {
136121
const backpackContainer = el.closest('[class*="backpack_backpack-container_"]');
137122
if (!backpackContainer) throw new Error("cannot find Backpack");
138-
let reactInternalInstance = backpackContainer[reactInternalKey];
123+
let reactInternalInstance = backpackContainer[getReactInternalKey()];
139124
while (!isBackpack(reactInternalInstance.stateNode)) {
140125
reactInternalInstance = reactInternalInstance.return;
141126
}
142127
return reactInternalInstance.stateNode;
143128
};
144129

145130
const getSpriteSelectorItemFromElement = (el) => {
146-
let reactInternalInstance = el[reactInternalKey];
131+
let reactInternalInstance = el[getReactInternalKey()];
147132
while (!reactInternalInstance.stateNode?.props?.dragType) {
148133
reactInternalInstance = reactInternalInstance.return;
149134
}
@@ -290,34 +275,6 @@ export default async function ({ addon, console, msg }) {
290275
}
291276
};
292277

293-
const isSortableHOC = (sortableHOCInstance) => {
294-
try {
295-
const SortableHOC = sortableHOCInstance.constructor;
296-
return (
297-
Array.isArray(sortableHOCInstance.props.items) &&
298-
(typeof sortableHOCInstance.props.selectedId === "string" ||
299-
typeof sortableHOCInstance.props.selectedItemIndex === "number") &&
300-
typeof sortableHOCInstance.containerBox !== "undefined" &&
301-
typeof SortableHOC.prototype.handleAddSortable === "function" &&
302-
typeof SortableHOC.prototype.handleRemoveSortable === "function" &&
303-
typeof SortableHOC.prototype.setRef === "function"
304-
);
305-
} catch {
306-
return false;
307-
}
308-
};
309-
310-
const verifySortableHOC = (sortableHOCInstance) => {
311-
const SortableHOC = sortableHOCInstance.constructor;
312-
if (
313-
isSortableHOC(sortableHOCInstance) &&
314-
typeof SortableHOC.prototype.componentDidMount === "undefined" &&
315-
typeof SortableHOC.prototype.componentDidUpdate === "undefined"
316-
)
317-
return;
318-
throw new Error("Can not comprehend SortableHOC");
319-
};
320-
321278
const isSpriteSelectorItem = (spriteSelectorItemInstance) => {
322279
try {
323280
const SpriteSelectorItem = spriteSelectorItemInstance.constructor;
@@ -1510,14 +1467,14 @@ export default async function ({ addon, console, msg }) {
15101467
reduxCondition: (state) => !state.scratchGui.mode.isPlayerOnly,
15111468
});
15121469
vm = addon.tab.traps.vm;
1513-
reactInternalKey = addon.tab.traps.getInternalKey(spriteSelectorItemElement);
1470+
setReactInternalKey(addon.tab.traps.getInternalKey(spriteSelectorItemElement));
15141471
const sortableHOCInstance = getSortableHOCFromElement(spriteSelectorItemElement);
1515-
let reactInternalInstance = spriteSelectorItemElement[reactInternalKey];
1472+
let reactInternalInstance = spriteSelectorItemElement[getReactInternalKey()];
15161473
while (!isSpriteSelectorItem(reactInternalInstance.stateNode)) {
15171474
reactInternalInstance = reactInternalInstance.child;
15181475
}
15191476
const spriteSelectorItemInstance = reactInternalInstance.stateNode;
1520-
verifySortableHOC(sortableHOCInstance);
1477+
verifySortableHOC(sortableHOCInstance, false);
15211478
verifySpriteSelectorItem(spriteSelectorItemInstance);
15221479
verifyVM(vm);
15231480
patchSortableHOC(sortableHOCInstance.constructor, TYPE_SPRITES);
@@ -1532,7 +1489,7 @@ export default async function ({ addon, console, msg }) {
15321489
reduxCondition: (state) => state.scratchGui.editorTab.activeTabIndex !== 0 && !state.scratchGui.mode.isPlayerOnly,
15331490
});
15341491
const sortableHOCInstance = getSortableHOCFromElement(selectorListItem);
1535-
verifySortableHOC(sortableHOCInstance);
1492+
verifySortableHOC(sortableHOCInstance, false);
15361493
patchSortableHOC(sortableHOCInstance.constructor, TYPE_ASSETS);
15371494
sortableHOCInstance.saInitialSetup();
15381495
}

0 commit comments

Comments
 (0)