-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathGlobal moving support userscript.user.js
More file actions
279 lines (223 loc) · 11.8 KB
/
Copy pathGlobal moving support userscript.user.js
File metadata and controls
279 lines (223 loc) · 11.8 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
// ==UserScript==
// @name Global moving support userscript
// @namespace https://github.com/JumpJets/Global-moving-support-userscript
// @version 2.0
// @description Configure mouse keys, speed multipliers, e.t.c. in script.
// @author XCanG
// @match *://*/*
// @grant none
// ==/UserScript==
(() => {
"use strict";
//? Configure variables below:
let us_hotkey = 2, //* Toggle hotkey. Mouse keys: LMB: 1, RMB: 2, MMB: 4, HistoryFwd: 16, HistBck: 8
us_speed_multiplier = 1.5, //* Speed multiplier (per pixel). Use negative value if you want to inverse scrolling direction.
us_allow_text_selection = true, //* Allow text selection when hotkey is LMB.
us_use_alternative_key = true, //* Enable alternative key mode. This mode allow you to hold another key (if your main key is LMB, then alternative key is RMB, in other cases it LMB) to further modify your scrolling speed. It can be either faster (if multiplier > 1) or slower (if < 1).
us_alternative_key_speed_multiplier = 5, //* Alternative mode speed multiplier. If > 1 then increase speed, if < 1 then decrease.
us_alternative_hotkey = us_hotkey === 1 ? 2 : 1,
us_inertia_mode = false, //* Enable inertia mode. Basically it allow you to continue scrolling after you release button in case your last scroll was fast. Similar functionality have any touch devices (mobile, tablet, e.t.c.).
us_inertia_friction = 1.75, //* Friction/negative acceleration of inertia. Smaller value will make inertia longer and further and vice versa. Should be > 0.
//! Don't touch variables below, they are unconfigurable.
us_moved = false, // True when mouse moved after hotkey was pressed
us_prevent_contextmenu = false, // Prevent context menu to show, it only work when RMB is scroll button and on mouseup you need to prevent menu from showing
us_event_mouse_leave = false, // True when mouse leave viewport
us_target, // Current DOM node where scroll happens
us_y = 0, // Coordinates
us_x = 0,
us_dy = 0, // Delta coordinates (for scroll position and acceleration on mouseup)
us_dx = 0,
us_relative_y_multiplier = 0, // A multiplier based on element total height / width divided to viewport height / width
us_relative_x_multiplier = 0,
us_scroll_pos_y = 0, // Scroll position (state) of element in either direction
us_scroll_pos_x = 0,
us_scrollBy_y = 0, // Variables for target.scrollBy()
us_scrollBy_x = 0,
us_interval, // Store interval for clearing when inertia mode is used
us_inertia_dy = 0,
us_inertia_dx = 0,
us_tickrate = 16.67, // Tickrate (ms, 1 / fps)
us_self_closed_tags = ["AREA", "BASE", "BR", "COL", "EMBED", "HR", "IMG", "INPUT", "LINK", "META", "PARAM", "SOURCE", "TRACK", "WBR"];
const scroll_mousemove = (e, { _tg = undefined, _up = false } = {}) => {
/* Event on mousemove, main scrolling function
* _tg = parent target when navigate up in DOM
* _up = value to determine that previous coordinates need to be reused from child element */
e?.preventDefault?.();
// console.debug("scroll mousemove, target?:", _tg, "up:", _up);
if ((e?.buttons & us_hotkey) !== us_hotkey) document.removeEventListener("mousemove", scroll_mousemove, { capture: true });
us_target = _tg ?? e?.target;
us_scrollBy_y = 0;
us_scrollBy_x = 0;
let vertical_scroll = false, // Variables to detect if element is scrollable in either direction
horizontal_scroll = false;
// scroll size = total element size, client size = only visible size. if smaller, then there is scroll in that direction
if ((us_target?.scrollHeight - us_target?.clientHeight > 6) && us_target?.clientHeight !== 0) vertical_scroll = true;
if ((us_target?.scrollWidth - us_target?.clientWidth > 6) && us_target?.clientHeight !== 0) horizontal_scroll = true;
// If there is no scroll, then we reinitialize move event with parent as target until we hit <html></html> node
if (!vertical_scroll && !horizontal_scroll && us_target !== document.documentElement) {
return scroll_mousemove(e, { _tg: us_target?.parentElement, _up: _up });
} else if (!vertical_scroll && !horizontal_scroll) {
return false;
}
// Set initial position variables, for any parent element with scroll they will be reused
if (!_up) {
us_dy = (e?.clientY ?? 0) - us_y;
us_dx = (e?.clientX ?? 0) - us_x;
us_y = e?.clientY ?? 0;
us_x = e?.clientX ?? 0;
}
// Set directional multipliers, the more to scroll, the faster scrolling step will be
us_relative_y_multiplier = us_target?.scrollHeight / us_target?.clientHeight || 1;
us_relative_x_multiplier = us_target?.scrollWidth / us_target?.clientWidth || 1;
// For each direction set scroll position of current element if it scrollable, otherwise set start / end marker, so that we do not try to scroll element later
vertical_scroll ? us_scroll_pos_y = us_target?.scrollTop / (us_target?.scrollHeight - us_target?.clientHeight) || 0 : us_dy < 0 ? us_scroll_pos_y = 0 : us_scroll_pos_y = 1;
horizontal_scroll ? us_scroll_pos_x = us_target?.scrollLeft / (us_target?.scrollWidth - us_target?.clientWidth) || 0 : us_dx < 0 ? us_scroll_pos_x = 0 : us_scroll_pos_x = 1;
// Calculate scrollBy value based on multiplier * relative multiplier * delta position (with direction)
// When alternative hotkey is pressed also multiplied to alternative key speed multiplier
if (us_use_alternative_key && ((e?.buttons & us_alternative_hotkey) === us_alternative_hotkey)) {
us_scrollBy_y = Math.round(us_speed_multiplier * us_alternative_key_speed_multiplier * us_relative_y_multiplier * us_dy);
us_scrollBy_x = Math.round(us_speed_multiplier * us_alternative_key_speed_multiplier * us_relative_x_multiplier * us_dx);
} else {
us_scrollBy_y = Math.round(us_speed_multiplier * us_relative_y_multiplier * us_dy);
us_scrollBy_x = Math.round(us_speed_multiplier * us_relative_x_multiplier * us_dx);
}
// If scrolled enough, prevent RMB menu to open, otherwise on mouseup contextmenu wont be prevented
if ((vertical_scroll || horizontal_scroll) && (us_scrollBy_y > 2 || us_scrollBy_y < -2 || us_scrollBy_x > 2 || us_scrollBy_x < -2)) us_prevent_contextmenu = true;
// Initialize scrolling
us_target?.scrollBy?.(us_scrollBy_x, us_scrollBy_y);
// If target is not <html></html> and scroll reached start / end, reinitialize function for parent element and set flag _up = true to reuse current positions
if (
us_target !== document.documentElement
&& (
us_scroll_pos_y === 0 && us_scrollBy_y < 0
|| us_scroll_pos_x === 0 && us_scrollBy_x < 0
|| us_scroll_pos_y === 1 && us_scrollBy_y > 0
|| us_scroll_pos_x === 1 && us_scrollBy_x > 0
)
&& us_target?.parentElement
) scroll_mousemove(e, { _tg: us_target?.parentElement, _up: true });
}
const scroll_mousedown = (e) => {
/* Statup event */
// Check if text selection is allowed and LMB is pressed while target have text nodes
if (
us_allow_text_selection
&& ((e?.buttons & 1) === 1)
&& (
Array.from(e?.target?.children ?? []).filter(el => !us_self_closed_tags.includes(el?.tagName)).length === 0 // If node is simple node without nested elements
&& Array.from(e?.target?.childNodes ?? []).map(el => el.nodeType).includes(Node.TEXT_NODE)
)
) return false;
if (us_moved && (e?.buttons & 1) === 1) e.preventDefault();
// console.debug("scroll mousedown", e.buttons);
// Create mousemove event
if ((e?.buttons & us_hotkey) === us_hotkey) {
us_moved = true;
us_event_mouse_leave = false;
us_y = e?.clientY ?? 0;
us_x = e?.clientX ?? 0;
us_dy = 0;
us_dx = 0;
us_scrollBy_y = 0;
us_scrollBy_x = 0;
if (((e?.buttons & 1) === 1) || ((e?.buttons & 4) === 4)) e.preventDefault();
document.addEventListener("mousemove", scroll_mousemove, { capture: true });
// Clear inertia interval if it used and still active after previous release while new event was created
if (us_inertia_mode && us_interval) clearInterval(us_interval);
}
}
const scroll_mouseup = (e) => {
/* Stop event
* NOTE: e.buttons = current state of pressed mouse buttons AFTER event fired
* e.button = mouse button, that being unpressed BEFORE event fired */
// console.debug("scroll mouseup", us_moved);
if (us_moved && (e?.buttons & us_hotkey) !== us_hotkey) {
// Prevent autoscroll from MMB (1) and contextmenu from other buttons when RMB is on hold (2, only prevent if scrolled)
if (e?.button === 1 || (e?.button !== 2 && us_prevent_contextmenu)) {
e.preventDefault();
us_prevent_contextmenu = false;
}
document.removeEventListener("mousemove", scroll_mousemove, { capture: true });
setTimeout(() => { us_moved = false }, 10);
us_x = 0;
us_y = 0;
// If inertia mode is enabled, continue scroll until threshold was reached (< .5 delta speed)
if (us_inertia_mode) {
us_inertia_dy = us_scrollBy_y;
us_inertia_dx = us_scrollBy_x;
us_interval = setInterval(() => {
// Threshold
if ((us_inertia_dy > -.5 && us_inertia_dy < .5) && (us_inertia_dx > -.5 && us_inertia_dx < .5)) clearInterval(us_interval);
us_inertia_dy -= us_inertia_dy * us_inertia_friction / us_tickrate;
us_inertia_dx -= us_inertia_dx * us_inertia_friction / us_tickrate;
us_target.scrollBy(Math.round(us_inertia_dx), Math.round(us_inertia_dy));
}, us_tickrate);
}
}
}
const scroll_prevent_contextmenu = (e) => {
/* Supplementary event when scroll hotkey is RMB and it trigger this event */
if (us_moved && us_prevent_contextmenu && ((e?.buttons & us_alternative_hotkey) !== us_alternative_hotkey)) {
// console.debug("scroll prevent contextmenu during fast scroll w/RMB");
e.preventDefault();
us_prevent_contextmenu = false;
us_moved = false;
} else if (us_moved && us_use_alternative_key && ((e?.buttons & us_alternative_hotkey) === us_alternative_hotkey)) {
// console.debug("scroll prevent contextmenu during RMB scroll");
e.preventDefault();
}
}
const scroll_mouseleave = (e) => {
/* Event when mouse leave viewport */
if (us_moved && !us_event_mouse_leave) {
// console.debug("scroll mouseleave");
us_event_mouse_leave = true;
}
}
const scroll_mouseenter = (e) => {
/* Event when mouse back into viewport and if buttons still pressed, then keep mousemove event */
const button =
us_hotkey === 1 ? 0
: us_hotkey === 2 ? 2
: us_hotkey === 4 ? 1
: us_hotkey === 8 ? 3
: 4;
if (us_moved && us_event_mouse_leave && e?.button !== button) {
// console.debug("scroll mouseenter");
document.removeEventListener("mousemove", scroll_mousemove, { capture: true });
us_x = 0;
us_y = 0;
us_prevent_contextmenu = false;
us_event_mouse_leave = false;
us_moved = false;
}
}
const set_tickrate = async () => {
/* Detecting tickrate (1 / FPS) */
us_tickrate = +(
Math.round(
await new Promise(resolve =>
requestAnimationFrame(t1 =>
requestAnimationFrame(t2 => resolve(t2 - t1))
)
)
+ "e+2")
+ "e-2")
}
// Adding events on page, e.persisted indicate page loaded from cache
window.addEventListener("pageshow", async (e) => {
await set_tickrate();
document.addEventListener("mousedown", scroll_mousedown, { capture: true });
document.addEventListener("mouseup", scroll_mouseup, { capture: true });
if (us_hotkey === 2 || (us_hotkey === 1 && us_use_alternative_key)) document.addEventListener("contextmenu", scroll_prevent_contextmenu, { capture: false });
document.addEventListener("mouseleave", scroll_mouseleave, { capture: false });
document.addEventListener("mouseenter", scroll_mouseenter, { capture: false });
});
window.addEventListener("pagehide", (e) => {
document.removeEventListener("mousedown", scroll_mousedown);
document.removeEventListener("mouseup", scroll_mouseup);
document.removeEventListener("mouseleave", scroll_mouseleave);
document.removeEventListener("mouseenter", scroll_mouseenter);
if (us_hotkey === 2 || (us_hotkey === 1 && us_use_alternative_key)) document.removeEventListener("contextmenu", scroll_prevent_contextmenu);
});
})();