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.
@@ -1,10 +1,11 @@
1
1
  /*!
2
- handy-scroll v2.0.2
2
+ handy-scroll v2.0.4
3
3
  https://amphiluke.github.io/handy-scroll/
4
- (c) 2024 Amphiluke
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
- let h = (n) => `Attribute ‘${n}’ must reference a valid container ‘id’`;
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 = /* @__PURE__ */ new Map();
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" }), e = document.createElement("style");
41
- e.textContent = o, t.appendChild(e), this.#s = document.createElement("div"), this.#s.classList.add("strut"), t.appendChild(this.#s), this.#o = this.attachInternals();
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.#f(), this.update();
45
+ this.#a(), this.#c(), this.#u(), this.#p(), this.update();
45
46
  }
46
47
  disconnectedCallback() {
47
- this.#w(), this.#p(), this.#e = this.#t = null;
48
+ this.#w(), this.#f(), this.#e = this.#t = null;
48
49
  }
49
50
  attributeChangedCallback(t) {
50
- if (this.#i.size) {
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.#p(), this.#u(), this.#f(), this.update();
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(h("owner"));
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(h("viewport"));
71
+ throw new DOMException(n("viewport"));
71
72
  }
72
73
  #u() {
73
- this.#i.set(this.#t, {
74
- scroll: () => this.#v(),
75
- ...this.#t === window ? { resize: () => this.update() } : {}
76
- }), this.#i.set(this, {
77
- scroll: () => {
78
- this.#r && !this.#h && this.#b(), this.#r = !0;
79
- }
80
- }), this.#i.set(this.#e, {
81
- scroll: () => {
82
- this.#l && this.#d(), this.#l = !0;
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.forEach((t, e) => {
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
- #f() {
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
- #p() {
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 e = this.#e.getBoundingClientRect(), i = this.#t === window ? window.innerHeight || document.documentElement.clientHeight : this.#t.getBoundingClientRect().bottom;
118
- t = e.bottom <= i || e.top > i;
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: e } = this.#e, { style: i } = this;
124
- i.width = `${t}px`, this.#t === window && (i.left = `${this.#e.getBoundingClientRect().left}px`), this.#s.style.width = `${e}px`, e > t && (i.height = `${this.offsetHeight - this.clientHeight + 1}px`), this.#d(), this.#v();
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.2",
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.10.0",
40
- "@stylistic/eslint-plugin-js": "^2.8.0",
41
- "eslint": "^9.10.0",
42
- "globals": "^15.9.0",
43
- "vite": "^5.4.5"
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
  }
@@ -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
- #eventHandlers = new Map();
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
- let style = document.createElement("style");
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.#eventHandlers.size) { // handle only dynamic changes when the element is completely connected
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.#eventHandlers.set(this.#viewport, {
116
- scroll: () => this.#recheckLatency(),
117
- ...(this.#viewport === window ? {resize: () => this.update()} : {}),
118
- });
119
- this.#eventHandlers.set(this, {
120
- scroll: () => {
121
- if (this.#syncingOwner && !this.#isLatent) {
122
- this.#syncOwner();
123
- }
124
- // Resume component->owner syncing after the component scrolling has finished
125
- // (it might be temporally disabled by the owner while syncing the component)
126
- this.#syncingOwner = true;
127
- },
128
- });
129
- this.#eventHandlers.set(this.#owner, {
130
- scroll: () => {
131
- if (this.#syncingComponent) {
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
- // Resume owner->component syncing after the owner scrolling has finished
135
- // (it might be temporally disabled by the component while syncing the owner)
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.#eventHandlers.forEach((handlers, el) => {
154
- Object.entries(handlers).forEach(([event, handler]) => el.removeEventListener(event, handler, false));
155
- });
156
- this.#eventHandlers.clear();
152
+ this.#eventController?.abort();
153
+ this.#eventController = null;
157
154
  }
158
155
 
159
156
  #addResizeObserver() {