@atlaskit/editor-plugin-table 3.1.3 → 3.2.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.
Files changed (28) hide show
  1. package/CHANGELOG.md +10 -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/toolbar.js +6 -0
  5. package/dist/cjs/plugins/table/ui/common-styles.js +9 -2
  6. package/dist/es2019/plugins/table/nodeviews/TableComponent.js +37 -3
  7. package/dist/es2019/plugins/table/nodeviews/TableStickyScrollbar.js +112 -0
  8. package/dist/es2019/plugins/table/toolbar.js +6 -0
  9. package/dist/es2019/plugins/table/ui/common-styles.js +33 -1
  10. package/dist/esm/plugins/table/nodeviews/TableComponent.js +37 -3
  11. package/dist/esm/plugins/table/nodeviews/TableStickyScrollbar.js +146 -0
  12. package/dist/esm/plugins/table/toolbar.js +6 -0
  13. package/dist/esm/plugins/table/ui/common-styles.js +8 -2
  14. package/dist/types/plugins/table/nodeviews/TableComponent.d.ts +1 -0
  15. package/dist/types/plugins/table/nodeviews/TableStickyScrollbar.d.ts +24 -0
  16. package/dist/types/plugins/table/toolbar.d.ts +4 -4
  17. package/dist/types/plugins/table/types.d.ts +3 -0
  18. package/dist/types/plugins/table/ui/common-styles.d.ts +1 -0
  19. package/dist/types-ts4.5/plugins/table/nodeviews/TableComponent.d.ts +1 -0
  20. package/dist/types-ts4.5/plugins/table/nodeviews/TableStickyScrollbar.d.ts +24 -0
  21. package/dist/types-ts4.5/plugins/table/toolbar.d.ts +4 -4
  22. package/dist/types-ts4.5/plugins/table/types.d.ts +3 -0
  23. package/dist/types-ts4.5/plugins/table/ui/common-styles.d.ts +1 -0
  24. package/package.json +6 -3
  25. package/src/plugins/table/nodeviews/TableComponent.tsx +54 -3
  26. package/src/plugins/table/nodeviews/TableStickyScrollbar.ts +204 -0
  27. package/src/plugins/table/toolbar.tsx +10 -6
  28. package/src/plugins/table/ui/common-styles.ts +38 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/editor-plugin-table",
3
- "version": "3.1.3",
3
+ "version": "3.2.0",
4
4
  "description": "Table plugin for the @atlaskit/editor",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org/"
@@ -28,12 +28,12 @@
28
28
  },
