@cocreate/scrollbar 0.0.0-stage → 1.12.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/src/index.js ADDED
@@ -0,0 +1,551 @@
1
+ /**
2
+ * CoCreateScrollbar - Modern Overlay Scrollbar JavaScript Engine
3
+ * Version 2.1.0
4
+ */
5
+ import Observer from '@cocreate/observer';
6
+ import './index.css';
7
+
8
+ const selector = '[scrollbar]';
9
+ const optionAttributes = [
10
+ 'scrollbar-type',
11
+ 'scrollbar-pos',
12
+ 'scrollbar-visible'
13
+ ];
14
+
15
+ const CoCreateScrollbar = (function () {
16
+ 'use strict';
17
+
18
+ var instances = new WeakMap();
19
+
20
+ /**
21
+ * Core ScrollInstance Class
22
+ * @private
23
+ * @environment client
24
+ */
25
+ function ScrollInstance(el, config) {
26
+ this.element = el;
27
+ this.options = Object.assign({
28
+ visible: 'auto',
29
+ fadeDelay: 1000,
30
+ hScroll: { behavior: 'scroll', pos: 'bottom' },
31
+ vScroll: { behavior: 'scroll', pos: 'right' }
32
+ }, config || {});
33
+
34
+ this.minThumbSize = 24;
35
+ this.scrollTimer = null;
36
+ this.isDragging = false;
37
+ this.dragAxis = null;
38
+ this.startPos = { x: 0, y: 0 };
39
+ this.startScroll = { x: 0, y: 0 };
40
+
41
+ this.viewport = null;
42
+ this.trackV = null;
43
+ this.thumbV = null;
44
+ this.trackH = null;
45
+ this.thumbH = null;
46
+ this.resizeObserver = null;
47
+ this.createdTrackV = false;
48
+ this.createdTrackH = false;
49
+ this.createdThumbV = false;
50
+ this.createdThumbH = false;
51
+ this.viewportClassAdded = false;
52
+ this.initialized = false;
53
+
54
+ this.init();
55
+ }
56
+
57
+ ScrollInstance.prototype.parseAttributes = function () {
58
+ var types = (this.element.getAttribute('scrollbar-type') || 'scrollx scrolly').toLowerCase();
59
+ this.options.hScroll.behavior = types.indexOf('scrollx') > -1 ? 'scroll' : 'disabled';
60
+ this.options.vScroll.behavior = types.indexOf('scrolly') > -1 ? 'scroll' : 'disabled';
61
+
62
+ var pos = (this.element.getAttribute('scrollbar-pos') || 'right bottom').toLowerCase();
63
+ this.options.vScroll.pos = pos.indexOf('left') > -1 ? 'left' : 'right';
64
+ this.options.hScroll.pos = pos.indexOf('top') > -1 ? 'top' : 'bottom';
65
+
66
+ var visible = (this.element.getAttribute('scrollbar-visible') || 'auto').toLowerCase();
67
+ this.options.visible = ['auto', 'show'].indexOf(visible) > -1 ? visible : 'auto';
68
+ };
69
+
70
+ ScrollInstance.prototype.applyOptions = function () {
71
+ this.element.classList.toggle('visible-show', this.options.visible === 'show');
72
+ this.element.classList.toggle('disable-y', this.options.vScroll.behavior !== 'scroll');
73
+ this.element.classList.toggle('disable-x', this.options.hScroll.behavior !== 'scroll');
74
+ if (this.trackV) {
75
+ this.trackV.classList.toggle('pos-left', this.options.vScroll.pos === 'left');
76
+ }
77
+ if (this.trackH) {
78
+ this.trackH.classList.toggle('pos-top', this.options.hScroll.pos === 'top');
79
+ }
80
+ };
81
+
82
+ ScrollInstance.prototype.refresh = function () {
83
+ this.parseAttributes();
84
+ this.applyOptions();
85
+ this.update();
86
+ };
87
+
88
+ /**
89
+ *
90
+ * @public
91
+ * @environment client
92
+ */
93
+ ScrollInstance.prototype.init = function () {
94
+ this.parseAttributes();
95
+
96
+ // The first non-track child is the existing native scroll container.
97
+ for (var i = 0; i < this.element.children.length; i++) {
98
+ var child = this.element.children[i];
99
+ if (!child.classList.contains('scrollbar-track')) {
100
+ this.viewport = child;
101
+ break;
102
+ }
103
+ }
104
+ if (!this.viewport) return;
105
+
106
+ this.viewportClassAdded = !this.viewport.classList.contains('scrollbar-viewport');
107
+ this.viewport.classList.add('scrollbar-viewport');
108
+
109
+ // Vertical Track & Thumb Setup
110
+ this.trackV = this.element.querySelector(':scope > .scrollbar-track-y');
111
+ if (!this.trackV) {
112
+ this.trackV = document.createElement('div');
113
+ this.trackV.className = 'scrollbar-track scrollbar-track-y';
114
+ this.thumbV = document.createElement('div');
115
+ this.thumbV.className = 'scrollbar-thumb';
116
+ this.trackV.appendChild(this.thumbV);
117
+ this.element.appendChild(this.trackV);
118
+ this.createdTrackV = true;
119
+ this.createdThumbV = true;
120
+ } else {
121
+ this.thumbV = this.trackV.querySelector('.scrollbar-thumb');
122
+ if (!this.thumbV) {
123
+ this.thumbV = document.createElement('div');
124
+ this.thumbV.className = 'scrollbar-thumb';
125
+ this.trackV.appendChild(this.thumbV);
126
+ this.createdThumbV = true;
127
+ }
128
+ }
129
+
130
+ // Horizontal Track & Thumb Setup
131
+ this.trackH = this.element.querySelector(':scope > .scrollbar-track-x');
132
+ if (!this.trackH) {
133
+ this.trackH = document.createElement('div');
134
+ this.trackH.className = 'scrollbar-track scrollbar-track-x';
135
+ this.thumbH = document.createElement('div');
136
+ this.thumbH.className = 'scrollbar-thumb';
137
+ this.trackH.appendChild(this.thumbH);
138
+ this.element.appendChild(this.trackH);
139
+ this.createdTrackH = true;
140
+ this.createdThumbH = true;
141
+ } else {
142
+ this.thumbH = this.trackH.querySelector('.scrollbar-thumb');
143
+ if (!this.thumbH) {
144
+ this.thumbH = document.createElement('div');
145
+ this.thumbH.className = 'scrollbar-thumb';
146
+ this.trackH.appendChild(this.thumbH);
147
+ this.createdThumbH = true;
148
+ }
149
+ }
150
+
151
+ // Setup Observers & Listeners
152
+ this.applyOptions();
153
+ this.setupObservers();
154
+ this.bindEvents();
155
+ this.update();
156
+ this.initialized = true;
157
+ };
158
+
159
+ ScrollInstance.prototype.setupObservers = function () {
160
+ var self = this;
161
+ if (typeof ResizeObserver !== 'undefined') {
162
+ this.resizeObserver = new ResizeObserver(function () { self.update(); });
163
+ this.resizeObserver.observe(this.element);
164
+ this.resizeObserver.observe(this.viewport);
165
+ }
166
+ };
167
+
168
+ ScrollInstance.prototype.update = function () {
169
+ if (!this.viewport) return;
170
+
171
+ var scrollTop = this.viewport.scrollTop;
172
+ var scrollLeft = this.viewport.scrollLeft;
173
+ var scrollHeight = this.viewport.scrollHeight;
174
+ var scrollWidth = this.viewport.scrollWidth;
175
+ var clientHeight = this.viewport.clientHeight;
176
+ var clientWidth = this.viewport.clientWidth;
177
+
178
+ // Vertical Axis Calculation
179
+ var maxScrollY = scrollHeight - clientHeight;
180
+ if (maxScrollY > 0 && this.options.vScroll.behavior === 'scroll') {
181
+ this.trackV.style.display = 'block';
182
+ var trackH = this.trackV.clientHeight;
183
+ var thumbH = Math.max((clientHeight / scrollHeight) * trackH, this.minThumbSize);
184
+ this.thumbV.style.height = thumbH + 'px';
185
+
186
+ var ratioY = scrollTop / maxScrollY;
187
+ var thumbY = ratioY * (trackH - thumbH);
188
+ this.thumbV.style.transform = 'translate3d(0, ' + thumbY + 'px, 0)';
189
+ } else {
190
+ this.trackV.style.display = 'none';
191
+ }
192
+
193
+ // Horizontal Axis Calculation
194
+ var maxScrollX = scrollWidth - clientWidth;
195
+ if (maxScrollX > 0 && this.options.hScroll.behavior === 'scroll') {
196
+ this.trackH.style.display = 'block';
197
+ var trackW = this.trackH.clientWidth;
198
+ var thumbW = Math.max((clientWidth / scrollWidth) * trackW, this.minThumbSize);
199
+ this.thumbH.style.width = thumbW + 'px';
200
+
201
+ var ratioX = scrollLeft / maxScrollX;
202
+ var thumbX = ratioX * (trackW - thumbW);
203
+ this.thumbH.style.transform = 'translate3d(' + thumbX + 'px, 0, 0)';
204
+ } else {
205
+ this.trackH.style.display = 'none';
206
+ }
207
+
208
+ // Dispatch Update Event
209
+ this.element.dispatchEvent(new CustomEvent('cocreate-scroll-update', {
210
+ detail: {
211
+ scrollTop: scrollTop,
212
+ scrollLeft: scrollLeft,
213
+ maxScrollY: maxScrollY,
214
+ maxScrollX: maxScrollX,
215
+ clientHeight: clientHeight,
216
+ clientWidth: clientWidth,
217
+ thumbVHeight: this.thumbV ? this.thumbV.clientHeight : 0,
218
+ thumbHWidth: this.thumbH ? this.thumbH.clientWidth : 0
219
+ }
220
+ }));
221
+ };
222
+
223
+ ScrollInstance.prototype.handleScroll = function () {
224
+ var self = this;
225
+ this.update();
226
+
227
+ if (this.options.visible === 'auto') {
228
+ this.element.classList.add('scrolling');
229
+ clearTimeout(this.scrollTimer);
230
+ this.scrollTimer = setTimeout(function () {
231
+ if (!self.isDragging) {
232
+ self.element.classList.remove('scrolling');
233
+ }
234
+ }, this.options.fadeDelay);
235
+ }
236
+ };
237
+
238
+ ScrollInstance.prototype.bindEvents = function () {
239
+ var self = this;
240
+ this._onScroll = this.handleScroll.bind(this);
241
+ this.viewport.addEventListener('scroll', this._onScroll, { passive: true });
242
+
243
+ // Thumb Dragging
244
+ this._onThumbVMousedown = function (e) { self.startDrag(e, 'v'); };
245
+ this.thumbV.addEventListener('mousedown', this._onThumbVMousedown);
246
+ this.thumbV.addEventListener('touchstart', this._onThumbVMousedown, { passive: false });
247
+
248
+ this._onThumbHMousedown = function (e) { self.startDrag(e, 'h'); };
249
+ this.thumbH.addEventListener('mousedown', this._onThumbHMousedown);
250
+ this.thumbH.addEventListener('touchstart', this._onThumbHMousedown, { passive: false });
251
+
252
+ // Track Click Jump Logic
253
+ this._onTrackVMousedown = function (e) {
254
+ if (e.target === self.thumbV) return;
255
+ var rect = self.trackV.getBoundingClientRect();
256
+ var clickY = e.clientY - rect.top - (self.thumbV.clientHeight / 2);
257
+ var maxTrackY = self.trackV.clientHeight - self.thumbV.clientHeight;
258
+ var ratio = Math.max(0, Math.min(1, clickY / maxTrackY));
259
+ self.viewport.scrollTo({
260
+ top: ratio * (self.viewport.scrollHeight - self.viewport.clientHeight),
261
+ behavior: 'smooth'
262
+ });
263
+ };
264
+ this.trackV.addEventListener('mousedown', this._onTrackVMousedown);
265
+
266
+ this._onTrackHMousedown = function (e) {
267
+ if (e.target === self.thumbH) return;
268
+ var rect = self.trackH.getBoundingClientRect();
269
+ var clickX = e.clientX - rect.left - (self.thumbH.clientWidth / 2);
270
+ var maxTrackX = self.trackH.clientWidth - self.thumbH.clientWidth;
271
+ var ratio = Math.max(0, Math.min(1, clickX / maxTrackX));
272
+ self.viewport.scrollTo({
273
+ left: ratio * (self.viewport.scrollWidth - self.viewport.clientWidth),
274
+ behavior: 'smooth'
275
+ });
276
+ };
277
+ this.trackH.addEventListener('mousedown', this._onTrackHMousedown);
278
+ };
279
+
280
+ ScrollInstance.prototype.startDrag = function (e, axis) {
281
+ e.stopPropagation();
282
+ if (e.cancelable) e.preventDefault();
283
+
284
+ var touch = e.touches ? e.touches[0] : e;
285
+ this.isDragging = true;
286
+ this.dragAxis = axis;
287
+ this.startPos = { x: touch.clientX, y: touch.clientY };
288
+ this.startScroll = { x: this.viewport.scrollLeft, y: this.viewport.scrollTop };
289
+
290
+ var thumb = axis === 'v' ? this.thumbV : this.thumbH;
291
+ var track = axis === 'v' ? this.trackV : this.trackH;
292
+
293
+ thumb.classList.add('is-dragging');
294
+ track.classList.add('is-dragging');
295
+ this.element.classList.add('is-dragging');
296
+ document.body.classList.add('scrollbar-dragging');
297
+
298
+ this._onMove = this.onDragMove.bind(this);
299
+ this._onEnd = this.onDragEnd.bind(this);
300
+
301
+ document.addEventListener('mousemove', this._onMove);
302
+ document.addEventListener('mouseup', this._onEnd, { once: true });
303
+ document.addEventListener('touchmove', this._onMove, { passive: false });
304
+ document.addEventListener('touchend', this._onEnd, { once: true });
305
+ };
306
+
307
+ ScrollInstance.prototype.onDragMove = function (e) {
308
+ if (!this.isDragging) return;
309
+ if (e.cancelable) e.preventDefault();
310
+
311
+ var touch = e.touches ? e.touches[0] : e;
312
+
313
+ if (this.dragAxis === 'v') {
314
+ var deltaY = touch.clientY - this.startPos.y;
315
+ var trackH = this.trackV.clientHeight;
316
+ var thumbH = this.thumbV.clientHeight;
317
+ var maxThumbY = trackH - thumbH;
318
+ var maxScrollY = this.viewport.scrollHeight - this.viewport.clientHeight;
319
+
320
+ if (maxThumbY > 0) {
321
+ this.viewport.scrollTop = this.startScroll.y + (deltaY / maxThumbY) * maxScrollY;
322
+ }
323
+ } else {
324
+ var deltaX = touch.clientX - this.startPos.x;
325
+ var trackW = this.trackH.clientWidth;
326
+ var thumbW = this.thumbH.clientWidth;
327
+ var maxThumbX = trackW - thumbW;
328
+ var maxScrollX = this.viewport.scrollWidth - this.viewport.clientWidth;
329
+
330
+ if (maxThumbX > 0) {
331
+ this.viewport.scrollLeft = this.startScroll.x + (deltaX / maxThumbX) * maxScrollX;
332
+ }
333
+ }
334
+ };
335
+
336
+ ScrollInstance.prototype.onDragEnd = function () {
337
+ var self = this;
338
+ this.isDragging = false;
339
+ this.thumbV.classList.remove('is-dragging');
340
+ this.trackV.classList.remove('is-dragging');
341
+ this.thumbH.classList.remove('is-dragging');
342
+ this.trackH.classList.remove('is-dragging');
343
+ this.element.classList.remove('is-dragging');
344
+ document.body.classList.remove('scrollbar-dragging');
345
+
346
+ document.removeEventListener('mousemove', this._onMove);
347
+ document.removeEventListener('mouseup', this._onEnd);
348
+ document.removeEventListener('touchmove', this._onMove);
349
+ document.removeEventListener('touchend', this._onEnd);
350
+
351
+ if (this.options.visible === 'auto') {
352
+ this.scrollTimer = setTimeout(function () {
353
+ self.element.classList.remove('scrolling');
354
+ }, this.options.fadeDelay);
355
+ }
356
+ };
357
+
358
+ /**
359
+ *
360
+ * @public
361
+ * @environment client
362
+ */
363
+ ScrollInstance.prototype.destroy = function () {
364
+ clearTimeout(this.scrollTimer);
365
+ if (this.resizeObserver) this.resizeObserver.disconnect();
366
+ if (this.viewport && this._onScroll) {
367
+ this.viewport.removeEventListener('scroll', this._onScroll);
368
+ }
369
+ if (this.thumbV && this._onThumbVMousedown) {
370
+ this.thumbV.removeEventListener('mousedown', this._onThumbVMousedown);
371
+ this.thumbV.removeEventListener('touchstart', this._onThumbVMousedown);
372
+ }
373
+ if (this.thumbH && this._onThumbHMousedown) {
374
+ this.thumbH.removeEventListener('mousedown', this._onThumbHMousedown);
375
+ this.thumbH.removeEventListener('touchstart', this._onThumbHMousedown);
376
+ }
377
+ if (this.trackV && this._onTrackVMousedown) {
378
+ this.trackV.removeEventListener('mousedown', this._onTrackVMousedown);
379
+ }
380
+ if (this.trackH && this._onTrackHMousedown) {
381
+ this.trackH.removeEventListener('mousedown', this._onTrackHMousedown);
382
+ }
383
+ document.removeEventListener('mousemove', this._onMove);
384
+ document.removeEventListener('mouseup', this._onEnd);
385
+ document.removeEventListener('touchmove', this._onMove);
386
+ document.removeEventListener('touchend', this._onEnd);
387
+ document.body.classList.remove('scrollbar-dragging');
388
+
389
+ if (this.createdTrackV) this.trackV.remove();
390
+ else if (this.createdThumbV) this.thumbV.remove();
391
+ if (this.createdTrackH) this.trackH.remove();
392
+ else if (this.createdThumbH) this.thumbH.remove();
393
+ if (this.viewportClassAdded && this.viewport) {
394
+ this.viewport.classList.remove('scrollbar-viewport');
395
+ }
396
+ this.element.classList.remove(
397
+ 'visible-show',
398
+ 'disable-y',
399
+ 'disable-x',
400
+ 'scrolling',
401
+ 'is-dragging'
402
+ );
403
+ instances.delete(this.element);
404
+ };
405
+
406
+ /* Static Manager Interface */
407
+ var CoCreateScrollbar = {
408
+ init: function (target, config) {
409
+ if (!target) return null;
410
+ var elements = typeof target === 'string' ? document.querySelectorAll(target) : (target.length !== undefined ? target : [target]);
411
+ var created = [];
412
+ for (var i = 0; i < elements.length; i++) {
413
+ var el = elements[i];
414
+ var inst = instances.get(el);
415
+ if (!inst) {
416
+ inst = new ScrollInstance(el, config);
417
+ if (!inst.initialized) continue;
418
+ instances.set(el, inst);
419
+ } else {
420
+ if (config) {
421
+ inst.refresh();
422
+ }
423
+ }
424
+ created.push(inst);
425
+ }
426
+ return created.length === 1 ? created[0] : created;
427
+ },
428
+
429
+ /**
430
+ *
431
+ * @public
432
+ * @environment client
433
+ */
434
+ get: function (el) {
435
+ return instances.get(el);
436
+ },
437
+
438
+ destroy: function (el) {
439
+ var inst = instances.get(el);
440
+ if (inst) inst.destroy();
441
+ },
442
+
443
+ /**
444
+ *
445
+ * @public
446
+ * @environment client
447
+ */
448
+ initAll: function () {
449
+ if (typeof document === 'undefined') return;
450
+ var targets = document.querySelectorAll(selector);
451
+ for (var i = 0; i < targets.length; i++) {
452
+ this.init(targets[i]);
453
+ }
454
+ }
455
+ };
456
+
457
+ return CoCreateScrollbar;
458
+ }());
459
+
460
+ Observer.init({
461
+ name: 'CoCreateOverlayScrollAdded',
462
+ types: ['addedNodes'],
463
+ selector: selector,
464
+ callback: function (mutation) {
465
+ CoCreateScrollbar.init(mutation.target);
466
+ }
467
+ });
468
+
469
+ Observer.init({
470
+ name: 'CoCreateOverlayScrollRemoved',
471
+ types: ['removedNodes'],
472
+ selector: selector,
473
+ callback: function (mutation) {
474
+ CoCreateScrollbar.destroy(mutation.target);
475
+ }
476
+ });
477
+
478
+ Observer.init({
479
+ name: 'CoCreateOverlayScrollAttributes',
480
+ types: ['attributes'],
481
+ attributeFilter: ['scrollbar'].concat(optionAttributes),
482
+ callback: function (mutation) {
483
+ var element = mutation.target;
484
+ if (mutation.attributeName === 'scrollbar') {
485
+ if (element.hasAttribute('scrollbar')) CoCreateScrollbar.init(element);
486
+ else CoCreateScrollbar.destroy(element);
487
+ return;
488
+ }
489
+
490
+ var instance = CoCreateScrollbar.get(element);
491
+ if (instance) instance.refresh();
492
+ }
493
+ });
494
+
495
+ CoCreateScrollbar.initAll();
496
+
497
+ export default CoCreateScrollbar;
498
+
499
+ /**
500
+ *
501
+ * @name scrollbar
502
+ * @kind member
503
+ * @attribute
504
+ * @environment client
505
+ */
506
+
507
+ /**
508
+ *
509
+ * @name scrollbar-pos
510
+ * @kind member
511
+ * @attribute
512
+ * @environment client
513
+ */
514
+
515
+ /**
516
+ *
517
+ * @name scrollbar-type
518
+ * @kind member
519
+ * @attribute
520
+ * @environment client
521
+ */
522
+
523
+ /**
524
+ *
525
+ * @name scrollbar-visible
526
+ * @kind member
527
+ * @attribute
528
+ * @environment client
529
+ */
530
+
531
+ /**
532
+ * A declarative custom scrollbar for native scroll containers with configurable axes, track positions, visibility, and automatic DOM lifecycle handling.
533
+ *
534
+ * @module scrollbar
535
+ * @environment client
536
+ */
537
+
538
+ /**
539
+ * Declarative selectors recognized by this module.
540
+ *
541
+ * @name declarative-selectors
542
+ * @kind member
543
+ * @selectors
544
+ * @environment client
545
+ * @example
546
+ * ```json
547
+ * [
548
+ * "[scrollbar]"
549
+ * ]
550
+ * ```
551
+ */
@@ -0,0 +1,65 @@
1
+ const path = require("path");
2
+ const MiniCssExtractPlugin = require("mini-css-extract-plugin");
3
+ const { EsbuildPlugin } = require("esbuild-loader");
4
+ const { FileUploader } = require("@cocreate/webpack");
5
+
6
+ module.exports = async (env, argv) => {
7
+ const isProduction = argv && argv.mode === "production";
8
+ const config = {
9
+ entry: {
10
+ "CoCreate-overlay-scroll": "./src/index.js"
11
+ },
12
+ output: {
13
+ path: path.resolve(__dirname, "dist"),
14
+ filename: isProduction ? "[name].min.js" : "[name].js",
15
+ libraryExport: "default",
16
+ library: ["CoCreate", "overlayScroll"],
17
+ clean: true
18
+ },
19
+ plugins: [
20
+ new MiniCssExtractPlugin({
21
+ filename: isProduction ? "[name].min.css" : "[name].css"
22
+ }),
23
+ new FileUploader(env, argv)
24
+ ],
25
+ mode: isProduction ? "production" : "development",
26
+ devtool: isProduction ? "source-map" : "eval-source-map",
27
+ module: {
28
+ rules: [
29
+ {
30
+ test: /.js$/,
31
+ exclude: /node_modules/,
32
+ use: {
33
+ loader: "esbuild-loader",
34
+ options: {
35
+ loader: "js",
36
+ target: "es2017"
37
+ }
38
+ }
39
+ },
40
+ {
41
+ test: /.css$/i,
42
+ use: [MiniCssExtractPlugin.loader, "css-loader"]
43
+ }
44
+ ]
45
+ },
46
+ optimization: {
47
+ minimize: isProduction,
48
+ minimizer: [
49
+ new EsbuildPlugin({
50
+ target: "es2017",
51
+ css: true
52
+ })
53
+ ],
54
+ splitChunks: {
55
+ cacheGroups: {
56
+ defaultVendors: false
57
+ }
58
+ }
59
+ },
60
+ performance: {
61
+ hints: isProduction ? "warning" : false
62
+ }
63
+ };
64
+ return config;
65
+ };