handy-scroll 1.1.4 → 2.0.0
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 +35 -93
- package/dist/handy-scroll.d.mts +14 -0
- package/dist/handy-scroll.mjs +124 -0
- package/package.json +16 -25
- package/src/handy-scroll.css +41 -0
- package/src/handy-scroll.mjs +223 -0
- package/dist/handy-scroll.css +0 -1
- package/dist/handy-scroll.d.ts +0 -10
- package/dist/handy-scroll.es6.js +0 -279
- package/dist/handy-scroll.es6.min.js +0 -6
- package/dist/handy-scroll.js +0 -282
- package/dist/handy-scroll.min.js +0 -6
- package/src/dom.js +0 -36
- package/src/handy-scroll-proto.js +0 -153
- package/src/handy-scroll.js +0 -84
- package/src/handy-scroll.less +0 -50
package/README.md
CHANGED
|
@@ -1,133 +1,75 @@
|
|
|
1
1
|
# handy-scroll
|
|
2
2
|
|
|
3
|
-
Handy dependency-free floating scrollbar
|
|
3
|
+
Handy dependency-free floating scrollbar web component.
|
|
4
4
|
|
|
5
5
|
## Synopsis
|
|
6
6
|
|
|
7
|
-
handy-scroll is a dependency-free
|
|
7
|
+
handy-scroll is a dependency-free web component which can be used to solve the problem of scrolling lengthy containers horizontally when those containers don’t fit into the viewport. The component is just a scrollbar which is attached at the bottom of the container’s visible area. It doesn’t get out of sight when the page is scrolled, thereby making horizontal scrolling of the container much handier.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
> [!NOTE]
|
|
10
|
+
> Current version of the component targets modern browsers only. If you need to support older browser versions, please stick to the former implementation [handy-scroll@1.x](https://github.com/Amphiluke/handy-scroll/tree/v1).
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
* Developing a Vue-based app? Consider using the [vue-handy-scroll](https://github.com/Amphiluke/vue-handy-scroll) component.
|
|
12
|
+
## Installation and import
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
If you use a bundler in your project, install handy-scroll as a dependency:
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
:bulb: **Tip:** If you don’t care about supporting Internet Explorer, feel free to use the file [handy-scroll.es6.min.js](dist/handy-scroll.es6.min.js), which is de facto the same as handy-scroll.min.js but is written in ES6, and is a bit smaller.
|
|
19
|
-
|
|
20
|
-
The handy-scroll package is available on npm, so you may add it to your project as usual:
|
|
21
|
-
|
|
22
|
-
```
|
|
16
|
+
```shell
|
|
23
17
|
npm install handy-scroll
|
|
24
18
|
```
|
|
25
19
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
The module exports a single object `handyScroll` which provides the following methods:
|
|
29
|
-
|
|
30
|
-
* [`mount`](#mounting-the-widget) — initializes and “mounts” the widgets in the specified containers;
|
|
31
|
-
* [`mounted`](#checking-widget-existence) — checks if the widget is already mounted in the given container;
|
|
32
|
-
* [`update`](#updating-scrollbar) — updates the widget parameters and position;
|
|
33
|
-
* [`destroy`](#destroying-the-widget) — destroys the widgets mounted in the specified containers and removes all related event handlers;
|
|
34
|
-
* [`destroyDetached`](#destroying-detached-widgets) — destroys handy-scroll widget instances whose containers were removed from the document.
|
|
35
|
-
|
|
36
|
-
### Mounting the widget
|
|
37
|
-
|
|
38
|
-
The only thing required to attach the widget to a static container (whose sizes will never change during the session) is a single call of the `handyScroll.mount()` method. The method expects a single argument, the target containers reference, which can be either an element, or a list of elements, or a selector.
|
|
20
|
+
Now you may import it wherever it’s needed:
|
|
39
21
|
|
|
40
22
|
```javascript
|
|
41
|
-
|
|
42
|
-
handyScroll.mount(document.getElementById("spacious-container"));
|
|
43
|
-
|
|
44
|
-
// mount widgets in all the container elements in the collection
|
|
45
|
-
handyScroll.mount(document.getElementsByClassName("spacious-container"));
|
|
46
|
-
handyScroll.mount([myDOMElement1, myDOMElement2, myDOMElement3]);
|
|
47
|
-
|
|
48
|
-
// mount widgets in all the container elements matching the selector
|
|
49
|
-
handyScroll.mount(".examples > .spacious-container");
|
|
23
|
+
import "handy-scroll";
|
|
50
24
|
```
|
|
51
25
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
There is another way to mount the handy-scroll widget without writing a single line of JavaScript code. Just add the attribute `data-handy-scroll` to the desired containers. As the DOM is ready the module will detect all such elements and will mount widgets automatically.
|
|
26
|
+
If you don’t use bundlers, just import the component as a module in your HTML files:
|
|
55
27
|
|
|
56
28
|
```html
|
|
57
|
-
<
|
|
58
|
-
<!-- Horizontally wide contents -->
|
|
59
|
-
</div>
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
### Checking widget existence
|
|
63
|
-
|
|
64
|
-
You may check if the widget is already mounted in the given container by calling the `handyScroll.mounted()` method.
|
|
65
|
-
|
|
66
|
-
```javascript
|
|
67
|
-
handyScroll.mount("#spacious-container");
|
|
68
|
-
console.log(handyScroll.mounted("#spacious-container")); // true
|
|
29
|
+
<script type="module" src="https://unpkg.com/handy-scroll"></script>
|
|
69
30
|
```
|
|
70
31
|
|
|
71
|
-
|
|
32
|
+
## Standard usage
|
|
72
33
|
|
|
73
|
-
|
|
34
|
+
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:
|
|
74
35
|
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
36
|
+
```html
|
|
37
|
+
<div id="horizontally-scrollable">
|
|
38
|
+
<!-- Horizontally wide contents -->
|
|
39
|
+
</div>
|
|
40
|
+
<handy-scroll owner="horizontally-scrollable"></handy-scroll>
|
|
79
41
|
```
|
|
80
42
|
|
|
81
|
-
|
|
43
|
+
## Custom viewport element
|
|
82
44
|
|
|
83
|
-
|
|
45
|
+
Standard use case above implies that handy-scroll will stick to the bottom of the browser window viewport. If instead you want to attach a floating scrollbar at the bottom of your custom scrollable “viewport” (e.g. a scrollable modal popup), then you need to link the component to your custom viewport element using the `viewport` attribute:
|
|
84
46
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
47
|
+
```html
|
|
48
|
+
<div id="custom-viewport">
|
|
49
|
+
<div id="horizontally-scrollable">
|
|
50
|
+
<!-- Horizontally wide contents -->
|
|
51
|
+
</div>
|
|
52
|
+
<handy-scroll owner="horizontally-scrollable" viewport="custom-viewport"></handy-scroll>
|
|
53
|
+
</div>
|
|
89
54
|
```
|
|
90
55
|
|
|
91
|
-
|
|
92
|
-
|
|
56
|
+
## API
|
|
93
57
|
|
|
94
|
-
###
|
|
58
|
+
### `HandyScroll.prototype.update()`
|
|
95
59
|
|
|
96
|
-
|
|
60
|
+
handy-scroll automatically tracks viewport changes in order to keep the component’s size, position and visibility in sync with the owner’s metrics. However there can be some cases when you’ll need to trigger the component update programmatically (e.g. after some changes in DOM). To do so, just call the method `update()` on the specific `<handy-scroll>` element:
|
|
97
61
|
|
|
98
62
|
```javascript
|
|
99
|
-
|
|
100
|
-
// ... the app re-renders the main view ...
|
|
101
|
-
document.querySelector(".main-view").innerHTML = "...";
|
|
102
|
-
// destroy handy-scroll widgets whose containers are not in the document anymore
|
|
103
|
-
handyScroll.destroyDetached();
|
|
63
|
+
document.getElementById("my-handy-scroll").update();
|
|
104
64
|
```
|
|
105
65
|
|
|
106
|
-
###
|
|
107
|
-
|
|
108
|
-
If you want to attach the widget to a container living in a positioned box (e.g. a modal popup with `position: fixed`) then you need to apply two special indicating class names in the markup. The module detects these indicating class names (they are prefixed with `handy-scroll-`) and switches to a special functioning mode.
|
|
109
|
-
|
|
110
|
-
```html
|
|
111
|
-
<div class="handy-scroll-viewport"><!-- (1) -->
|
|
112
|
-
<div class="handy-scroll-body"><!-- (2) -->
|
|
113
|
-
<div class="spacious-container">
|
|
114
|
-
<!-- Horizontally wide contents -->
|
|
115
|
-
</div>
|
|
116
|
-
</div>
|
|
117
|
-
</div>
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
The `.handy-scroll-viewport` element (1) is a positioned block (with any type of positioning except `static`) which serves for correct positioning of the widget. Note that this element itself should _not_ be scrollable. The `.handy-scroll-body` element (2) is a vertically scrollable block (with `overflow: auto`) which encloses the target container the widget is mounted in. After applying these special class names, you may initialise the widget as usual:
|
|
121
|
-
|
|
122
|
-
```javascript
|
|
123
|
-
handyScroll.mount(".spacious-container");
|
|
124
|
-
```
|
|
66
|
+
### `HandyScroll.prototype.owner`
|
|
125
67
|
|
|
126
|
-
|
|
68
|
+
Reflects the value of the `owner` attribute, which in turn should reference the `id` attribute of the horizontally-scrollable container (owner).
|
|
127
69
|
|
|
128
|
-
###
|
|
70
|
+
### `HandyScroll.prototype.viewport`
|
|
129
71
|
|
|
130
|
-
|
|
72
|
+
Reflects the value of the `viewport` attribute, which (if present) should reference the `id` attribute of the element serving as custom viewport.
|
|
131
73
|
|
|
132
74
|
## Live demos
|
|
133
75
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
declare class HandyScroll extends HTMLElement {
|
|
2
|
+
#private;
|
|
3
|
+
static get observedAttributes(): string[];
|
|
4
|
+
get owner(): string | null;
|
|
5
|
+
set owner(ownerId: string);
|
|
6
|
+
get viewport(): string | null;
|
|
7
|
+
set viewport(viewportId: string);
|
|
8
|
+
constructor();
|
|
9
|
+
connectedCallback(): void;
|
|
10
|
+
disconnectedCallback(): void;
|
|
11
|
+
attributeChangedCallback(name: string): void;
|
|
12
|
+
update(): void;
|
|
13
|
+
}
|
|
14
|
+
export default HandyScroll;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
handy-scroll v2.0.0
|
|
3
|
+
https://amphiluke.github.io/handy-scroll/
|
|
4
|
+
(c) 2024 Amphiluke
|
|
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 {
|
|
9
|
+
static get observedAttributes() {
|
|
10
|
+
return ["owner", "viewport"];
|
|
11
|
+
}
|
|
12
|
+
#o = null;
|
|
13
|
+
#t = null;
|
|
14
|
+
#e = null;
|
|
15
|
+
#s = null;
|
|
16
|
+
#i = /* @__PURE__ */ new Map();
|
|
17
|
+
#n = null;
|
|
18
|
+
#r = !0;
|
|
19
|
+
#l = !0;
|
|
20
|
+
get owner() {
|
|
21
|
+
return this.getAttribute("owner");
|
|
22
|
+
}
|
|
23
|
+
set owner(t) {
|
|
24
|
+
this.setAttribute("owner", t);
|
|
25
|
+
}
|
|
26
|
+
get viewport() {
|
|
27
|
+
return this.getAttribute("viewport");
|
|
28
|
+
}
|
|
29
|
+
set viewport(t) {
|
|
30
|
+
this.setAttribute("viewport", t);
|
|
31
|
+
}
|
|
32
|
+
get #h() {
|
|
33
|
+
return this.#o.states.has("latent");
|
|
34
|
+
}
|
|
35
|
+
set #h(t) {
|
|
36
|
+
this.#o.states[t ? "add" : "delete"]("latent");
|
|
37
|
+
}
|
|
38
|
+
constructor() {
|
|
39
|
+
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();
|
|
42
|
+
}
|
|
43
|
+
connectedCallback() {
|
|
44
|
+
this.#a(), this.#c(), this.#u(), this.#f(), this.update();
|
|
45
|
+
}
|
|
46
|
+
disconnectedCallback() {
|
|
47
|
+
this.#w(), this.#p(), this.#e = this.#t = null;
|
|
48
|
+
}
|
|
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());
|
|
51
|
+
}
|
|
52
|
+
#a() {
|
|
53
|
+
let t = this.getAttribute("owner");
|
|
54
|
+
if (this.#e = document.getElementById(t), !this.#e)
|
|
55
|
+
throw new DOMException(h("owner"));
|
|
56
|
+
}
|
|
57
|
+
#c() {
|
|
58
|
+
if (!this.hasAttribute("viewport")) {
|
|
59
|
+
this.#t = window;
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
let t = this.getAttribute("viewport");
|
|
63
|
+
if (this.#t = document.getElementById(t), !this.#t)
|
|
64
|
+
throw new DOMException(h("viewport"));
|
|
65
|
+
}
|
|
66
|
+
#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
|
+
});
|
|
86
|
+
}
|
|
87
|
+
#w() {
|
|
88
|
+
this.#i.forEach((t, e) => {
|
|
89
|
+
Object.entries(t).forEach(([i, s]) => e.removeEventListener(i, s, !1));
|
|
90
|
+
}), this.#i.clear();
|
|
91
|
+
}
|
|
92
|
+
#f() {
|
|
93
|
+
this.#t !== window && (this.#n = new ResizeObserver(([t]) => {
|
|
94
|
+
t.contentBoxSize?.[0]?.inlineSize && this.update();
|
|
95
|
+
}), this.#n.observe(this.#t));
|
|
96
|
+
}
|
|
97
|
+
#p() {
|
|
98
|
+
this.#n?.disconnect(), this.#n = null;
|
|
99
|
+
}
|
|
100
|
+
#b() {
|
|
101
|
+
let { scrollLeft: t } = this;
|
|
102
|
+
this.#e.scrollLeft !== t && (this.#l = !1, this.#e.scrollLeft = t);
|
|
103
|
+
}
|
|
104
|
+
#d() {
|
|
105
|
+
let { scrollLeft: t } = this.#e;
|
|
106
|
+
this.scrollLeft !== t && (this.#r = !1, this.scrollLeft = t);
|
|
107
|
+
}
|
|
108
|
+
#v() {
|
|
109
|
+
let t = this.scrollWidth <= this.offsetWidth;
|
|
110
|
+
if (!t) {
|
|
111
|
+
let e = this.#e.getBoundingClientRect(), i = this.#t === window ? window.innerHeight || document.documentElement.clientHeight : this.#t.getBoundingClientRect().bottom;
|
|
112
|
+
t = e.bottom <= i || e.top > i;
|
|
113
|
+
}
|
|
114
|
+
this.#h !== t && (this.#h = t);
|
|
115
|
+
}
|
|
116
|
+
update() {
|
|
117
|
+
let { clientWidth: t, scrollWidth: e } = this.#e, { style: i } = this;
|
|
118
|
+
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
|
+
}
|
|
120
|
+
}
|
|
121
|
+
customElements.define("handy-scroll", r);
|
|
122
|
+
export {
|
|
123
|
+
r as default
|
|
124
|
+
};
|
package/package.json
CHANGED
|
@@ -1,29 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "handy-scroll",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Handy dependency-free floating scrollbar
|
|
5
|
-
"main": "./dist/handy-scroll.min.js",
|
|
6
|
-
"module": "./src/handy-scroll.js",
|
|
7
|
-
"style": "./dist/handy-scroll.css",
|
|
8
|
-
"types": "./dist/handy-scroll.d.ts",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Handy dependency-free floating scrollbar web component",
|
|
9
5
|
"exports": {
|
|
10
6
|
".": {
|
|
11
|
-
"types": "./dist/handy-scroll.d.
|
|
12
|
-
"import": "./
|
|
13
|
-
|
|
14
|
-
},
|
|
15
|
-
"./dist/*.css": "./dist/*.css"
|
|
7
|
+
"types": "./dist/handy-scroll.d.mts",
|
|
8
|
+
"import": "./dist/handy-scroll.mjs"
|
|
9
|
+
}
|
|
16
10
|
},
|
|
11
|
+
"main": "./dist/handy-scroll.mjs",
|
|
17
12
|
"type": "module",
|
|
18
13
|
"files": [
|
|
19
14
|
"dist",
|
|
20
15
|
"src"
|
|
21
16
|
],
|
|
22
17
|
"scripts": {
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
18
|
+
"lint": "eslint",
|
|
19
|
+
"dev": "vite",
|
|
20
|
+
"build": "vite build",
|
|
21
|
+
"preview": "vite preview"
|
|
27
22
|
},
|
|
28
23
|
"repository": {
|
|
29
24
|
"type": "git",
|
|
@@ -31,7 +26,7 @@
|
|
|
31
26
|
},
|
|
32
27
|
"keywords": [
|
|
33
28
|
"scrollbar",
|
|
34
|
-
"
|
|
29
|
+
"component",
|
|
35
30
|
"user-interface"
|
|
36
31
|
],
|
|
37
32
|
"author": "Amphiluke",
|
|
@@ -41,14 +36,10 @@
|
|
|
41
36
|
},
|
|
42
37
|
"homepage": "https://amphiluke.github.io/handy-scroll/",
|
|
43
38
|
"devDependencies": {
|
|
44
|
-
"@
|
|
45
|
-
"@
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"eslint": "^8.50.0",
|
|
50
|
-
"husky": "8.0.3",
|
|
51
|
-
"less": "^4.2.0",
|
|
52
|
-
"rollup": "^3.29.4"
|
|
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"
|
|
53
44
|
}
|
|
54
45
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
:host {
|
|
2
|
+
bottom: 0;
|
|
3
|
+
min-height: 17px; /* based on https://codepen.io/sambible/post/browser-scrollbar-widths (fixes #3) */
|
|
4
|
+
overflow: auto;
|
|
5
|
+
position: fixed;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.strut {
|
|
9
|
+
height: 1px;
|
|
10
|
+
overflow: hidden;
|
|
11
|
+
pointer-events: none;
|
|
12
|
+
|
|
13
|
+
&::before {
|
|
14
|
+
content: "\A0"; /* fixes Amphiluke/floating-scroll#6 */
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
:host,
|
|
19
|
+
.strut {
|
|
20
|
+
font-size: 1px;
|
|
21
|
+
line-height: 0;
|
|
22
|
+
margin: 0;
|
|
23
|
+
padding: 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
:host(:state(latent)) {
|
|
27
|
+
bottom: 110vh;
|
|
28
|
+
|
|
29
|
+
.strut::before {
|
|
30
|
+
content: "\A0\A0"; /* changing content fixes eventual bug with widget re-rendering in Chrome */
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
:host([viewport]) {
|
|
35
|
+
display: block;
|
|
36
|
+
position: sticky;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
:host([viewport]:state(latent)) {
|
|
40
|
+
position: fixed;
|
|
41
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import css from "./handy-scroll.css?inline";
|
|
2
|
+
|
|
3
|
+
let getAttributeErrorMessage = (attribute) => `Attribute ‘${attribute}’ must reference a valid container ‘id’`;
|
|
4
|
+
|
|
5
|
+
class HandyScroll extends HTMLElement {
|
|
6
|
+
static get observedAttributes() {
|
|
7
|
+
return ["owner", "viewport"];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
#internals = null;
|
|
11
|
+
|
|
12
|
+
#viewport = null;
|
|
13
|
+
#owner = null;
|
|
14
|
+
#strut = null;
|
|
15
|
+
|
|
16
|
+
#eventHandlers = new Map();
|
|
17
|
+
#resizeObserver = null;
|
|
18
|
+
|
|
19
|
+
#syncingOwner = true;
|
|
20
|
+
#syncingComponent = true;
|
|
21
|
+
|
|
22
|
+
get owner() {
|
|
23
|
+
return this.getAttribute("owner");
|
|
24
|
+
}
|
|
25
|
+
set owner(ownerId) {
|
|
26
|
+
this.setAttribute("owner", ownerId);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get viewport() {
|
|
30
|
+
return this.getAttribute("viewport");
|
|
31
|
+
}
|
|
32
|
+
set viewport(viewportId) {
|
|
33
|
+
this.setAttribute("viewport", viewportId);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get #isLatent() {
|
|
37
|
+
return this.#internals.states.has("latent");
|
|
38
|
+
}
|
|
39
|
+
set #isLatent(value) {
|
|
40
|
+
this.#internals.states[value ? "add" : "delete"]("latent");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
constructor() {
|
|
44
|
+
super();
|
|
45
|
+
let shadowRoot = this.attachShadow({mode: "open"});
|
|
46
|
+
|
|
47
|
+
let style = document.createElement("style");
|
|
48
|
+
style.textContent = css;
|
|
49
|
+
shadowRoot.appendChild(style);
|
|
50
|
+
|
|
51
|
+
this.#strut = document.createElement("div");
|
|
52
|
+
this.#strut.classList.add("strut");
|
|
53
|
+
shadowRoot.appendChild(this.#strut);
|
|
54
|
+
|
|
55
|
+
this.#internals = this.attachInternals();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
connectedCallback() {
|
|
59
|
+
this.#bindOwner();
|
|
60
|
+
this.#bindViewport();
|
|
61
|
+
this.#addEventHandlers();
|
|
62
|
+
this.#addResizeObserver();
|
|
63
|
+
this.update();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
disconnectedCallback() {
|
|
67
|
+
this.#removeEventHandlers();
|
|
68
|
+
this.#removeResizeObserver();
|
|
69
|
+
this.#owner = this.#viewport = null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
attributeChangedCallback(name) {
|
|
73
|
+
if (!this.#eventHandlers.size) { // handle only dynamic changes when the element is completely connected
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (name === "owner") {
|
|
77
|
+
this.#bindOwner();
|
|
78
|
+
} else if (name === "viewport") {
|
|
79
|
+
this.#bindViewport();
|
|
80
|
+
}
|
|
81
|
+
this.#removeEventHandlers();
|
|
82
|
+
this.#removeResizeObserver();
|
|
83
|
+
this.#addEventHandlers();
|
|
84
|
+
this.#addResizeObserver();
|
|
85
|
+
this.update();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
#bindOwner() {
|
|
89
|
+
let ownerId = this.getAttribute("owner");
|
|
90
|
+
this.#owner = document.getElementById(ownerId);
|
|
91
|
+
if (!this.#owner) {
|
|
92
|
+
throw new DOMException(getAttributeErrorMessage("owner"));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
#bindViewport() {
|
|
97
|
+
if (!this.hasAttribute("viewport")) {
|
|
98
|
+
this.#viewport = window;
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
let viewportId = this.getAttribute("viewport");
|
|
102
|
+
this.#viewport = document.getElementById(viewportId);
|
|
103
|
+
if (!this.#viewport) {
|
|
104
|
+
throw new DOMException(getAttributeErrorMessage("viewport"));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
#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) {
|
|
126
|
+
this.#syncComponent();
|
|
127
|
+
}
|
|
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
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
#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
|
+
}
|
|
152
|
+
|
|
153
|
+
#addResizeObserver() {
|
|
154
|
+
if (this.#viewport === window) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
this.#resizeObserver = new ResizeObserver(([entry]) => {
|
|
158
|
+
if (entry.contentBoxSize?.[0]?.inlineSize) {
|
|
159
|
+
this.update();
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
this.#resizeObserver.observe(this.#viewport);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
#removeResizeObserver() {
|
|
166
|
+
this.#resizeObserver?.disconnect();
|
|
167
|
+
this.#resizeObserver = null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
#syncOwner() {
|
|
171
|
+
let {scrollLeft} = this;
|
|
172
|
+
if (this.#owner.scrollLeft !== scrollLeft) {
|
|
173
|
+
// Prevents owner’s “scroll” event handler from syncing back again the component’s scroll position
|
|
174
|
+
this.#syncingComponent = false;
|
|
175
|
+
// Note that this makes owner’s “scroll” event handlers execute
|
|
176
|
+
this.#owner.scrollLeft = scrollLeft;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
#syncComponent() {
|
|
181
|
+
let {scrollLeft} = this.#owner;
|
|
182
|
+
if (this.scrollLeft !== scrollLeft) {
|
|
183
|
+
// Prevents component’s “scroll” event handler from syncing back again the owner’s scroll position
|
|
184
|
+
this.#syncingOwner = false;
|
|
185
|
+
// Note that this makes component’s “scroll” event handlers execute
|
|
186
|
+
this.scrollLeft = scrollLeft;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
#recheckLatency() {
|
|
191
|
+
let isLatent = this.scrollWidth <= this.offsetWidth;
|
|
192
|
+
if (!isLatent) {
|
|
193
|
+
let ownerRect = this.#owner.getBoundingClientRect();
|
|
194
|
+
let maxVisibleY = (this.#viewport === window) ?
|
|
195
|
+
window.innerHeight || document.documentElement.clientHeight :
|
|
196
|
+
this.#viewport.getBoundingClientRect().bottom;
|
|
197
|
+
isLatent = ((ownerRect.bottom <= maxVisibleY) || (ownerRect.top > maxVisibleY));
|
|
198
|
+
}
|
|
199
|
+
if (this.#isLatent !== isLatent) {
|
|
200
|
+
this.#isLatent = isLatent;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
update() {
|
|
205
|
+
let {clientWidth, scrollWidth} = this.#owner;
|
|
206
|
+
let {style} = this;
|
|
207
|
+
style.width = `${clientWidth}px`;
|
|
208
|
+
if (this.#viewport === window) {
|
|
209
|
+
style.left = `${this.#owner.getBoundingClientRect().left}px`;
|
|
210
|
+
}
|
|
211
|
+
this.#strut.style.width = `${scrollWidth}px`;
|
|
212
|
+
// Fit component height to the native scroll bar height if needed
|
|
213
|
+
if (scrollWidth > clientWidth) {
|
|
214
|
+
style.height = `${this.offsetHeight - this.clientHeight + 1}px`; // +1px JIC
|
|
215
|
+
}
|
|
216
|
+
this.#syncComponent();
|
|
217
|
+
this.#recheckLatency(); // fixes issue Amphiluke/floating-scroll#2
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
customElements.define("handy-scroll", HandyScroll);
|
|
222
|
+
|
|
223
|
+
export default HandyScroll;
|
package/dist/handy-scroll.css
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
.handy-scroll{bottom:0;min-height:17px;overflow:auto;position:fixed}.handy-scroll div{height:1px;overflow:hidden;pointer-events:none}.handy-scroll div:before{content:"\A0"}.handy-scroll,.handy-scroll div{font-size:1px;line-height:0;margin:0;padding:0}.handy-scroll-hidden{bottom:9999px}.handy-scroll-hidden div:before{content:"\A0\A0"}.handy-scroll-viewport{position:relative}.handy-scroll-body{overflow:auto}.handy-scroll-viewport .handy-scroll{left:0;position:absolute}.handy-scroll-hoverable .handy-scroll{opacity:0;transition:opacity .5s .3s}.handy-scroll-hoverable:hover .handy-scroll{opacity:1}
|
package/dist/handy-scroll.d.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
declare namespace handyScroll {
|
|
2
|
-
export function mount(containerRef: HTMLElement | NodeList | HTMLCollection | HTMLElement[] | string): void;
|
|
3
|
-
export function mounted(containerRef: HTMLElement | string): boolean;
|
|
4
|
-
export function update(containerRef: HTMLElement | NodeList | HTMLCollection | HTMLElement[] | string): void;
|
|
5
|
-
export function destroy(containerRef: HTMLElement | NodeList | HTMLCollection | HTMLElement[] | string): void;
|
|
6
|
-
export function destroyDetached(): void;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export default handyScroll;
|
|
10
|
-
export as namespace handyScroll;
|