handy-scroll 2.0.1 → 2.0.3

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/README.md CHANGED
@@ -29,6 +29,12 @@ If you don’t use bundlers, just import the component as a module in your HTML
29
29
  <script type="module" src="https://esm.run/handy-scroll"></script>
30
30
  ```
31
31
 
32
+ or in your ES modules:
33
+
34
+ ```javascript
35
+ import "https://esm.run/handy-scroll";
36
+ ```
37
+
32
38
  ## Standard usage
33
39
 
34
40
  Drop the custom element `<handy-scroll>` where you need in your markup and link the component to the horizontally-scrollable target using the `owner` attribute:
@@ -1,19 +1,19 @@
1
1
  /*!
2
- handy-scroll v2.0.0
2
+ handy-scroll v2.0.3
3
3
  https://amphiluke.github.io/handy-scroll/
4
4
  (c) 2024 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]){display:block;position:sticky}:host([viewport]:state(latent)){position:fixed}';
7
- let h = (n) => `Attribute ‘${n}’ must reference a valid container ‘id’`;
8
- class r extends HTMLElement {
6
+ const h = ':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 n = (s) => `Attribute ‘${s}’ must reference a valid container ‘id’`;
8
+ class o extends HTMLElement {
9
9
  static get observedAttributes() {
10
- return ["owner", "viewport"];
10
+ return ["owner", "viewport", "hidden"];
11
11
  }
12
12
  #o = null;
13
13
  #t = null;
14
14
  #e = null;
15
15
  #s = null;
16
- #i = /* @__PURE__ */ new Map();
16
+ #i = null;
17
17
  #n = null;
18
18
  #r = !0;
19
19
  #l = !0;
@@ -38,21 +38,27 @@ class r extends HTMLElement {
38
38
  constructor() {
39
39
  super();
40
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
+ e.textContent = h, t.appendChild(e), this.#s = document.createElement("div"), this.#s.classList.add("strut"), t.appendChild(this.#s), this.#o = this.attachInternals();
42
42
  }
43
43
  connectedCallback() {
44
- this.#a(), this.#c(), this.#u(), this.#f(), this.update();
44
+ this.#a(), this.#c(), this.#u(), this.#p(), this.update();
45
45
  }
46
46
  disconnectedCallback() {
47
- this.#w(), this.#p(), this.#e = this.#t = null;
47
+ this.#w(), this.#f(), this.#e = this.#t = null;
48
48
  }
49
49
  attributeChangedCallback(t) {
50
- this.#i.size && (t === "owner" ? this.#a() : t === "viewport" && this.#c(), this.#w(), this.#p(), this.#u(), this.#f(), this.update());
50
+ if (this.#i) {
51
+ if (t === "hidden") {
52
+ this.hasAttribute("hidden") || this.update();
53
+ return;
54
+ }
55
+ t === "owner" ? this.#a() : t === "viewport" && this.#c(), this.#w(), this.#f(), this.#u(), this.#p(), this.update();
56
+ }
51
57
  }
52
58
  #a() {
53
59
  let t = this.getAttribute("owner");
54
60
  if (this.#e = document.getElementById(t), !this.#e)
55
- throw new DOMException(h("owner"));
61
+ throw new DOMException(n("owner"));
56
62
  }
57
63
  #c() {
58
64
  if (!this.hasAttribute("viewport")) {
@@ -61,40 +67,30 @@ class r extends HTMLElement {
61
67
  }
62
68
  let t = this.getAttribute("viewport");
63
69
  if (this.#t = document.getElementById(t), !this.#t)
64
- throw new DOMException(h("viewport"));
70
+ throw new DOMException(n("viewport"));
65
71
  }
66
72
  #u() {
67
- this.#i.set(this.#t, {
68
- scroll: () => this.#v(),
69
- ...this.#t === window ? { resize: () => this.update() } : {}
70
- }), this.#i.set(this, {
71
- scroll: () => {
72
- this.#r && !this.#h && this.#b(), this.#r = !0;
73
- }
74
- }), this.#i.set(this.#e, {
75
- scroll: () => {
76
- this.#l && this.#d(), this.#l = !0;
77
- },
78
- focusin: () => {
79
- setTimeout(() => {
80
- this.isConnected && this.#d();
81
- }, 0);
82
- }
83
- }), this.#i.forEach((t, e) => {
84
- Object.entries(t).forEach(([i, s]) => e.addEventListener(i, s, !1));
85
- });
73
+ this.#i = new AbortController();
74
+ let t = { signal: this.#i.signal };
75
+ this.#t.addEventListener("scroll", () => this.#v(), t), this.#t === window && this.#t.addEventListener("resize", () => this.update(), t), this.addEventListener("scroll", () => {
76
+ this.#r && !this.#h && this.#b(), this.#r = !0;
77
+ }, t), this.#e.addEventListener("scroll", () => {
78
+ this.#l && this.#d(), this.#l = !0;
79
+ }, t), this.#e.addEventListener("focusin", () => {
80
+ setTimeout(() => {
81
+ this.isConnected && this.#d();
82
+ }, 0);
83
+ }, t);
86
84
  }
87
85
  #w() {
88
- this.#i.forEach((t, e) => {
89
- Object.entries(t).forEach(([i, s]) => e.removeEventListener(i, s, !1));
90
- }), this.#i.clear();
86
+ this.#i?.abort(), this.#i = null;
91
87
  }
92
- #f() {
88
+ #p() {
93
89
  this.#t !== window && (this.#n = new ResizeObserver(([t]) => {
94
90
  t.contentBoxSize?.[0]?.inlineSize && this.update();
95
91
  }), this.#n.observe(this.#t));
96
92
  }
97
- #p() {
93
+ #f() {
98
94
  this.#n?.disconnect(), this.#n = null;
99
95
  }
100
96
  #b() {
@@ -118,7 +114,7 @@ class r extends HTMLElement {
118
114
  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();
119
115
  }
120
116
  }
121
- customElements.define("handy-scroll", r);
117
+ customElements.define("handy-scroll", o);
122
118
  export {
123
- r as default
119
+ o as default
124
120
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "handy-scroll",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
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.9.1",
40
- "@stylistic/eslint-plugin-js": "^2.7.2",
41
- "eslint": "^9.9.1",
42
- "globals": "^15.9.0",
43
- "vite": "^5.4.2"
39
+ "@eslint/js": "^9.12.0",
40
+ "@stylistic/eslint-plugin-js": "^2.9.0",
41
+ "eslint": "^9.12.0",
42
+ "globals": "^15.10.0",
43
+ "vite": "^5.4.8"
44
44
  }
45
45
  }
@@ -31,8 +31,11 @@
31
31
  }
32
32
  }
33
33
 
34
- :host([viewport]) {
34
+ :host([viewport]:not([hidden])) {
35
35
  display: block;
36
+ }
37
+
38
+ :host([viewport]) {
36
39
  position: sticky;
37
40
  }
38
41
 
@@ -4,7 +4,7 @@ let getAttributeErrorMessage = (attribute) => `Attribute ‘${attribute}’ must
4
4
 
