forked from aminomancer/uc.css.js
-
Notifications
You must be signed in to change notification settings - Fork 0
/
fluentRevealTabs.uc.js
346 lines (320 loc) · 15.4 KB
/
fluentRevealTabs.uc.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
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
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
// ==UserScript==
// @name Fluent Reveal Tabs
// @version 1.1.3
// @author aminomancer
// @homepage https://github.com/aminomancer/uc.css.js
// @description Adds a visual effect to tabs similar to the spotlight gradient effect on Windows 10's start menu tiles. When hovering a tab, a subtle radial gradient is applied under the mouse. Inspired by this [proof of concept](https://www.reddit.com/r/FirefoxCSS/comments/ng5lnt/proof_of_concept_legacy_edge_like_interaction/) by black7375.
// @downloadURL https://cdn.jsdelivr.net/gh/aminomancer/uc.css.js@master/JS/fluentRevealTabs.uc.js
// @updateURL https://cdn.jsdelivr.net/gh/aminomancer/uc.css.js@master/JS/fluentRevealTabs.uc.js
// @license This Source Code Form is subject to the terms of the Creative Commons Attribution-NonCommercial-ShareAlike International License, v. 4.0. If a copy of the CC BY-NC-SA 4.0 was not distributed with this file, You can obtain one at http://creativecommons.org/licenses/by-nc-sa/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA.
// ==/UserScript==
(function() {
class FluentRevealEffect {
// user configuration
static options = {
// whether to show the effect if the tab is selected. this doesn't look
// good with my theme so I set it to false.
showOnSelectedTab: false,
// whether to show the effect on pinned tabs. likewise, doesn't look good
// with my theme but may work with yours.
showOnPinnedTab: false,
// the color of the gradient. default is sort of a faint baby blue.
// you may prefer just white, e.g. hsla(0, 0%, 100%, 0.05)
lightColor: "hsla(224, 100%, 80%, 0.05)",
// how wide the radial gradient is. 50px looks best with my theme, but
// default proton tabs are larger so you may want to try 60 or even 70.
gradientSize: 50,
// whether to show an additional light burst when clicking a tab. I don't
// recommend this since it doesn't play nicely with dragging & dropping if
// you release while your mouse is outside the tab box. I can probably fix
// this issue but I don't think it's a great fit for tabs anyway.
clickEffect: false,
};
/**
* sleep for n ms
* @param {integer} ms (how long to wait)
* @returns a promise resolved after the passed number of milliseconds
*/
static sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// instantiate the handler for a given window
constructor() {
this._options = FluentRevealEffect.options;
gBrowser.tabContainer.addEventListener("TabOpen", e =>
this.applyEffect(e.target.querySelector(".tab-content"), true)
);
gBrowser.tabs.forEach(tab =>
this.applyEffect(tab.querySelector(".tab-content"), true)
);
}
/**
* main event handler. handles all the mouse behavior.
* @param {object} e (event)
*/
handleEvent(e) {
// grab the colors and behavior from the event. this allows us to apply
// different colors/behavior to different elements and makes the script
// more adaptable for future expansion or user extension.
let {
gradientSize,
lightColor,
clickEffect,
} = e.currentTarget.fluentRevealState;
// calculate gradient display coordinates based on mouse and element coords.
let x = e.pageX - this.getOffset(e.currentTarget).left - window.scrollX;
let y = e.pageY - this.getOffset(e.currentTarget).top - window.scrollY;
// the effect is actually applied to the element by setting its
// background-color value to this.
let cssLightEffect = `radial-gradient(circle ${gradientSize}px at ${x}px ${y}px, ${lightColor}, rgba(255,255,255,0)), radial-gradient(circle ${70}px at ${x}px ${y}px, rgba(255,255,255,0), ${lightColor}, rgba(255,255,255,0), rgba(255,255,255,0))`;
switch (e.type) {
case "mousemove":
// if the element is a tab, check if it's selected or pinned and check
// if the user options hide the effect on selected or pinned tabs.
// determines if we should avoid showing the effect on the element at
// the current time.
if (this.shouldClear(e.currentTarget)) {
return this.clearEffect(e.currentTarget);
}
// mousemove events still trigger while the element is clicked. so if
// the click effect is enabled and the element is pressed, we want to
// apply a different effect than we normally would.
this.drawEffect(
e.currentTarget,
x,
y,
lightColor,
gradientSize,
clickEffect && e.currentTarget.fluentRevealState.is_pressed
? cssLightEffect
: null
);
break;
case "mouseleave":
// mouse left the element so remove the background-image property.
this.clearEffect(e.currentTarget);
break;
case "mousedown":
// again, check if it's selected or pinned
if (this.shouldClear(e.currentTarget)) {
return this.clearEffect(e.currentTarget);
}
e.currentTarget.fluentRevealState.is_pressed = true;
this.drawEffect(
e.currentTarget,
x,
y,
lightColor,
gradientSize,
cssLightEffect
);
break;
case "mouseup":
if (this.shouldClear(e.currentTarget)) {
return this.clearEffect(e.currentTarget);
}
e.currentTarget.fluentRevealState.is_pressed = false;
this.drawEffect(e.currentTarget, x, y, lightColor, gradientSize);
break;
}
}
/**
* Reveal Effect
* https://github.com/d2phap/fluent-reveal-effect
*
* MIT License
* Copyright (c) 2018 Duong Dieu Phap
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/**
* main entry point for applying all the script behavior to an element.
* @param {object} element (a DOM node to apply the effect to)
* @param {boolean} isTab (pass true if applying to a child of a tab)
* @param {object} options (an object containing options similar to the
* static options at the top of the script)
*/
applyEffect(element, isTab = false, options = this._options) {
// you may pass an options object when calling this method, but the
// options object passed does not necessarily contain ALL the properties
// of the static options object at the top of the script. if you pass just
// {gradientSize, lightColor} then clickEffect would be undefined rather
// than true or false. undefined is falsy so it's parsed like false. but
// if the default (static) clickEffect option was set to true, then it
// should default to true when you don't pass it, not default to false. so
// we need to set each of these values equal to 1) the option in the
// passed options object if it exists, or 2) the option in the static
// options object. if we just said let {clickEffect, gradientSize,
// lightColor} = options; then any values not passed in the options object
// would default to false. instead we're gonna set each one individually.
// I haven't run into this issue before so please let me know if there's a
// faster/shorter way of doing this.
let { clickEffect } =
options.clickEffect === undefined ? this._options : options;
let { gradientSize } =
options.gradientSize === undefined ? this._options : options;
let { lightColor } =
options.lightColor === undefined ? this._options : options;
// cache the values on the element itself. this is how we can support different
// options for different elements, something the library doesn't support.
element.fluentRevealState = {
clickEffect,
lightColor,
gradientSize,
isTab,
is_pressed: false,
};
// make sure we don't add duplicate event listeners if applyEffect() is
// somehow called more than once on the same element. this shouldn't
// normally happen since the script itself only ever invokes the method
// when a tab is created. but if you want to mess around with the script,
// apply it to additional elements, this is a good safeguard against
// listeners piling up.
if (!element.getAttribute("fluent-reveal-hover")) {
element.setAttribute("fluent-reveal-hover", true);
element.addEventListener("mousemove", this);
element.addEventListener("mouseleave", this);
}
// only set up the click effect if the option is enabled and the element
// doesn't already have a click effect.
if (clickEffect && !element.getAttribute("fluent-reveal-click")) {
element.setAttribute("fluent-reveal-click", true);
element.addEventListener("mousedown", this);
element.addEventListener("mouseup", this);
}
}
/**
* completely remove the script behavior from a given element. isn't actually
* used by the script, but it's here if you ever need it for some reason. usage:
* fluentRevealFx.revertElement(gBrowser.selectedTab.querySelector(".tab-content"))
* @param {object} element (a DOM node)
*/
revertElement(element) {
// this isn't really necessary but just for the sake of completeness...
try {
// try to delete the property
delete element.fluentRevealState;
} catch (e) {
// if it's undeletable (e.g. the element was sealed) then at least negate it.
element.fluentRevealState = null;
}
if (element.getAttribute("fluent-reveal-hover")) {
element.removeAttribute("fluent-reveal-hover");
element.removeEventListener("mousemove", this);
element.removeEventListener("mouseleave", this);
}
if (element.getAttribute("fluent-reveal-click")) {
element.removeAttribute("fluent-reveal-click");
element.removeEventListener("mousedown", this);
element.removeEventListener("mouseup", this);
}
}
/**
* invoked when the mouse leaves an element, or when effects would otherwise
* be applied to a selected/pinned tab if user options prevent it.
* @param {object} element (a DOM node)
*/
clearEffect(element) {
element.fluentRevealState.is_pressed = false;
// the original library memoized the element's computed background-image on
// applyEffect(), and set the inline style's background-image back to the
// memoized background-image when clearing the effect. this would work fine
// if you have total control of the DOM, such as if you were using the
// library for a website you control. but since we're hacking a browser, we
// can't be using inline styles willy-nilly. if we left an inline style
// every time we cleared the effect, it would override firefox's internal
// CSS rules. it would basically mean the background-image of the element
// could only ever be defined by the script. that wouldn't be a problem for
// the script as-is, because we only apply the effect to elements that
// shouldn't ever have a background-image defined by CSS in the first
// place. so instead of doing that we just remove the inline
// background-image property altogether, so the element can go back to
// displaying whatever background-image CSS tells it to.
element.style.removeProperty("background-image");
}
/**
* test whether the effect should be removed/forgone on a given element
* because the element is a selected or pinned tab.
* @param {object} element (a DOM node)
* @returns {boolean} (true if effect should not be shown)
*/
shouldClear(element) {
// if it's not a tab then it never needs to be skipped
if (!element.fluentRevealState.isTab) return false;
// the effect isn't actually applied to the tab itself but to
// .tab-content, so traverse up to the actual tab element which holds
// properties like selected, pinned.
let tab = element.tab || element.closest("tab");
return (
(!this._options.showOnSelectedTab && tab.selected) ||
(!this._options.showOnPinnedTab && tab.pinned)
);
}
/**
* used to calculate the x and y coordinates used in drawing the gradient
* @param {object} element (a DOM node)
* @returns {object} (an object containing top and left coordinates)
*/
getOffset(element) {
return {
top: element.getBoundingClientRect().top,
left: element.getBoundingClientRect().left,
};
}
/**
* finally draw the specified effect on a given element, that is, give the
* element an inline background-image property
* @param {object} element (a DOM node)
* @param {integer} x (x coordinate for gradient center)
* @param {integer} y (y coordinate for gradient center)
* @param {string} lightColor (any color value accepted by CSS, e.g. "#FFF",
* "rgba(125, 125, 125, 0.5)", or
* "hsla(50, 0%, 100%, 0.2)")
* @param {integer} gradientSize (how many pixels wide the gradient should be)
* @param {string} cssLightEffect (technically, any background-image value accepted by
* CSS, but should be a radial-gradient() function,
* surrounded by quotes)
*/
drawEffect(element, x, y, lightColor, gradientSize, cssLightEffect = null) {
if (!element) return;
element.style.backgroundImage =
cssLightEffect ??
`radial-gradient(circle ${gradientSize}px at ${x}px ${y}px, ${lightColor}, rgba(255,255,255,0))`;
}
}
function init() {
// instantiate the class on a global property to share the methods with
// other scripts if desired.
window.fluentRevealFx = new FluentRevealEffect();
}
if (gBrowserInit.delayedStartupFinished) {
init();
} else {
let delayedListener = (subject, topic) => {
if (topic == "browser-delayed-startup-finished" && subject == window) {
Services.obs.removeObserver(delayedListener, topic);
init();
}
};
Services.obs.addObserver(
delayedListener,
"browser-delayed-startup-finished"
);
}
})();