29
29
  "dependencies": {
30
30
  "@atlaskit/adf-schema": "^29.1.0",
31
- "@atlaskit/editor-common": "^75.6.0",
31
+ "@atlaskit/editor-common": "^75.8.0",
32
32
  "@atlaskit/editor-palette": "1.5.1",
33
33
  "@atlaskit/editor-plugin-analytics": "^0.2.0",
34
34
  "@atlaskit/editor-plugin-content-insertion": "^0.1.0",
35
35
  "@atlaskit/editor-prosemirror": "1.1.0",
36
- "@atlaskit/editor-shared-styles": "^2.7.0",
36
+ "@atlaskit/editor-shared-styles": "^2.8.0",
37
37
  "@atlaskit/editor-tables": "^2.3.0",
38
38
  "@atlaskit/icon": "^21.12.0",
39
39
  "@atlaskit/platform-feature-flags": "^0.2.1",
@@ -102,6 +102,9 @@
102
102
  "platform.editor.custom-table-width": {
103
103
  "type": "boolean"
104
104
  },
105
+ "platform.editor.table-sticky-scrollbar": {
106
+ "type": "boolean"
107
+ },
105
108
  "platform.editor.update-table-cell-width-via-step": {
106
109
  "type": "boolean"
107
110
  },
@@ -27,7 +27,10 @@ import {
27
27
  } from '@atlaskit/editor-common/utils';
28
28
  import type { Node as PmNode } from '@atlaskit/editor-prosemirror/model';
29
29
  import type { EditorView } from '@atlaskit/editor-prosemirror/view';
30
- import { akEditorTableToolbarSize as tableToolbarSize } from '@atlaskit/editor-shared-styles';
30
+ import {
31
+ MAX_BROWSER_SCROLLBAR_HEIGHT,
32
+ akEditorTableToolbarSize as tableToolbarSize,
33
+ } from '@atlaskit/editor-shared-styles';
31
34
  import { findTable, isTableSelected } from '@atlaskit/editor-tables/utils';
32
35
  import { getBooleanFF } from '@atlaskit/platform-feature-flags';
33
36
 
@@ -65,6 +68,7 @@ import {
65
68
 
66
69
  import { OverflowShadowsObserver } from './OverflowShadowsObserver';
67
70
  import { TableContainer } from './TableContainer';
71
+ import { TableStickyScrollbar } from './TableStickyScrollbar';
68
72
  import type { TableOptions } from './types';
69
73
 
70
74
  const isIE11 = browser.ie_version === 11;
@@ -72,6 +76,7 @@ const isIE11 = browser.ie_version === 11;
72
76
  // componentDidUpdate is called multiple times. The isOverflowing value is correct only on the last update.
73
77
  // To make sure we capture the last update, we use setTimeout.
74
78
  const initialOverflowCaptureTimeroutDelay = 300;
79
+
75
80
  export interface ComponentProps {
76
81
  view: EditorView;
77
82
  getNode: () => PmNode;
@@ -117,6 +122,7 @@ class TableComponent extends React.Component<ComponentProps, TableState> {
117
122
  private containerWidth?: EditorContainerWidth;
118
123
  private layoutSize?: number;
119
124
  private overflowShadowsObserver?: OverflowShadowsObserver;
125
+ private stickyScrollbar?: TableStickyScrollbar;
120
126
 
121
127
  private isInitialOverflowSent: boolean;
122
128
  private initialOverflowCaptureTimerId?: ReturnType<typeof setTimeout>;
@@ -154,7 +160,18 @@ class TableComponent extends React.Component<ComponentProps, TableState> {
154
160
  componentDidMount() {
155
161
  const { allowColumnResizing, eventDispatcher, options } = this.props;
156
162
  if (allowColumnResizing && this.wrapper && !isIE11) {
157
- this.wrapper.addEventListener('scroll', this.handleScrollDebounced);
163
+ this.wrapper.addEventListener('scroll', this.handleScrollDebounced, {
164
+ passive: true,
165
+ });
166
+
167
+ if (getBooleanFF('platform.editor.table-sticky-scrollbar')) {
168
+ if (this.table) {
169
+ this.stickyScrollbar = new TableStickyScrollbar(
170
+ this.wrapper,
171
+ this.props.view,
172
+ );
173
+ }
174
+ }
158
175
  }
159
176
 
160
177
  if (allowColumnResizing) {
@@ -193,6 +210,12 @@ class TableComponent extends React.Component<ComponentProps, TableState> {
193
210
  this.wrapper.removeEventListener('scroll', this.handleScrollDebounced);
194
211
  }
195
212
 
213
+ if (getBooleanFF('platform.editor.table-sticky-scrollbar')) {
214
+ if (this.stickyScrollbar) {
215
+ this.stickyScrollbar.dispose();
216
+ }
217
+ }
218
+
196
219
  this.handleScrollDebounced.cancel();
197
220
  this.scaleTableDebounced.cancel();
198
221
  this.handleTableResizingDebounced.cancel();
@@ -453,6 +476,12 @@ class TableComponent extends React.Component<ComponentProps, TableState> {
453
476
  className={ClassName.TABLE_STICKY_SENTINEL_TOP}
454
477
  data-testid="sticky-sentinel-top"
455
478
  />
479
+ {getBooleanFF('platform.editor.table-sticky-scrollbar') && (
480
+ <div
481
+ className={ClassName.TABLE_STICKY_SCROLLBAR_SENTINEL_TOP}
482
+ data-testid="sticky-scrollbar-sentinel-top"
483
+ />
484
+ )}
456
485
  {allowControls && rowControls}
457
486
  <div
458
487
  style={shadowStyle(showBeforeShadow)}
@@ -473,7 +502,6 @@ class TableComponent extends React.Component<ComponentProps, TableState> {
473
502
  }}
474
503
  />
475
504
  )}
476
-
477
505
  <div
478
506
  className={classnames(ClassName.TABLE_NODE_WRAPPER)}
479
507
  ref={(elem) => {
@@ -488,6 +516,17 @@ class TableComponent extends React.Component<ComponentProps, TableState> {
488
516
  }
489
517
  }}
490
518
  />
519
+ {getBooleanFF('platform.editor.table-sticky-scrollbar') && (
520
+ <div
521
+ className={ClassName.TABLE_STICKY_SCROLLBAR_CONTAINER}
522
+ style={{
523
+ height: MAX_BROWSER_SCROLLBAR_HEIGHT,
524
+ display: 'none',
525
+ }}
526
+ >
527
+ <div style={{ width: tableRef?.clientWidth }}></div>
528
+ </div>
529
+ )}
491
530
  <div
492
531
  style={shadowStyle(showAfterShadow)}
493
532
  className={ClassName.TABLE_RIGHT_SHADOW}
@@ -526,6 +565,12 @@ class TableComponent extends React.Component<ComponentProps, TableState> {
526
565
  className={ClassName.TABLE_STICKY_SENTINEL_BOTTOM}
527
566
  data-testid="sticky-sentinel-bottom"
528
567
  />
568
+ {getBooleanFF('platform.editor.table-sticky-scrollbar') && (
569
+ <div
570
+ className={ClassName.TABLE_STICKY_SCROLLBAR_SENTINEL_BOTTOM}
571
+ data-testid="sticky-scrollbar-sentinel-bottom"
572
+ />
573
+ )}
529
574
  </TableContainer>
530
575
  );
531
576
  }
@@ -535,6 +580,12 @@ class TableComponent extends React.Component<ComponentProps, TableState> {
535
580
  return;
536
581
  }
537
582
 
583
+ if (getBooleanFF('platform.editor.table-sticky-scrollbar')) {
584
+ if (this.stickyScrollbar) {
585
+ this.stickyScrollbar.scrollLeft(this.wrapper.scrollLeft);
586
+ }
587
+ }
588
+
538
589
  if (this.table) {
539
590
  // sync sticky header row to table scroll
540
591
  const headers = this.table.querySelectorAll('tr[data-header-row]');
@@ -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
+ }
@@ -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