-
Notifications
You must be signed in to change notification settings - Fork 1.8k
/
gridPanel.ts
1407 lines (1154 loc) · 60.2 KB
/
gridPanel.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import { GridOptionsWrapper } from "../gridOptionsWrapper";
import { ColumnController } from "../columnController/columnController";
import { ColumnApi } from "../columnController/columnApi";
import { RowRenderer } from "../rendering/rowRenderer";
import { Autowired, Context, Optional, PostConstruct, PreDestroy } from "../context/context";
import { EventService } from "../eventService";
import { BodyHeightChangedEvent, BodyScrollEvent, Events } from "../events";
import { DragListenerParams, DragService } from "../dragAndDrop/dragService";
import { IRangeController } from "../interfaces/iRangeController";
import { Constants } from "../constants";
import { MouseEventService } from "./mouseEventService";
import { IClipboardService } from "../interfaces/iClipboardService";
import { FocusedCellController } from "../focusedCellController";
import { IContextMenuFactory } from "../interfaces/iContextMenuFactory";
import { ScrollVisibleService, SetScrollsVisibleParams } from "./scrollVisibleService";
import { Column } from "../entities/column";
import { RowContainerComponent } from "../rendering/rowContainerComponent";
import { RowNode } from "../entities/rowNode";
import { PaginationAutoPageSizeService, PaginationProxy } from "../rowModels/paginationProxy";
import { PopupEditorWrapper } from "../rendering/cellEditors/popupEditorWrapper";
import { AlignedGridsService } from "../alignedGridsService";
import { PinnedRowModel } from "../rowModels/pinnedRowModel";
import { GridApi } from "../gridApi";
import { AnimationFrameService } from "../misc/animationFrameService";
import { RowComp } from "../rendering/rowComp";
import { NavigationService } from "./navigationService";
import { CellComp } from "../rendering/cellComp";
import { ValueService } from "../valueService/valueService";
import { LongTapEvent, TouchListener } from "../widgets/touchListener";
import { ComponentRecipes } from "../components/framework/componentRecipes";
import { DragAndDropService } from "../dragAndDrop/dragAndDropService";
import { RowDragFeature } from "./rowDragFeature";
import { HeightScaler } from "../rendering/heightScaler";
import { IOverlayWrapperComp } from "../rendering/overlays/overlayWrapperComponent";
import { Component } from "../widgets/component";
import { AutoHeightCalculator } from "../rendering/autoHeightCalculator";
import { ColumnAnimationService } from "../rendering/columnAnimationService";
import { AutoWidthCalculator } from "../rendering/autoWidthCalculator";
import { Beans } from "../rendering/beans";
import { RefSelector } from "../widgets/componentAnnotations";
import { HeaderRootComp } from "../headerRendering/headerRootComp";
import { ResizeObserverService } from "../misc/resizeObserverService";
import { _ } from "../utils";
// in the html below, it is important that there are no white space between some of the divs, as if there is white space,
// it won't render correctly in safari, as safari renders white space as a gap
const GRID_PANEL_NORMAL_TEMPLATE =
`<div class="ag-root ag-font-style" role="grid" unselectable="on">
<ag-header-root ref="headerRoot" unselectable="on"></ag-header-root>
<div class="ag-floating-top" ref="eTop" role="presentation" unselectable="on">
<div class="ag-pinned-left-floating-top" ref="eLeftTop" role="presentation" unselectable="on"></div>
<div class="ag-floating-top-viewport" ref="eTopViewport" role="presentation" unselectable="on">
<div class="ag-floating-top-container" ref="eTopContainer" role="presentation" unselectable="on"></div>
</div>
<div class="ag-pinned-right-floating-top" ref="eRightTop" role="presentation" unselectable="on"></div>
<div class="ag-floating-top-full-width-container" ref="eTopFullWidthContainer" role="presentation" unselectable="on"></div>
</div>
<div class="ag-body-viewport" ref="eBodyViewport" role="presentation" unselectable="on">
<div class="ag-pinned-left-cols-container" ref="eLeftContainer" role="presentation" unselectable="on"></div>
<div class="ag-center-cols-clipper" ref="eCenterColsClipper">
<div class="ag-center-cols-viewport" ref="eCenterViewport" role="presentation" unselectable="on">
<div class="ag-center-cols-container" ref="eCenterContainer" role="presentation" unselectable="on"></div>
</div>
</div>
<div class="ag-pinned-right-cols-container" ref="eRightContainer" role="presentation" unselectable="on"></div>
<div class="ag-full-width-container" ref="eFullWidthContainer" role="presentation" unselectable="on"></div>
</div>
<div class="ag-floating-bottom" ref="eBottom" role="presentation" unselectable="on">
<div class="ag-pinned-left-floating-bottom" ref="eLeftBottom" role="presentation" unselectable="on"></div>
<div class="ag-floating-bottom-viewport" ref="eBottomViewport" role="presentation" unselectable="on">
<div class="ag-floating-bottom-container" ref="eBottomContainer" role="presentation" unselectable="on"></div>
</div>
<div class="ag-pinned-right-floating-bottom" ref="eRightBottom" role="presentation" unselectable="on"></div>
<div class="ag-floating-bottom-full-width-container" ref="eBottomFullWidthContainer" role="presentation" unselectable="on"></div>
</div>
<div class="ag-body-horizontal-scroll" ref="eHorizontalScrollBody">
<div class="ag-horizontal-left-spacer" ref="eHorizontalLeftSpacer"></div>
<div class="ag-body-horizontal-scroll-viewport" ref="eBodyHorizontalScrollViewport">
<div class="ag-body-horizontal-scroll-container" ref="eBodyHorizontalScrollContainer"></div>
</div>
<div class="ag-horizontal-right-spacer" ref="eHorizontalRightSpacer"></div>
</div>
<div class="ag-overlay" ref="eOverlay"></div>
</div>`;
export type RowContainerComponentNames =
'fullWidth' |
'body' |
'pinnedLeft' |
'pinnedRight' |
'floatingTop' |
'floatingTopPinnedLeft' |
'floatingTopPinnedRight' |
'floatingTopFullWidth' |
'floatingBottom' |
'floatingBottomPinnedLeft' |
'floatingBottomPinnedRight' |
'floatingBottomFullWith';
export type RowContainerComponents = { [K in RowContainerComponentNames]: RowContainerComponent };
export class GridPanel extends Component {
@Autowired('alignedGridsService') private alignedGridsService: AlignedGridsService;
@Autowired('gridOptionsWrapper') private gridOptionsWrapper: GridOptionsWrapper;
@Autowired('columnController') private columnController: ColumnController;
@Autowired('rowRenderer') private rowRenderer: RowRenderer;
@Autowired('pinnedRowModel') private pinnedRowModel: PinnedRowModel;
@Autowired('eventService') private eventService: EventService;
@Autowired('context') private context: Context;
@Autowired('animationFrameService') private animationFrameService: AnimationFrameService;
@Autowired('navigationService') private navigationService: NavigationService;
@Autowired('autoHeightCalculator') private autoHeightCalculator: AutoHeightCalculator;
@Autowired('columnAnimationService') private columnAnimationService: ColumnAnimationService;
@Autowired('autoWidthCalculator') private autoWidthCalculator: AutoWidthCalculator;
@Autowired('paginationAutoPageSizeService') private paginationAutoPageSizeService: PaginationAutoPageSizeService;
@Autowired('beans') private beans: Beans;
@Autowired('paginationProxy') private paginationProxy: PaginationProxy;
@Autowired('columnApi') private columnApi: ColumnApi;
@Autowired('gridApi') private gridApi: GridApi;
@Autowired('dragService') private dragService: DragService;
@Autowired('mouseEventService') private mouseEventService: MouseEventService;
@Autowired('focusedCellController') private focusedCellController: FocusedCellController;
@Autowired('$scope') private $scope: any;
@Autowired('scrollVisibleService') private scrollVisibleService: ScrollVisibleService;
@Autowired('valueService') private valueService: ValueService;
@Autowired('componentRecipes') private componentRecipes: ComponentRecipes;
@Autowired('dragAndDropService') private dragAndDropService: DragAndDropService;
@Autowired('heightScaler') private heightScaler: HeightScaler;
@Autowired('enterprise') private enterprise: boolean;
@Autowired('resizeObserverService') private resizeObserverService: ResizeObserverService;
@Optional('rangeController') private rangeController: IRangeController;
@Optional('contextMenuFactory') private contextMenuFactory: IContextMenuFactory;
@Optional('clipboardService') private clipboardService: IClipboardService;
// @RefSelector('eBody') private eBody: HTMLElement;
@RefSelector('eBodyViewport') private eBodyViewport: HTMLElement;
@RefSelector('eCenterContainer') private eCenterContainer: HTMLElement;
@RefSelector('eCenterViewport') private eCenterViewport: HTMLElement;
@RefSelector('eLeftContainer') private eLeftContainer: HTMLElement;
@RefSelector('eRightContainer') private eRightContainer: HTMLElement;
@RefSelector('eCenterColsClipper') private eCenterColsClipper: HTMLElement;
// fake horizontal scroller
@RefSelector('eHorizontalScrollBody') private eHorizontalScrollBody: HTMLElement;
@RefSelector('eHorizontalLeftSpacer') private eHorizontalLeftSpacer: HTMLElement;
@RefSelector('eHorizontalRightSpacer') private eHorizontalRightSpacer: HTMLElement;
@RefSelector('eBodyHorizontalScrollViewport') private eBodyHorizontalScrollViewport: HTMLElement;
@RefSelector('eBodyHorizontalScrollContainer') private eBodyHorizontalScrollContainer: HTMLElement;
@RefSelector('eFullWidthContainer') private eFullWidthContainer: HTMLElement;
@RefSelector('eTop') private eTop: HTMLElement;
@RefSelector('eLeftTop') private eLeftTop: HTMLElement;
@RefSelector('eRightTop') private eRightTop: HTMLElement;
@RefSelector('eTopContainer') private eTopContainer: HTMLElement;
@RefSelector('eTopViewport') private eTopViewport: HTMLElement;
@RefSelector('eTopFullWidthContainer') private eTopFullWidthContainer: HTMLElement;
@RefSelector('eBottom') private eBottom: HTMLElement;
@RefSelector('eLeftBottom') private eLeftBottom: HTMLElement;
@RefSelector('eRightBottom') private eRightBottom: HTMLElement;
@RefSelector('eBottomContainer') private eBottomContainer: HTMLElement;
@RefSelector('eBottomViewport') private eBottomViewport: HTMLElement;
@RefSelector('eBottomFullWidthContainer') private eBottomFullWidthContainer: HTMLElement;
@RefSelector('headerRoot') headerRootComp: HeaderRootComp;
private rowContainerComponents: RowContainerComponents;
private eAllCellContainers: HTMLElement[];
private eOverlay: HTMLElement;
private scrollLeft = -1;
private scrollTop = -1;
private lastHorizontalScrollMillis = 0;
private horizontalScroller: HTMLElement;
private bodyHeight: number;
// properties we use a lot, so keep reference
private enableRtl: boolean;
private scrollWidth: number;
// used to track if pinned panels are showing, so we can turn them off if not
private pinningRight: boolean;
private pinningLeft: boolean;
private overlayWrapper: IOverlayWrapperComp;
private printLayout: boolean;
constructor() {
super(GRID_PANEL_NORMAL_TEMPLATE);
}
public getVScrollPosition(): { top: number, bottom: number } {
const result = {
top: this.eBodyViewport.scrollTop,
bottom: this.eBodyViewport.scrollTop + this.eBodyViewport.offsetHeight
};
return result;
}
public getHScrollPosition(): { left: number, right: number } {
const result = {
left: this.eCenterViewport.scrollLeft,
right: this.eCenterViewport.scrollLeft + this.eCenterViewport.offsetWidth
};
return result;
}
// we override this, as the base class is missing the annotation
@PreDestroy
public destroy() {
super.destroy();
}
private onRowDataChanged(): void {
this.showOrHideOverlay();
}
private showOrHideOverlay(): void {
const isEmpty = this.paginationProxy.isEmpty();
const isSuppressNoRowsOverlay = this.gridOptionsWrapper.isSuppressNoRowsOverlay();
const method = isEmpty && !isSuppressNoRowsOverlay ? 'showNoRowsOverlay' : 'hideOverlay';
this[method]();
}
private onNewColumnsLoaded(): void {
// hide overlay if columns and rows exist, this can happen if columns are loaded after data.
// this problem exists before of the race condition between the services (column controller in this case)
// and the view (grid panel). if the model beans were all initialised first, and then the view beans second,
// this race condition would not happen.
if (this.columnController.isReady() && !this.paginationProxy.isEmpty()) {
this.hideOverlay();
}
}
@PostConstruct
private init() {
this.instantiate(this.context);
this.scrollWidth = this.gridOptionsWrapper.getScrollbarWidth();
this.enableRtl = this.gridOptionsWrapper.isEnableRtl();
this.printLayout = this.gridOptionsWrapper.getDomLayout() === Constants.DOM_LAYOUT_PRINT;
// these elements have different CSS when layout changes
this.gridOptionsWrapper.addLayoutElement(this.getGui());
this.gridOptionsWrapper.addLayoutElement(this.eBodyViewport);
this.suppressScrollOnFloatingRow();
this.setupRowAnimationCssClass();
this.buildRowContainerComponents();
this.addEventListeners();
this.addDragListeners();
this.addScrollListener();
this.setupOverlay();
if (this.gridOptionsWrapper.isRowModelDefault() && !this.gridOptionsWrapper.getRowData()) {
this.showLoadingOverlay();
}
this.setPinnedContainerSize();
this.setHeaderAndFloatingHeights();
this.disableBrowserDragging();
this.addShortcutKeyListeners();
this.addMouseListeners();
this.addKeyboardEvents();
this.addBodyViewportListener();
this.addStopEditingWhenGridLosesFocus();
this.mockContextMenuForIPad();
this.addRowDragListener();
if (this.$scope) {
this.addAngularApplyCheck();
}
this.onDisplayedColumnsWidthChanged();
this.gridApi.registerGridComp(this);
this.alignedGridsService.registerGridComp(this);
this.headerRootComp.registerGridComp(this);
this.navigationService.registerGridComp(this);
this.heightScaler.registerGridComp(this);
this.autoHeightCalculator.registerGridComp(this);
this.columnAnimationService.registerGridComp(this);
this.autoWidthCalculator.registerGridComp(this);
this.paginationAutoPageSizeService.registerGridComp(this);
this.beans.registerGridComp(this);
this.rowRenderer.registerGridComp(this);
if (this.rangeController) {
this.rangeController.registerGridComp(this);
}
[this.eCenterViewport, this.eBodyViewport].forEach(viewport => {
const unsubscribeFromResize = this.resizeObserverService.observeResize(
viewport, this.onCenterViewportResized.bind(this));
this.addDestroyFunc(() => unsubscribeFromResize());
});
}
private onDomLayoutChanged(): void {
const newPrintLayout = this.gridOptionsWrapper.getDomLayout() === Constants.DOM_LAYOUT_PRINT;
if (this.printLayout !== newPrintLayout) {
this.printLayout = newPrintLayout;
this.setWidthsOfContainers();
// pinned containers are always hidden for print layout
this.setPinnedContainerSize();
}
}
private onCenterViewportResized(): void {
this.checkViewportAndScrolls();
}
// used by ColumnAnimationService
public setColumnMovingCss(moving: boolean): void {
this.addOrRemoveCssClass('ag-column-moving', moving);
}
private setupOverlay(): void {
this.overlayWrapper = this.componentRecipes.newOverlayWrapperComponent();
this.eOverlay = this.queryForHtmlElement('[ref="eOverlay"]');
this.overlayWrapper.hideOverlay(this.eOverlay);
}
private addRowDragListener(): void {
const rowDragFeature = new RowDragFeature(this.eBodyViewport, this);
this.context.wireBean(rowDragFeature);
this.dragAndDropService.addDropTarget(rowDragFeature);
}
private addStopEditingWhenGridLosesFocus(): void {
if (!this.gridOptionsWrapper.isStopEditingWhenGridLosesFocus()) { return; }
const focusOutListener = (event: FocusEvent): void => {
// this is the element the focus is moving to
const elementWithFocus = event.relatedTarget;
// see if the element the focus is going to is part of the grid
let clickInsideGrid = false;
let pointer: any = elementWithFocus;
while (_.exists(pointer) && !clickInsideGrid) {
const isPopup = !!this.gridOptionsWrapper.getDomData(pointer, PopupEditorWrapper.DOM_KEY_POPUP_EDITOR_WRAPPER);
const isBody = this.eBodyViewport === pointer || this.eBottom === pointer || this.eTop === pointer;
clickInsideGrid = isPopup || isBody;
pointer = pointer.parentNode;
}
if (!clickInsideGrid) {
this.rowRenderer.stopEditing();
}
};
this.addDestroyableEventListener(this.eBodyViewport, 'focusout', focusOutListener);
this.addDestroyableEventListener(this.eTop, 'focusout', focusOutListener);
this.addDestroyableEventListener(this.eBottom, 'focusout', focusOutListener);
}
private addAngularApplyCheck(): void {
// this makes sure if we queue up requests, we only execute oe
let applyTriggered = false;
const listener = () => {
// only need to do one apply at a time
if (applyTriggered) { return; }
applyTriggered = true; // mark 'need apply' to true
window.setTimeout(() => {
applyTriggered = false;
this.$scope.$apply();
}, 0);
};
// these are the events we need to do an apply after - these are the ones that can end up
// with columns added or removed
this.addDestroyableEventListener(this.eventService, Events.EVENT_DISPLAYED_COLUMNS_CHANGED, listener);
this.addDestroyableEventListener(this.eventService, Events.EVENT_VIRTUAL_COLUMNS_CHANGED, listener);
}
// if we do not do this, then the user can select a pic in the grid (eg an image in a custom cell renderer)
// and then that will start the browser native drag n' drop, which messes up with our own drag and drop.
private disableBrowserDragging(): void {
this.addGuiEventListener('dragstart', (event: MouseEvent) => {
if (event.target instanceof HTMLImageElement) {
event.preventDefault();
return false;
}
});
}
private addEventListeners(): void {
this.addDestroyableEventListener(this.eventService, Events.EVENT_DISPLAYED_COLUMNS_CHANGED, this.onDisplayedColumnsChanged.bind(this));
this.addDestroyableEventListener(this.eventService, Events.EVENT_DISPLAYED_COLUMNS_WIDTH_CHANGED, this.onDisplayedColumnsWidthChanged.bind(this));
this.addDestroyableEventListener(this.eventService, Events.EVENT_PINNED_ROW_DATA_CHANGED, this.setHeaderAndFloatingHeights.bind(this));
this.addDestroyableEventListener(this.eventService, Events.EVENT_ROW_DATA_CHANGED, this.onRowDataChanged.bind(this));
this.addDestroyableEventListener(this.eventService, Events.EVENT_ROW_DATA_UPDATED, this.onRowDataChanged.bind(this));
this.addDestroyableEventListener(this.eventService, Events.EVENT_NEW_COLUMNS_LOADED, this.onNewColumnsLoaded.bind(this));
this.addDestroyableEventListener(this.gridOptionsWrapper, GridOptionsWrapper.PROP_HEADER_HEIGHT, this.setHeaderAndFloatingHeights.bind(this));
this.addDestroyableEventListener(this.gridOptionsWrapper, GridOptionsWrapper.PROP_PIVOT_HEADER_HEIGHT, this.setHeaderAndFloatingHeights.bind(this));
this.addDestroyableEventListener(this.gridOptionsWrapper, GridOptionsWrapper.PROP_GROUP_HEADER_HEIGHT, this.setHeaderAndFloatingHeights.bind(this));
this.addDestroyableEventListener(this.gridOptionsWrapper, GridOptionsWrapper.PROP_PIVOT_GROUP_HEADER_HEIGHT, this.setHeaderAndFloatingHeights.bind(this));
this.addDestroyableEventListener(this.gridOptionsWrapper, GridOptionsWrapper.PROP_FLOATING_FILTERS_HEIGHT, this.setHeaderAndFloatingHeights.bind(this));
this.addDestroyableEventListener(this.gridOptionsWrapper, GridOptionsWrapper.PROP_DOM_LAYOUT, this.onDomLayoutChanged.bind(this));
}
private addDragListeners(): void {
if (
!this.gridOptionsWrapper.isEnableRangeSelection() || // no range selection if no property
_.missing(this.rangeController) // no range selection if not enterprise version
) {
return;
}
const containers = [
this.eLeftContainer,
this.eRightContainer,
this.eCenterContainer,
this.eTop,
this.eBottom
];
containers.forEach(container => {
const params: DragListenerParams = {
eElement: container,
onDragStart: this.rangeController.onDragStart.bind(this.rangeController),
onDragStop: this.rangeController.onDragStop.bind(this.rangeController),
onDragging: this.rangeController.onDragging.bind(this.rangeController),
// for range selection by dragging the mouse, we want to ignore the event if shift key is pressed,
// as shift key click is another type of range selection
skipMouseEvent: mouseEvent => mouseEvent.shiftKey
};
this.dragService.addDragSource(params);
this.addDestroyFunc(() => this.dragService.removeDragSource(params));
});
}
private addMouseListeners(): void {
const eventNames = ['click', 'mousedown', 'dblclick', 'contextmenu', 'mouseover', 'mouseout'];
eventNames.forEach(eventName => {
const listener = this.processMouseEvent.bind(this, eventName);
this.eAllCellContainers.forEach(container =>
this.addDestroyableEventListener(container, eventName, listener)
);
});
}
private addKeyboardEvents(): void {
const eventNames = ['keydown', 'keypress'];
eventNames.forEach(eventName => {
const listener = this.processKeyboardEvent.bind(this, eventName);
this.eAllCellContainers.forEach(container => {
this.addDestroyableEventListener(container, eventName, listener);
});
});
}
private addBodyViewportListener(): void {
// we want to listen for clicks directly on the eBodyViewport, so the user has a way of showing
// the context menu if no rows or columns are displayed, or user simply clicks outside of a cell
const listener = (mouseEvent: MouseEvent) => {
const target = _.getTarget(mouseEvent);
if (target === this.eBodyViewport) {
// show it
this.onContextMenu(mouseEvent, null, null, null, null);
this.preventDefaultOnContextMenu(mouseEvent);
}
};
//For some reason listening only to this.eBody doesn't work... Maybe because the event is consumed somewhere else?
//In any case, not expending much time on this, if anyone comes across this and knows how to make this work with
//one listener please go ahead and change it...
this.addDestroyableEventListener(this.eBodyViewport, 'contextmenu', listener);
}
// + rangeController - used to know when to scroll when user is dragging outside the
// main viewport while doing a range selection
public getBodyClientRect(): ClientRect {
if (!this.eBodyViewport) { return; }
return this.eBodyViewport.getBoundingClientRect();
}
private getRowForEvent(event: Event): RowComp {
let sourceElement = _.getTarget(event);
while (sourceElement) {
const renderedRow = this.gridOptionsWrapper.getDomData(sourceElement, RowComp.DOM_DATA_KEY_RENDERED_ROW);
if (renderedRow) {
return renderedRow;
}
sourceElement = sourceElement.parentElement;
}
return null;
}
private processKeyboardEvent(eventName: string, keyboardEvent: KeyboardEvent): void {
const renderedCell = this.mouseEventService.getRenderedCellForEvent(keyboardEvent);
if (!renderedCell) { return; }
switch (eventName) {
case 'keydown':
// first see if it's a scroll key, page up / down, home / end etc
const wasScrollKey = this.navigationService.handlePageScrollingKey(keyboardEvent);
// if not a scroll key, then we pass onto cell
if (!wasScrollKey) {
renderedCell.onKeyDown(keyboardEvent);
}
break;
case 'keypress':
renderedCell.onKeyPress(keyboardEvent);
break;
}
}
// gets called by rowRenderer when new data loaded, as it will want to scroll to the top
public scrollToTop(): void {
this.eBodyViewport.scrollTop = 0;
}
private processMouseEvent(eventName: string, mouseEvent: MouseEvent): void {
if (
!this.mouseEventService.isEventFromThisGrid(mouseEvent) ||
_.isStopPropagationForAgGrid(mouseEvent)
) {
return;
}
const rowComp = this.getRowForEvent(mouseEvent);
const cellComp = this.mouseEventService.getRenderedCellForEvent(mouseEvent);
if (eventName === "contextmenu") {
this.handleContextMenuMouseEvent(mouseEvent, null, rowComp, cellComp);
} else {
if (cellComp) {
cellComp.onMouseEvent(eventName, mouseEvent);
}
if (rowComp) {
rowComp.onMouseEvent(eventName, mouseEvent);
}
}
this.preventDefaultOnContextMenu(mouseEvent);
}
private mockContextMenuForIPad(): void {
// we do NOT want this when not in ipad, otherwise we will be doing
if (!_.isUserAgentIPad()) { return; }
this.eAllCellContainers.forEach(container => {
const touchListener = new TouchListener(container);
const longTapListener = (event: LongTapEvent) => {
const rowComp = this.getRowForEvent(event.touchEvent);
const cellComp = this.mouseEventService.getRenderedCellForEvent(event.touchEvent);
this.handleContextMenuMouseEvent(null, event.touchEvent, rowComp, cellComp);
};
this.addDestroyableEventListener(touchListener, TouchListener.EVENT_LONG_TAP, longTapListener);
this.addDestroyFunc(() => touchListener.destroy());
});
}
private handleContextMenuMouseEvent(mouseEvent: MouseEvent, touchEvent: TouchEvent, rowComp: RowComp, cellComp: CellComp) {
const rowNode = rowComp ? rowComp.getRowNode() : null;
const column = cellComp ? cellComp.getColumn() : null;
let value = null;
if (column) {
const event = mouseEvent ? mouseEvent : touchEvent;
cellComp.dispatchCellContextMenuEvent(event);
value = this.valueService.getValue(column, rowNode);
}
this.onContextMenu(mouseEvent, touchEvent, rowNode, column, value);
}
private onContextMenu(mouseEvent: MouseEvent, touchEvent: TouchEvent, rowNode: RowNode, column: Column, value: any): void {
// to allow us to debug in chrome, we ignore the event if ctrl is pressed.
// not everyone wants this, so first 'if' below allows to turn this hack off.
if (!this.gridOptionsWrapper.isAllowContextMenuWithControlKey()) {
// then do the check
if (mouseEvent && (mouseEvent.ctrlKey || mouseEvent.metaKey)) { return; }
}
if (this.contextMenuFactory && !this.gridOptionsWrapper.isSuppressContextMenu()) {
const eventOrTouch: (MouseEvent | Touch) = mouseEvent ? mouseEvent : touchEvent.touches[0];
this.contextMenuFactory.showMenu(rowNode, column, value, eventOrTouch);
const event = mouseEvent ? mouseEvent : touchEvent;
event.preventDefault();
}
}
private preventDefaultOnContextMenu(mouseEvent: MouseEvent): void {
// if we don't do this, then middle click will never result in a 'click' event, as 'mousedown'
// will be consumed by the browser to mean 'scroll' (as you can scroll with the middle mouse
// button in the browser). so this property allows the user to receive middle button clicks if
// they want.
if (this.gridOptionsWrapper.isSuppressMiddleClickScrolls() && mouseEvent.which === 2) {
mouseEvent.preventDefault();
}
}
private addShortcutKeyListeners(): void {
this.eAllCellContainers.forEach((container) => {
this.addDestroyableEventListener(container, 'keydown', (event: KeyboardEvent) => {
const renderedCell = this.mouseEventService.getRenderedCellForEvent(event);
if (
// if the cell the event came from is editing, then we do not
// want to do the default shortcut keys, otherwise the editor
// (eg a text field) would not be able to do the normal cut/copy/paste
renderedCell && renderedCell.isEditing() ||
// for copy / paste, we don't want to execute when the event
// was from a child grid (happens in master detail)
!this.mouseEventService.isEventFromThisGrid(event)
) {
return;
}
if (event.ctrlKey || event.metaKey) {
switch (event.which) {
case Constants.KEY_A:
return this.onCtrlAndA(event);
case Constants.KEY_C:
return this.onCtrlAndC(event);
case Constants.KEY_V:
return this.onCtrlAndV(event);
case Constants.KEY_D:
return this.onCtrlAndD(event);
}
}
});
});
}
private onCtrlAndA(event: KeyboardEvent): boolean {
const {columnController, pinnedRowModel, paginationProxy, rangeController} = this;
const {PINNED_BOTTOM, PINNED_TOP} = Constants;
if (rangeController && paginationProxy.isRowsToRender()) {
const [isEmptyPinnedTop, isEmptyPinnedBottom] = [
pinnedRowModel.isEmpty(PINNED_TOP),
pinnedRowModel.isEmpty(PINNED_BOTTOM)
];
const floatingStart = isEmptyPinnedTop ? null : PINNED_TOP;
let floatingEnd: string;
let rowEnd: number;
if (isEmptyPinnedBottom) {
floatingEnd = null;
rowEnd = this.paginationProxy.getTotalRowCount() - 1;
} else {
floatingEnd = PINNED_BOTTOM;
rowEnd = pinnedRowModel.getPinnedBottomRowData().length - 1;
}
const allDisplayedColumns = columnController.getAllDisplayedColumns();
if (_.missingOrEmpty(allDisplayedColumns)) { return; }
rangeController.setRange({
rowStart: 0,
floatingStart: floatingStart,
rowEnd: rowEnd,
floatingEnd: floatingEnd,
columnStart: allDisplayedColumns[0],
columnEnd: allDisplayedColumns[allDisplayedColumns.length - 1]
});
}
event.preventDefault();
return false;
}
private onCtrlAndC(event: KeyboardEvent): boolean {
if (!this.clipboardService) { return; }
const focusedCell = this.focusedCellController.getFocusedCell();
this.clipboardService.copyToClipboard();
event.preventDefault();
// the copy operation results in loosing focus on the cell,
// because of the trickery the copy logic uses with a temporary
// widget. so we set it back again.
if (focusedCell) {
this.focusedCellController.setFocusedCell(focusedCell.rowIndex, focusedCell.column, focusedCell.floating, true);
}
return false;
}
private onCtrlAndV(event: KeyboardEvent): boolean {
if (!this.enterprise || this.gridOptionsWrapper.isSuppressClipboardPaste()) {
return;
}
this.clipboardService.pasteFromClipboard();
return false;
}
private onCtrlAndD(event: KeyboardEvent): boolean {
if (!this.enterprise) { return; }
this.clipboardService.copyRangeDown();
event.preventDefault();
return false;
}
// Valid values for position are bottom, middle and top
// position should be {'top','middle','bottom', or undefined/null}.
// if undefined/null, then the grid will to the minimal amount of scrolling,
// eg if grid needs to scroll up, it scrolls until row is on top,
// if grid needs to scroll down, it scrolls until row is on bottom,
// if row is already in view, grid does not scroll
// fixme - how does this work in the new way
public ensureIndexVisible(index: any, position?: string) {
// if for print or auto height, everything is always visible
if (this.printLayout) { return; }
const rowCount = this.paginationProxy.getTotalRowCount();
if (typeof index !== 'number' || index < 0 || index >= rowCount) {
console.warn('invalid row index for ensureIndexVisible: ' + index);
return;
}
this.paginationProxy.goToPageWithIndex(index);
const rowNode = this.paginationProxy.getRow(index);
const paginationOffset = this.paginationProxy.getPixelOffset();
const rowTopPixel = rowNode.rowTop - paginationOffset;
const rowBottomPixel = rowTopPixel + rowNode.rowHeight;
const scrollPosition = this.getVScrollPosition();
const heightOffset = this.heightScaler.getOffset();
const vScrollTop = scrollPosition.top + heightOffset;
const vScrollBottom = scrollPosition.bottom + heightOffset;
const viewportHeight = vScrollBottom - vScrollTop;
// work out the pixels for top, middle and bottom up front,
// make the if/else below easier to read
const pxTop = this.heightScaler.getScrollPositionForPixel(rowTopPixel);
const pxBottom = this.heightScaler.getScrollPositionForPixel(rowBottomPixel - viewportHeight);
// make sure if middle, the row is not outside the top of the grid
const pxMiddle = Math.min((pxTop + pxBottom) / 2, rowTopPixel);
const rowBelowViewport = vScrollTop > rowTopPixel;
const rowAboveViewport = vScrollBottom < rowBottomPixel;
let newScrollPosition: number = null;
if (position === 'top') {
newScrollPosition = pxTop;
} else if (position === 'bottom') {
newScrollPosition = pxBottom;
} else if (position === 'middle') {
newScrollPosition = pxMiddle;
} else if (rowBelowViewport) {
// if row is before, scroll up with row at top
newScrollPosition = pxTop;
} else if (rowAboveViewport) {
// if row is below, scroll down with row at bottom
newScrollPosition = pxBottom;
}
if (newScrollPosition !== null) {
this.eBodyViewport.scrollTop = newScrollPosition;
this.rowRenderer.redrawAfterScroll();
}
// so when we return back to user, the cells have rendered
this.animationFrameService.flushAllFrames();
}
// + moveColumnController
public getCenterWidth(): number {
return this.eCenterViewport.clientWidth;
}
public isVerticalScrollShowing(): boolean {
return _.isVerticalScrollShowing(this.eBodyViewport);
}
public isHorizontalScrollShowing(): boolean {
return _.isHorizontalScrollShowing(this.eCenterViewport);
}
// gets called every time the viewport size changes. we use this to check visibility of scrollbars
// in the grid panel, and also to check size and position of viewport for row and column virtualisation.
public checkViewportAndScrolls(): void {
// results in updating anything that depends on scroll showing
this.updateScrollVisibleService();
// fires event if height changes, used by PaginationService, HeightScalerService, RowRenderer
this.checkBodyHeight();
// check for virtual columns for ColumnController
this.onHorizontalViewportChanged();
this.setPinnedContainerSize();
}
private updateScrollVisibleService(): void {
// because of column animation (which takes 200ms), we have to do this twice.
// eg if user removes cols anywhere except at the RHS, then the cols on the RHS
// will animate to the left to fill the gap. this animation means just after
// the cols are removed, the remaining cols are still in the original location
// at the start of the animation, so pre animation the H scrollbar is still needed,
// but post animation it is not.
this.updateScrollVisibleServiceImpl();
setTimeout(this.updateScrollVisibleServiceImpl.bind(this), 500);
}
private updateScrollVisibleServiceImpl(): void {
const params: SetScrollsVisibleParams = {
horizontalScrollShowing: false,
verticalScrollShowing: false
};
params.verticalScrollShowing = this.isVerticalScrollShowing();
params.horizontalScrollShowing = !this.gridOptionsWrapper.isSuppressHorizontalScroll() && this.isHorizontalScrollShowing();
this.scrollVisibleService.setScrollsVisible(params);
this.setHorizontalScrollVisible(params.horizontalScrollShowing);
this.setVerticalScrollPaddingVisible(params.verticalScrollShowing);
}
private setHorizontalScrollVisible(visible: boolean): void {
const height = visible ? (this.gridOptionsWrapper.getScrollbarWidth() || 0) : 0;
_.setFixedHeight(this.eBodyHorizontalScrollViewport, height);
_.setFixedHeight(this.eBodyHorizontalScrollContainer, height);
_.setFixedHeight(this.eHorizontalScrollBody, height);
}
private setVerticalScrollPaddingVisible(show: boolean): void {
const scroller = show ? `scroll` : `hidden`;
this.eTop.style.overflowY = this.eBottom.style.overflowY = scroller;
this.setFakeHScrollSpacerWidths();
}
public ensureColumnVisible(key: any): void {
const column = this.columnController.getGridColumn(key);
if (!column) { return; }
if (column.isPinned()) {
console.warn('calling ensureIndexVisible on a ' + column.getPinned() + ' pinned column doesn\'t make sense for column ' + column.getColId());
return;
}
if (!this.columnController.isColumnDisplayed(column)) {
console.warn('column is not currently visible');
return;
}
const colLeftPixel = column.getLeft();
const colRightPixel = colLeftPixel + column.getActualWidth();
const viewportWidth = this.eCenterViewport.clientWidth;
const scrollPosition = this.getCenterViewportScrollLeft();
const bodyWidth = this.columnController.getBodyContainerWidth();
let viewportLeftPixel: number;
let viewportRightPixel: number;
// the logic of working out left and right viewport px is both here and in the ColumnController,
// need to refactor it out to one place
if (this.enableRtl) {
viewportLeftPixel = bodyWidth - scrollPosition - viewportWidth;
viewportRightPixel = bodyWidth - scrollPosition;
} else {
viewportLeftPixel = scrollPosition;
viewportRightPixel = viewportWidth + scrollPosition;
}
const viewportScrolledPastCol = viewportLeftPixel > colLeftPixel;
const viewportScrolledBeforeCol = viewportRightPixel < colRightPixel;
const colToSmallForViewport = viewportWidth < column.getActualWidth();
const alignColToLeft = viewportScrolledPastCol || colToSmallForViewport;
const alignColToRight = viewportScrolledBeforeCol;
let newScrollPosition = this.getCenterViewportScrollLeft();
if (alignColToLeft || alignColToRight) {
if (this.enableRtl) {
newScrollPosition = alignColToLeft ? (bodyWidth - viewportWidth - colLeftPixel) : (bodyWidth - colRightPixel);
} else {
newScrollPosition = alignColToLeft ? colLeftPixel : (colRightPixel - viewportWidth);
}
this.setCenterViewportScrollLeft(newScrollPosition);
} else {
// otherwise, col is already in view, so do nothing
}
// this will happen anyway, as the move will cause a 'scroll' event on the body, however
// it is possible that the ensureColumnVisible method is called from within ag-Grid and
// the caller will need to have the columns rendered to continue, which will be before
// the event has been worked on (which is the case for cell navigation).
this.onHorizontalViewportChanged();
// so when we return back to user, the cells have rendered
this.animationFrameService.flushAllFrames();
}
public showLoadingOverlay() {
if (!this.gridOptionsWrapper.isSuppressLoadingOverlay()) {
this.overlayWrapper.showLoadingOverlay(this.eOverlay);
}
}
public showNoRowsOverlay() {
if (!this.gridOptionsWrapper.isSuppressNoRowsOverlay()) {
this.overlayWrapper.showNoRowsOverlay(this.eOverlay);
}
}
public hideOverlay() {
this.overlayWrapper.hideOverlay(this.eOverlay);
}
// method will call itself if no available width. this covers if the grid
// isn't visible, but is just about to be visible.
public sizeColumnsToFit(nextTimeout?: number) {
const availableWidth = this.eBodyViewport.clientWidth;
if (availableWidth > 0) {
this.columnController.sizeColumnsToFit(availableWidth, "sizeColumnsToFit");
return;
}
if (nextTimeout === undefined) {
window.setTimeout(() => {
this.sizeColumnsToFit(100);
}, 0);
} else if (nextTimeout === 100) {
window.setTimeout(() => {
this.sizeColumnsToFit(500);
}, 100);
} else if (nextTimeout === 500) {
window.setTimeout(() => {
this.sizeColumnsToFit(-1);
}, 500);
} else {
console.warn('ag-Grid: tried to call sizeColumnsToFit() but the grid is coming back with ' +
'zero width, maybe the grid is not visible yet on the screen?');
}
}
// used by autoWidthCalculator and autoHeightCalculator
public getCenterContainer(): HTMLElement {
return this.eCenterContainer;
}
public getDropTargetBodyContainers(): HTMLElement[] {
return [this.eCenterViewport, this.eTopViewport, this.eBottomViewport];
}
public getDropTargetLeftContainers(): HTMLElement[] {
return [this.eLeftContainer, this.eLeftBottom, this.eLeftTop];
}
public getDropTargetRightContainers(): HTMLElement[] {
return [this.eRightContainer, this.eRightBottom, this.eRightTop];
}
private buildRowContainerComponents() {
this.eAllCellContainers = [
this.eLeftContainer, this.eRightContainer, this.eCenterContainer,
this.eTop, this.eBottom, this.eFullWidthContainer];
this.rowContainerComponents = {