handy-scroll 2.0.2 → 2.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/handy-scroll.mjs +32 -41
- package/package.json +6 -6
- package/src/handy-scroll.mjs +39 -42
package/dist/handy-scroll.mjs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
handy-scroll v2.0.
|
|
2
|
+
handy-scroll v2.0.4
|
|
3
3
|
https://amphiluke.github.io/handy-scroll/
|
|
4
|
-
(c)
|
|
4
|
+
(c) 2025 Amphiluke
|
|
5
5
|
*/
|
|
6
|
-
const o = ':host{bottom:0;min-height:17px;overflow:auto;position:fixed}.strut{height:1px;overflow:hidden;pointer-events:none;&:before{content:" "}}:host,.strut{font-size:1px;line-height:0;margin:0;padding:0}:host(:state(latent)){bottom:110vh;.strut:before{content:" "}}:host([viewport]:not([hidden])){display:block}:host([viewport]){position:sticky}:host([viewport]:state(latent)){position:fixed}';
|
|
7
|
-
|
|
6
|
+
const o = ':host{bottom:0;min-height:17px;overflow:auto;position:fixed}.strut{height:1px;overflow:hidden;pointer-events:none;&:before{content:" "}}:host,.strut{font-size:1px;line-height:0;margin:0;padding:0}:host(:state(latent)){bottom:110vh;.strut:before{content:" "}}:host([viewport]:not([hidden])){display:block}:host([viewport]){position:sticky}:host([viewport]:state(latent)){position:fixed}', h = new CSSStyleSheet();
|
|
7
|
+
h.replaceSync(o);
|
|
8
|
+
let n = (s) => `Attribute ‘${s}’ must reference a valid container ‘id’`;
|
|
8
9
|
class r extends HTMLElement {
|
|
9
10
|
static get observedAttributes() {
|
|
10
11
|
return ["owner", "viewport", "hidden"];
|
|
@@ -13,7 +14,7 @@ class r extends HTMLElement {
|
|
|
13
14
|
#t = null;
|
|
14
15
|
#e = null;
|
|
15
16
|
#s = null;
|
|
16
|
-
#i =
|
|
17
|
+
#i = null;
|
|
17
18
|
#n = null;
|
|
18
19
|
#r = !0;
|
|
19
20
|
#l = !0;
|
|
@@ -37,28 +38,28 @@ class r extends HTMLElement {
|
|
|
37
38
|
}
|
|
38
39
|
constructor() {
|
|
39
40
|
super();
|
|
40
|
-
let t = this.attachShadow({ mode: "open" })
|
|
41
|
-
|
|
41
|
+
let t = this.attachShadow({ mode: "open" });
|
|
42
|
+
t.adoptedStyleSheets = [h], this.#s = document.createElement("div"), this.#s.classList.add("strut"), t.appendChild(this.#s), this.#o = this.attachInternals();
|
|
42
43
|
}
|
|
43
44
|
connectedCallback() {
|
|
44
|
-
this.#a(), this.#c(), this.#u(), this.#
|
|
45
|
+
this.#a(), this.#c(), this.#u(), this.#p(), this.update();
|
|
45
46
|
}
|
|
46
47
|
disconnectedCallback() {
|
|
47
|
-
this.#w(), this.#
|
|
48
|
+
this.#w(), this.#f(), this.#e = this.#t = null;
|
|
48
49
|
}
|
|
49
50
|
attributeChangedCallback(t) {
|
|
50
|
-
if (this.#i
|
|
51
|
+
if (this.#i) {
|
|
51
52
|
if (t === "hidden") {
|
|
52
53
|
this.hasAttribute("hidden") || this.update();
|
|
53
54
|
return;
|
|
54
55
|
}
|
|
55
|
-
t === "owner" ? this.#a() : t === "viewport" && this.#c(), this.#w(), this.#
|
|
56
|
+
t === "owner" ? this.#a() : t === "viewport" && this.#c(), this.#w(), this.#f(), this.#u(), this.#p(), this.update();
|
|
56
57
|
}
|
|
57
58
|
}
|
|
58
59
|
#a() {
|
|
59
60
|
let t = this.getAttribute("owner");
|
|
60
61
|
if (this.#e = document.getElementById(t), !this.#e)
|
|
61
|
-
throw new DOMException(
|
|
62
|
+
throw new DOMException(n("owner"));
|
|
62
63
|
}
|
|
63
64
|
#c() {
|
|
64
65
|
if (!this.hasAttribute("viewport")) {
|
|
@@ -67,40 +68,30 @@ class r extends HTMLElement {
|
|
|
67
68
|
}
|
|
68
69
|
let t = this.getAttribute("viewport");
|
|
69
70
|
if (this.#t = document.getElementById(t), !this.#t)
|
|
70
|
-
throw new DOMException(
|
|
71
|
+
throw new DOMException(n("viewport"));
|
|
71
72
|
}
|
|
72
73
|
#u() {
|
|
73
|
-
this.#i
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
focusin: () => {
|
|
85
|
-
setTimeout(() => {
|
|
86
|
-
this.isConnected && this.#d();
|
|
87
|
-
}, 0);
|
|
88
|
-
}
|
|
89
|
-
}), this.#i.forEach((t, e) => {
|
|
90
|
-
Object.entries(t).forEach(([i, s]) => e.addEventListener(i, s, !1));
|
|
91
|
-
});
|
|
74
|
+
this.#i = new AbortController();
|
|
75
|
+
let t = { signal: this.#i.signal };
|
|
76
|
+
this.#t.addEventListener("scroll", () => this.#v(), t), this.#t === window && this.#t.addEventListener("resize", () => this.update(), t), this.addEventListener("scroll", () => {
|
|
77
|
+
this.#r && !this.#h && this.#b(), this.#r = !0;
|
|
78
|
+
}, t), this.#e.addEventListener("scroll", () => {
|
|
79
|
+
this.#l && this.#d(), this.#l = !0;
|
|
80
|
+
}, t), this.#e.addEventListener("focusin", () => {
|
|
81
|
+
setTimeout(() => {
|
|
82
|
+
this.isConnected && this.#d();
|
|
83
|
+
}, 0);
|
|
84
|
+
}, t);
|
|
92
85
|
}
|
|
93
86
|
#w() {
|
|
94
|
-
this.#i
|
|
95
|
-
Object.entries(t).forEach(([i, s]) => e.removeEventListener(i, s, !1));
|
|
96
|
-
}), this.#i.clear();
|
|
87
|
+
this.#i?.abort(), this.#i = null;
|
|
97
88
|
}
|
|
98
|
-
#
|
|
89
|
+
#p() {
|
|
99
90
|
this.#t !== window && (this.#n = new ResizeObserver(([t]) => {
|
|
100
91
|
t.contentBoxSize?.[0]?.inlineSize && this.update();
|
|
101
92
|
}), this.#n.observe(this.#t));
|
|
102
93
|
}
|
|
103
|
-
#
|
|
94
|
+
#f() {
|
|
104
95
|
this.#n?.disconnect(), this.#n = null;
|
|
105
96
|
}
|
|
106
97
|
#b() {
|
|
@@ -114,14 +105,14 @@ class r extends HTMLElement {
|
|
|
114
105
|
#v() {
|
|
115
106
|
let t = this.scrollWidth <= this.offsetWidth;
|
|
116
107
|
if (!t) {
|
|
117
|
-
let
|
|
118
|
-
t =
|
|
108
|
+
let i = this.#e.getBoundingClientRect(), e = this.#t === window ? window.innerHeight || document.documentElement.clientHeight : this.#t.getBoundingClientRect().bottom;
|
|
109
|
+
t = i.bottom <= e || i.top > e;
|
|
119
110
|
}
|
|
120
111
|
this.#h !== t && (this.#h = t);
|
|
121
112
|
}
|
|
122
113
|
update() {
|
|
123
|
-
let { clientWidth: t, scrollWidth:
|
|
124
|
-
|
|
114
|
+
let { clientWidth: t, scrollWidth: i } = this.#e, { style: e } = this;
|
|
115
|
+
e.width = `${t}px`, this.#t === window && (e.left = `${this.#e.getBoundingClientRect().left}px`), this.#s.style.width = `${i}px`, i > t && (e.height = `${this.offsetHeight - this.clientHeight + 1}px`), this.#d(), this.#v();
|
|
125
116
|
}
|
|
126
117
|
}
|
|
127
118
|
customElements.define("handy-scroll", r);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "handy-scroll",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.4",
|
|
4
4
|
"description": "Handy dependency-free floating scrollbar web component",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -36,10 +36,10 @@
|
|
|
36
36
|
},
|
|
37
37
|
"homepage": "https://amphiluke.github.io/handy-scroll/",
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@eslint/js": "^9.
|
|
40
|
-
"@stylistic/eslint-plugin-js": "^2.
|
|
41
|
-
"eslint": "^9.
|
|
42
|
-
"globals": "^
|
|
43
|
-
"vite": "^
|
|
39
|
+
"@eslint/js": "^9.24.0",
|
|
40
|
+
"@stylistic/eslint-plugin-js": "^4.2.0",
|
|
41
|
+
"eslint": "^9.24.0",
|
|
42
|
+
"globals": "^16.0.0",
|
|
43
|
+
"vite": "^6.2.5"
|
|
44
44
|
}
|
|
45
45
|
}
|
package/src/handy-scroll.mjs
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import css from "./handy-scroll.css?inline";
|
|
2
2
|
|
|
3
|
+
const stylesheet = new CSSStyleSheet();
|
|
4
|
+
stylesheet.replaceSync(css);
|
|
5
|
+
|
|
3
6
|
let getAttributeErrorMessage = (attribute) => `Attribute ‘${attribute}’ must reference a valid container ‘id’`;
|
|
4
7
|
|
|
5
8
|
class HandyScroll extends HTMLElement {
|
|
@@ -13,7 +16,7 @@ class HandyScroll extends HTMLElement {
|
|
|
13
16
|
#owner = null;
|
|
14
17
|
#strut = null;
|
|
15
18
|
|
|
16
|
-
#
|
|
19
|
+
#eventController = null;
|
|
17
20
|
#resizeObserver = null;
|
|
18
21
|
|
|
19
22
|
#syncingOwner = true;
|
|
@@ -44,9 +47,7 @@ class HandyScroll extends HTMLElement {
|
|
|
44
47
|
super();
|
|
45
48
|
let shadowRoot = this.attachShadow({mode: "open"});
|
|
46
49
|
|
|
47
|
-
|
|
48
|
-
style.textContent = css;
|
|
49
|
-
shadowRoot.appendChild(style);
|
|
50
|
+
shadowRoot.adoptedStyleSheets = [stylesheet];
|
|
50
51
|
|
|
51
52
|
this.#strut = document.createElement("div");
|
|
52
53
|
this.#strut.classList.add("strut");
|
|
@@ -70,7 +71,7 @@ class HandyScroll extends HTMLElement {
|
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
attributeChangedCallback(name) {
|
|
73
|
-
if (!this.#
|
|
74
|
+
if (!this.#eventController) { // handle only dynamic changes when the element is completely connected
|
|
74
75
|
return;
|
|
75
76
|
}
|
|
76
77
|
if (name === "hidden") {
|
|
@@ -112,48 +113,44 @@ class HandyScroll extends HTMLElement {
|
|
|
112
113
|
}
|
|
113
114
|
|
|
114
115
|
#addEventHandlers() {
|
|
115
|
-
this.#
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
this.#
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
116
|
+
this.#eventController = new AbortController();
|
|
117
|
+
let options = {signal: this.#eventController.signal};
|
|
118
|
+
|
|
119
|
+
this.#viewport.addEventListener("scroll", () => this.#recheckLatency(), options);
|
|
120
|
+
if (this.#viewport === window) {
|
|
121
|
+
this.#viewport.addEventListener("resize", () => this.update(), options);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
this.addEventListener("scroll", () => {
|
|
125
|
+
if (this.#syncingOwner && !this.#isLatent) {
|
|
126
|
+
this.#syncOwner();
|
|
127
|
+
}
|
|
128
|
+
// Resume component->owner syncing after the component scrolling has finished
|
|
129
|
+
// (it might be temporally disabled by the owner while syncing the component)
|
|
130
|
+
this.#syncingOwner = true;
|
|
131
|
+
}, options);
|
|
132
|
+
|
|
133
|
+
this.#owner.addEventListener("scroll", () => {
|
|
134
|
+
if (this.#syncingComponent) {
|
|
135
|
+
this.#syncComponent();
|
|
136
|
+
}
|
|
137
|
+
// Resume owner->component syncing after the owner scrolling has finished
|
|
138
|
+
// (it might be temporally disabled by the component while syncing the owner)
|
|
139
|
+
this.#syncingComponent = true;
|
|
140
|
+
}, options);
|
|
141
|
+
this.#owner.addEventListener("focusin", () => {
|
|
142
|
+
setTimeout(() => {
|
|
143
|
+
// The widget might be destroyed before the timer is triggered (issue #14)
|
|
144
|
+
if (this.isConnected) {
|
|
132
145
|
this.#syncComponent();
|
|
133
146
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
this.#syncingComponent = true;
|
|
137
|
-
},
|
|
138
|
-
focusin: () => {
|
|
139
|
-
setTimeout(() => {
|
|
140
|
-
// The widget might be destroyed before the timer is triggered (issue #14)
|
|
141
|
-
if (this.isConnected) {
|
|
142
|
-
this.#syncComponent();
|
|
143
|
-
}
|
|
144
|
-
}, 0);
|
|
145
|
-
},
|
|
146
|
-
});
|
|
147
|
-
this.#eventHandlers.forEach((handlers, el) => {
|
|
148
|
-
Object.entries(handlers).forEach(([event, handler]) => el.addEventListener(event, handler, false));
|
|
149
|
-
});
|
|
147
|
+
}, 0);
|
|
148
|
+
}, options);
|
|
150
149
|
}
|
|
151
150
|
|
|
152
151
|
#removeEventHandlers() {
|
|
153
|
-
this.#
|
|
154
|
-
|
|
155
|
-
});
|
|
156
|
-
this.#eventHandlers.clear();
|
|
152
|
+
this.#eventController?.abort();
|
|
153
|
+
this.#eventController = null;
|
|
157
154
|
}
|
|
158
155
|
|
|
159
156
|
#addResizeObserver() {
|