@atlaskit/editor-plugin-table 3.1.3 → 3.2.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.
Files changed (34) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/cjs/plugins/table/nodeviews/TableComponent.js +36 -2
  3. package/dist/cjs/plugins/table/nodeviews/TableStickyScrollbar.js +154 -0
  4. package/dist/cjs/plugins/table/pm-plugins/sticky-headers/nodeviews/tableRow.js +37 -4
  5. package/dist/cjs/plugins/table/toolbar.js +6 -0
  6. package/dist/cjs/plugins/table/ui/common-styles.js +9 -2
  7. package/dist/es2019/plugins/table/nodeviews/TableComponent.js +37 -3
  8. package/dist/es2019/plugins/table/nodeviews/TableStickyScrollbar.js +112 -0
  9. package/dist/es2019/plugins/table/pm-plugins/sticky-headers/nodeviews/tableRow.js +39 -6
  10. package/dist/es2019/plugins/table/toolbar.js +6 -0
  11. package/dist/es2019/plugins/table/ui/common-styles.js +33 -1
  12. package/dist/esm/plugins/table/nodeviews/TableComponent.js +37 -3
  13. package/dist/esm/plugins/table/nodeviews/TableStickyScrollbar.js +146 -0
  14. package/dist/esm/plugins/table/pm-plugins/sticky-headers/nodeviews/tableRow.js +37 -4
  15. package/dist/esm/plugins/table/toolbar.js +6 -0
  16. package/dist/esm/plugins/table/ui/common-styles.js +8 -2
  17. package/dist/types/plugins/table/nodeviews/TableComponent.d.ts +1 -0
  18. package/dist/types/plugins/table/nodeviews/TableStickyScrollbar.d.ts +24 -0
  19. package/dist/types/plugins/table/pm-plugins/sticky-headers/nodeviews/tableRow.d.ts +5 -0
  20. package/dist/types/plugins/table/toolbar.d.ts +4 -4
  21. package/dist/types/plugins/table/types.d.ts +3 -0
  22. package/dist/types/plugins/table/ui/common-styles.d.ts +1 -0
  23. package/dist/types-ts4.5/plugins/table/nodeviews/TableComponent.d.ts +1 -0
  24. package/dist/types-ts4.5/plugins/table/nodeviews/TableStickyScrollbar.d.ts +24 -0
  25. package/dist/types-ts4.5/plugins/table/pm-plugins/sticky-headers/nodeviews/tableRow.d.ts +5 -0
  26. package/dist/types-ts4.5/plugins/table/toolbar.d.ts +4 -4
  27. package/dist/types-ts4.5/plugins/table/types.d.ts +3 -0
  28. package/dist/types-ts4.5/plugins/table/ui/common-styles.d.ts +1 -0
  29. package/package.json +6 -3
  30. package/src/plugins/table/nodeviews/TableComponent.tsx +54 -3
  31. package/src/plugins/table/nodeviews/TableStickyScrollbar.ts +204 -0
  32. package/src/plugins/table/pm-plugins/sticky-headers/nodeviews/tableRow.ts +40 -3
  33. package/src/plugins/table/toolbar.tsx +10 -6
  34. package/src/plugins/table/ui/common-styles.ts +38 -0
