1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 | import { IDisposable } from '@lumino/disposable';
|
11 |
|
12 | import { ElementExt } from '@lumino/domutils';
|
13 |
|
14 | import { Drag } from '@lumino/dragdrop';
|
15 |
|
16 | import { Message } from '@lumino/messaging';
|
17 |
|
18 | import { ISignal, Signal } from '@lumino/signaling';
|
19 |
|
20 | import { Widget } from './widget';
|
21 |
|
22 |
|
23 |
|
24 |
|
25 | export class ScrollBar extends Widget {
|
26 | |
27 |
|
28 |
|
29 |
|
30 |
|
31 | constructor(options: ScrollBar.IOptions = {}) {
|
32 | super({ node: Private.createNode() });
|
33 | this.addClass('lm-ScrollBar');
|
34 |
|
35 | this.addClass('p-ScrollBar');
|
36 |
|
37 | this.setFlag(Widget.Flag.DisallowLayout);
|
38 |
|
39 |
|
40 | this._orientation = options.orientation || 'vertical';
|
41 | this.dataset['orientation'] = this._orientation;
|
42 |
|
43 |
|
44 | if (options.maximum !== undefined) {
|
45 | this._maximum = Math.max(0, options.maximum);
|
46 | }
|
47 | if (options.page !== undefined) {
|
48 | this._page = Math.max(0, options.page);
|
49 | }
|
50 | if (options.value !== undefined) {
|
51 | this._value = Math.max(0, Math.min(options.value, this._maximum));
|
52 | }
|
53 | }
|
54 |
|
55 | |
56 |
|
57 |
|
58 |
|
59 |
|
60 |
|
61 | get thumbMoved(): ISignal<this, number> {
|
62 | return this._thumbMoved;
|
63 | }
|
64 |
|
65 | |
66 |
|
67 |
|
68 |
|
69 |
|
70 |
|
71 | get stepRequested(): ISignal<this, 'decrement' | 'increment'> {
|
72 | return this._stepRequested;
|
73 | }
|
74 |
|
75 | |
76 |
|
77 |
|
78 |
|
79 |
|
80 |
|
81 | get pageRequested(): ISignal<this, 'decrement' | 'increment'> {
|
82 | return this._pageRequested;
|
83 | }
|
84 |
|
85 | |
86 |
|
87 |
|
88 | get orientation(): ScrollBar.Orientation {
|
89 | return this._orientation;
|
90 | }
|
91 |
|
92 | |
93 |
|
94 |
|
95 | set orientation(value: ScrollBar.Orientation) {
|
96 |
|
97 | if (this._orientation === value) {
|
98 | return;
|
99 | }
|
100 |
|
101 |
|
102 | this._releaseMouse();
|
103 |
|
104 |
|
105 | this._orientation = value;
|
106 | this.dataset['orientation'] = value;
|
107 |
|
108 |
|
109 | this.update();
|
110 | }
|
111 |
|
112 | |
113 |
|
114 |
|
115 | get value(): number {
|
116 | return this._value;
|
117 | }
|
118 |
|
119 | |
120 |
|
121 |
|
122 |
|
123 |
|
124 |
|
125 | set value(value: number) {
|
126 |
|
127 | value = Math.max(0, Math.min(value, this._maximum));
|
128 |
|
129 |
|
130 | if (this._value === value) {
|
131 | return;
|
132 | }
|
133 |
|
134 |
|
135 | this._value = value;
|
136 |
|
137 |
|
138 | this.update();
|
139 | }
|
140 |
|
141 | |
142 |
|
143 |
|
144 |
|
145 |
|
146 |
|
147 |
|
148 |
|
149 | get page(): number {
|
150 | return this._page;
|
151 | }
|
152 |
|
153 | |
154 |
|
155 |
|
156 |
|
157 |
|
158 |
|
159 | set page(value: number) {
|
160 |
|
161 | value = Math.max(0, value);
|
162 |
|
163 |
|
164 | if (this._page === value) {
|
165 | return;
|
166 | }
|
167 |
|
168 |
|
169 | this._page = value;
|
170 |
|
171 |
|
172 | this.update();
|
173 | }
|
174 |
|
175 | |
176 |
|
177 |
|
178 | get maximum(): number {
|
179 | return this._maximum;
|
180 | }
|
181 |
|
182 | |
183 |
|
184 |
|
185 |
|
186 |
|
187 |
|
188 | set maximum(value: number) {
|
189 |
|
190 | value = Math.max(0, value);
|
191 |
|
192 |
|
193 | if (this._maximum === value) {
|
194 | return;
|
195 | }
|
196 |
|
197 |
|
198 | this._maximum = value;
|
199 |
|
200 |
|
201 | this._value = Math.min(this._value, value);
|
202 |
|
203 |
|
204 | this.update();
|
205 | }
|
206 |
|
207 | |
208 |
|
209 |
|
210 |
|
211 |
|
212 |
|
213 | get decrementNode(): HTMLDivElement {
|
214 | return this.node.getElementsByClassName(
|
215 | 'lm-ScrollBar-button'
|
216 | )[0] as HTMLDivElement;
|
217 | }
|
218 |
|
219 | |
220 |
|
221 |
|
222 |
|
223 |
|
224 |
|
225 | get incrementNode(): HTMLDivElement {
|
226 | return this.node.getElementsByClassName(
|
227 | 'lm-ScrollBar-button'
|
228 | )[1] as HTMLDivElement;
|
229 | }
|
230 |
|
231 | |
232 |
|
233 |
|
234 |
|
235 |
|
236 |
|
237 | get trackNode(): HTMLDivElement {
|
238 | return this.node.getElementsByClassName(
|
239 | 'lm-ScrollBar-track'
|
240 | )[0] as HTMLDivElement;
|
241 | }
|
242 |
|
243 | |
244 |
|
245 |
|
246 |
|
247 |
|
248 |
|
249 | get thumbNode(): HTMLDivElement {
|
250 | return this.node.getElementsByClassName(
|
251 | 'lm-ScrollBar-thumb'
|
252 | )[0] as HTMLDivElement;
|
253 | }
|
254 |
|
255 | |
256 |
|
257 |
|
258 |
|
259 |
|
260 |
|
261 |
|
262 |
|
263 |
|
264 |
|
265 |
|
266 | handleEvent(event: Event): void {
|
267 | switch (event.type) {
|
268 | case 'mousedown':
|
269 | this._evtMouseDown(event as MouseEvent);
|
270 | break;
|
271 | case 'mousemove':
|
272 | this._evtMouseMove(event as MouseEvent);
|
273 | break;
|
274 | case 'mouseup':
|
275 | this._evtMouseUp(event as MouseEvent);
|
276 | break;
|
277 | case 'keydown':
|
278 | this._evtKeyDown(event as KeyboardEvent);
|
279 | break;
|
280 | case 'contextmenu':
|
281 | event.preventDefault();
|
282 | event.stopPropagation();
|
283 | break;
|
284 | }
|
285 | }
|
286 |
|
287 | |
288 |
|
289 |
|
290 | protected onBeforeAttach(msg: Message): void {
|
291 | this.node.addEventListener('mousedown', this);
|
292 | this.update();
|
293 | }
|
294 |
|
295 | |
296 |
|
297 |
|
298 | protected onAfterDetach(msg: Message): void {
|
299 | this.node.removeEventListener('mousedown', this);
|
300 | this._releaseMouse();
|
301 | }
|
302 |
|
303 | |
304 |
|
305 |
|
306 | protected onUpdateRequest(msg: Message): void {
|
307 |
|
308 | let value = (this._value * 100) / this._maximum;
|
309 | let page = (this._page * 100) / (this._page + this._maximum);
|
310 |
|
311 |
|
312 | value = Math.max(0, Math.min(value, 100));
|
313 | page = Math.max(0, Math.min(page, 100));
|
314 |
|
315 |
|
316 | let thumbStyle = this.thumbNode.style;
|
317 |
|
318 |
|
319 | if (this._orientation === 'horizontal') {
|
320 | thumbStyle.top = '';
|
321 | thumbStyle.height = '';
|
322 | thumbStyle.left = `${value}%`;
|
323 | thumbStyle.width = `${page}%`;
|
324 | thumbStyle.transform = `translate(${-value}%, 0%)`;
|
325 | } else {
|
326 | thumbStyle.left = '';
|
327 | thumbStyle.width = '';
|
328 | thumbStyle.top = `${value}%`;
|
329 | thumbStyle.height = `${page}%`;
|
330 | thumbStyle.transform = `translate(0%, ${-value}%)`;
|
331 | }
|
332 | }
|
333 |
|
334 | |
335 |
|
336 |
|
337 | private _evtKeyDown(event: KeyboardEvent): void {
|
338 |
|
339 | event.preventDefault();
|
340 | event.stopPropagation();
|
341 |
|
342 |
|
343 | if (event.keyCode !== 27) {
|
344 | return;
|
345 | }
|
346 |
|
347 |
|
348 | let value = this._pressData ? this._pressData.value : -1;
|
349 |
|
350 |
|
351 | this._releaseMouse();
|
352 |
|
353 |
|
354 | if (value !== -1) {
|
355 | this._moveThumb(value);
|
356 | }
|
357 | }
|
358 |
|
359 | |
360 |
|
361 |
|
362 | private _evtMouseDown(event: MouseEvent): void {
|
363 |
|
364 | if (event.button !== 0) {
|
365 | return;
|
366 | }
|
367 |
|
368 |
|
369 |
|
370 | this.activate();
|
371 |
|
372 |
|
373 | if (this._pressData) {
|
374 | return;
|
375 | }
|
376 |
|
377 |
|
378 | let part = Private.findPart(this, event.target as HTMLElement);
|
379 |
|
380 |
|
381 | if (!part) {
|
382 | return;
|
383 | }
|
384 |
|
385 |
|
386 | event.preventDefault();
|
387 | event.stopPropagation();
|
388 |
|
389 |
|
390 | let override = Drag.overrideCursor('default');
|
391 |
|
392 |
|
393 | this._pressData = {
|
394 | part,
|
395 | override,
|
396 | delta: -1,
|
397 | value: -1,
|
398 | mouseX: event.clientX,
|
399 | mouseY: event.clientY
|
400 | };
|
401 |
|
402 |
|
403 | document.addEventListener('mousemove', this, true);
|
404 | document.addEventListener('mouseup', this, true);
|
405 | document.addEventListener('keydown', this, true);
|
406 | document.addEventListener('contextmenu', this, true);
|
407 |
|
408 |
|
409 | if (part === 'thumb') {
|
410 |
|
411 | let thumbNode = this.thumbNode;
|
412 |
|
413 |
|
414 | let thumbRect = thumbNode.getBoundingClientRect();
|
415 |
|
416 |
|
417 | if (this._orientation === 'horizontal') {
|
418 | this._pressData.delta = event.clientX - thumbRect.left;
|
419 | } else {
|
420 | this._pressData.delta = event.clientY - thumbRect.top;
|
421 | }
|
422 |
|
423 |
|
424 | thumbNode.classList.add('lm-mod-active');
|
425 |
|
426 | thumbNode.classList.add('p-mod-active');
|
427 |
|
428 |
|
429 |
|
430 | this._pressData.value = this._value;
|
431 |
|
432 |
|
433 | return;
|
434 | }
|
435 |
|
436 |
|
437 | if (part === 'track') {
|
438 |
|
439 | let thumbRect = this.thumbNode.getBoundingClientRect();
|
440 |
|
441 |
|
442 | let dir: 'decrement' | 'increment';
|
443 | if (this._orientation === 'horizontal') {
|
444 | dir = event.clientX < thumbRect.left ? 'decrement' : 'increment';
|
445 | } else {
|
446 | dir = event.clientY < thumbRect.top ? 'decrement' : 'increment';
|
447 | }
|
448 |
|
449 |
|
450 | this._repeatTimer = window.setTimeout(this._onRepeat, 350);
|
451 |
|
452 |
|
453 | this._pageRequested.emit(dir);
|
454 |
|
455 |
|
456 | return;
|
457 | }
|
458 |
|
459 |
|
460 | if (part === 'decrement') {
|
461 |
|
462 | this.decrementNode.classList.add('lm-mod-active');
|
463 |
|
464 | this.decrementNode.classList.add('p-mod-active');
|
465 |
|
466 |
|
467 |
|
468 | this._repeatTimer = window.setTimeout(this._onRepeat, 350);
|
469 |
|
470 |
|
471 | this._stepRequested.emit('decrement');
|
472 |
|
473 |
|
474 | return;
|
475 | }
|
476 |
|
477 |
|
478 | if (part === 'increment') {
|
479 |
|
480 | this.incrementNode.classList.add('lm-mod-active');
|
481 |
|
482 | this.incrementNode.classList.add('p-mod-active');
|
483 |
|
484 |
|
485 |
|
486 | this._repeatTimer = window.setTimeout(this._onRepeat, 350);
|
487 |
|
488 |
|
489 | this._stepRequested.emit('increment');
|
490 |
|
491 |
|
492 | return;
|
493 | }
|
494 | }
|
495 |
|
496 | |
497 |
|
498 |
|
499 | private _evtMouseMove(event: MouseEvent): void {
|
500 |
|
501 | if (!this._pressData) {
|
502 | return;
|
503 | }
|
504 |
|
505 |
|
506 | event.preventDefault();
|
507 | event.stopPropagation();
|
508 |
|
509 |
|
510 | this._pressData.mouseX = event.clientX;
|
511 | this._pressData.mouseY = event.clientY;
|
512 |
|
513 |
|
514 | if (this._pressData.part !== 'thumb') {
|
515 | return;
|
516 | }
|
517 |
|
518 |
|
519 | let thumbRect = this.thumbNode.getBoundingClientRect();
|
520 | let trackRect = this.trackNode.getBoundingClientRect();
|
521 |
|
522 |
|
523 | let trackPos: number;
|
524 | let trackSpan: number;
|
525 | if (this._orientation === 'horizontal') {
|
526 | trackPos = event.clientX - trackRect.left - this._pressData.delta;
|
527 | trackSpan = trackRect.width - thumbRect.width;
|
528 | } else {
|
529 | trackPos = event.clientY - trackRect.top - this._pressData.delta;
|
530 | trackSpan = trackRect.height - thumbRect.height;
|
531 | }
|
532 |
|
533 |
|
534 | let value = trackSpan === 0 ? 0 : (trackPos * this._maximum) / trackSpan;
|
535 |
|
536 |
|
537 | this._moveThumb(value);
|
538 | }
|
539 |
|
540 | |
541 |
|
542 |
|
543 | private _evtMouseUp(event: MouseEvent): void {
|
544 |
|
545 | if (event.button !== 0) {
|
546 | return;
|
547 | }
|
548 |
|
549 |
|
550 | event.preventDefault();
|
551 | event.stopPropagation();
|
552 |
|
553 |
|
554 | this._releaseMouse();
|
555 | }
|
556 |
|
557 | |
558 |
|
559 |
|
560 | private _releaseMouse(): void {
|
561 |
|
562 | if (!this._pressData) {
|
563 | return;
|
564 | }
|
565 |
|
566 |
|
567 | clearTimeout(this._repeatTimer);
|
568 | this._repeatTimer = -1;
|
569 |
|
570 |
|
571 | this._pressData.override.dispose();
|
572 | this._pressData = null;
|
573 |
|
574 |
|
575 | document.removeEventListener('mousemove', this, true);
|
576 | document.removeEventListener('mouseup', this, true);
|
577 | document.removeEventListener('keydown', this, true);
|
578 | document.removeEventListener('contextmenu', this, true);
|
579 |
|
580 |
|
581 | this.thumbNode.classList.remove('lm-mod-active');
|
582 | this.decrementNode.classList.remove('lm-mod-active');
|
583 | this.incrementNode.classList.remove('lm-mod-active');
|
584 |
|
585 | this.thumbNode.classList.remove('p-mod-active');
|
586 | this.decrementNode.classList.remove('p-mod-active');
|
587 | this.incrementNode.classList.remove('p-mod-active');
|
588 |
|
589 | }
|
590 |
|
591 | |
592 |
|
593 |
|
594 | private _moveThumb(value: number): void {
|
595 |
|
596 | value = Math.max(0, Math.min(value, this._maximum));
|
597 |
|
598 |
|
599 | if (this._value === value) {
|
600 | return;
|
601 | }
|
602 |
|
603 |
|
604 | this._value = value;
|
605 |
|
606 |
|
607 | this.update();
|
608 |
|
609 |
|
610 | this._thumbMoved.emit(value);
|
611 | }
|
612 |
|
613 | |
614 |
|
615 |
|
616 | private _onRepeat = () => {
|
617 |
|
618 | this._repeatTimer = -1;
|
619 |
|
620 |
|
621 | if (!this._pressData) {
|
622 | return;
|
623 | }
|
624 |
|
625 |
|
626 | let part = this._pressData.part;
|
627 |
|
628 |
|
629 | if (part === 'thumb') {
|
630 | return;
|
631 | }
|
632 |
|
633 |
|
634 | this._repeatTimer = window.setTimeout(this._onRepeat, 20);
|
635 |
|
636 |
|
637 | let mouseX = this._pressData.mouseX;
|
638 | let mouseY = this._pressData.mouseY;
|
639 |
|
640 |
|
641 | if (part === 'decrement') {
|
642 |
|
643 | if (!ElementExt.hitTest(this.decrementNode, mouseX, mouseY)) {
|
644 | return;
|
645 | }
|
646 |
|
647 |
|
648 | this._stepRequested.emit('decrement');
|
649 |
|
650 |
|
651 | return;
|
652 | }
|
653 |
|
654 |
|
655 | if (part === 'increment') {
|
656 |
|
657 | if (!ElementExt.hitTest(this.incrementNode, mouseX, mouseY)) {
|
658 | return;
|
659 | }
|
660 |
|
661 |
|
662 | this._stepRequested.emit('increment');
|
663 |
|
664 |
|
665 | return;
|
666 | }
|
667 |
|
668 |
|
669 | if (part === 'track') {
|
670 |
|
671 | if (!ElementExt.hitTest(this.trackNode, mouseX, mouseY)) {
|
672 | return;
|
673 | }
|
674 |
|
675 |
|
676 | let thumbNode = this.thumbNode;
|
677 |
|
678 |
|
679 | if (ElementExt.hitTest(thumbNode, mouseX, mouseY)) {
|
680 | return;
|
681 | }
|
682 |
|
683 |
|
684 | let thumbRect = thumbNode.getBoundingClientRect();
|
685 |
|
686 |
|
687 | let dir: 'decrement' | 'increment';
|
688 | if (this._orientation === 'horizontal') {
|
689 | dir = mouseX < thumbRect.left ? 'decrement' : 'increment';
|
690 | } else {
|
691 | dir = mouseY < thumbRect.top ? 'decrement' : 'increment';
|
692 | }
|
693 |
|
694 |
|
695 | this._pageRequested.emit(dir);
|
696 |
|
697 |
|
698 | return;
|
699 | }
|
700 | };
|
701 |
|
702 | private _value = 0;
|
703 | private _page = 10;
|
704 | private _maximum = 100;
|
705 | private _repeatTimer = -1;
|
706 | private _orientation: ScrollBar.Orientation;
|
707 | private _pressData: Private.IPressData | null = null;
|
708 | private _thumbMoved = new Signal<this, number>(this);
|
709 | private _stepRequested = new Signal<this, 'decrement' | 'increment'>(this);
|
710 | private _pageRequested = new Signal<this, 'decrement' | 'increment'>(this);
|
711 | }
|
712 |
|
713 |
|
714 |
|
715 |
|
716 | export namespace ScrollBar {
|
717 | |
718 |
|
719 |
|
720 | export type Orientation = 'horizontal' | 'vertical';
|
721 |
|
722 | |
723 |
|
724 |
|
725 | export interface IOptions {
|
726 | |
727 |
|
728 |
|
729 |
|
730 |
|
731 | orientation?: Orientation;
|
732 |
|
733 | |
734 |
|
735 |
|
736 |
|
737 |
|
738 | value?: number;
|
739 |
|
740 | |
741 |
|
742 |
|
743 |
|
744 |
|
745 | page?: number;
|
746 |
|
747 | |
748 |
|
749 |
|
750 |
|
751 |
|
752 | maximum?: number;
|
753 | }
|
754 | }
|
755 |
|
756 |
|
757 |
|
758 |
|
759 | namespace Private {
|
760 | |
761 |
|
762 |
|
763 | export type ScrollBarPart = 'thumb' | 'track' | 'decrement' | 'increment';
|
764 |
|
765 | |
766 |
|
767 |
|
768 | export interface IPressData {
|
769 | |
770 |
|
771 |
|
772 | part: ScrollBarPart;
|
773 |
|
774 | |
775 |
|
776 |
|
777 | delta: number;
|
778 |
|
779 | |
780 |
|
781 |
|
782 | value: number;
|
783 |
|
784 | |
785 |
|
786 |
|
787 | override: IDisposable;
|
788 |
|
789 | |
790 |
|
791 |
|
792 | mouseX: number;
|
793 |
|
794 | |
795 |
|
796 |
|
797 | mouseY: number;
|
798 | }
|
799 |
|
800 | |
801 |
|
802 |
|
803 | export function createNode(): HTMLElement {
|
804 | let node = document.createElement('div');
|
805 | let decrement = document.createElement('div');
|
806 | let increment = document.createElement('div');
|
807 | let track = document.createElement('div');
|
808 | let thumb = document.createElement('div');
|
809 | decrement.className = 'lm-ScrollBar-button';
|
810 | increment.className = 'lm-ScrollBar-button';
|
811 | decrement.dataset['action'] = 'decrement';
|
812 | increment.dataset['action'] = 'increment';
|
813 | track.className = 'lm-ScrollBar-track';
|
814 | thumb.className = 'lm-ScrollBar-thumb';
|
815 |
|
816 | decrement.classList.add('p-ScrollBar-button');
|
817 | increment.classList.add('p-ScrollBar-button');
|
818 | track.classList.add('p-ScrollBar-track');
|
819 | thumb.classList.add('p-ScrollBar-thumb');
|
820 |
|
821 | track.appendChild(thumb);
|
822 | node.appendChild(decrement);
|
823 | node.appendChild(track);
|
824 | node.appendChild(increment);
|
825 | return node;
|
826 | }
|
827 |
|
828 | |
829 |
|
830 |
|
831 | export function findPart(
|
832 | scrollBar: ScrollBar,
|
833 | target: HTMLElement
|
834 | ): ScrollBarPart | null {
|
835 |
|
836 | if (scrollBar.thumbNode.contains(target)) {
|
837 | return 'thumb';
|
838 | }
|
839 |
|
840 |
|
841 | if (scrollBar.trackNode.contains(target)) {
|
842 | return 'track';
|
843 | }
|
844 |
|
845 |
|
846 | if (scrollBar.decrementNode.contains(target)) {
|
847 | return 'decrement';
|
848 | }
|
849 |
|
850 |
|
851 | if (scrollBar.incrementNode.contains(target)) {
|
852 | return 'increment';
|
853 | }
|
854 |
|
855 |
|
856 | return null;
|
857 | }
|
858 | }
|