Skip to content

Commit

Permalink
Use AbortController for event handler removal
Browse files Browse the repository at this point in the history
  • Loading branch information
Amphiluke committed Oct 6, 2024
1 parent 70ca0b4 commit 58b5cc5
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 80 deletions.
66 changes: 28 additions & 38 deletions dist/handy-scroll.mjs
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
/*!
handy-scroll v2.0.2
handy-scroll v2.0.3
https://amphiluke.github.io/handy-scroll/
(c) 2024 Amphiluke
*/
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}';
let h = (n) => `Attribute ‘${n}’ must reference a valid container ‘id’`;
class r extends HTMLElement {
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}';
let n = (s) => `Attribute ‘${s}’ must reference a valid container ‘id’`;
class o extends HTMLElement {
static get observedAttributes() {
return ["owner", "viewport", "hidden"];
}
#o = null;
#t = null;
#e = null;
#s = null;
#i = /* @__PURE__ */ new Map();
#i = null;
#n = null;
#r = !0;
#l = !0;
Expand All @@ -38,27 +38,27 @@ class r extends HTMLElement {
constructor() {
super();
let t = this.attachShadow({ mode: "open" }), e = document.createElement("style");
e.textContent = o, t.appendChild(e), this.#s = document.createElement("div"), this.#s.classList.add("strut"), t.appendChild(this.#s), this.#o = this.attachInternals();
e.textContent = h, t.appendChild(e), this.#s = document.createElement("div"), this.#s.classList.add("strut"), t.appendChild(this.#s), this.#o = this.attachInternals();
}
connectedCallback() {
this.#a(), this.#c(), this.#u(), this.#f(), this.update();
this.#a(), this.#c(), this.#u(), this.#p(), this.update();
}
disconnectedCallback() {
this.#w(), this.#p(), this.#e = this.#t = null;
this.#w(), this.#f(), this.#e = this.#t = null;
}
attributeChangedCallback(t) {
if (this.#i.size) {
if (this.#i) {
if (t === "hidden") {
this.hasAttribute("hidden") || this.update();
return;
}
t === "owner" ? this.#a() : t === "viewport" && this.#c(), this.#w(), this.#p(), this.#u(), this.#f(), this.update();
t === "owner" ? this.#a() : t === "viewport" && this.#c(), this.#w(), this.#f(), this.#u(), this.#p(), this.update();
}
}
#a() {
let t = this.getAttribute("owner");
if (this.#e = document.getElementById(t), !this.#e)
throw new DOMException(h("owner"));
throw new DOMException(n("owner"));
}
#c() {
if (!this.hasAttribute("viewport")) {
Expand All @@ -67,40 +67,30 @@ class r extends HTMLElement {
}
let t = this.getAttribute("viewport");
if (this.#t = document.getElementById(t), !this.#t)
throw new DOMException(h("viewport"));
throw new DOMException(n("viewport"));
}
#u() {
this.#i.set(this.#t, {
scroll: () => this.#v(),
...this.#t === window ? { resize: () => this.update() } : {}
}), this.#i.set(this, {
scroll: () => {
this.#r && !this.#h && this.#b(), this.#r = !0;
}
}), this.#i.set(this.#e, {
scroll: () => {
this.#l && this.#d(), this.#l = !0;
},
focusin: () => {
setTimeout(() => {
this.isConnected && this.#d();
}, 0);
}
}), this.#i.forEach((t, e) => {
Object.entries(t).forEach(([i, s]) => e.addEventListener(i, s, !1));
});
this.#i = new AbortController();
let t = { signal: this.#i.signal };
this.#t.addEventListener("scroll", () => this.#v(), t), this.#t === window && this.#t.addEventListener("resize", () => this.update(), t), this.addEventListener("scroll", () => {
this.#r && !this.#h && this.#b(), this.#r = !0;
}, t), this.#e.addEventListener("scroll", () => {
this.#l && this.#d(), this.#l = !0;
}, t), this.#e.addEventListener("focusin", () => {
setTimeout(() => {
this.isConnected && this.#d();
}, 0);
}, t);
}
#w() {
this.#i.forEach((t, e) => {
Object.entries(t).forEach(([i, s]) => e.removeEventListener(i, s, !1));
}), this.#i.clear();
this.#i?.abort(), this.#i = null;
}
#f() {
#p() {
this.#t !== window && (this.#n = new ResizeObserver(([t]) => {
t.contentBoxSize?.[0]?.inlineSize && this.update();
}), this.#n.observe(this.#t));
}
#p() {
#f() {
this.#n?.disconnect(), this.#n = null;
}
#b() {
Expand All @@ -124,7 +114,7 @@ class r extends HTMLElement {
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();
}
}
customElements.define("handy-scroll", r);
customElements.define("handy-scroll", o);
export {
r as default
o as default
};
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "handy-scroll",
"version": "2.0.2",
"version": "2.0.3",
"description": "Handy dependency-free floating scrollbar web component",
"exports": {
".": {
Expand Down
74 changes: 35 additions & 39 deletions src/handy-scroll.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class HandyScroll extends HTMLElement {
#owner = null;
#strut = null;

#eventHandlers = new Map();
#eventController = null;
#resizeObserver = null;

#syncingOwner = true;
Expand Down Expand Up @@ -70,7 +70,7 @@ class HandyScroll extends HTMLElement {
}

attributeChangedCallback(name) {
if (!this.#eventHandlers.size) { // handle only dynamic changes when the element is completely connected
if (!this.#eventController) { // handle only dynamic changes when the element is completely connected
return;
}
if (name === "hidden") {
Expand Down Expand Up @@ -112,48 +112,44 @@ class HandyScroll extends HTMLElement {
}

#addEventHandlers() {
this.#eventHandlers.set(this.#viewport, {
scroll: () => this.#recheckLatency(),
...(this.#viewport === window ? {resize: () => this.update()} : {}),
});
this.#eventHandlers.set(this, {
scroll: () => {
if (this.#syncingOwner && !this.#isLatent) {
this.#syncOwner();
}
// Resume component->owner syncing after the component scrolling has finished
// (it might be temporally disabled by the owner while syncing the component)
this.#syncingOwner = true;
},
});
this.#eventHandlers.set(this.#owner, {
scroll: () => {
if (this.#syncingComponent) {
this.#eventController = new AbortController();
let options = {signal: this.#eventController.signal};

this.#viewport.addEventListener("scroll", () => this.#recheckLatency(), options);
if (this.#viewport === window) {
this.#viewport.addEventListener("resize", () => this.update(), options);
}

this.addEventListener("scroll", () => {
if (this.#syncingOwner && !this.#isLatent) {
this.#syncOwner();
}
// Resume component->owner syncing after the component scrolling has finished
// (it might be temporally disabled by the owner while syncing the component)
this.#syncingOwner = true;
}, options);

this.#owner.addEventListener("scroll", () => {
if (this.#syncingComponent) {
this.#syncComponent();
}
// Resume owner->component syncing after the owner scrolling has finished
// (it might be temporally disabled by the component while syncing the owner)
this.#syncingComponent = true;
}, options);
this.#owner.addEventListener("focusin", () => {
setTimeout(() => {
// The widget might be destroyed before the timer is triggered (issue #14)
if (this.isConnected) {
this.#syncComponent();
}
// Resume owner->component syncing after the owner scrolling has finished
// (it might be temporally disabled by the component while syncing the owner)
this.#syncingComponent = true;
},
focusin: () => {
setTimeout(() => {
// The widget might be destroyed before the timer is triggered (issue #14)
if (this.isConnected) {
this.#syncComponent();
}
}, 0);
},
});
this.#eventHandlers.forEach((handlers, el) => {
Object.entries(handlers).forEach(([event, handler]) => el.addEventListener(event, handler, false));
});
}, 0);
}, options);
}

#removeEventHandlers() {
this.#eventHandlers.forEach((handlers, el) => {
Object.entries(handlers).forEach(([event, handler]) => el.removeEventListener(event, handler, false));
});
this.#eventHandlers.clear();
this.#eventController?.abort();
this.#eventController = null;
}

#addResizeObserver() {
Expand Down

0 comments on commit 58b5cc5

Please sign in to comment.