@@ -0,0 +1,204 @@
1
+ import rafSchedule from 'raf-schd';
2
+
3
+ import { findOverflowScrollParent } from '@atlaskit/editor-common/ui';
4
+ import type { EditorView } from '@atlaskit/editor-prosemirror/view';
5
+
6
+ import { TableCssClassName as ClassName } from '../types';
7
+
8
+ type SentinelState = 'above' | 'visible' | 'below';
9
+
10
+ export class TableStickyScrollbar {
11
+ private wrapper: HTMLDivElement;
12
+ private view: EditorView;
13
+ private editorScrollableElement?: HTMLElement | Document;
14
+ private intersectionObserver?: IntersectionObserver;
15
+ private stickyScrollbarContainerElement?: HTMLDivElement | null;
16
+
17
+ private sentinels: {
18
+ bottom?: HTMLElement | null;
19
+ top?: HTMLElement | null;
20
+ } = {};
21
+
22
+ private topSentinelState?: SentinelState;
23
+ private bottomSentinelState?: SentinelState;
24
+
25
+ constructor(wrapper: HTMLDivElement, view: EditorView) {
26
+ this.wrapper = wrapper;
27
+ this.view = view;
28
+
29
+ this.init();
30
+ }
31
+
32
+ dispose() {
33
+ if (this.stickyScrollbarContainerElement) {
34
+ this.stickyScrollbarContainerElement.removeEventListener(
35
+ 'scroll',
36
+ this.handleScrollDebounced,
37
+ );
38
+ this.handleScrollDebounced.cancel();
39
+ }
40
+
41
+ this.deleteIntesactionObserver();
42
+ }
43
+
44
+ scrollLeft(left: number) {
45
+ if (this.stickyScrollbarContainerElement) {
46
+ this.stickyScrollbarContainerElement.scrollLeft = left;
47
+ }
48
+ }
49
+
50
+ private init() {
51
+ if (!this.wrapper) {
52
+ return;
53
+ }
54
+
55
+ this.stickyScrollbarContainerElement =
56
+ this.wrapper.parentElement?.querySelector(
57
+ `.${ClassName.TABLE_STICKY_SCROLLBAR_CONTAINER}`,
58
+ );
59
+
60
+ if (this.stickyScrollbarContainerElement) {
61
+ this.stickyScrollbarContainerElement.addEventListener(
62
+ 'scroll',
63
+ this.handleScrollDebounced,
64
+ { passive: true },
65
+ );
66
+ }
67
+
68
+ this.createIntersectionObserver();
69
+ }
70
+
71
+ private createIntersectionObserver() {
72
+ this.editorScrollableElement =
73
+ (findOverflowScrollParent(this.view.dom) as HTMLElement) ||
74
+ window.document;
75
+
76
+ if (!this.editorScrollableElement || !this.wrapper) {
77
+ return;
78
+ }
79
+
80
+ this.intersectionObserver = new IntersectionObserver(
81
+ (entries: IntersectionObserverEntry[], _: IntersectionObserver) => {
82
+ if (!this.stickyScrollbarContainerElement) {
83
+ return;
84
+ }
85
+
86
+ entries.forEach((entry) => {
87
+ const target = entry.target as HTMLElement;
88
+ // if the rootBounds has 0 height, e.g. confluence preview mode, we do nothing.
89
+ if (entry.rootBounds?.height === 0) {
90
+ return;
91
+ }
92
+
93
+ if (
94
+ target.classList.contains(
95
+ ClassName.TABLE_STICKY_SCROLLBAR_SENTINEL_BOTTOM,
96
+ )
97
+ ) {
98
+ this.sentenialBottomCallback(entry);
99
+ }
100
+
101
+ if (
102
+ target.classList.contains(
103
+ ClassName.TABLE_STICKY_SCROLLBAR_SENTINEL_TOP,
104
+ )
105
+ ) {
106
+ this.sentenialTopCallback(entry);
107
+ }
108
+ });
109
+ },
110
+ { root: this.editorScrollableElement },
111
+ );
112
+
113
+ this.sentinels.bottom = this.wrapper?.parentElement
114
+ ?.getElementsByClassName(ClassName.TABLE_STICKY_SCROLLBAR_SENTINEL_BOTTOM)
115
+ ?.item(0) as HTMLElement;
116
+
117
+ this.sentinels.top = this.wrapper?.parentElement
118
+ ?.getElementsByClassName(ClassName.TABLE_STICKY_SCROLLBAR_SENTINEL_TOP)
119
+ ?.item(0) as HTMLElement;
120
+
121
+ [this.sentinels.bottom, this.sentinels.top].forEach((el) =>
122
+ this.intersectionObserver!.observe(el),
123
+ );
124
+ }
125
+
126
+ private deleteIntesactionObserver() {
127
+ if (this.intersectionObserver) {
128
+ if (this.sentinels.bottom) {
129
+ this.intersectionObserver.unobserve(this.sentinels.bottom);
130
+ }
131
+ this.intersectionObserver.disconnect();
132
+ }
133
+ }
134
+
135
+ private sentenialBottomCallback(entry: IntersectionObserverEntry) {
136
+ const sentinelIsAboveScrollArea =
137
+ entry.boundingClientRect.top < (entry.rootBounds?.top || 0);
138
+
139
+ this.bottomSentinelState = sentinelIsAboveScrollArea
140
+ ? 'above'
141
+ : entry.isIntersecting
142
+ ? 'visible'
143
+ : 'below';
144
+
145
+ this.toggle();
146
+ }
147
+
148
+ private sentenialTopCallback(entry: IntersectionObserverEntry) {
149
+ const sentinelIsBelowScrollArea =
150
+ (entry.rootBounds?.bottom || 0) < entry.boundingClientRect.top;
151
+
152
+ this.topSentinelState = sentinelIsBelowScrollArea
153
+ ? 'below'
154
+ : entry.isIntersecting
155
+ ? 'visible'
156
+ : 'above';
157
+
158
+ this.toggle();
159
+ }
160
+
161
+ private toggle() {
162
+ if (
163
+ (this.topSentinelState === 'visible' ||
164
+ this.topSentinelState === 'above') &&
165
+ this.bottomSentinelState === 'below'
166
+ ) {
167
+ this.show();
168
+ } else {
169
+ this.hide();
170
+ }
171
+ }
172
+
173
+ private hide() {
174
+ if (
175
+ this.stickyScrollbarContainerElement &&
176
+ this.stickyScrollbarContainerElement.style.display !== 'none'
177
+ ) {
178
+ this.stickyScrollbarContainerElement.style.display = 'none';
179
+ }
180
+ }
181
+
182
+ private show() {
183
+ if (
184
+ this.stickyScrollbarContainerElement &&
185
+ this.stickyScrollbarContainerElement.style.display !== 'block'
186
+ ) {
187
+ this.stickyScrollbarContainerElement.style.display = 'block';
188
+ }
189
+ }
190
+
191
+ private handleScroll = (event: Event) => {
192
+ if (
193
+ !this.stickyScrollbarContainerElement ||
194
+ !this.wrapper ||
195
+ event.target !== this.stickyScrollbarContainerElement
196
+ ) {
197
+ return;
198
+ }
199
+
200
+ this.wrapper.scrollLeft = this.stickyScrollbarContainerElement.scrollLeft;
201
+ };
202
+
203
+ private handleScrollDebounced = rafSchedule(this.handleScroll);
204
+ }
@@ -150,10 +150,13 @@ export class TableRowNodeView implements NodeView {
150
150
 
151
151
  this.listening = true;
152
152
 
153
- this.dom.addEventListener('wheel', this.headerRowMouseScroll.bind(this));
153
+ this.dom.addEventListener('wheel', this.headerRowMouseScroll.bind(this), {
154
+ passive: true,
155
+ });
154
156
  this.dom.addEventListener(
155
157
  'touchmove',
156
158
  this.headerRowMouseScroll.bind(this),
159
+ { passive: true },
157
160
  );
158
161
  }
