11import { DOCUMENT } from '@angular/common' ;
22import {
3- ChangeDetectionStrategy , ChangeDetectorRef , Component ,
4- ElementRef , EventEmitter , Inject , Input , OnChanges , OnDestroy , OnInit , Output , SimpleChanges , TemplateRef
3+ AfterViewInit ,
4+ ChangeDetectionStrategy ,
5+ ChangeDetectorRef ,
6+ Component ,
7+ ElementRef ,
8+ EventEmitter ,
9+ HostListener ,
10+ Inject ,
11+ Input ,
12+ OnChanges ,
13+ OnDestroy ,
14+ OnInit ,
15+ Output ,
16+ SimpleChanges ,
17+ TemplateRef
518} from '@angular/core' ;
619import { fromEvent , Subscription } from 'rxjs' ;
720import { debounceTime } from 'rxjs/operators' ;
@@ -12,30 +25,35 @@ import { debounceTime } from 'rxjs/operators';
1225 styleUrls : [ './back-top.component.scss' ] ,
1326 changeDetection : ChangeDetectionStrategy . OnPush ,
1427 preserveWhitespaces : false ,
15- } )
16- export class BackTopComponent implements OnInit , OnChanges , OnDestroy {
28+ } )
29+ export class BackTopComponent implements OnChanges , OnInit , OnDestroy , AfterViewInit {
1730 @Input ( ) customTemplate : TemplateRef < any > ;
1831 @Input ( ) visibleHeight = 300 ;
1932 @Input ( ) bottom = '50px' ;
2033 @Input ( ) right = '30px' ;
2134 @Input ( ) scrollTarget : HTMLElement ;
22- @Output ( ) backTopEvent = new EventEmitter < boolean > ( ) ;
35+ @Input ( ) draggable = false ;
36+ @Output ( ) backTopEvent = new EventEmitter < boolean > ( ) ;
37+ @Output ( ) dragEvent = new EventEmitter < boolean > ( ) ;
2338
2439 currScrollTop = 0 ;
40+ duration = 0 ;
41+ cursorTimer : any ;
42+ dragBoundary : any ;
43+ moveCursor = false ;
2544 isVisible = false ;
26- SCROLL_REFRESH_INTERVAL = 100 ;
2745 target : HTMLElement | Window ;
2846 subs : Subscription = new Subscription ( ) ;
2947 document : Document ;
48+
49+ SCROLL_REFRESH_INTERVAL = 100 ;
50+ MOUSEDOWN_DELAY = 180 ;
51+ RESIZE_DELAY = 300 ;
52+
3053 constructor ( private cdr : ChangeDetectorRef , private el : ElementRef , @Inject ( DOCUMENT ) private doc : any ) {
3154 this . document = this . doc ;
3255 }
3356
34- ngOnInit ( ) {
35- this . addScrollEvent ( ) ;
36- this . showButton ( ) ;
37- }
38-
3957 ngOnChanges ( changes : SimpleChanges ) : void {
4058 if ( changes [ 'scrollTarget' ] ) {
4159 if ( this . subs ) {
@@ -46,6 +64,42 @@ export class BackTopComponent implements OnInit, OnChanges, OnDestroy {
4664 }
4765 }
4866
67+ ngOnInit ( ) : void {
68+ this . addScrollEvent ( ) ;
69+ this . showButton ( ) ;
70+ }
71+
72+ ngAfterViewInit ( ) : void {
73+ if ( this . draggable ) {
74+ // 窗口大小改变时,如缩放比例或组件超出显示范围重置到默认位置
75+ this . subs . add (
76+ fromEvent ( window , 'resize' )
77+ . pipe ( debounceTime ( this . RESIZE_DELAY ) )
78+ . subscribe ( ( ) => {
79+ const dom = this . dragBoundary ?. dom || this . el . nativeElement . querySelector ( 'div.devui-backtop' ) ;
80+ if ( dom ) {
81+ // 不显示时无法获取getBoundingClientRect,使用style获取
82+ const style = getComputedStyle ( dom ) ;
83+ const left = parseInt ( style . left , 10 ) || 0 ;
84+ const top = parseInt ( style . top , 10 ) || 0 ;
85+ if ( window . devicePixelRatio !== 1 || left > window . innerWidth || top > window . innerHeight ) {
86+ this . onMouseUp ( ) ;
87+ this . duration = 0 ;
88+ dom . style . left = 'unset' ;
89+ dom . style . top = 'unset' ;
90+ }
91+ }
92+ } )
93+ ) ;
94+ }
95+ }
96+
97+ ngOnDestroy ( ) : void {
98+ if ( this . subs ) {
99+ this . subs . unsubscribe ( ) ;
100+ }
101+ }
102+
49103 addScrollEvent ( ) {
50104 this . subs . add (
51105 fromEvent ( this . getScrollTarget ( ) , 'scroll' )
@@ -67,9 +121,11 @@ export class BackTopComponent implements OnInit, OnChanges, OnDestroy {
67121 }
68122
69123 showButton ( ) {
70- this . currScrollTop = this . target === window ?
71- ( window . pageYOffset || this . document . documentElement . scrollTop || this . document . body . scrollTop ) : this . scrollTarget . scrollTop ;
72- if ( this . isVisible !== ( this . currScrollTop >= this . visibleHeight ) ) {
124+ this . currScrollTop =
125+ this . target === window
126+ ? window . pageYOffset || this . document . documentElement . scrollTop || this . document . body . scrollTop
127+ : this . scrollTarget . scrollTop ;
128+ if ( this . isVisible !== this . currScrollTop >= this . visibleHeight ) {
73129 this . isVisible = ! this . isVisible ;
74130 }
75131 }
@@ -85,9 +141,95 @@ export class BackTopComponent implements OnInit, OnChanges, OnDestroy {
85141 this . backTopEvent . emit ( true ) ;
86142 }
87143
88- ngOnDestroy ( ) {
89- if ( this . subs ) {
90- this . subs . unsubscribe ( ) ;
144+ setDragBoundary ( ) {
145+ const dom = this . el . nativeElement . querySelector ( 'div.devui-backtop' ) ;
146+ let boxRect ;
147+ let minLeft ;
148+ let minTop ;
149+ let maxLeft ;
150+ let maxTop ;
151+ if ( dom ) {
152+ const { width, height } = dom . getBoundingClientRect ( ) ;
153+ if ( this . scrollTarget ) {
154+ boxRect = this . scrollTarget . getBoundingClientRect ( ) ;
155+ minLeft = - 1 * boxRect . x ;
156+ minTop = - 1 * boxRect . y ;
157+ maxLeft = window . innerWidth - boxRect . x - width ;
158+ maxTop = window . innerHeight - boxRect . y - height ;
159+ } else {
160+ boxRect = { x : 0 , y : 0 } ;
161+ minLeft = 0 ;
162+ minTop = 0 ;
163+ maxLeft = window . innerWidth - width ;
164+ maxTop = window . innerHeight - height ;
165+ }
166+ this . dragBoundary = { dom, boxRect, minLeft, minTop, maxLeft, maxTop } ;
167+ }
168+ }
169+
170+ // mousedown mouseup click 按顺序触发且前一个结束触发下一个
171+ mousedownEvent ( event : MouseEvent ) {
172+ if ( this . draggable ) {
173+ event . preventDefault ( ) ;
174+ this . cursorTimer = setTimeout ( ( ) => {
175+ this . setDragBoundary ( ) ;
176+ this . duration = new Date ( ) . getTime ( ) ;
177+ this . moveCursor = true ;
178+ this . cdr . markForCheck ( ) ;
179+ this . dragEvent . emit ( true ) ;
180+ } , this . MOUSEDOWN_DELAY ) ;
181+ }
182+ }
183+
184+ mouseleaveEvent ( ) {
185+ if ( this . draggable && this . cursorTimer ) {
186+ clearTimeout ( this . cursorTimer ) ;
187+ }
188+ }
189+
190+ clickEvent ( event : MouseEvent ) {
191+ if ( this . draggable && this . duration > this . MOUSEDOWN_DELAY ) {
192+ event . stopPropagation ( ) ;
193+ this . duration = 0 ;
194+ }
195+ }
196+
197+ @HostListener ( 'document:mouseup' , [ ] )
198+ onMouseUp ( ) {
199+ if ( this . draggable ) {
200+ if ( this . cursorTimer ) {
201+ clearTimeout ( this . cursorTimer ) ;
202+ }
203+ if ( this . moveCursor ) {
204+ this . moveCursor = false ;
205+ this . cdr . markForCheck ( ) ;
206+ this . dragEvent . emit ( false ) ;
207+ }
208+ this . duration = this . duration && new Date ( ) . getTime ( ) - this . duration ;
209+ }
210+ }
211+
212+ @HostListener ( 'document:mousemove' , [ '$event' ] )
213+ onMouseMove ( event : MouseEvent ) {
214+ if ( this . draggable && this . moveCursor && this . dragBoundary ) {
215+ // 先判断再执行阻止默认事件,否则可能影响鼠标拖选功能
216+ event . preventDefault ( ) ;
217+ const posX = event . movementX ;
218+ const posY = event . movementY ;
219+ const rect = this . dragBoundary . dom . getBoundingClientRect ( ) ;
220+ const left = rect . x - this . dragBoundary . boxRect . x + posX ;
221+ const top = rect . y - this . dragBoundary . boxRect . y + posY ;
222+ const isLeft = left < this . dragBoundary . minLeft ;
223+ const isRight = left > this . dragBoundary . maxLeft ;
224+ const isTop = top < this . dragBoundary . minTop ;
225+ const isBottom = top > this . dragBoundary . maxTop ;
226+ this . dragBoundary . dom . style . left = `${ isRight ? this . dragBoundary . maxLeft : isLeft ? this . dragBoundary . minLeft : left } px` ;
227+ this . dragBoundary . dom . style . top = `${ isBottom ? this . dragBoundary . maxTop : isTop ? this . dragBoundary . minTop : top } px` ;
228+ // 如到达边界释放拖拽动作,避免鼠标偏移较大距离后返回主视窗仍可拖拽
229+ if ( [ isLeft , isRight , isTop , isBottom ] . includes ( true ) ) {
230+ this . onMouseUp ( ) ;
231+ this . duration = 0 ;
232+ }
91233 }
92234 }
93235}
0 commit comments