5
5
  class HandyScroll extends HTMLElement {
6
6
  static get observedAttributes() {
7
- return ["owner", "viewport"];
7
+ return ["owner", "viewport", "hidden"];
8
8
  }
9
9
 
10
10
  #internals = null;
@@ -13,7 +13,7 @@ class HandyScroll extends HTMLElement {
13
13
  #owner = null;
14
14
  #strut = null;
15
15
 
16
- #eventHandlers = new Map();
16
+ #eventController = null;
17
17
  #resizeObserver = null;
18
18
 
19
19
  #syncingOwner = true;
@@ -70,7 +70,13 @@ class HandyScroll extends HTMLElement {
70
70
  }
71
71
 
72
72
  attributeChangedCallback(name) {
73
- if (!this.#eventHandlers.size) { // handle only dynamic changes when the element is completely connected
73
+ if (!this.#eventController) { // handle only dynamic changes when the element is completely connected
74
+ return;
75
+ }
76
+ if (name === "hidden") {
77
+ if (!this.hasAttribute("hidden")) {
78
+ this.update();
79
+ }
74
80
  return;
75
81
  }
76
82
  if (name === "owner") {
@@ -106,48 +112,44 @@ class HandyScroll extends HTMLElement {
106
112
  }
107
113
 
108
114
  #addEventHandlers() {
109
- this.#eventHandlers.set(this.#viewport, {
110
- scroll: () => this.#recheckLatency(),
111
- ...(this.#viewport === window ? {resize: () => this.update()} : {}),
112
- });
113
- this.#eventHandlers.set(this, {
114
- scroll: () => {
115
- if (this.#syncingOwner && !this.#isLatent) {
116
- this.#syncOwner();
117
- }
118
- // Resume component->owner syncing after the component scrolling has finished
119
- // (it might be temporally disabled by the owner while syncing the component)
120
- this.#syncingOwner = true;
121
- },
122
- });
123
- this.#eventHandlers.set(this.#owner, {
124
- scroll: () => {
125
- if (this.#syncingComponent) {
115
+ this.#eventController = new AbortController();
116
+ let options = {signal: this.#eventController.signal};
117
+
118
+ this.#viewport.addEventListener("scroll", () => this.#recheckLatency(), options);
119
+ if (this.#viewport === window) {
120
+ this.#viewport.addEventListener("resize", () => this.update(), options);
121
+ }
122
+
123
+ this.addEventListener("scroll", () => {
124
+ if (this.#syncingOwner && !this.#isLatent) {
125
+ this.#syncOwner();
126
+ }
127
+ // Resume component->owner syncing after the component scrolling has finished
128
+ // (it might be temporally disabled by the owner while syncing the component)
129
+ this.#syncingOwner = true;
130
+ }, options);
131
+
132
+ this.#owner.addEventListener("scroll", () => {
133
+ if (this.#syncingComponent) {
134
+ this.#syncComponent();
135
+ }
136
+ // Resume owner->component syncing after the owner scrolling has finished
137
+ // (it might be temporally disabled by the component while syncing the owner)
138
+ this.#syncingComponent = true;
139
+ }, options);
140
+ this.#owner.addEventListener("focusin", () => {
141
+ setTimeout(() => {
142
+ // The widget might be destroyed before the timer is triggered (issue #14)
143
+ if (this.isConnected) {
126
144
  this.#syncComponent();
127
145
  }
128
- // Resume owner->component syncing after the owner scrolling has finished
129
- // (it might be temporally disabled by the component while syncing the owner)
130
- this.#syncingComponent = true;
131
- },
132
- focusin: () => {
133
- setTimeout(() => {
134
- // The widget might be destroyed before the timer is triggered (issue #14)
135
- if (this.isConnected) {
136
- this.#syncComponent();
137
- }
138
- }, 0);
139
- },
140
- });
141
- this.#eventHandlers.forEach((handlers, el) => {
142
- Object.entries(handlers).forEach(([event, handler]) => el.addEventListener(event, handler, false));
143
- });
146
+ }, 0);
147
+ }, options);
144
148
  }
145
149
 
146
150
  #removeEventHandlers() {
147
- this.#eventHandlers.forEach((handlers, el) => {
148
- Object.entries(handlers).forEach(([event, handler]) => el.removeEventListener(event, handler, false));
149
- });
150
- this.#eventHandlers.clear();
151
+ this.#eventController?.abort();
152
+ this.#eventController = null;
151
153
  }
152
154
 
153
155
  #addResizeObserver() {