159
162
 
@@ -164,7 +167,7 @@ export class TableRowNodeView implements NodeView {
164
167
  if (this.intersectionObserver) {
165
168
  this.intersectionObserver.disconnect();
166
169
  // ED-16211 Once intersection observer is disconnected, we need to remove the isObserved from the sentinels
167
- // Otherwise when new intersection observer is created it will not observe because it thinks its already being observed
170
+ // Otherwise when newer intersection observer is created it will not observe because it thinks its already being observed
168
171
  [this.sentinels.top, this.sentinels.bottom].forEach((el) => {
169
172
  if (el) {
170
173
  delete el.dataset.isObserved;
@@ -522,9 +525,27 @@ export class TableRowNodeView implements NodeView {
522
525
  return false;
523
526
  };
524
527
 
528
+ /**
529
+ * Manually refire the intersection observers.
530
+ * Useful when the header may have detached from the table.
531
+ */
532
+ refireIntersectionObservers = () => {
533
+ if (this.isSticky) {
534
+ [this.sentinels.top, this.sentinels.bottom].forEach((el) => {
535
+ if (el && this.intersectionObserver) {
536
+ this.intersectionObserver.unobserve(el);
537
+ this.intersectionObserver.observe(el);
538
+ }
539
+ });
540
+ }
541
+ };
542
+
525
543
  makeHeaderRowSticky = (tree: TableDOMElements, scrollTop?: number) => {
526
544
  // If header row height is more than 50% of viewport height don't do this
527
- if (this.stickyRowHeight && this.stickyRowHeight > window.innerHeight / 2) {
545
+ if (
546
+ this.isSticky ||
547
+ (this.stickyRowHeight && this.stickyRowHeight > window.innerHeight / 2)
548
+ ) {
528
549
  return;
529
550
  }
530
551
 
@@ -552,6 +573,22 @@ export class TableRowNodeView implements NodeView {
552
573
  table.classList.add(ClassName.TABLE_STICKY);
553
574
 
554
575
  this.isSticky = true;
576
+
577
+ /**
578
+ * The logic below is not desirable, but acts as a fail safe for scenarios where the sticky header
579
+ * detaches from the table. This typically happens during a fast scroll by the user which causes
580
+ * the intersection observer logic to not fire as expected.
581
+ */
582
+ this.editorScrollableElement?.addEventListener(
583
+ 'scrollend',
584
+ this.refireIntersectionObservers,
585
+ { passive: true, once: true },
586
+ );
587
+
588
+ const fastScrollThresholdMs = 500;
589
+ setTimeout(() => {
590
+ this.refireIntersectionObservers();
591
+ }, fastScrollThresholdMs);
555
592
  }
556
593
 
557
594
  this.dom.style.top = `${domTop}px`;
@@ -33,13 +33,13 @@ import {
33
33
  getNodeName,
34
34
  isReferencedSource,
35
35
  } from '@atlaskit/editor-common/utils';
36
- import { Node as PMNode } from '@atlaskit/editor-prosemirror/model';
37
- import { EditorState } from '@atlaskit/editor-prosemirror/state';
36
+ import type { Node as PMNode } from '@atlaskit/editor-prosemirror/model';
37
+ import type { EditorState } from '@atlaskit/editor-prosemirror/state';
38
38
  import { findParentDomRefOfType } from '@atlaskit/editor-prosemirror/utils';
39
- import { EditorView } from '@atlaskit/editor-prosemirror/view';
39
+ import type { EditorView } from '@atlaskit/editor-prosemirror/view';
40
40
  import { akEditorFloatingPanelZIndex } from '@atlaskit/editor-shared-styles';
41
41
  import { shortcutStyle } from '@atlaskit/editor-shared-styles/shortcut';
42
- import { Rect } from '@atlaskit/editor-tables/table-map';
42
+ import type { Rect } from '@atlaskit/editor-tables/table-map';
43
43
  import {
44
44
  findCellRectClosestToPos,
45
45
  findTable,
@@ -48,6 +48,7 @@ import {
48
48
  splitCell,
49
49
  } from '@atlaskit/editor-tables/utils';
50
50
  import RemoveIcon from '@atlaskit/icon/glyph/editor/remove';
51
+ import { getBooleanFF } from '@atlaskit/platform-feature-flags';
51
52
 
52
53
  import {
53
54
  clearHoverSelection,
@@ -79,13 +80,13 @@ import { pluginKey as tableResizingPluginKey } from './pm-plugins/table-resizing
79
80
  import { getNewResizeStateFromSelectedColumns } from './pm-plugins/table-resizing/utils/resize-state';
80
81
  import { pluginKey as tableWidthPluginKey } from './pm-plugins/table-width';
81
82
  import { canMergeCells } from './transforms';
82
- import {
83
+ import type {
83
84
  PluginConfig,
84
- TableCssClassName,
85
85
  ToolbarMenuConfig,
86
86
  ToolbarMenuContext,
87
87
  ToolbarMenuState,
88
88
  } from './types';
89
+ import { TableCssClassName } from './types';
89
90
  import { messages as ContextualMenuMessages } from './ui/FloatingContextualMenu/ContextualMenu';
90
91
  import tableMessages from './ui/messages';
91
92
  import {
@@ -520,6 +521,9 @@ export const getToolbarConfig =
520
521
  getDomRef,
521
522
  nodeType,
522
523
  offset: [0, 18],
524
+ absoluteOffset: getBooleanFF('platform.editor.table-sticky-scrollbar')
525
+ ? { top: -6 }
526
+ : { top: 0 },
523
527
  zIndex: akEditorFloatingPanelZIndex + 1, // Place the context menu slightly above the others
524
528
  items: [
525
529
  menu,
@@ -14,6 +14,7 @@ import {
14
14
  akEditorTableToolbarSize,
15
15
  akEditorUnitZIndex,
16
16
  getSelectionStyles,
17
+ MAX_BROWSER_SCROLLBAR_HEIGHT,
17
18
  relativeFontSizeToBase16,
18
19
  SelectionStyle,
19
20
  } from '@atlaskit/editor-shared-styles';
@@ -75,6 +76,7 @@ const cornerControlHeight = tableToolbarSize + 1;
75
76
  its center should be aligned with the edge
76
77
  */
77
78
  export const insertColumnButtonOffset = tableInsertColumnButtonSize / 2;
79
+ export const tableRowHeight = 44;
78
80
 
79
81
  const rangeSelectionStyles = `
80
82
  .${ClassName.NODEVIEW_WRAPPER}.${akEditorSelectedNodeClassName} table tbody tr {
@@ -114,6 +116,41 @@ const sentinelStyles = `.${ClassName.TABLE_CONTAINER} {
114
116
  }
115
117
  }`;
116
118
 
119
+ const stickyScrollbarSentinelStyles = `.${ClassName.TABLE_CONTAINER} {
120
+ > .${ClassName.TABLE_STICKY_SCROLLBAR_SENTINEL_BOTTOM},
121
+ > .${ClassName.TABLE_STICKY_SCROLLBAR_SENTINEL_TOP} {
122
+ position: absolute;
123
+ width: 100%;
124
+ height: 1px;
125
+ margin-top: -1px;
126
+ // need this to avoid sentinel being focused via keyboard
127
+ // this still allows it to be detected by intersection observer
128
+ visibility: hidden;
129
+ }
130
+ > .${ClassName.TABLE_STICKY_SCROLLBAR_SENTINEL_TOP} {
131
+ top: ${columnControlsDecorationHeight + tableRowHeight * 3}px;
132
+ }
133
+ > .${ClassName.TABLE_STICKY_SCROLLBAR_SENTINEL_BOTTOM} {
134
+ bottom: ${MAX_BROWSER_SCROLLBAR_HEIGHT}px;
135
+ }
136
+ }`;
137
+
138
+ const stickyScrollbarContainerStyles = `.${ClassName.TABLE_CONTAINER} {
139
+ > .${ClassName.TABLE_STICKY_SCROLLBAR_CONTAINER} {
140
+ width: 100%;
141
+ display: none;
142
+ overflow-x: auto;
143
+ position: sticky;
144
+ bottom: 0;
145
+ }
146
+ }`;
147
+
148
+ const stickyScrollbarStyles = () => {
149
+ return getBooleanFF('platform.editor.table-sticky-scrollbar')
150
+ ? `${stickyScrollbarContainerStyles} ${stickyScrollbarSentinelStyles}`
151
+ : '';
152
+ };
153
+
117
154
  const shadowSentinelStyles = `
118
155
  .${ClassName.TABLE_SHADOW_SENTINEL_LEFT},
119
156
  .${ClassName.TABLE_SHADOW_SENTINEL_RIGHT} {
@@ -417,6 +454,7 @@ export const tableStyles = (
417
454
 
418
455
  ${sentinelStyles}
419
456
  ${OverflowShadow(props)}
457
+ ${stickyScrollbarStyles()}
420
458
 
421
459
  .${ClassName.TABLE_STICKY} .${ClassName.TABLE_STICKY_SHADOW} {
422
460
  height: 0; // stop overflow flash & set correct height in update-overflow-shadows.ts