handy-scroll 1.0.6 → 1.1.1

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
@@ -30,7 +30,8 @@ The module exports a single object `handyScroll` which provides the following me
30
30
  * [`mount`](#mounting-the-widget) — initializes and “mounts” the widgets in the specified containers;
31
31
  * [`mounted`](#checking-widget-existence) — checks if the widget is already mounted in the given container;
32
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.
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.
34
35
 
35
36
  ### Mounting the widget
36
37
 
@@ -69,7 +70,7 @@ console.log(handyScroll.mounted("#spacious-container")); // true
69
70
 
70
71
  ### Updating scrollbar
71
72
 
72
- If you mount the widget in a container whose size and/or content may dynamically change, then you need a way to update the scrollbar each time the container’s sizes change. This can be done by invoking the method `handyScroll.update()` as in the example below.
73
+ If you mount the widget in a container whose size and/or content may dynamically change, you need a way to update the scrollbar each time the container’s sizes change. This can be done by invoking the method `handyScroll.update()` as in the example below.
73
74
 
74
75
  ```javascript
75
76
  handyScroll.mount(".spacious-container");
@@ -89,6 +90,19 @@ handyScroll.destroy(".spacious-container");
89
90
 
90
91
  The method expects a single argument, the target containers reference, which can be either an element, or a list of elements, or a selector.
91
92
 
93
+
94
+ ### Destroying detached widgets
95
+
96
+ If your app completely re-renders a large portion of DOM where handy-scroll widgets were mounted, actual container references are lost, and therefore you cannot unmount the widgets and perform related cleanup using the `destroy` method. In this case, you may just call the `handyScroll.destroyDetached()` method, and the module will find all “zombie” instances and will destroy them for you.
97
+
98
+ ```javascript
99
+ handyScroll.mount(".main-view .spacious-container");
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();
104
+ ```
105
+
92
106
  ### Special cases
93
107
 
94
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.
@@ -115,10 +129,6 @@ The [handy-scroll.css](dist/handy-scroll.css) file provides some basic styles fo
115
129
 
116
130
  You can make the widget more “unobtrusive” so that it will appear only when the mouse pointer hovers over the scrollable container. To do so just apply the class `handy-scroll-hoverable` to the desired scrollable container owning the widget.
117
131
 
118
- ## Integration with Angular
119
-
120
- If you have problems with the widget integration into your Angular app, please consult [this instruction](doc/angular-integration.md) first.
121
-
122
132
  ## Live demos
123
133
 
124
134
  Check out some usage demos [here](https://amphiluke.github.io/handy-scroll/).
@@ -0,0 +1,10 @@
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;
@@ -1,13 +1,13 @@
1
1
  /*!
2
- handy-scroll v1.0.6
2
+ handy-scroll v1.1.1
3
3
  https://amphiluke.github.io/handy-scroll/
4
- (c) 2021 Amphiluke
4
+ (c) 2022 Amphiluke
5
5
  */
6
6
  (function (global, factory) {
7
7
  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
8
8
  typeof define === 'function' && define.amd ? define(factory) :
9
9
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.handyScroll = factory());
10
- }(this, (function () { 'use strict';
10
+ })(this, (function () { 'use strict';
11
11
 
12
12
  let slice = Array.prototype.slice;
13
13
 
@@ -116,7 +116,12 @@ https://amphiluke.github.io/handy-scroll/
116
116
  instance.skipSyncWidget = false;
117
117
  },
118
118
  focusin() {
119
- setTimeout(() => instance.syncWidget(), 0);
119
+ setTimeout(() => {
120
+ // The widget might be destroyed before the timer is triggered (issue #14)
121
+ if (instance.widget) {
122
+ instance.syncWidget();
123
+ }
124
+ }, 0);
120
125
  }
121
126
  }
122
127
  }
@@ -195,12 +200,12 @@ https://amphiluke.github.io/handy-scroll/
195
200
  }
196
201
  };
197
202
 
198
- let instances = []; // if it were not for IE it would be better to use WeakMap (container -> instance)
203
+ let instances = []; // if it were not for IE, it would be better to use Map (container -> instance)
199
204
 
200
205
  let handyScroll = {
201
206
  /**
202
207
  * Mount widgets in the given containers
203
- * @param {HTMLElement|NodeList|HTMLCollection|Array|String} containerRef - Widget container reference (either an element, or a list of elements, or a selector)
208
+ * @param {HTMLElement|NodeList|HTMLCollection|HTMLElement[]|String} containerRef - Widget container reference (either an element, or a list of elements, or a selector)
204
209
  */
205
210
  mount(containerRef) {
206
211
  dom.$$(containerRef).forEach(container => {
@@ -225,7 +230,7 @@ https://amphiluke.github.io/handy-scroll/
225
230
 
226
231
  /**
227
232
  * Update widget parameters and position
228
- * @param {HTMLElement|NodeList|HTMLCollection|Array|String} containerRef - Widget container reference (either an element, or a list of elements, or a selector)
233
+ * @param {HTMLElement|NodeList|HTMLCollection|HTMLElement[]|String} containerRef - Widget container reference (either an element, or a list of elements, or a selector)
229
234
  */
230
235
  update(containerRef) {
231
236
  dom.$$(containerRef).forEach(container => {
@@ -241,7 +246,7 @@ https://amphiluke.github.io/handy-scroll/
241
246
 
242
247
  /**
243
248
  * Destroy widgets mounted in the given containers
244
- * @param {HTMLElement|NodeList|HTMLCollection|Array|String} containerRef - Widget container reference (either an element, or a list of elements, or a selector)
249
+ * @param {HTMLElement|NodeList|HTMLCollection|HTMLElement[]|String} containerRef - Widget container reference (either an element, or a list of elements, or a selector)
245
250
  */
246
251
  destroy(containerRef) {
247
252
  dom.$$(containerRef).forEach(container => {
@@ -253,6 +258,19 @@ https://amphiluke.github.io/handy-scroll/
253
258
  return false;
254
259
  });
255
260
  });
261
+ },
262
+
263
+ /**
264
+ * Destroy handyScroll widgets whose containers are not in the document anymore
265
+ */
266
+ destroyDetached() {
267
+ instances = instances.filter(instance => {
268
+ if (!dom.body.contains(instance.container)) {
269
+ instance.destroy();
270
+ return false;
271
+ }
272
+ return true;
273
+ });
256
274
  }
257
275
  };
258
276
 
@@ -264,4 +282,4 @@ https://amphiluke.github.io/handy-scroll/
264
282
 
265
283
  return handyScroll;
266
284
 
267
- })));
285
+ }));
@@ -1,6 +1,6 @@
1
1
  /*!
2
- handy-scroll v1.0.6
2
+ handy-scroll v1.1.1
3
3
  https://amphiluke.github.io/handy-scroll/
4
- (c) 2021 Amphiluke
4
+ (c) 2022 Amphiluke
5
5
  */
6
- !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).handyScroll=t()}(this,(function(){"use strict";let e=Array.prototype.slice,t="object"==typeof document&&!!document.documentElement,i={isDOMAvailable:t,doc:t?document:null,html:t?document.documentElement:null,body:t?document.body:null,ready(e){"loading"===i.doc.readyState?i.doc.addEventListener("DOMContentLoaded",(()=>{e()}),{once:!0}):e()},$:e=>"string"==typeof e?i.body.querySelector(e):e,$$:t=>Array.isArray(t)?t:t.nodeType===Node.ELEMENT_NODE?[t]:"string"==typeof t?e.call(i.body.querySelectorAll(t)):e.call(t)},n={init(e){let t=this,n=i.$$(".handy-scroll-body").filter((t=>t.contains(e)));n.length&&(t.scrollBody=n[0]),t.container=e,t.visible=!0,t.initWidget(),t.update(),t.addEventHandlers(),t.skipSyncContainer=t.skipSyncWidget=!1},initWidget(){let e=this,t=e.widget=i.doc.createElement("div");t.classList.add("handy-scroll");let n=i.doc.createElement("div");n.style.width=`${e.container.scrollWidth}px`,t.appendChild(n),e.container.appendChild(t)},addEventHandlers(){let e=this;(e.eventHandlers=[{el:e.scrollBody||window,handlers:{scroll(){e.checkVisibility()},resize(){e.update()}}},{el:e.widget,handlers:{scroll(){e.visible&&!e.skipSyncContainer&&e.syncContainer(),e.skipSyncContainer=!1}}},{el:e.container,handlers:{scroll(){e.skipSyncWidget||e.syncWidget(),e.skipSyncWidget=!1},focusin(){setTimeout((()=>e.syncWidget()),0)}}}]).forEach((({el:e,handlers:t})=>{Object.keys(t).forEach((i=>e.addEventListener(i,t[i],!1)))}))},checkVisibility(){let e=this,{widget:t,container:n,scrollBody:l}=e,o=t.scrollWidth<=t.offsetWidth;if(!o){let e=n.getBoundingClientRect(),t=l?l.getBoundingClientRect().bottom:window.innerHeight||i.html.clientHeight;o=e.bottom<=t||e.top>t}e.visible===o&&(e.visible=!o,t.classList.toggle("handy-scroll-hidden"))},syncContainer(){let e=this,{scrollLeft:t}=e.widget;e.container.scrollLeft!==t&&(e.skipSyncWidget=!0,e.container.scrollLeft=t)},syncWidget(){let e=this,{scrollLeft:t}=e.container;e.widget.scrollLeft!==t&&(e.skipSyncContainer=!0,e.widget.scrollLeft=t)},update(){let e=this,{widget:t,container:i,scrollBody:n}=e,{clientWidth:l,scrollWidth:o}=i;t.style.width=`${l}px`,n||(t.style.left=`${i.getBoundingClientRect().left}px`),t.firstElementChild.style.width=`${o}px`,o>l&&(t.style.height=t.offsetHeight-t.clientHeight+1+"px"),e.syncWidget(),e.checkVisibility()},destroy(){let e=this;e.eventHandlers.forEach((({el:e,handlers:t})=>{Object.keys(t).forEach((i=>e.removeEventListener(i,t[i],!1)))})),e.widget.parentNode.removeChild(e.widget),e.eventHandlers=e.widget=e.container=e.scrollBody=null}},l=[],o={mount(e){i.$$(e).forEach((e=>{if(o.mounted(e))return;let t=Object.create(n);l.push(t),t.init(e)}))},mounted(e){let t=i.$(e);return l.some((e=>e.container===t))},update(e){i.$$(e).forEach((e=>{l.some((t=>t.container===e&&(t.update(),!0)))}))},destroy(e){i.$$(e).forEach((e=>{l.some(((t,i)=>t.container===e&&(l.splice(i,1)[0].destroy(),!0)))}))}};return i.isDOMAvailable&&i.ready((()=>{o.mount("[data-handy-scroll]")})),o}));
6
+ !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).handyScroll=t()}(this,(function(){"use strict";let e=Array.prototype.slice,t="object"==typeof document&&!!document.documentElement,i={isDOMAvailable:t,doc:t?document:null,html:t?document.documentElement:null,body:t?document.body:null,ready(e){"loading"===i.doc.readyState?i.doc.addEventListener("DOMContentLoaded",(()=>{e()}),{once:!0}):e()},$:e=>"string"==typeof e?i.body.querySelector(e):e,$$:t=>Array.isArray(t)?t:t.nodeType===Node.ELEMENT_NODE?[t]:"string"==typeof t?e.call(i.body.querySelectorAll(t)):e.call(t)},n={init(e){let t=this,n=i.$$(".handy-scroll-body").filter((t=>t.contains(e)));n.length&&(t.scrollBody=n[0]),t.container=e,t.visible=!0,t.initWidget(),t.update(),t.addEventHandlers(),t.skipSyncContainer=t.skipSyncWidget=!1},initWidget(){let e=this,t=e.widget=i.doc.createElement("div");t.classList.add("handy-scroll");let n=i.doc.createElement("div");n.style.width=`${e.container.scrollWidth}px`,t.appendChild(n),e.container.appendChild(t)},addEventHandlers(){let e=this;(e.eventHandlers=[{el:e.scrollBody||window,handlers:{scroll(){e.checkVisibility()},resize(){e.update()}}},{el:e.widget,handlers:{scroll(){e.visible&&!e.skipSyncContainer&&e.syncContainer(),e.skipSyncContainer=!1}}},{el:e.container,handlers:{scroll(){e.skipSyncWidget||e.syncWidget(),e.skipSyncWidget=!1},focusin(){setTimeout((()=>{e.widget&&e.syncWidget()}),0)}}}]).forEach((({el:e,handlers:t})=>{Object.keys(t).forEach((i=>e.addEventListener(i,t[i],!1)))}))},checkVisibility(){let e=this,{widget:t,container:n,scrollBody:l}=e,o=t.scrollWidth<=t.offsetWidth;if(!o){let e=n.getBoundingClientRect(),t=l?l.getBoundingClientRect().bottom:window.innerHeight||i.html.clientHeight;o=e.bottom<=t||e.top>t}e.visible===o&&(e.visible=!o,t.classList.toggle("handy-scroll-hidden"))},syncContainer(){let e=this,{scrollLeft:t}=e.widget;e.container.scrollLeft!==t&&(e.skipSyncWidget=!0,e.container.scrollLeft=t)},syncWidget(){let e=this,{scrollLeft:t}=e.container;e.widget.scrollLeft!==t&&(e.skipSyncContainer=!0,e.widget.scrollLeft=t)},update(){let e=this,{widget:t,container:i,scrollBody:n}=e,{clientWidth:l,scrollWidth:o}=i;t.style.width=`${l}px`,n||(t.style.left=`${i.getBoundingClientRect().left}px`),t.firstElementChild.style.width=`${o}px`,o>l&&(t.style.height=t.offsetHeight-t.clientHeight+1+"px"),e.syncWidget(),e.checkVisibility()},destroy(){let e=this;e.eventHandlers.forEach((({el:e,handlers:t})=>{Object.keys(t).forEach((i=>e.removeEventListener(i,t[i],!1)))})),e.widget.parentNode.removeChild(e.widget),e.eventHandlers=e.widget=e.container=e.scrollBody=null}},l=[],o={mount(e){i.$$(e).forEach((e=>{if(o.mounted(e))return;let t=Object.create(n);l.push(t),t.init(e)}))},mounted(e){let t=i.$(e);return l.some((e=>e.container===t))},update(e){i.$$(e).forEach((e=>{l.some((t=>t.container===e&&(t.update(),!0)))}))},destroy(e){i.$$(e).forEach((e=>{l.some(((t,i)=>t.container===e&&(l.splice(i,1)[0].destroy(),!0)))}))},destroyDetached(){l=l.filter((e=>!!i.body.contains(e.container)||(e.destroy(),!1)))}};return i.isDOMAvailable&&i.ready((()=>{o.mount("[data-handy-scroll]")})),o}));
@@ -1,16 +1,17 @@
1
1
  /*!
2
- handy-scroll v1.0.6
2
+ handy-scroll v1.1.1
3
3
  https://amphiluke.github.io/handy-scroll/
4
- (c) 2021 Amphiluke
4
+ (c) 2022 Amphiluke
5
5
  */
6
6
  (function (global, factory) {
7
7
  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
8
8
  typeof define === 'function' && define.amd ? define(factory) :
9
9
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.handyScroll = factory());
10
- }(this, (function () { 'use strict';
10
+ })(this, (function () { 'use strict';
11
11
 
12
- var slice = Array.prototype.slice; // Precaution to avoid reference errors when imported for SSR (issue #13)
12
+ var slice = Array.prototype.slice;
13
13
 
14
+ // Precaution to avoid reference errors when imported for SSR (issue #13)
14
15
  var isDOMAvailable = typeof document === "object" && !!document.documentElement;
15
16
  var dom = {
16
17
  isDOMAvailable: isDOMAvailable,
@@ -33,7 +34,6 @@ https://amphiluke.github.io/handy-scroll/
33
34
  // ref is a selector
34
35
  return dom.body.querySelector(ref);
35
36
  }
36
-
37
37
  return ref; // ref is already an element
38
38
  },
39
39
  $$: function $$(ref) {
@@ -41,17 +41,14 @@ https://amphiluke.github.io/handy-scroll/
41
41
  // ref is an array of elements
42
42
  return ref;
43
43
  }
44
-
45
44
  if (ref.nodeType === Node.ELEMENT_NODE) {
46
45
  // ref is an element
47
46
  return [ref];
48
47
  }
49
-
50
48
  if (typeof ref === "string") {
51
49
  // ref is a selector
52
50
  return slice.call(dom.body.querySelectorAll(ref));
53
51
  }
54
-
55
52
  return slice.call(ref); // ref is an array-like object (NodeList or HTMLCollection)
56
53
  }
57
54
  };
@@ -62,18 +59,15 @@ https://amphiluke.github.io/handy-scroll/
62
59
  var scrollBodies = dom.$$(".handy-scroll-body").filter(function (node) {
63
60
  return node.contains(container);
64
61
  });
65
-
66
62
  if (scrollBodies.length) {
67
63
  instance.scrollBody = scrollBodies[0];
68
64
  }
69
-
70
65
  instance.container = container;
71
66
  instance.visible = true;
72
67
  instance.initWidget();
73
68
  instance.update(); // recalculate scrollbar parameters and set its visibility
74
-
75
- instance.addEventHandlers(); // Set skipSync flags to their initial values (because update() above calls syncWidget())
76
-
69
+ instance.addEventHandlers();
70
+ // Set skipSync flags to their initial values (because update() above calls syncWidget())
77
71
  instance.skipSyncContainer = instance.skipSyncWidget = false;
78
72
  },
79
73
  initWidget: function initWidget() {
@@ -103,10 +97,9 @@ https://amphiluke.github.io/handy-scroll/
103
97
  scroll: function scroll() {
104
98
  if (instance.visible && !instance.skipSyncContainer) {
105
99
  instance.syncContainer();
106
- } // Resume widget->container syncing after the widget scrolling has finished
100
+ }
101
+ // Resume widget->container syncing after the widget scrolling has finished
107
102
  // (it might be temporally disabled by the container while syncing the widget)
108
-
109
-
110
103
  instance.skipSyncContainer = false;
111
104
  }
112
105
  }
@@ -116,22 +109,24 @@ https://amphiluke.github.io/handy-scroll/
116
109
  scroll: function scroll() {
117
110
  if (!instance.skipSyncWidget) {
118
111
  instance.syncWidget();
119
- } // Resume container->widget syncing after the container scrolling has finished
112
+ }
113
+ // Resume container->widget syncing after the container scrolling has finished
120
114
  // (it might be temporally disabled by the widget while syncing the container)
121
-
122
-
123
115
  instance.skipSyncWidget = false;
124
116
  },
125
117
  focusin: function focusin() {
126
118
  setTimeout(function () {
127
- return instance.syncWidget();
119
+ // The widget might be destroyed before the timer is triggered (issue #14)
120
+ if (instance.widget) {
121
+ instance.syncWidget();
122
+ }
128
123
  }, 0);
129
124
  }
130
125
  }
131
126
  }];
132
127
  eventHandlers.forEach(function (_ref) {
133
128
  var el = _ref.el,
134
- handlers = _ref.handlers;
129
+ handlers = _ref.handlers;
135
130
  Object.keys(handlers).forEach(function (event) {
136
131
  return el.addEventListener(event, handlers[event], false);
137
132
  });
@@ -140,41 +135,37 @@ https://amphiluke.github.io/handy-scroll/
140
135
  checkVisibility: function checkVisibility() {
141
136
  var instance = this;
142
137
  var widget = instance.widget,
143
- container = instance.container,
144
- scrollBody = instance.scrollBody;
138
+ container = instance.container,
139
+ scrollBody = instance.scrollBody;
145
140
  var mustHide = widget.scrollWidth <= widget.offsetWidth;
146
-
147
141
  if (!mustHide) {
148
142
  var containerRect = container.getBoundingClientRect();
149
143
  var maxVisibleY = scrollBody ? scrollBody.getBoundingClientRect().bottom : window.innerHeight || dom.html.clientHeight;
150
144
  mustHide = containerRect.bottom <= maxVisibleY || containerRect.top > maxVisibleY;
151
145
  }
152
-
153
146
  if (instance.visible === mustHide) {
154
- instance.visible = !mustHide; // We cannot simply hide the scrollbar since its scrollLeft property will not update in that case
155
-
147
+ instance.visible = !mustHide;
148
+ // We cannot simply hide the scrollbar since its scrollLeft property will not update in that case
156
149
  widget.classList.toggle("handy-scroll-hidden");
157
150
  }
158
151
  },
159
152
  syncContainer: function syncContainer() {
160
153
  var instance = this;
161
154
  var scrollLeft = instance.widget.scrollLeft;
162
-
163
155
  if (instance.container.scrollLeft !== scrollLeft) {
164
156
  // Prevents container’s “scroll” event handler from syncing back again widget scroll position
165
- instance.skipSyncWidget = true; // Note that this makes container’s “scroll” event handlers execute
166
-
157
+ instance.skipSyncWidget = true;
158
+ // Note that this makes container’s “scroll” event handlers execute
167
159
  instance.container.scrollLeft = scrollLeft;
168
160
  }
169
161
  },
170
162
  syncWidget: function syncWidget() {
171
163
  var instance = this;
172
164
  var scrollLeft = instance.container.scrollLeft;
173
-
174
165
  if (instance.widget.scrollLeft !== scrollLeft) {
175
166
  // Prevents widget’s “scroll” event handler from syncing back again container scroll position
176
- instance.skipSyncContainer = true; // Note that this makes widget’s “scroll” event handlers execute
177
-
167
+ instance.skipSyncContainer = true;
168
+ // Note that this makes widget’s “scroll” event handlers execute
178
169
  instance.widget.scrollLeft = scrollLeft;
179
170
  }
180
171
  },
@@ -182,18 +173,16 @@ https://amphiluke.github.io/handy-scroll/
182
173
  update: function update() {
183
174
  var instance = this;
184
175
  var widget = instance.widget,
185
- container = instance.container,
186
- scrollBody = instance.scrollBody;
176
+ container = instance.container,
177
+ scrollBody = instance.scrollBody;
187
178
  var clientWidth = container.clientWidth,
188
- scrollWidth = container.scrollWidth;
179
+ scrollWidth = container.scrollWidth;
189
180
  widget.style.width = clientWidth + "px";
190
-
191
181
  if (!scrollBody) {
192
182
  widget.style.left = container.getBoundingClientRect().left + "px";
193
183
  }
194
-
195
- widget.firstElementChild.style.width = scrollWidth + "px"; // Fit widget height to the native scroll bar height if needed
196
-
184
+ widget.firstElementChild.style.width = scrollWidth + "px";
185
+ // Fit widget height to the native scroll bar height if needed
197
186
  if (scrollWidth > clientWidth) {
198
187
  widget.style.height = widget.offsetHeight - widget.clientHeight + 1 + "px"; // +1px JIC
199
188
  }
@@ -206,7 +195,7 @@ https://amphiluke.github.io/handy-scroll/
206
195
  var instance = this;
207
196
  instance.eventHandlers.forEach(function (_ref2) {
208
197
  var el = _ref2.el,
209
- handlers = _ref2.handlers;
198
+ handlers = _ref2.handlers;
210
199
  Object.keys(handlers).forEach(function (event) {
211
200
  return el.removeEventListener(event, handlers[event], false);
212
201
  });
@@ -216,25 +205,23 @@ https://amphiluke.github.io/handy-scroll/
216
205
  }
217
206
  };
218
207
 
219
- var instances = []; // if it were not for IE it would be better to use WeakMap (container -> instance)
208
+ var instances = []; // if it were not for IE, it would be better to use Map (container -> instance)
220
209
 
221
210
  var handyScroll = {
222
211
  /**
223
212
  * Mount widgets in the given containers
224
- * @param {HTMLElement|NodeList|HTMLCollection|Array|String} containerRef - Widget container reference (either an element, or a list of elements, or a selector)
213
+ * @param {HTMLElement|NodeList|HTMLCollection|HTMLElement[]|String} containerRef - Widget container reference (either an element, or a list of elements, or a selector)
225
214
  */
226
215
  mount: function mount(containerRef) {
227
216
  dom.$$(containerRef).forEach(function (container) {
228
217
  if (handyScroll.mounted(container)) {
229
218
  return;
230
219
  }
231
-
232
220
  var instance = Object.create(handyScrollProto);
233
221
  instances.push(instance);
234
222
  instance.init(container);
235
223
  });
236
224
  },
237
-
238
225
  /**
239
226
  * Check if a widget is already mounted in the given container
240
227
  * @param {HTMLElement|String} containerRef - Widget container reference (either an element, or a selector)
@@ -246,10 +233,9 @@ https://amphiluke.github.io/handy-scroll/
246
233
  return instance.container === container;
247
234
  });
248
235
  },
249
-
250
236
  /**
251
237
  * Update widget parameters and position
252
- * @param {HTMLElement|NodeList|HTMLCollection|Array|String} containerRef - Widget container reference (either an element, or a list of elements, or a selector)
238
+ * @param {HTMLElement|NodeList|HTMLCollection|HTMLElement[]|String} containerRef - Widget container reference (either an element, or a list of elements, or a selector)
253
239
  */
254
240
  update: function update(containerRef) {
255
241
  dom.$$(containerRef).forEach(function (container) {
@@ -258,15 +244,13 @@ https://amphiluke.github.io/handy-scroll/
258
244
  instance.update();
259
245
  return true;
260
246
  }
261
-
262
247
  return false;
263
248
  });
264
249
  });
265
250
  },
266
-
267
251
  /**
268
252
  * Destroy widgets mounted in the given containers
269
- * @param {HTMLElement|NodeList|HTMLCollection|Array|String} containerRef - Widget container reference (either an element, or a list of elements, or a selector)
253
+ * @param {HTMLElement|NodeList|HTMLCollection|HTMLElement[]|String} containerRef - Widget container reference (either an element, or a list of elements, or a selector)
270
254
  */
271
255
  destroy: function destroy(containerRef) {
272
256
  dom.$$(containerRef).forEach(function (container) {
@@ -275,13 +259,23 @@ https://amphiluke.github.io/handy-scroll/
275
259
  instances.splice(index, 1)[0].destroy();
276
260
  return true;
277
261
  }
278
-
279
262
  return false;
280
263
  });
281
264
  });
265
+ },
266
+ /**
267
+ * Destroy handyScroll widgets whose containers are not in the document anymore
268
+ */
269
+ destroyDetached: function destroyDetached() {
270
+ instances = instances.filter(function (instance) {
271
+ if (!dom.body.contains(instance.container)) {
272
+ instance.destroy();
273
+ return false;
274
+ }
275
+ return true;
276
+ });
282
277
  }
283
278
  };
284
-
285
279
  if (dom.isDOMAvailable) {
286
280
  dom.ready(function () {
287
281
  handyScroll.mount("[data-handy-scroll]");
@@ -290,4 +284,4 @@ https://amphiluke.github.io/handy-scroll/
290
284
 
291
285
  return handyScroll;
292
286
 
293
- })));
287
+ }));
@@ -1,6 +1,6 @@
1
1
  /*!
2
- handy-scroll v1.0.6
2
+ handy-scroll v1.1.1
3
3
  https://amphiluke.github.io/handy-scroll/
4
- (c) 2021 Amphiluke
4
+ (c) 2022 Amphiluke
5
5
  */
6
- !function(e,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):(e="undefined"!=typeof globalThis?globalThis:e||self).handyScroll=n()}(this,(function(){"use strict";var e=Array.prototype.slice,n="object"==typeof document&&!!document.documentElement,t={isDOMAvailable:n,doc:n?document:null,html:n?document.documentElement:null,body:n?document.body:null,ready:function(e){"loading"===t.doc.readyState?t.doc.addEventListener("DOMContentLoaded",(function(){e()}),{once:!0}):e()},$:function(e){return"string"==typeof e?t.body.querySelector(e):e},$$:function(n){return Array.isArray(n)?n:n.nodeType===Node.ELEMENT_NODE?[n]:"string"==typeof n?e.call(t.body.querySelectorAll(n)):e.call(n)}},i={init:function(e){var n=this,i=t.$$(".handy-scroll-body").filter((function(n){return n.contains(e)}));i.length&&(n.scrollBody=i[0]),n.container=e,n.visible=!0,n.initWidget(),n.update(),n.addEventHandlers(),n.skipSyncContainer=n.skipSyncWidget=!1},initWidget:function(){var e=this,n=e.widget=t.doc.createElement("div");n.classList.add("handy-scroll");var i=t.doc.createElement("div");i.style.width=e.container.scrollWidth+"px",n.appendChild(i),e.container.appendChild(n)},addEventHandlers:function(){var e=this;(e.eventHandlers=[{el:e.scrollBody||window,handlers:{scroll:function(){e.checkVisibility()},resize:function(){e.update()}}},{el:e.widget,handlers:{scroll:function(){e.visible&&!e.skipSyncContainer&&e.syncContainer(),e.skipSyncContainer=!1}}},{el:e.container,handlers:{scroll:function(){e.skipSyncWidget||e.syncWidget(),e.skipSyncWidget=!1},focusin:function(){setTimeout((function(){return e.syncWidget()}),0)}}}]).forEach((function(e){var n=e.el,t=e.handlers;Object.keys(t).forEach((function(e){return n.addEventListener(e,t[e],!1)}))}))},checkVisibility:function(){var e=this,n=e.widget,i=e.container,o=e.scrollBody,c=n.scrollWidth<=n.offsetWidth;if(!c){var r=i.getBoundingClientRect(),l=o?o.getBoundingClientRect().bottom:window.innerHeight||t.html.clientHeight;c=r.bottom<=l||r.top>l}e.visible===c&&(e.visible=!c,n.classList.toggle("handy-scroll-hidden"))},syncContainer:function(){var e=this,n=e.widget.scrollLeft;e.container.scrollLeft!==n&&(e.skipSyncWidget=!0,e.container.scrollLeft=n)},syncWidget:function(){var e=this,n=e.container.scrollLeft;e.widget.scrollLeft!==n&&(e.skipSyncContainer=!0,e.widget.scrollLeft=n)},update:function(){var e=this,n=e.widget,t=e.container,i=e.scrollBody,o=t.clientWidth,c=t.scrollWidth;n.style.width=o+"px",i||(n.style.left=t.getBoundingClientRect().left+"px"),n.firstElementChild.style.width=c+"px",c>o&&(n.style.height=n.offsetHeight-n.clientHeight+1+"px"),e.syncWidget(),e.checkVisibility()},destroy:function(){var e=this;e.eventHandlers.forEach((function(e){var n=e.el,t=e.handlers;Object.keys(t).forEach((function(e){return n.removeEventListener(e,t[e],!1)}))})),e.widget.parentNode.removeChild(e.widget),e.eventHandlers=e.widget=e.container=e.scrollBody=null}},o=[],c={mount:function(e){t.$$(e).forEach((function(e){if(!c.mounted(e)){var n=Object.create(i);o.push(n),n.init(e)}}))},mounted:function(e){var n=t.$(e);return o.some((function(e){return e.container===n}))},update:function(e){t.$$(e).forEach((function(e){o.some((function(n){return n.container===e&&(n.update(),!0)}))}))},destroy:function(e){t.$$(e).forEach((function(e){o.some((function(n,t){return n.container===e&&(o.splice(t,1)[0].destroy(),!0)}))}))}};return t.isDOMAvailable&&t.ready((function(){c.mount("[data-handy-scroll]")})),c}));
6
+ !function(t,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):(t="undefined"!=typeof globalThis?globalThis:t||self).handyScroll=n()}(this,(function(){"use strict";var t=Array.prototype.slice,n="object"==typeof document&&!!document.documentElement,e={isDOMAvailable:n,doc:n?document:null,html:n?document.documentElement:null,body:n?document.body:null,ready:function(t){"loading"===e.doc.readyState?e.doc.addEventListener("DOMContentLoaded",(function(){t()}),{once:!0}):t()},$:function(t){return"string"==typeof t?e.body.querySelector(t):t},$$:function(n){return Array.isArray(n)?n:n.nodeType===Node.ELEMENT_NODE?[n]:"string"==typeof n?t.call(e.body.querySelectorAll(n)):t.call(n)}},i={init:function(t){var n=this,i=e.$$(".handy-scroll-body").filter((function(n){return n.contains(t)}));i.length&&(n.scrollBody=i[0]),n.container=t,n.visible=!0,n.initWidget(),n.update(),n.addEventHandlers(),n.skipSyncContainer=n.skipSyncWidget=!1},initWidget:function(){var t=this,n=t.widget=e.doc.createElement("div");n.classList.add("handy-scroll");var i=e.doc.createElement("div");i.style.width=t.container.scrollWidth+"px",n.appendChild(i),t.container.appendChild(n)},addEventHandlers:function(){var t=this;(t.eventHandlers=[{el:t.scrollBody||window,handlers:{scroll:function(){t.checkVisibility()},resize:function(){t.update()}}},{el:t.widget,handlers:{scroll:function(){t.visible&&!t.skipSyncContainer&&t.syncContainer(),t.skipSyncContainer=!1}}},{el:t.container,handlers:{scroll:function(){t.skipSyncWidget||t.syncWidget(),t.skipSyncWidget=!1},focusin:function(){setTimeout((function(){t.widget&&t.syncWidget()}),0)}}}]).forEach((function(t){var n=t.el,e=t.handlers;Object.keys(e).forEach((function(t){return n.addEventListener(t,e[t],!1)}))}))},checkVisibility:function(){var t=this,n=t.widget,i=t.container,o=t.scrollBody,c=n.scrollWidth<=n.offsetWidth;if(!c){var r=i.getBoundingClientRect(),l=o?o.getBoundingClientRect().bottom:window.innerHeight||e.html.clientHeight;c=r.bottom<=l||r.top>l}t.visible===c&&(t.visible=!c,n.classList.toggle("handy-scroll-hidden"))},syncContainer:function(){var t=this,n=t.widget.scrollLeft;t.container.scrollLeft!==n&&(t.skipSyncWidget=!0,t.container.scrollLeft=n)},syncWidget:function(){var t=this,n=t.container.scrollLeft;t.widget.scrollLeft!==n&&(t.skipSyncContainer=!0,t.widget.scrollLeft=n)},update:function(){var t=this,n=t.widget,e=t.container,i=t.scrollBody,o=e.clientWidth,c=e.scrollWidth;n.style.width=o+"px",i||(n.style.left=e.getBoundingClientRect().left+"px"),n.firstElementChild.style.width=c+"px",c>o&&(n.style.height=n.offsetHeight-n.clientHeight+1+"px"),t.syncWidget(),t.checkVisibility()},destroy:function(){var t=this;t.eventHandlers.forEach((function(t){var n=t.el,e=t.handlers;Object.keys(e).forEach((function(t){return n.removeEventListener(t,e[t],!1)}))})),t.widget.parentNode.removeChild(t.widget),t.eventHandlers=t.widget=t.container=t.scrollBody=null}},o=[],c={mount:function(t){e.$$(t).forEach((function(t){if(!c.mounted(t)){var n=Object.create(i);o.push(n),n.init(t)}}))},mounted:function(t){var n=e.$(t);return o.some((function(t){return t.container===n}))},update:function(t){e.$$(t).forEach((function(t){o.some((function(n){return n.container===t&&(n.update(),!0)}))}))},destroy:function(t){e.$$(t).forEach((function(t){o.some((function(n,e){return n.container===t&&(o.splice(e,1)[0].destroy(),!0)}))}))},destroyDetached:function(){o=o.filter((function(t){return!!e.body.contains(t.container)||(t.destroy(),!1)}))}};return e.isDOMAvailable&&e.ready((function(){c.mount("[data-handy-scroll]")})),c}));
package/package.json CHANGED
@@ -1,20 +1,26 @@
1
1
  {
2
2
  "name": "handy-scroll",
3
- "version": "1.0.6",
3
+ "version": "1.1.1",
4
4
  "description": "Handy dependency-free floating scrollbar widget",
5
- "main": "dist/handy-scroll.min.js",
6
- "module": "src/handy-scroll.js",
7
- "style": "dist/handy-scroll.css",
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",
9
+ "exports": {
10
+ "types": "./dist/handy-scroll.d.ts",
11
+ "import": "./src/handy-scroll.js",
12
+ "require": "./dist/handy-scroll.js"
13
+ },
14
+ "type": "module",
8
15
  "files": [
9
16
  "dist",
10
- "doc",
11
17
  "src"
12
18
  ],
13
19
  "scripts": {
14
20
  "prepare": "husky install",
15
21
  "lint": "eslint src/*.js",
16
22
  "test": "echo \"Error: no test specified\" && exit 1",
17
- "build": "rollup -c && lessc src/handy-scroll.less | cleancss -O2 -o dist/handy-scroll.css"
23
+ "build": "rollup -c --bundleConfigAsCjs && lessc src/handy-scroll.less | cleancss -O2 -o dist/handy-scroll.css"
18
24
  },
19
25
  "repository": {
20
26
  "type": "git",
@@ -32,14 +38,14 @@
32
38
  },
33
39
  "homepage": "https://amphiluke.github.io/handy-scroll/",
34
40
  "devDependencies": {
35
- "@babel/core": "^7.15.0",
36
- "@babel/preset-env": "^7.15.0",
37
- "clean-css-cli": "^5.3.3",
38
- "eslint": "^7.32.0",
39
- "husky": "^7.0.1",
40
- "less": "^4.1.1",
41
- "rollup": "^2.56.2",
42
- "rollup-plugin-babel": "^4.4.0",
43
- "rollup-plugin-terser": "^7.0.2"
41
+ "@babel/core": "^7.20.5",
42
+ "@babel/preset-env": "^7.20.2",
43
+ "@rollup/plugin-babel": "^6.0.3",
44
+ "@rollup/plugin-terser": "^0.2.0",
45
+ "clean-css-cli": "^5.6.1",
46
+ "eslint": "^8.29.0",
47
+ "husky": "8.0.2",
48
+ "less": "^4.1.3",
49
+ "rollup": "^3.6.0"
44
50
  }
45
51
  }
@@ -66,7 +66,12 @@ let handyScrollProto = {
66
66
  instance.skipSyncWidget = false;
67
67
  },
68
68
  focusin() {
69
- setTimeout(() => instance.syncWidget(), 0);
69
+ setTimeout(() => {
70
+ // The widget might be destroyed before the timer is triggered (issue #14)
71
+ if (instance.widget) {
72
+ instance.syncWidget();
73
+ }
74
+ }, 0);
70
75
  }
71
76
  }
72
77
  }
@@ -1,12 +1,12 @@
1
1
  import dom from "./dom.js";
2
2
  import handyScrollProto from "./handy-scroll-proto";
3
3
 
4
- let instances = []; // if it were not for IE it would be better to use WeakMap (container -> instance)
4
+ let instances = []; // if it were not for IE, it would be better to use Map (container -> instance)
5
5
 
6
6
  let handyScroll = {
7
7
  /**
8
8
  * Mount widgets in the given containers
9
- * @param {HTMLElement|NodeList|HTMLCollection|Array|String} containerRef - Widget container reference (either an element, or a list of elements, or a selector)
9
+ * @param {HTMLElement|NodeList|HTMLCollection|HTMLElement[]|String} containerRef - Widget container reference (either an element, or a list of elements, or a selector)
10
10
  */
11
11
  mount(containerRef) {
12
12
  dom.$$(containerRef).forEach(container => {
@@ -31,7 +31,7 @@ let handyScroll = {
31
31
 
32
32
  /**
33
33
  * Update widget parameters and position
34
- * @param {HTMLElement|NodeList|HTMLCollection|Array|String} containerRef - Widget container reference (either an element, or a list of elements, or a selector)
34
+ * @param {HTMLElement|NodeList|HTMLCollection|HTMLElement[]|String} containerRef - Widget container reference (either an element, or a list of elements, or a selector)
35
35
  */
36
36
  update(containerRef) {
37
37
  dom.$$(containerRef).forEach(container => {
@@ -47,7 +47,7 @@ let handyScroll = {
47
47
 
48
48
  /**
49
49
  * Destroy widgets mounted in the given containers
50
- * @param {HTMLElement|NodeList|HTMLCollection|Array|String} containerRef - Widget container reference (either an element, or a list of elements, or a selector)
50
+ * @param {HTMLElement|NodeList|HTMLCollection|HTMLElement[]|String} containerRef - Widget container reference (either an element, or a list of elements, or a selector)
51
51
  */
52
52
  destroy(containerRef) {
53
53
  dom.$$(containerRef).forEach(container => {
@@ -59,6 +59,19 @@ let handyScroll = {
59
59
  return false;
60
60
  });
61
61
  });
62
+ },
63
+
64
+ /**
65
+ * Destroy handyScroll widgets whose containers are not in the document anymore
66
+ */
67
+ destroyDetached() {
68
+ instances = instances.filter(instance => {
69
+ if (!dom.body.contains(instance.container)) {
70
+ instance.destroy();
71
+ return false;
72
+ }
73
+ return true;
74
+ });
62
75
  }
63
76
  };
64
77
 
@@ -1,45 +0,0 @@
1
- # Using handy-scroll in an Angular app
2
-
3
- handy-scroll is a “VanillaJS” module, which means that you may need to perform a few additional steps to make it ready for use with your favorite framework. Here are the steps to install and use the handy-scroll widget in a sample Angular app. This instruction is partly adopted from the [“official” Angular CLI guide](https://github.com/angular/angular-cli/wiki/stories-third-party-lib) on installing a third party library in an Angular project.
4
-
5
- 1. Install the handy-scroll package locally as your project’s dependency:
6
- ```
7
- npm install --save handy-scroll
8
- ```
9
- 1. Include widget styles into your project’s [global styles](https://github.com/angular/angular-cli/wiki/stories-global-styles). You may import widget styles in your global stylesheet file `styles.css`:
10
- ```css
11
- @import "../node_modules/handy-scroll/dist/handy-scroll.css";
12
- ```
13
- 1. Create a `typings.d.ts` file in your `src/` folder. This file will be automatically included as global type definition.
14
- 1. In `src/typings.d.ts`, add the following code:
15
- ```typescript
16
- declare module 'handy-scroll';
17
- ```
18
- 1. In the component or file that uses the handy-scroll widget, add the following code:
19
- ```typescript
20
- import handyScroll from 'handy-scroll';
21
- ```
22
- 1. You may now use the widget API in your code as usual. E.g. in a component:
23
- ```typescript
24
- import {AfterViewInit, Component, ElementRef, ViewChild} from '@angular/core';
25
- import handyScroll from 'handy-scroll'; // 1) import
26
-
27
- @Component({
28
- selector: 'app-root',
29
- template: `
30
- <div #mycontainer class="my-container">
31
- <div>{{ "Long contents. ".repeat(2000) }}</div>
32
- </div>
33
- `,
34
- styles: ['.my-container {overflow:auto} .my-container div {min-width:150vw}']
35
- })
36
- export class AppComponent implements AfterViewInit {
37
- @ViewChild('mycontainer') myContainer: ElementRef;
38
-
39
- ngAfterViewInit() {
40
- handyScroll.mount(this.myContainer.nativeElement); // 2) use
41
- }
42
- }
43
- ```
44
-
45
- You may also play with a [live demo](https://stackblitz.com/edit/angular-handy-scroll-integration).