@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.
- package/CHANGELOG.md +16 -0
- package/dist/cjs/plugins/table/nodeviews/TableComponent.js +36 -2
- package/dist/cjs/plugins/table/nodeviews/TableStickyScrollbar.js +154 -0
- package/dist/cjs/plugins/table/pm-plugins/sticky-headers/nodeviews/tableRow.js +37 -4
- package/dist/cjs/plugins/table/toolbar.js +6 -0
- package/dist/cjs/plugins/table/ui/common-styles.js +9 -2
- package/dist/es2019/plugins/table/nodeviews/TableComponent.js +37 -3
- package/dist/es2019/plugins/table/nodeviews/TableStickyScrollbar.js +112 -0
- package/dist/es2019/plugins/table/pm-plugins/sticky-headers/nodeviews/tableRow.js +39 -6
- package/dist/es2019/plugins/table/toolbar.js +6 -0
- package/dist/es2019/plugins/table/ui/common-styles.js +33 -1
- package/dist/esm/plugins/table/nodeviews/TableComponent.js +37 -3
- package/dist/esm/plugins/table/nodeviews/TableStickyScrollbar.js +146 -0
- package/dist/esm/plugins/table/pm-plugins/sticky-headers/nodeviews/tableRow.js +37 -4
- package/dist/esm/plugins/table/toolbar.js +6 -0
- package/dist/esm/plugins/table/ui/common-styles.js +8 -2
- package/dist/types/plugins/table/nodeviews/TableComponent.d.ts +1 -0
- package/dist/types/plugins/table/nodeviews/TableStickyScrollbar.d.ts +24 -0
- package/dist/types/plugins/table/pm-plugins/sticky-headers/nodeviews/tableRow.d.ts +5 -0
- package/dist/types/plugins/table/toolbar.d.ts +4 -4
- package/dist/types/plugins/table/types.d.ts +3 -0
- package/dist/types/plugins/table/ui/common-styles.d.ts +1 -0
- package/dist/types-ts4.5/plugins/table/nodeviews/TableComponent.d.ts +1 -0
- package/dist/types-ts4.5/plugins/table/nodeviews/TableStickyScrollbar.d.ts +24 -0
- package/dist/types-ts4.5/plugins/table/pm-plugins/sticky-headers/nodeviews/tableRow.d.ts +5 -0
- package/dist/types-ts4.5/plugins/table/toolbar.d.ts +4 -4
- package/dist/types-ts4.5/plugins/table/types.d.ts +3 -0
- package/dist/types-ts4.5/plugins/table/ui/common-styles.d.ts +1 -0
- package/package.json +6 -3
- package/src/plugins/table/nodeviews/TableComponent.tsx +54 -3
- package/src/plugins/table/nodeviews/TableStickyScrollbar.ts +204 -0
- package/src/plugins/table/pm-plugins/sticky-headers/nodeviews/tableRow.ts +40 -3
- package/src/plugins/table/toolbar.tsx +10 -6
- package/src/plugins/table/ui/common-styles.ts +38 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import _defineProperty from "@babel/runtime/helpers/defineProperty";
|
|
2
|
+
import rafSchedule from 'raf-schd';
|
|
3
|
+
import { findOverflowScrollParent } from '@atlaskit/editor-common/ui';
|
|
4
|
+
import { TableCssClassName as ClassName } from '../types';
|
|
5
|
+
export class TableStickyScrollbar {
|
|
6
|
+
constructor(wrapper, view) {
|
|
7
|
+
_defineProperty(this, "sentinels", {});
|
|
8
|
+
_defineProperty(this, "handleScroll", event => {
|
|
9
|
+
if (!this.stickyScrollbarContainerElement || !this.wrapper || event.target !== this.stickyScrollbarContainerElement) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
this.wrapper.scrollLeft = this.stickyScrollbarContainerElement.scrollLeft;
|
|
13
|
+
});
|
|
14
|
+
_defineProperty(this, "handleScrollDebounced", rafSchedule(this.handleScroll));
|
|
15
|
+
this.wrapper = wrapper;
|
|
16
|
+
this.view = view;
|
|
17
|
+
this.init();
|
|
18
|
+
}
|
|
19
|
+
dispose() {
|
|
20
|
+
if (this.stickyScrollbarContainerElement) {
|
|
21
|
+
this.stickyScrollbarContainerElement.removeEventListener('scroll', this.handleScrollDebounced);
|
|
22
|
+
this.handleScrollDebounced.cancel();
|
|
23
|
+
}
|
|
24
|
+
this.deleteIntesactionObserver();
|
|
25
|
+
}
|
|
26
|
+
scrollLeft(left) {
|
|
27
|
+
if (this.stickyScrollbarContainerElement) {
|
|
28
|
+
this.stickyScrollbarContainerElement.scrollLeft = left;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
init() {
|
|
32
|
+
var _this$wrapper$parentE;
|
|
33
|
+
if (!this.wrapper) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
this.stickyScrollbarContainerElement = (_this$wrapper$parentE = this.wrapper.parentElement) === null || _this$wrapper$parentE === void 0 ? void 0 : _this$wrapper$parentE.querySelector(`.${ClassName.TABLE_STICKY_SCROLLBAR_CONTAINER}`);
|
|
37
|
+
if (this.stickyScrollbarContainerElement) {
|
|
38
|
+
this.stickyScrollbarContainerElement.addEventListener('scroll', this.handleScrollDebounced, {
|
|
39
|
+
passive: true
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
this.createIntersectionObserver();
|
|
43
|
+
}
|
|
44
|
+
createIntersectionObserver() {
|
|
45
|
+
var _this$wrapper, _this$wrapper$parentE2, _this$wrapper$parentE3, _this$wrapper2, _this$wrapper2$parent, _this$wrapper2$parent2;
|
|
46
|
+
this.editorScrollableElement = findOverflowScrollParent(this.view.dom) || window.document;
|
|
47
|
+
if (!this.editorScrollableElement || !this.wrapper) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
this.intersectionObserver = new IntersectionObserver((entries, _) => {
|
|
51
|
+
if (!this.stickyScrollbarContainerElement) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
entries.forEach(entry => {
|
|
55
|
+
var _entry$rootBounds;
|
|
56
|
+
const target = entry.target;
|
|
57
|
+
// if the rootBounds has 0 height, e.g. confluence preview mode, we do nothing.
|
|
58
|
+
if (((_entry$rootBounds = entry.rootBounds) === null || _entry$rootBounds === void 0 ? void 0 : _entry$rootBounds.height) === 0) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (target.classList.contains(ClassName.TABLE_STICKY_SCROLLBAR_SENTINEL_BOTTOM)) {
|
|
62
|
+
this.sentenialBottomCallback(entry);
|
|
63
|
+
}
|
|
64
|
+
if (target.classList.contains(ClassName.TABLE_STICKY_SCROLLBAR_SENTINEL_TOP)) {
|
|
65
|
+
this.sentenialTopCallback(entry);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}, {
|
|
69
|
+
root: this.editorScrollableElement
|
|
70
|
+
});
|
|
71
|
+
this.sentinels.bottom = (_this$wrapper = this.wrapper) === null || _this$wrapper === void 0 ? void 0 : (_this$wrapper$parentE2 = _this$wrapper.parentElement) === null || _this$wrapper$parentE2 === void 0 ? void 0 : (_this$wrapper$parentE3 = _this$wrapper$parentE2.getElementsByClassName(ClassName.TABLE_STICKY_SCROLLBAR_SENTINEL_BOTTOM)) === null || _this$wrapper$parentE3 === void 0 ? void 0 : _this$wrapper$parentE3.item(0);
|
|
72
|
+
this.sentinels.top = (_this$wrapper2 = this.wrapper) === null || _this$wrapper2 === void 0 ? void 0 : (_this$wrapper2$parent = _this$wrapper2.parentElement) === null || _this$wrapper2$parent === void 0 ? void 0 : (_this$wrapper2$parent2 = _this$wrapper2$parent.getElementsByClassName(ClassName.TABLE_STICKY_SCROLLBAR_SENTINEL_TOP)) === null || _this$wrapper2$parent2 === void 0 ? void 0 : _this$wrapper2$parent2.item(0);
|
|
73
|
+
[this.sentinels.bottom, this.sentinels.top].forEach(el => this.intersectionObserver.observe(el));
|
|
74
|
+
}
|
|
75
|
+
deleteIntesactionObserver() {
|
|
76
|
+
if (this.intersectionObserver) {
|
|
77
|
+
if (this.sentinels.bottom) {
|
|
78
|
+
this.intersectionObserver.unobserve(this.sentinels.bottom);
|
|
79
|
+
}
|
|
80
|
+
this.intersectionObserver.disconnect();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
sentenialBottomCallback(entry) {
|
|
84
|
+
var _entry$rootBounds2;
|
|
85
|
+
const sentinelIsAboveScrollArea = entry.boundingClientRect.top < (((_entry$rootBounds2 = entry.rootBounds) === null || _entry$rootBounds2 === void 0 ? void 0 : _entry$rootBounds2.top) || 0);
|
|
86
|
+
this.bottomSentinelState = sentinelIsAboveScrollArea ? 'above' : entry.isIntersecting ? 'visible' : 'below';
|
|
87
|
+
this.toggle();
|
|
88
|
+
}
|
|
89
|
+
sentenialTopCallback(entry) {
|
|
90
|
+
var _entry$rootBounds3;
|
|
91
|
+
const sentinelIsBelowScrollArea = (((_entry$rootBounds3 = entry.rootBounds) === null || _entry$rootBounds3 === void 0 ? void 0 : _entry$rootBounds3.bottom) || 0) < entry.boundingClientRect.top;
|
|
92
|
+
this.topSentinelState = sentinelIsBelowScrollArea ? 'below' : entry.isIntersecting ? 'visible' : 'above';
|
|
93
|
+
this.toggle();
|
|
94
|
+
}
|
|
95
|
+
toggle() {
|
|
96
|
+
if ((this.topSentinelState === 'visible' || this.topSentinelState === 'above') && this.bottomSentinelState === 'below') {
|
|
97
|
+
this.show();
|
|
98
|
+
} else {
|
|
99
|
+
this.hide();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
hide() {
|
|
103
|
+
if (this.stickyScrollbarContainerElement && this.stickyScrollbarContainerElement.style.display !== 'none') {
|
|
104
|
+
this.stickyScrollbarContainerElement.style.display = 'none';
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
show() {
|
|
108
|
+
if (this.stickyScrollbarContainerElement && this.stickyScrollbarContainerElement.style.display !== 'block') {
|
|
109
|
+
this.stickyScrollbarContainerElement.style.display = 'block';
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -137,10 +137,24 @@ export class TableRowNodeView {
|
|
|
137
137
|
// otherwise make it non-sticky
|
|
138
138
|
return false;
|
|
139
139
|
});
|
|
140
|
+
/**
|
|
141
|
+
* Manually refire the intersection observers.
|
|
142
|
+
* Useful when the header may have detached from the table.
|
|
143
|
+
*/
|
|
144
|
+
_defineProperty(this, "refireIntersectionObservers", () => {
|
|
145
|
+
if (this.isSticky) {
|
|
146
|
+
[this.sentinels.top, this.sentinels.bottom].forEach(el => {
|
|
147
|
+
if (el && this.intersectionObserver) {
|
|
148
|
+
this.intersectionObserver.unobserve(el);
|
|
149
|
+
this.intersectionObserver.observe(el);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
});
|
|
140
154
|
_defineProperty(this, "makeHeaderRowSticky", (tree, scrollTop) => {
|
|
141
155
|
var _tbody$firstChild;
|
|
142
156
|
// If header row height is more than 50% of viewport height don't do this
|
|
143
|
-
if (this.stickyRowHeight && this.stickyRowHeight > window.innerHeight / 2) {
|
|
157
|
+
if (this.isSticky || this.stickyRowHeight && this.stickyRowHeight > window.innerHeight / 2) {
|
|
144
158
|
return;
|
|
145
159
|
}
|
|
146
160
|
const {
|
|
@@ -160,10 +174,25 @@ export class TableRowNodeView {
|
|
|
160
174
|
}
|
|
161
175
|
const domTop = currentTableTop > 0 ? scrollTop : scrollTop + currentTableTop;
|
|
162
176
|
if (!this.isSticky) {
|
|
177
|
+
var _this$editorScrollabl;
|
|
163
178
|
syncStickyRowToTable(table);
|
|
164
179
|
this.dom.classList.add('sticky');
|
|
165
180
|
table.classList.add(ClassName.TABLE_STICKY);
|
|
166
181
|
this.isSticky = true;
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* The logic below is not desirable, but acts as a fail safe for scenarios where the sticky header
|
|
185
|
+
* detaches from the table. This typically happens during a fast scroll by the user which causes
|
|
186
|
+
* the intersection observer logic to not fire as expected.
|
|
187
|
+
*/
|
|
188
|
+
(_this$editorScrollabl = this.editorScrollableElement) === null || _this$editorScrollabl === void 0 ? void 0 : _this$editorScrollabl.addEventListener('scrollend', this.refireIntersectionObservers, {
|
|
189
|
+
passive: true,
|
|
190
|
+
once: true
|
|
191
|
+
});
|
|
192
|
+
const fastScrollThresholdMs = 500;
|
|
193
|
+
setTimeout(() => {
|
|
194
|
+
this.refireIntersectionObservers();
|
|
195
|
+
}, fastScrollThresholdMs);
|
|
167
196
|
}
|
|
168
197
|
this.dom.style.top = `${domTop}px`;
|
|
169
198
|
updateTableMargin(table);
|
|
@@ -245,8 +274,12 @@ export class TableRowNodeView {
|
|
|
245
274
|
this.eventDispatcher.on('widthPlugin', this.updateStickyHeaderWidth);
|
|
246
275
|
this.eventDispatcher.on(tablePluginKey.key, this.onTablePluginState);
|
|
247
276
|
this.listening = true;
|
|
248
|
-
this.dom.addEventListener('wheel', this.headerRowMouseScroll.bind(this)
|
|
249
|
-
|
|
277
|
+
this.dom.addEventListener('wheel', this.headerRowMouseScroll.bind(this), {
|
|
278
|
+
passive: true
|
|
279
|
+
});
|
|
280
|
+
this.dom.addEventListener('touchmove', this.headerRowMouseScroll.bind(this), {
|
|
281
|
+
passive: true
|
|
282
|
+
});
|
|
250
283
|
}
|
|
251
284
|
unsubscribe() {
|
|
252
285
|
if (!this.listening) {
|
|
@@ -255,7 +288,7 @@ export class TableRowNodeView {
|
|
|
255
288
|
if (this.intersectionObserver) {
|
|
256
289
|
this.intersectionObserver.disconnect();
|
|
257
290
|
// ED-16211 Once intersection observer is disconnected, we need to remove the isObserved from the sentinels
|
|
258
|
-
// Otherwise when
|
|
291
|
+
// Otherwise when newer intersection observer is created it will not observe because it thinks its already being observed
|
|
259
292
|
[this.sentinels.top, this.sentinels.bottom].forEach(el => {
|
|
260
293
|
if (el) {
|
|
261
294
|
delete el.dataset.isObserved;
|
|
@@ -316,10 +349,10 @@ export class TableRowNodeView {
|
|
|
316
349
|
table
|
|
317
350
|
} = this.tree;
|
|
318
351
|
entries.forEach(entry => {
|
|
319
|
-
var _this$
|
|
352
|
+
var _this$editorScrollabl2;
|
|
320
353
|
// On resize of the parent scroll element we need to adjust the width
|
|
321
354
|
// of the sticky header
|
|
322
|
-
if (entry.target.className === ((_this$
|
|
355
|
+
if (entry.target.className === ((_this$editorScrollabl2 = this.editorScrollableElement) === null || _this$editorScrollabl2 === void 0 ? void 0 : _this$editorScrollabl2.className)) {
|
|
323
356
|
this.updateStickyHeaderWidth();
|
|
324
357
|
} else {
|
|
325
358
|
const newHeight = entry.contentRect ? entry.contentRect.height : entry.target.offsetHeight;
|
|
@@ -12,6 +12,7 @@ import { akEditorFloatingPanelZIndex } from '@atlaskit/editor-shared-styles';
|
|
|
12
12
|
import { shortcutStyle } from '@atlaskit/editor-shared-styles/shortcut';
|
|
13
13
|
import { findCellRectClosestToPos, findTable, getSelectionRect, isSelectionType, splitCell } from '@atlaskit/editor-tables/utils';
|
|
14
14
|
import RemoveIcon from '@atlaskit/icon/glyph/editor/remove';
|
|
15
|
+
import { getBooleanFF } from '@atlaskit/platform-feature-flags';
|
|
15
16
|
import { clearHoverSelection, hoverColumns, hoverMergedCells, hoverRows, hoverTable, removeDescendantNodes } from './commands';
|
|
16
17
|
import { deleteColumnsWithAnalytics, deleteRowsWithAnalytics, deleteTableWithAnalytics, distributeColumnsWidthsWithAnalytics, emptyMultipleCellsWithAnalytics, insertColumnWithAnalytics, insertRowWithAnalytics, mergeCellsWithAnalytics, setColorWithAnalytics, sortColumnWithAnalytics, splitCellWithAnalytics, toggleHeaderColumnWithAnalytics, toggleHeaderRowWithAnalytics, toggleNumberColumnWithAnalytics, wrapTableInExpandWithAnalytics } from './commands-with-analytics';
|
|
17
18
|
import { getPluginState } from './pm-plugins/plugin-factory';
|
|
@@ -337,6 +338,11 @@ export const getToolbarConfig = (getEditorContainerWidth, editorAnalyticsAPI, ge
|
|
|
337
338
|
getDomRef,
|
|
338
339
|
nodeType,
|
|
339
340
|
offset: [0, 18],
|
|
341
|
+
absoluteOffset: getBooleanFF('platform.editor.table-sticky-scrollbar') ? {
|
|
342
|
+
top: -6
|
|
343
|
+
} : {
|
|
344
|
+
top: 0
|
|
345
|
+
},
|
|
340
346
|
zIndex: akEditorFloatingPanelZIndex + 1,
|
|
341
347
|
// Place the context menu slightly above the others
|
|
342
348
|
items: [menu, separator(menu.hidden), ...cellItems, ...colorPicker, {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { css } from '@emotion/react';
|
|
2
2
|
import { tableMarginTop, tableSharedStyle } from '@atlaskit/editor-common/styles';
|
|
3
|
-
import { akEditorSelectedNodeClassName, akEditorSmallZIndex, akEditorStickyHeaderZIndex, akEditorTableCellOnStickyHeaderZIndex, akEditorTableNumberColumnWidth, akEditorTableToolbarSize, akEditorUnitZIndex, getSelectionStyles, relativeFontSizeToBase16, SelectionStyle } from '@atlaskit/editor-shared-styles';
|
|
3
|
+
import { akEditorSelectedNodeClassName, akEditorSmallZIndex, akEditorStickyHeaderZIndex, akEditorTableCellOnStickyHeaderZIndex, akEditorTableNumberColumnWidth, akEditorTableToolbarSize, akEditorUnitZIndex, getSelectionStyles, MAX_BROWSER_SCROLLBAR_HEIGHT, relativeFontSizeToBase16, SelectionStyle } from '@atlaskit/editor-shared-styles';
|
|
4
4
|
import { scrollbarStyles } from '@atlaskit/editor-shared-styles/scrollbar';
|
|
5
5
|
import { getBooleanFF } from '@atlaskit/platform-feature-flags';
|
|
6
6
|
import { B300, N0, N20A, N300, N40A, R500 } from '@atlaskit/theme/colors';
|
|
@@ -16,6 +16,7 @@ const cornerControlHeight = tableToolbarSize + 1;
|
|
|
16
16
|
its center should be aligned with the edge
|
|
17
17
|
*/
|
|
18
18
|
export const insertColumnButtonOffset = tableInsertColumnButtonSize / 2;
|
|
19
|
+
export const tableRowHeight = 44;
|
|
19
20
|
const rangeSelectionStyles = `
|
|
20
21
|
.${ClassName.NODEVIEW_WRAPPER}.${akEditorSelectedNodeClassName} table tbody tr {
|
|
21
22
|
th,td {
|
|
@@ -48,6 +49,36 @@ const sentinelStyles = `.${ClassName.TABLE_CONTAINER} {
|
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
51
|
}`;
|
|
52
|
+
const stickyScrollbarSentinelStyles = `.${ClassName.TABLE_CONTAINER} {
|
|
53
|
+
> .${ClassName.TABLE_STICKY_SCROLLBAR_SENTINEL_BOTTOM},
|
|
54
|
+
> .${ClassName.TABLE_STICKY_SCROLLBAR_SENTINEL_TOP} {
|
|
55
|
+
position: absolute;
|
|
56
|
+
width: 100%;
|
|
57
|
+
height: 1px;
|
|
58
|
+
margin-top: -1px;
|
|
59
|
+
// need this to avoid sentinel being focused via keyboard
|
|
60
|
+
// this still allows it to be detected by intersection observer
|
|
61
|
+
visibility: hidden;
|
|
62
|
+
}
|
|
63
|
+
> .${ClassName.TABLE_STICKY_SCROLLBAR_SENTINEL_TOP} {
|
|
64
|
+
top: ${columnControlsDecorationHeight + tableRowHeight * 3}px;
|
|
65
|
+
}
|
|
66
|
+
> .${ClassName.TABLE_STICKY_SCROLLBAR_SENTINEL_BOTTOM} {
|
|
67
|
+
bottom: ${MAX_BROWSER_SCROLLBAR_HEIGHT}px;
|
|
68
|
+
}
|
|
69
|
+
}`;
|
|
70
|
+
const stickyScrollbarContainerStyles = `.${ClassName.TABLE_CONTAINER} {
|
|
71
|
+
> .${ClassName.TABLE_STICKY_SCROLLBAR_CONTAINER} {
|
|
72
|
+
width: 100%;
|
|
73
|
+
display: none;
|
|
74
|
+
overflow-x: auto;
|
|
75
|
+
position: sticky;
|
|
76
|
+
bottom: 0;
|
|
77
|
+
}
|
|
78
|
+
}`;
|
|
79
|
+
const stickyScrollbarStyles = () => {
|
|
80
|
+
return getBooleanFF('platform.editor.table-sticky-scrollbar') ? `${stickyScrollbarContainerStyles} ${stickyScrollbarSentinelStyles}` : '';
|
|
81
|
+
};
|
|
51
82
|
const shadowSentinelStyles = `
|
|
52
83
|
.${ClassName.TABLE_SHADOW_SENTINEL_LEFT},
|
|
53
84
|
.${ClassName.TABLE_SHADOW_SENTINEL_RIGHT} {
|
|
@@ -349,6 +380,7 @@ export const tableStyles = props => {
|
|
|
349
380
|
|
|
350
381
|
${sentinelStyles}
|
|
351
382
|
${OverflowShadow(props)}
|
|
383
|
+
${stickyScrollbarStyles()}
|
|
352
384
|
|
|
353
385
|
.${ClassName.TABLE_STICKY} .${ClassName.TABLE_STICKY_SHADOW} {
|
|
354
386
|
height: 0; // stop overflow flash & set correct height in update-overflow-shadows.ts
|
|
@@ -18,7 +18,7 @@ import { createDispatch } from '@atlaskit/editor-common/event-dispatcher';
|
|
|
18
18
|
import { getParentNodeWidth } from '@atlaskit/editor-common/node-width';
|
|
19
19
|
import { tableMarginSides } from '@atlaskit/editor-common/styles';
|
|
20
20
|
import { analyticsEventKey, browser, isValidPosition } from '@atlaskit/editor-common/utils';
|
|
21
|
-
import { akEditorTableToolbarSize as tableToolbarSize } from '@atlaskit/editor-shared-styles';
|
|
21
|
+
import { MAX_BROWSER_SCROLLBAR_HEIGHT, akEditorTableToolbarSize as tableToolbarSize } from '@atlaskit/editor-shared-styles';
|
|
22
22
|
import { findTable, isTableSelected } from '@atlaskit/editor-tables/utils';
|
|
23
23
|
import { getBooleanFF } from '@atlaskit/platform-feature-flags';
|
|
24
24
|
import { autoSizeTable, clearHoverSelection } from '../commands';
|
|
@@ -34,6 +34,7 @@ import TableFloatingControls from '../ui/TableFloatingControls';
|
|
|
34
34
|
import { containsHeaderRow, isTableNested, tablesHaveDifferentColumnWidths, tablesHaveDifferentNoOfColumns } from '../utils';
|
|
35
35
|
import { OverflowShadowsObserver } from './OverflowShadowsObserver';
|
|
36
36
|
import { TableContainer } from './TableContainer';
|
|
37
|
+
import { TableStickyScrollbar } from './TableStickyScrollbar';
|
|
37
38
|
var isIE11 = browser.ie_version === 11;
|
|
38
39
|
// When table is inserted via paste, keyboard shortcut or quickInsert,
|
|
39
40
|
// componentDidUpdate is called multiple times. The isOverflowing value is correct only on the last update.
|
|
@@ -87,6 +88,11 @@ var TableComponent = /*#__PURE__*/function (_React$Component) {
|
|
|
87
88
|
if (!_this.wrapper || event.target !== _this.wrapper) {
|
|
88
89
|
return;
|
|
89
90
|
}
|
|
91
|
+
if (getBooleanFF('platform.editor.table-sticky-scrollbar')) {
|
|
92
|
+
if (_this.stickyScrollbar) {
|
|
93
|
+
_this.stickyScrollbar.scrollLeft(_this.wrapper.scrollLeft);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
90
96
|
if (_this.table) {
|
|
91
97
|
// sync sticky header row to table scroll
|
|
92
98
|
var headers = _this.table.querySelectorAll('tr[data-header-row]');
|
|
@@ -297,7 +303,14 @@ var TableComponent = /*#__PURE__*/function (_React$Component) {
|
|
|
297
303
|
eventDispatcher = _this$props7.eventDispatcher,
|
|
298
304
|
options = _this$props7.options;
|
|
299
305
|
if (allowColumnResizing && this.wrapper && !isIE11) {
|
|
300
|
-
this.wrapper.addEventListener('scroll', this.handleScrollDebounced
|
|
306
|
+
this.wrapper.addEventListener('scroll', this.handleScrollDebounced, {
|
|
307
|
+
passive: true
|
|
308
|
+
});
|
|
309
|
+
if (getBooleanFF('platform.editor.table-sticky-scrollbar')) {
|
|
310
|
+
if (this.table) {
|
|
311
|
+
this.stickyScrollbar = new TableStickyScrollbar(this.wrapper, this.props.view);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
301
314
|
}
|
|
302
315
|
if (allowColumnResizing) {
|
|
303
316
|
/**
|
|
@@ -329,6 +342,11 @@ var TableComponent = /*#__PURE__*/function (_React$Component) {
|
|
|
329
342
|
if (this.wrapper && !isIE11) {
|
|
330
343
|
this.wrapper.removeEventListener('scroll', this.handleScrollDebounced);
|
|
331
344
|
}
|
|
345
|
+
if (getBooleanFF('platform.editor.table-sticky-scrollbar')) {
|
|
346
|
+
if (this.stickyScrollbar) {
|
|
347
|
+
this.stickyScrollbar.dispose();
|
|
348
|
+
}
|
|
349
|
+
}
|
|
332
350
|
this.handleScrollDebounced.cancel();
|
|
333
351
|
this.scaleTableDebounced.cancel();
|
|
334
352
|
this.handleTableResizingDebounced.cancel();
|
|
@@ -501,6 +519,9 @@ var TableComponent = /*#__PURE__*/function (_React$Component) {
|
|
|
501
519
|
}, /*#__PURE__*/React.createElement("div", {
|
|
502
520
|
className: ClassName.TABLE_STICKY_SENTINEL_TOP,
|
|
503
521
|
"data-testid": "sticky-sentinel-top"
|
|
522
|
+
}), getBooleanFF('platform.editor.table-sticky-scrollbar') && /*#__PURE__*/React.createElement("div", {
|
|
523
|
+
className: ClassName.TABLE_STICKY_SCROLLBAR_SENTINEL_TOP,
|
|
524
|
+
"data-testid": "sticky-scrollbar-sentinel-top"
|
|
504
525
|
}), allowControls && rowControls, /*#__PURE__*/React.createElement("div", {
|
|
505
526
|
style: shadowStyle(showBeforeShadow),
|
|
506
527
|
className: ClassName.TABLE_LEFT_SHADOW
|
|
@@ -523,7 +544,17 @@ var TableComponent = /*#__PURE__*/function (_React$Component) {
|
|
|
523
544
|
}
|
|
524
545
|
}
|
|
525
546
|
}
|
|
526
|
-
}), /*#__PURE__*/React.createElement("div", {
|
|
547
|
+
}), getBooleanFF('platform.editor.table-sticky-scrollbar') && /*#__PURE__*/React.createElement("div", {
|
|
548
|
+
className: ClassName.TABLE_STICKY_SCROLLBAR_CONTAINER,
|
|
549
|
+
style: {
|
|
550
|
+
height: MAX_BROWSER_SCROLLBAR_HEIGHT,
|
|
551
|
+
display: 'none'
|
|
552
|
+
}
|
|
553
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
554
|
+
style: {
|
|
555
|
+
width: tableRef === null || tableRef === void 0 ? void 0 : tableRef.clientWidth
|
|
556
|
+
}
|
|
557
|
+
})), /*#__PURE__*/React.createElement("div", {
|
|
527
558
|
style: shadowStyle(showAfterShadow),
|
|
528
559
|
className: ClassName.TABLE_RIGHT_SHADOW
|
|
529
560
|
}), this.state.stickyHeader && /*#__PURE__*/React.createElement("div", {
|
|
@@ -540,6 +571,9 @@ var TableComponent = /*#__PURE__*/function (_React$Component) {
|
|
|
540
571
|
})), /*#__PURE__*/React.createElement("div", {
|
|
541
572
|
className: ClassName.TABLE_STICKY_SENTINEL_BOTTOM,
|
|
542
573
|
"data-testid": "sticky-sentinel-bottom"
|
|
574
|
+
}), getBooleanFF('platform.editor.table-sticky-scrollbar') && /*#__PURE__*/React.createElement("div", {
|
|
575
|
+
className: ClassName.TABLE_STICKY_SCROLLBAR_SENTINEL_BOTTOM,
|
|
576
|
+
"data-testid": "sticky-scrollbar-sentinel-bottom"
|
|
543
577
|
}));
|
|
544
578
|
}
|
|
545
579
|
}]);
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
|
|
2
|
+
import _createClass from "@babel/runtime/helpers/createClass";
|
|
3
|
+
import _defineProperty from "@babel/runtime/helpers/defineProperty";
|
|
4
|
+
import rafSchedule from 'raf-schd';
|
|
5
|
+
import { findOverflowScrollParent } from '@atlaskit/editor-common/ui';
|
|
6
|
+
import { TableCssClassName as ClassName } from '../types';
|
|
7
|
+
export var TableStickyScrollbar = /*#__PURE__*/function () {
|
|
8
|
+
function TableStickyScrollbar(wrapper, view) {
|
|
9
|
+
var _this = this;
|
|
10
|
+
_classCallCheck(this, TableStickyScrollbar);
|
|
11
|
+
_defineProperty(this, "sentinels", {});
|
|
12
|
+
_defineProperty(this, "handleScroll", function (event) {
|
|
13
|
+
if (!_this.stickyScrollbarContainerElement || !_this.wrapper || event.target !== _this.stickyScrollbarContainerElement) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
_this.wrapper.scrollLeft = _this.stickyScrollbarContainerElement.scrollLeft;
|
|
17
|
+
});
|
|
18
|
+
_defineProperty(this, "handleScrollDebounced", rafSchedule(this.handleScroll));
|
|
19
|
+
this.wrapper = wrapper;
|
|
20
|
+
this.view = view;
|
|
21
|
+
this.init();
|
|
22
|
+
}
|
|
23
|
+
_createClass(TableStickyScrollbar, [{
|
|
24
|
+
key: "dispose",
|
|
25
|
+
value: function dispose() {
|
|
26
|
+
if (this.stickyScrollbarContainerElement) {
|
|
27
|
+
this.stickyScrollbarContainerElement.removeEventListener('scroll', this.handleScrollDebounced);
|
|
28
|
+
this.handleScrollDebounced.cancel();
|
|
29
|
+
}
|
|
30
|
+
this.deleteIntesactionObserver();
|
|
31
|
+
}
|
|
32
|
+
}, {
|
|
33
|
+
key: "scrollLeft",
|
|
34
|
+
value: function scrollLeft(left) {
|
|
35
|
+
if (this.stickyScrollbarContainerElement) {
|
|
36
|
+
this.stickyScrollbarContainerElement.scrollLeft = left;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}, {
|
|
40
|
+
key: "init",
|
|
41
|
+
value: function init() {
|
|
42
|
+
var _this$wrapper$parentE;
|
|
43
|
+
if (!this.wrapper) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
this.stickyScrollbarContainerElement = (_this$wrapper$parentE = this.wrapper.parentElement) === null || _this$wrapper$parentE === void 0 ? void 0 : _this$wrapper$parentE.querySelector(".".concat(ClassName.TABLE_STICKY_SCROLLBAR_CONTAINER));
|
|
47
|
+
if (this.stickyScrollbarContainerElement) {
|
|
48
|
+
this.stickyScrollbarContainerElement.addEventListener('scroll', this.handleScrollDebounced, {
|
|
49
|
+
passive: true
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
this.createIntersectionObserver();
|
|
53
|
+
}
|
|
54
|
+
}, {
|
|
55
|
+
key: "createIntersectionObserver",
|
|
56
|
+
value: function createIntersectionObserver() {
|
|
57
|
+
var _this2 = this,
|
|
58
|
+
_this$wrapper,
|
|
59
|
+
_this$wrapper$parentE2,
|
|
60
|
+
_this$wrapper$parentE3,
|
|
61
|
+
_this$wrapper2,
|
|
62
|
+
_this$wrapper2$parent,
|
|
63
|
+
_this$wrapper2$parent2;
|
|
64
|
+
this.editorScrollableElement = findOverflowScrollParent(this.view.dom) || window.document;
|
|
65
|
+
if (!this.editorScrollableElement || !this.wrapper) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
this.intersectionObserver = new IntersectionObserver(function (entries, _) {
|
|
69
|
+
if (!_this2.stickyScrollbarContainerElement) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
entries.forEach(function (entry) {
|
|
73
|
+
var _entry$rootBounds;
|
|
74
|
+
var target = entry.target;
|
|
75
|
+
// if the rootBounds has 0 height, e.g. confluence preview mode, we do nothing.
|
|
76
|
+
if (((_entry$rootBounds = entry.rootBounds) === null || _entry$rootBounds === void 0 ? void 0 : _entry$rootBounds.height) === 0) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (target.classList.contains(ClassName.TABLE_STICKY_SCROLLBAR_SENTINEL_BOTTOM)) {
|
|
80
|
+
_this2.sentenialBottomCallback(entry);
|
|
81
|
+
}
|
|
82
|
+
if (target.classList.contains(ClassName.TABLE_STICKY_SCROLLBAR_SENTINEL_TOP)) {
|
|
83
|
+
_this2.sentenialTopCallback(entry);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}, {
|
|
87
|
+
root: this.editorScrollableElement
|
|
88
|
+
});
|
|
89
|
+
this.sentinels.bottom = (_this$wrapper = this.wrapper) === null || _this$wrapper === void 0 ? void 0 : (_this$wrapper$parentE2 = _this$wrapper.parentElement) === null || _this$wrapper$parentE2 === void 0 ? void 0 : (_this$wrapper$parentE3 = _this$wrapper$parentE2.getElementsByClassName(ClassName.TABLE_STICKY_SCROLLBAR_SENTINEL_BOTTOM)) === null || _this$wrapper$parentE3 === void 0 ? void 0 : _this$wrapper$parentE3.item(0);
|
|
90
|
+
this.sentinels.top = (_this$wrapper2 = this.wrapper) === null || _this$wrapper2 === void 0 ? void 0 : (_this$wrapper2$parent = _this$wrapper2.parentElement) === null || _this$wrapper2$parent === void 0 ? void 0 : (_this$wrapper2$parent2 = _this$wrapper2$parent.getElementsByClassName(ClassName.TABLE_STICKY_SCROLLBAR_SENTINEL_TOP)) === null || _this$wrapper2$parent2 === void 0 ? void 0 : _this$wrapper2$parent2.item(0);
|
|
91
|
+
[this.sentinels.bottom, this.sentinels.top].forEach(function (el) {
|
|
92
|
+
return _this2.intersectionObserver.observe(el);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}, {
|
|
96
|
+
key: "deleteIntesactionObserver",
|
|
97
|
+
value: function deleteIntesactionObserver() {
|
|
98
|
+
if (this.intersectionObserver) {
|
|
99
|
+
if (this.sentinels.bottom) {
|
|
100
|
+
this.intersectionObserver.unobserve(this.sentinels.bottom);
|
|
101
|
+
}
|
|
102
|
+
this.intersectionObserver.disconnect();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}, {
|
|
106
|
+
key: "sentenialBottomCallback",
|
|
107
|
+
value: function sentenialBottomCallback(entry) {
|
|
108
|
+
var _entry$rootBounds2;
|
|
109
|
+
var sentinelIsAboveScrollArea = entry.boundingClientRect.top < (((_entry$rootBounds2 = entry.rootBounds) === null || _entry$rootBounds2 === void 0 ? void 0 : _entry$rootBounds2.top) || 0);
|
|
110
|
+
this.bottomSentinelState = sentinelIsAboveScrollArea ? 'above' : entry.isIntersecting ? 'visible' : 'below';
|
|
111
|
+
this.toggle();
|
|
112
|
+
}
|
|
113
|
+
}, {
|
|
114
|
+
key: "sentenialTopCallback",
|
|
115
|
+
value: function sentenialTopCallback(entry) {
|
|
116
|
+
var _entry$rootBounds3;
|
|
117
|
+
var sentinelIsBelowScrollArea = (((_entry$rootBounds3 = entry.rootBounds) === null || _entry$rootBounds3 === void 0 ? void 0 : _entry$rootBounds3.bottom) || 0) < entry.boundingClientRect.top;
|
|
118
|
+
this.topSentinelState = sentinelIsBelowScrollArea ? 'below' : entry.isIntersecting ? 'visible' : 'above';
|
|
119
|
+
this.toggle();
|
|
120
|
+
}
|
|
121
|
+
}, {
|
|
122
|
+
key: "toggle",
|
|
123
|
+
value: function toggle() {
|
|
124
|
+
if ((this.topSentinelState === 'visible' || this.topSentinelState === 'above') && this.bottomSentinelState === 'below') {
|
|
125
|
+
this.show();
|
|
126
|
+
} else {
|
|
127
|
+
this.hide();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}, {
|
|
131
|
+
key: "hide",
|
|
132
|
+
value: function hide() {
|
|
133
|
+
if (this.stickyScrollbarContainerElement && this.stickyScrollbarContainerElement.style.display !== 'none') {
|
|
134
|
+
this.stickyScrollbarContainerElement.style.display = 'none';
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}, {
|
|
138
|
+
key: "show",
|
|
139
|
+
value: function show() {
|
|
140
|
+
if (this.stickyScrollbarContainerElement && this.stickyScrollbarContainerElement.style.display !== 'block') {
|
|
141
|
+
this.stickyScrollbarContainerElement.style.display = 'block';
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}]);
|
|
145
|
+
return TableStickyScrollbar;
|
|
146
|
+
}();
|
|
@@ -142,10 +142,24 @@ export var TableRowNodeView = /*#__PURE__*/function () {
|
|
|
142
142
|
// otherwise make it non-sticky
|
|
143
143
|
return false;
|
|
144
144
|
});
|
|
145
|
+
/**
|
|
146
|
+
* Manually refire the intersection observers.
|
|
147
|
+
* Useful when the header may have detached from the table.
|
|
148
|
+
*/
|
|
149
|
+
_defineProperty(this, "refireIntersectionObservers", function () {
|
|
150
|
+
if (_this.isSticky) {
|
|
151
|
+
[_this.sentinels.top, _this.sentinels.bottom].forEach(function (el) {
|
|
152
|
+
if (el && _this.intersectionObserver) {
|
|
153
|
+
_this.intersectionObserver.unobserve(el);
|
|
154
|
+
_this.intersectionObserver.observe(el);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
});
|
|
145
159
|
_defineProperty(this, "makeHeaderRowSticky", function (tree, scrollTop) {
|
|
146
160
|
var _tbody$firstChild;
|
|
147
161
|
// If header row height is more than 50% of viewport height don't do this
|
|
148
|
-
if (_this.stickyRowHeight && _this.stickyRowHeight > window.innerHeight / 2) {
|
|
162
|
+
if (_this.isSticky || _this.stickyRowHeight && _this.stickyRowHeight > window.innerHeight / 2) {
|
|
149
163
|
return;
|
|
150
164
|
}
|
|
151
165
|
var table = tree.table,
|
|
@@ -163,10 +177,25 @@ export var TableRowNodeView = /*#__PURE__*/function () {
|
|
|
163
177
|
}
|
|
164
178
|
var domTop = currentTableTop > 0 ? scrollTop : scrollTop + currentTableTop;
|
|
165
179
|
if (!_this.isSticky) {
|
|
180
|
+
var _this$editorScrollabl;
|
|
166
181
|
syncStickyRowToTable(table);
|
|
167
182
|
_this.dom.classList.add('sticky');
|
|
168
183
|
table.classList.add(ClassName.TABLE_STICKY);
|
|
169
184
|
_this.isSticky = true;
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* The logic below is not desirable, but acts as a fail safe for scenarios where the sticky header
|
|
188
|
+
* detaches from the table. This typically happens during a fast scroll by the user which causes
|
|
189
|
+
* the intersection observer logic to not fire as expected.
|
|
190
|
+
*/
|
|
191
|
+
(_this$editorScrollabl = _this.editorScrollableElement) === null || _this$editorScrollabl === void 0 ? void 0 : _this$editorScrollabl.addEventListener('scrollend', _this.refireIntersectionObservers, {
|
|
192
|
+
passive: true,
|
|
193
|
+
once: true
|
|
194
|
+
});
|
|
195
|
+
var fastScrollThresholdMs = 500;
|
|
196
|
+
setTimeout(function () {
|
|
197
|
+
_this.refireIntersectionObservers();
|
|
198
|
+
}, fastScrollThresholdMs);
|
|
170
199
|
}
|
|
171
200
|
_this.dom.style.top = "".concat(domTop, "px");
|
|
172
201
|
updateTableMargin(table);
|
|
@@ -263,8 +292,12 @@ export var TableRowNodeView = /*#__PURE__*/function () {
|
|
|
263
292
|
this.eventDispatcher.on('widthPlugin', this.updateStickyHeaderWidth);
|
|
264
293
|
this.eventDispatcher.on(tablePluginKey.key, this.onTablePluginState);
|
|
265
294
|
this.listening = true;
|
|
266
|
-
this.dom.addEventListener('wheel', this.headerRowMouseScroll.bind(this)
|
|
267
|
-
|
|
295
|
+
this.dom.addEventListener('wheel', this.headerRowMouseScroll.bind(this), {
|
|
296
|
+
passive: true
|
|
297
|
+
});
|
|
298
|
+
this.dom.addEventListener('touchmove', this.headerRowMouseScroll.bind(this), {
|
|
299
|
+
passive: true
|
|
300
|
+
});
|
|
268
301
|
}
|
|
269
302
|
}, {
|
|
270
303
|
key: "unsubscribe",
|
|
@@ -275,7 +308,7 @@ export var TableRowNodeView = /*#__PURE__*/function () {
|
|
|
275
308
|
if (this.intersectionObserver) {
|
|
276
309
|
this.intersectionObserver.disconnect();
|
|
277
310
|
// ED-16211 Once intersection observer is disconnected, we need to remove the isObserved from the sentinels
|
|
278
|
-
// Otherwise when
|
|
311
|
+
// Otherwise when newer intersection observer is created it will not observe because it thinks its already being observed
|
|
279
312
|
[this.sentinels.top, this.sentinels.bottom].forEach(function (el) {
|
|
280
313
|
if (el) {
|
|
281
314
|
delete el.dataset.isObserved;
|
|
@@ -12,6 +12,7 @@ import { akEditorFloatingPanelZIndex } from '@atlaskit/editor-shared-styles';
|
|
|
12
12
|
import { shortcutStyle } from '@atlaskit/editor-shared-styles/shortcut';
|
|
13
13
|
import { findCellRectClosestToPos, findTable, getSelectionRect, isSelectionType, splitCell } from '@atlaskit/editor-tables/utils';
|
|
14
14
|
import RemoveIcon from '@atlaskit/icon/glyph/editor/remove';
|
|
15
|
+
import { getBooleanFF } from '@atlaskit/platform-feature-flags';
|
|
15
16
|
import { clearHoverSelection, hoverColumns, hoverMergedCells, hoverRows, hoverTable, removeDescendantNodes } from './commands';
|
|
16
17
|
import { deleteColumnsWithAnalytics, deleteRowsWithAnalytics, deleteTableWithAnalytics, distributeColumnsWidthsWithAnalytics, emptyMultipleCellsWithAnalytics, insertColumnWithAnalytics, insertRowWithAnalytics, mergeCellsWithAnalytics, setColorWithAnalytics, sortColumnWithAnalytics, splitCellWithAnalytics, toggleHeaderColumnWithAnalytics, toggleHeaderRowWithAnalytics, toggleNumberColumnWithAnalytics, wrapTableInExpandWithAnalytics } from './commands-with-analytics';
|
|
17
18
|
import { getPluginState } from './pm-plugins/plugin-factory';
|
|
@@ -343,6 +344,11 @@ export var getToolbarConfig = function getToolbarConfig(getEditorContainerWidth,
|
|
|
343
344
|
getDomRef: getDomRef,
|
|
344
345
|
nodeType: nodeType,
|
|
345
346
|
offset: [0, 18],
|
|
347
|
+
absoluteOffset: getBooleanFF('platform.editor.table-sticky-scrollbar') ? {
|
|
348
|
+
top: -6
|
|
349
|
+
} : {
|
|
350
|
+
top: 0
|
|
351
|
+
},
|
|
346
352
|
zIndex: akEditorFloatingPanelZIndex + 1,
|
|
347
353
|
// Place the context menu slightly above the others
|
|
348
354
|
items: [menu, separator(menu.hidden)].concat(cellItems, colorPicker, [{
|