@atlaskit/editor-plugin-table
Version:
Table plugin for the @atlaskit/editor
992 lines (956 loc) • 47.1 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
var _possibleConstructorReturn2 = _interopRequireDefault(require("@babel/runtime/helpers/possibleConstructorReturn"));
var _getPrototypeOf2 = _interopRequireDefault(require("@babel/runtime/helpers/getPrototypeOf"));
var _inherits2 = _interopRequireDefault(require("@babel/runtime/helpers/inherits"));
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
var _debounce = _interopRequireDefault(require("lodash/debounce"));
var _throttle = _interopRequireDefault(require("lodash/throttle"));
var _analytics = require("@atlaskit/editor-common/analytics");
var _nesting = require("@atlaskit/editor-common/nesting");
var _nodeVisibility = require("@atlaskit/editor-common/node-visibility");
var _styles = require("@atlaskit/editor-common/styles");
var _ui = require("@atlaskit/editor-common/ui");
var _utils = require("@atlaskit/editor-prosemirror/utils");
var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
var _expValEquals = require("@atlaskit/tmp-editor-statsig/exp-val-equals");
var _editorContentAreaHeight = require("../pm-plugins/editor-content-area-height");
var _pluginFactory = require("../pm-plugins/plugin-factory");
var _pluginKey = require("../pm-plugins/plugin-key");
var _commands = require("../pm-plugins/sticky-headers/commands");
var _dom = require("../pm-plugins/table-resizing/utils/dom");
var _dom2 = require("../pm-plugins/utils/dom");
var _nodes = require("../pm-plugins/utils/nodes");
var _types = require("../types");
var _consts = require("../ui/consts");
var _TableNodeViewBase = _interopRequireDefault(require("./TableNodeViewBase"));
function _callSuper(t, o, e) { return o = (0, _getPrototypeOf2.default)(o), (0, _possibleConstructorReturn2.default)(t, _isNativeReflectConstruct() ? Reflect.construct(o, e || [], (0, _getPrototypeOf2.default)(t).constructor) : o.apply(t, e)); }
function _isNativeReflectConstruct() { try { var t = !Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); } catch (t) {} return (_isNativeReflectConstruct = function _isNativeReflectConstruct() { return !!t; })(); }
// limit scroll event calls
var HEADER_ROW_SCROLL_THROTTLE_TIMEOUT = 200;
// timeout for resetting the scroll class - if it's too long then users won't be able to click on the header cells,
// if too short it would trigger too many dom updates.
var HEADER_ROW_SCROLL_RESET_DEBOUNCE_TIMEOUT = 400;
var TableRowNativeStickyWithFallback = exports.default = /*#__PURE__*/function (_ref) {
function TableRowNativeStickyWithFallback(node, view, getPos, eventDispatcher, api) {
var _api$limitedMode;
var _this;
(0, _classCallCheck2.default)(this, TableRowNativeStickyWithFallback);
_this = _callSuper(this, TableRowNativeStickyWithFallback, [node, view, getPos, eventDispatcher]);
(0, _defineProperty2.default)(_this, "cleanup", function () {
var _this$onEditorContent, _this2;
if (_this.isStickyHeaderEnabled) {
_this.unsubscribe();
_this.nodeVisibilityObserverCleanupFn && _this.nodeVisibilityObserverCleanupFn();
var tree = (0, _dom2.getTree)(_this.dom);
if (tree) {
_this.makeRowHeaderNotLegacySticky(tree.table, true);
}
_this.emitOff(false);
}
if (_this.tableContainerObserver) {
_this.tableContainerObserver.disconnect();
}
(_this$onEditorContent = (_this2 = _this).onEditorContentAreaHeightChange) === null || _this$onEditorContent === void 0 || _this$onEditorContent.call(_this2);
});
(0, _defineProperty2.default)(_this, "colControlsOffset", 0);
(0, _defineProperty2.default)(_this, "focused", false);
(0, _defineProperty2.default)(_this, "topPosEditorElement", 0);
(0, _defineProperty2.default)(_this, "sentinels", {});
(0, _defineProperty2.default)(_this, "sentinelData", {
top: {
isIntersecting: false,
boundingClientRect: null,
rootBounds: null
},
bottom: {
isIntersecting: false,
boundingClientRect: null,
rootBounds: null
}
});
(0, _defineProperty2.default)(_this, "listening", false);
(0, _defineProperty2.default)(_this, "padding", 0);
(0, _defineProperty2.default)(_this, "top", 0);
(0, _defineProperty2.default)(_this, "hasScrolledSinceLoad", false);
(0, _defineProperty2.default)(_this, "disableNativeSticky", false);
/**
* Methods
*/
(0, _defineProperty2.default)(_this, "headerRowMouseScrollEnd", (0, _debounce.default)(function () {
_this.dom.classList.remove('no-pointer-events');
}, HEADER_ROW_SCROLL_RESET_DEBOUNCE_TIMEOUT));
// When the header is sticky, the header row is set to position: fixed
// This prevents mouse wheel scrolling on the scroll-parent div when user's mouse is hovering the header row.
// This fix sets pointer-events: none on the header row briefly to avoid this behaviour
(0, _defineProperty2.default)(_this, "headerRowMouseScroll", (0, _throttle.default)(function () {
if (_this.isLegacySticky) {
_this.dom.classList.add('no-pointer-events');
_this.headerRowMouseScrollEnd();
}
}, HEADER_ROW_SCROLL_THROTTLE_TIMEOUT));
(0, _defineProperty2.default)(_this, "toggleDisableNativeSticky", function (headerHeight, viewportHeight) {
if (!_this.disableNativeSticky && headerHeight > viewportHeight * 0.5) {
_this.disableNativeSticky = true;
if (_this.isNativeSticky === undefined) {
_this.dom.classList.remove(_types.TableCssClassName.NATIVE_STICKY);
}
}
if (_this.disableNativeSticky && headerHeight <= viewportHeight * 0.5) {
_this.disableNativeSticky = false;
}
});
_this.isHeaderRow = (0, _nodes.supportedHeaderRow)(node);
_this.isLegacySticky = false;
var _getPluginState = (0, _pluginFactory.getPluginState)(view.state),
pluginConfig = _getPluginState.pluginConfig;
_this.isStickyHeaderEnabled = !!pluginConfig.stickyHeaders;
_this.api = api;
if (api !== null && api !== void 0 && (_api$limitedMode = api.limitedMode) !== null && _api$limitedMode !== void 0 && (_api$limitedMode = _api$limitedMode.sharedState.currentState()) !== null && _api$limitedMode !== void 0 && (_api$limitedMode = _api$limitedMode.limitedModePluginKey.getState(view.state)) !== null && _api$limitedMode !== void 0 && _api$limitedMode.documentSizeBreachesThreshold) {
_this.isStickyHeaderEnabled = false;
// eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners
document.addEventListener('limited-mode-activated', _this.cleanup);
}
var pos = _this.getPos();
_this.isInNestedTable = false;
if (pos) {
_this.isInNestedTable = (0, _nesting.getParentOfTypeCount)(view.state.schema.nodes.table)(view.state.doc.resolve(pos)) > 1;
}
if (_this.isHeaderRow) {
_this.dom.setAttribute('data-vc-nvs', 'true');
var _nodeVisibilityManage = (0, _nodeVisibility.nodeVisibilityManager)(view.dom),
observe = _nodeVisibilityManage.observe;
_this.nodeVisibilityObserverCleanupFn = observe({
element: _this.contentDOM,
onFirstVisible: function onFirstVisible() {
_this.subscribeWhenRowVisible();
}
});
}
if (_this.isHeaderRow && _this.isStickyHeaderEnabled && (0, _platformFeatureFlags.fg)('platform_editor_table_sticky_header_patch_4')) {
var _api$table;
_this.onEditorContentAreaHeightChange = api === null || api === void 0 || (_api$table = api.table) === null || _api$table === void 0 ? void 0 : _api$table.sharedState.onChange(function (_ref2) {
var nextSharedState = _ref2.nextSharedState;
if (nextSharedState !== null && nextSharedState !== void 0 && nextSharedState.editorContentAreaHeight && (nextSharedState === null || nextSharedState === void 0 ? void 0 : nextSharedState.editorContentAreaHeight) !== _this.editorContentAreaHeight) {
var _this$stickyRowHeight;
_this.editorContentAreaHeight = nextSharedState.editorContentAreaHeight;
_this.toggleDisableNativeSticky((_this$stickyRowHeight = _this.stickyRowHeight) !== null && _this$stickyRowHeight !== void 0 ? _this$stickyRowHeight : 0, nextSharedState.editorContentAreaHeight);
}
});
}
return _this;
}
(0, _inherits2.default)(TableRowNativeStickyWithFallback, _ref);
return (0, _createClass2.default)(TableRowNativeStickyWithFallback, [{
key: "subscribeWhenRowVisible",
value: function subscribeWhenRowVisible() {
if (this.listening) {
return;
}
this.dom.setAttribute('data-header-row', 'true');
if (this.isStickyHeaderEnabled) {
this.subscribe();
}
}
/**
* Variables
*/
}, {
key: "update",
value:
/**
* Methods: Nodeview Lifecycle
*/
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function update(node) {
// do nothing if nodes were identical
if (node === this.node) {
return true;
}
// see if we're changing into a header row or
// changing away from one
var newNodeIsHeaderRow = (0, _nodes.supportedHeaderRow)(node);
if (this.isHeaderRow !== newNodeIsHeaderRow) {
if (!newNodeIsHeaderRow && this.isHeaderRow) {
var _this$dom$closest;
(_this$dom$closest = this.dom.closest(".".concat(_types.TableCssClassName.TABLE_NODE_WRAPPER))) === null || _this$dom$closest === void 0 || _this$dom$closest.classList.remove(_types.TableCssClassName.TABLE_NODE_WRAPPER_NO_OVERFLOW);
}
return false; // re-create nodeview
}
// node is different but no need to re-create nodeview
this.node = node;
// don't do anything if we're just a regular tr
if (!this.isHeaderRow) {
return true;
}
// something changed, sync widths
if (this.isStickyHeaderEnabled) {
var tbody = this.dom.parentElement;
var table = tbody && tbody.parentElement;
(0, _dom.syncStickyRowToTable)(table);
}
return true;
}
}, {
key: "destroy",
value: function destroy() {
if (this.isStickyHeaderEnabled) {
var _this$nodeVisibilityO;
this.unsubscribe();
this.overflowObserver && this.overflowObserver.disconnect();
this.overflowObserverEntries = undefined;
this.stickyStateObserver && this.stickyStateObserver.disconnect();
(_this$nodeVisibilityO = this.nodeVisibilityObserver) === null || _this$nodeVisibilityO === void 0 || _this$nodeVisibilityO.disconnect();
this.nodeVisibilityObserverCleanupFn && this.nodeVisibilityObserverCleanupFn();
var tree = (0, _dom2.getTree)(this.dom);
if (tree) {
this.makeRowHeaderNotLegacySticky(tree.table, true);
}
this.emitOff(true);
}
// eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners
document.removeEventListener('limited-mode-activated', this.cleanup);
if (this.tableContainerObserver) {
this.tableContainerObserver.disconnect();
}
}
}, {
key: "ignoreMutation",
value: function ignoreMutation(mutationRecord) {
/* tableRows are not directly editable by the user
* so it should be safe to ignore mutations that we cause
* by updating styles and classnames on this DOM element
*
* Update: should not ignore mutations for row selection to avoid known issue with table selection highlight in firefox
* Related bug report: https://bugzilla.mozilla.org/show_bug.cgi?id=1289673
* */
var isTableSelection = mutationRecord.type === 'selection' && mutationRecord.target.nodeName === 'TR';
/**
* Update: should not ignore mutations when an node is added, as this interferes with
* prosemirrors handling of some language inputs in Safari (ie. Pinyin, Hiragana).
*
* In paticular, when a composition occurs at the start of the first node inside a table cell, if the resulting mutation
* from the composition end is ignored than prosemirror will end up with; invalid table markup nesting and a misplaced
* selection and insertion.
*/
var isNodeInsertion = mutationRecord.type === 'childList' && mutationRecord.target.nodeName === 'TR' && mutationRecord.addedNodes.length;
if (isTableSelection || isNodeInsertion) {
return false;
}
return true;
}
}, {
key: "subscribe",
value: function subscribe() {
var _this3 = this;
// Ignored via go/ees005
// eslint-disable-next-line @atlaskit/editor/no-as-casting
this.editorScrollableElement = (0, _ui.findOverflowScrollParent)(this.view.dom) || window;
if (this.editorScrollableElement) {
this.initObservers();
this.topPosEditorElement = (0, _dom2.getTop)(this.editorScrollableElement);
if ((0, _platformFeatureFlags.fg)('platform_editor_table_sticky_header_patch_5')) {
this.scrollListener = function () {
var _this3$overflowObserv;
if (_this3.hasScrolledSinceLoad) {
return;
}
_this3.hasScrolledSinceLoad = true;
if (!_this3.overflowObserver) {
return;
}
// Re-check intersection state now that scrolling has occurred
var entries = (_this3$overflowObserv = _this3.overflowObserverEntries) !== null && _this3$overflowObserv !== void 0 ? _this3$overflowObserv : _this3.overflowObserver.takeRecords();
_this3.overflowObserverEntries = undefined;
/** NOTE: This logic is duplicated in the overflowObserver callback
* to avoid conflicting with a follow up refactor where this will
* be cleaned up.
*/
entries.forEach(function (entry) {
var tableWrapper = _this3.dom.closest(".".concat(_types.TableCssClassName.TABLE_NODE_WRAPPER));
if (tableWrapper && tableWrapper instanceof HTMLElement && (!(0, _dom2.areAllRectsZero)(entry) && (0, _expValEquals.expValEquals)('platform_editor_table_sticky_header_patch_10', 'isEnabled', true) || !(0, _expValEquals.expValEquals)('platform_editor_table_sticky_header_patch_10', 'isEnabled', true))) {
if (entry.isIntersecting) {
tableWrapper.classList.add(_types.TableCssClassName.TABLE_NODE_WRAPPER_NO_OVERFLOW);
_this3.dom.classList.add(_types.TableCssClassName.NATIVE_STICKY);
_this3.isNativeSticky = true;
} else {
tableWrapper.classList.remove(_types.TableCssClassName.TABLE_NODE_WRAPPER_NO_OVERFLOW);
_this3.dom.classList.remove(_types.TableCssClassName.NATIVE_STICKY);
_this3.isNativeSticky = false;
}
_this3.refreshLegacyStickyState();
}
});
};
// eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners
this.editorScrollableElement.addEventListener('scroll', this.scrollListener, {
passive: true,
once: true
});
}
}
this.eventDispatcher.on('widthPlugin', this.updateStickyHeaderWidth.bind(this));
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.eventDispatcher.on(_pluginKey.pluginKey.key, this.onTablePluginState.bind(this));
this.listening = true;
// Ignored via go/ees005
// eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners
this.dom.addEventListener('wheel', this.headerRowMouseScroll.bind(this), {
passive: true
});
// Ignored via go/ees005
// eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners
this.dom.addEventListener('touchmove', this.headerRowMouseScroll.bind(this), {
passive: true
});
}
}, {
key: "unsubscribe",
value: function unsubscribe() {
if (!this.listening) {
return;
}
if (this.intersectionObserver) {
this.intersectionObserver.disconnect();
// ED-16211 Once intersection observer is disconnected, we need to remove the isObserved from the sentinels
// Otherwise when newer intersection observer is created it will not observe because it thinks its already being observed
[this.sentinels.top, this.sentinels.bottom].forEach(function (el) {
if (el) {
delete el.dataset.isObserved;
}
});
}
if (this.resizeObserver) {
this.resizeObserver.disconnect();
}
if (this.scrollListener && this.editorScrollableElement) {
// eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners
this.editorScrollableElement.removeEventListener('scroll', this.scrollListener);
this.scrollListener = undefined;
}
this.eventDispatcher.off('widthPlugin', this.updateStickyHeaderWidth);
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.eventDispatcher.off(_pluginKey.pluginKey.key, this.onTablePluginState);
this.listening = false;
// Ignored via go/ees005
// eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners
this.dom.removeEventListener('wheel', this.headerRowMouseScroll);
// Ignored via go/ees005
// eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners
this.dom.removeEventListener('touchmove', this.headerRowMouseScroll);
}
}, {
key: "initOverflowObserver",
value: function initOverflowObserver() {
var _this4 = this;
var tableWrapper = this.dom.closest(".".concat(_types.TableCssClassName.TABLE_NODE_WRAPPER));
if (!tableWrapper) {
return;
}
var options = {
root: tableWrapper,
threshold: 1
};
this.overflowObserver = new IntersectionObserver(function (entries, observer) {
entries.forEach(function (entry) {
if (!(observer.root instanceof HTMLElement)) {
return;
}
// Only apply classes if page has scrolled since load
if (!_this4.hasScrolledSinceLoad && (0, _platformFeatureFlags.fg)('platform_editor_table_sticky_header_patch_5')) {
_this4.overflowObserverEntries = entries;
return;
}
if (entry.isIntersecting) {
if ((0, _platformFeatureFlags.fg)('platform_editor_table_sticky_header_patch_4')) {
observer.root.classList.add(_types.TableCssClassName.TABLE_NODE_WRAPPER_NO_OVERFLOW);
if (!_this4.disableNativeSticky) {
_this4.dom.classList.add(_types.TableCssClassName.NATIVE_STICKY);
}
} else {
observer.root.classList.add(_types.TableCssClassName.TABLE_NODE_WRAPPER_NO_OVERFLOW);
_this4.dom.classList.add(_types.TableCssClassName.NATIVE_STICKY);
}
_this4.isNativeSticky = true;
} else {
if ((0, _platformFeatureFlags.fg)('platform_editor_table_sticky_header_patch_4')) {
observer.root.classList.remove(_types.TableCssClassName.TABLE_NODE_WRAPPER_NO_OVERFLOW);
_this4.dom.classList.remove(_types.TableCssClassName.NATIVE_STICKY);
} else {
observer.root.classList.remove(_types.TableCssClassName.TABLE_NODE_WRAPPER_NO_OVERFLOW);
_this4.dom.classList.remove(_types.TableCssClassName.NATIVE_STICKY);
}
_this4.isNativeSticky = false;
}
_this4.refreshLegacyStickyState();
if ((0, _expValEquals.expValEquals)('platform_editor_table_sticky_header_patch_9', 'isEnabled', true)) {
var _this4$api;
(_this4$api = _this4.api) === null || _this4$api === void 0 || (_this4$api = _this4$api.analytics) === null || _this4$api === void 0 || (_this4$api = _this4$api.actions) === null || _this4$api === void 0 || _this4$api.fireAnalyticsEvent({
action: _analytics.TABLE_ACTION.STICKY_HEADER_METHOD_TOGGLED,
actionSubject: _analytics.ACTION_SUBJECT.TABLE,
actionSubjectId: _analytics.ACTION_SUBJECT_ID.TABLE_STICKY_HEADER,
eventType: _analytics.EVENT_TYPE.UI,
attributes: {
nativeStickyHeaderEnabled: entry.isIntersecting
}
});
}
});
}, options);
}
/**
* This observer is used to track the 'stuck' state of the header row.
* This roughly mimics `(at)container scroll-state(stuck: top)` in CSS,
* but with full browser support.
*/
}, {
key: "initStickyStateObserver",
value: function initStickyStateObserver() {
var _this5 = this;
if (!this.editorScrollableElement || !(this.editorScrollableElement instanceof Element)) {
return;
}
var options = {
root: this.editorScrollableElement,
rootMargin: "-".concat(_styles.tableMarginTop + 1, "px 0px 0px 0px"),
threshold: 1
};
this.stickyStateObserver = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
var _entry$rootBounds;
var tableContainer = _this5.dom.closest(".".concat(_types.TableCssClassName.TABLE_CONTAINER));
if (entry.intersectionRect.top === ((_entry$rootBounds = entry.rootBounds) === null || _entry$rootBounds === void 0 ? void 0 : _entry$rootBounds.top) && (!_this5.disableNativeSticky || !(0, _platformFeatureFlags.fg)('platform_editor_table_sticky_header_patch_4'))) {
_this5.dom.classList.add(_types.TableCssClassName.NATIVE_STICKY_ACTIVE);
if (tableContainer && tableContainer instanceof HTMLElement) {
tableContainer.dataset.tableHeaderIsStuck = 'true';
}
} else {
_this5.dom.classList.remove(_types.TableCssClassName.NATIVE_STICKY_ACTIVE);
if (tableContainer && tableContainer instanceof HTMLElement) {
if ((0, _platformFeatureFlags.fg)('platform_editor_table_sticky_header_patch_3')) {
delete tableContainer.dataset.tableHeaderIsStuck;
} else {
tableContainer.dataset.tableHeaderIsStuck = 'false';
}
}
}
});
}, options);
}
// initialize intersection observer to track if table is within scroll area
}, {
key: "initObservers",
value: function initObservers() {
var _this6 = this;
if (!this.dom || this.dom.dataset.isObserved) {
return;
}
this.dom.dataset.isObserved = 'true';
this.createIntersectionObserver();
this.createResizeObserver();
if (!this.intersectionObserver || !this.resizeObserver) {
return;
}
if (this.isHeaderRow && !this.isInNestedTable) {
var _this$stickyStateObse;
if ((0, _expValEquals.expValEquals)('platform_editor_native_anchor_with_dnd', 'isEnabled', true)) {
var _this$dom$getAttribut;
this.dom.style.setProperty('anchor-name', (_this$dom$getAttribut = this.dom.getAttribute('data-node-anchor')) !== null && _this$dom$getAttribut !== void 0 ? _this$dom$getAttribut : '');
}
this.initOverflowObserver();
if ((0, _platformFeatureFlags.fg)('platform_editor_table_sticky_header_patch_4')) {
this.initNodeVisibilityObserver();
}
var closestTable = this.dom.closest('table');
if (closestTable) {
var _this$overflowObserve;
(_this$overflowObserve = this.overflowObserver) === null || _this$overflowObserve === void 0 || _this$overflowObserve.observe(closestTable);
if ((0, _platformFeatureFlags.fg)('platform_editor_table_sticky_header_patch_4')) {
var _this$nodeVisibilityO2;
(_this$nodeVisibilityO2 = this.nodeVisibilityObserver) === null || _this$nodeVisibilityO2 === void 0 || _this$nodeVisibilityO2.observe(closestTable);
}
}
this.initStickyStateObserver();
(_this$stickyStateObse = this.stickyStateObserver) === null || _this$stickyStateObse === void 0 || _this$stickyStateObse.observe(this.dom);
}
this.resizeObserver.observe(this.dom);
if (this.editorScrollableElement) {
// Ignored via go/ees005
// eslint-disable-next-line @atlaskit/editor/no-as-casting
this.resizeObserver.observe(this.editorScrollableElement);
}
window.requestAnimationFrame(function () {
var getTableContainer = function getTableContainer() {
var _getTree;
return (_getTree = (0, _dom2.getTree)(_this6.dom)) === null || _getTree === void 0 ? void 0 : _getTree.wrapper.closest(".".concat(_types.TableCssClassName.NODEVIEW_WRAPPER));
};
// we expect tree to be defined after animation frame
var tableContainer = getTableContainer();
if (tableContainer) {
var getSentinelTop = function getSentinelTop() {
return tableContainer &&
// Ignored via go/ees005
// eslint-disable-next-line @atlaskit/editor/no-as-casting
tableContainer.getElementsByClassName(_types.TableCssClassName.TABLE_STICKY_SENTINEL_TOP).item(0);
};
var getSentinelBottom = function getSentinelBottom() {
// Multiple bottom sentinels may be found if there are nested tables.
// We need to make sure we get the last one which will belong to the parent table.
var bottomSentinels = tableContainer && tableContainer.getElementsByClassName(_types.TableCssClassName.TABLE_STICKY_SENTINEL_BOTTOM);
return (
// eslint-disable-next-line @atlaskit/editor/no-as-casting
bottomSentinels && bottomSentinels.item(bottomSentinels.length - 1)
);
};
var sentinelsInDom = function sentinelsInDom() {
return getSentinelTop() !== null && getSentinelBottom() !== null;
};
var observeStickySentinels = function observeStickySentinels() {
_this6.sentinels.top = getSentinelTop();
_this6.sentinels.bottom = getSentinelBottom();
[_this6.sentinels.top, _this6.sentinels.bottom].forEach(function (el) {
// skip if already observed for another row on this table
if (el && !el.dataset.isObserved) {
el.dataset.isObserved = 'true';
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
_this6.intersectionObserver.observe(el);
}
});
};
var isInitialProsemirrorToDomRender = tableContainer.hasAttribute('data-prosemirror-initial-toDOM-render');
// Sentinels may be in the DOM but they're part of the prosemirror placeholder structure which is replaced with the fully rendered React node.
if (sentinelsInDom() && !isInitialProsemirrorToDomRender) {
// great - DOM ready, observe as normal
observeStickySentinels();
} else {
// concurrent loading issue - here TableRow is too eager trying to
// observe sentinels before they are in the DOM, use MutationObserver
// to wait for sentinels to be added to the parent Table node DOM
// then attach the IntersectionObserver
_this6.tableContainerObserver = new MutationObserver(function () {
// Check if the tableContainer is still connected to the DOM. It can become disconnected when the placholder
// prosemirror node is replaced with the fully rendered React node (see _handleTableRef).
if (!tableContainer || !tableContainer.isConnected) {
tableContainer = getTableContainer();
}
if (sentinelsInDom()) {
var _this6$tableContainer;
observeStickySentinels();
(_this6$tableContainer = _this6.tableContainerObserver) === null || _this6$tableContainer === void 0 || _this6$tableContainer.disconnect();
}
});
var mutatingNode = tableContainer;
if (mutatingNode && _this6.tableContainerObserver) {
_this6.tableContainerObserver.observe(mutatingNode, {
subtree: true,
childList: true
});
}
}
}
});
}
// initialise intersection observer to track whether table is in scroll area
}, {
key: "initNodeVisibilityObserver",
value: function initNodeVisibilityObserver() {
var _this7 = this;
this.nodeVisibilityObserver = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
if (!_this7.isNativeSticky) {
return;
}
if (entry.intersectionRatio !== 0 && entry.intersectionRatio !== 1) {
return;
}
if (_this7.disableNativeSticky === true) {
_this7.dom.classList.remove(_types.TableCssClassName.NATIVE_STICKY);
}
if (_this7.disableNativeSticky === false) {
_this7.dom.classList.add(_types.TableCssClassName.NATIVE_STICKY);
}
});
}, {
threshold: [0, 0.05, 0.95, 1]
});
}
// updating bottom sentinel position if sticky header height changes
// to allocate for new header height
}, {
key: "createResizeObserver",
value: function createResizeObserver() {
var _this8 = this;
this.resizeObserver = new ResizeObserver(function (entries) {
var tree = (0, _dom2.getTree)(_this8.dom);
if (!tree) {
return;
}
var table = tree.table;
entries.forEach(function (entry) {
var _this8$editorScrollab;
// On resize of the parent scroll element we need to adjust the width
// of the sticky header
// Ignored via go/ees005
// eslint-disable-next-line @atlaskit/editor/no-as-casting
if (entry.target.className === ((_this8$editorScrollab = _this8.editorScrollableElement) === null || _this8$editorScrollab === void 0 ? void 0 : _this8$editorScrollab.className)) {
_this8.updateStickyHeaderWidth();
} else {
var newHeight = entry.contentRect ? entry.contentRect.height :
// Ignored via go/ees005
// eslint-disable-next-line @atlaskit/editor/no-as-casting
entry.target.offsetHeight;
if (_this8.sentinels.bottom &&
// When the table header is sticky, it would be taller by a 1px (border-bottom),
// So we adding this check to allow a 1px difference.
Math.abs(newHeight - (_this8.stickyRowHeight || 0)) > _consts.stickyHeaderBorderBottomWidth) {
_this8.stickyRowHeight = newHeight;
_this8.sentinels.bottom.style.bottom = "".concat(_consts.tableScrollbarOffset + _consts.stickyRowOffsetTop + newHeight, "px");
(0, _dom.updateStickyMargins)(table);
}
if ((0, _platformFeatureFlags.fg)('platform_editor_table_sticky_header_patch_4')) {
var _this8$editorContentA;
var viewportHeight = (_this8$editorContentA = _this8.editorContentAreaHeight) !== null && _this8$editorContentA !== void 0 ? _this8$editorContentA : _editorContentAreaHeight.INITIAL_STATIC_VIEWPORT_HEIGHT;
_this8.toggleDisableNativeSticky(newHeight, viewportHeight);
}
}
});
});
}
}, {
key: "createIntersectionObserver",
value: function createIntersectionObserver() {
var _this9 = this;
this.intersectionObserver = new IntersectionObserver(function (entries, _) {
var _this9$editorScrollab, _this9$editorScrollab2;
// IMPORTANT: please try and avoid using entry.rootBounds it's terribly inconsistent. For example; sometimes it may return
// 0 height. In safari it will multiply all values by the window scale factor, however chrome & firfox won't.
// This is why i just get the scroll view bounding rect here and use it, and fallback to the entry.rootBounds if needed.
var rootBounds = (_this9$editorScrollab = _this9.editorScrollableElement) === null || _this9$editorScrollab === void 0 || (_this9$editorScrollab2 = _this9$editorScrollab.getBoundingClientRect) === null || _this9$editorScrollab2 === void 0 ? void 0 : _this9$editorScrollab2.call(_this9$editorScrollab);
entries.forEach(function (entry) {
var target = entry.target,
isIntersecting = entry.isIntersecting,
boundingClientRect = entry.boundingClientRect;
// This observer only every looks at the top/bottom sentinels, we can assume if it's not one then it's the other.
var targetKey = target.classList.contains(_types.TableCssClassName.TABLE_STICKY_SENTINEL_TOP) ? 'top' : 'bottom';
// Cache the latest sentinel information
_this9.sentinelData[targetKey] = {
isIntersecting: isIntersecting,
boundingClientRect: boundingClientRect,
rootBounds: rootBounds !== null && rootBounds !== void 0 ? rootBounds : entry.rootBounds
};
// Keep the other sentinels rootBounds in sync with this latest one
_this9.sentinelData[targetKey === 'top' ? 'bottom' : targetKey].rootBounds = rootBounds !== null && rootBounds !== void 0 ? rootBounds : entry.rootBounds;
});
_this9.refreshLegacyStickyState();
}, {
threshold: 0,
root: this.editorScrollableElement
});
}
}, {
key: "refreshLegacyStickyState",
value: function refreshLegacyStickyState() {
var tree = (0, _dom2.getTree)(this.dom);
if (!tree) {
return;
}
var table = tree.table;
if (this.isNativeSticky) {
this.makeRowHeaderNotLegacySticky(table);
return;
}
var shouldStick = this.shouldSticky();
if (this.isLegacySticky !== shouldStick) {
if (shouldStick) {
var _this$sentinelData$to;
// The rootRect is kept in sync across sentinels so it doesn't matter which one we use.
var rootRect = (_this$sentinelData$to = this.sentinelData.top.rootBounds) !== null && _this$sentinelData$to !== void 0 ? _this$sentinelData$to : this.sentinelData.bottom.rootBounds;
this.makeHeaderRowLegacySticky(tree, rootRect === null || rootRect === void 0 ? void 0 : rootRect.top);
} else {
this.makeRowHeaderNotLegacySticky(table);
}
}
}
}, {
key: "shouldSticky",
value: function shouldSticky() {
if (
// is Safari
navigator.userAgent.includes('AppleWebKit') && !navigator.userAgent.includes('Chrome')) {
var pos = this.getPos();
if (typeof pos === 'number') {
var $tableRowPos = this.view.state.doc.resolve(pos);
// layout -> layout column -> table -> table row
if ($tableRowPos.depth >= 3) {
var _findParentNodeCloses;
var isInsideLayout = (_findParentNodeCloses = (0, _utils.findParentNodeClosestToPos)($tableRowPos, function (node) {
return node.type.name === 'layoutColumn';
})) === null || _findParentNodeCloses === void 0 ? void 0 : _findParentNodeCloses.node;
if (isInsideLayout) {
return false;
}
}
}
}
return this.isHeaderSticky();
}
}, {
key: "isHeaderSticky",
value: function isHeaderSticky() {
var _sentinelTop$rootBoun;
/*
# Overview
I'm going to list all the view states associated with the sentinels and when they should trigger sticky headers.
The format of the states are; {top|bottom}:{in|above|below}
ie sentinel:view-position -- both "above" and "below" are equal to out of the viewport
For example; "top:in" means top sentinel is within the viewport. "bottom:above" means the bottom sentinel is
above and out of the viewport
This will hopefully simplify things and make it easier to determine when sticky should/shouldn't be triggered.
# States
top:in / bottom:in - NOT sticky
top:in / bottom:above - NOT sticky - NOTE: This is an inversion clause
top:in / bottom:below - NOT sticky
top:above / bottom:in - STICKY
top:above / bottom:above - NOT sticky
top:above / bottom:below - STICKY
top:below / bottom:in - NOT sticky - NOTE: This is an inversion clause
top:below / bottom:above - NOT sticky - NOTE: This is an inversion clause
top:below / bottom:below - NOT sticky
# Summary
The only time the header should be sticky is when the top sentinel is above the view and the bottom sentinel
is in or below it.
*/
var _this$sentinelData = this.sentinelData,
sentinelTop = _this$sentinelData.top,
sentinelBottom = _this$sentinelData.bottom;
// The rootRect is kept in sync across sentinels so it doesn't matter which one we use.
var rootBounds = (_sentinelTop$rootBoun = sentinelTop.rootBounds) !== null && _sentinelTop$rootBoun !== void 0 ? _sentinelTop$rootBoun : sentinelBottom.rootBounds;
if (!rootBounds || !sentinelTop.boundingClientRect || !sentinelBottom.boundingClientRect) {
return false;
}
// Normalize the sentinels to y points
// Since the sentinels are actually rects 1px high we want make sure we normalise the inner most values closest to the table.
var sentinelTopY = sentinelTop.boundingClientRect.bottom;
var sentinelBottomY = sentinelBottom.boundingClientRect.top;
// If header row height is more than 50% of viewport height don't do this
var isRowHeaderTooLarge = this.stickyRowHeight && this.stickyRowHeight > window.innerHeight * 0.5;
var isTopSentinelAboveScrollArea = !sentinelTop.isIntersecting && sentinelTopY <= rootBounds.top;
var isBottomSentinelInOrBelowScrollArea = sentinelBottom.isIntersecting || sentinelBottomY > rootBounds.bottom;
// This makes sure that the top sentinel is actually above the bottom sentinel, and that they havn't inverted.
var isTopSentinelAboveBottomSentinel = sentinelTopY < sentinelBottomY;
return isTopSentinelAboveScrollArea && isBottomSentinelInOrBelowScrollArea && isTopSentinelAboveBottomSentinel && !isRowHeaderTooLarge;
}
/* receive external events */
}, {
key: "onTablePluginState",
value: function onTablePluginState(state) {
var tableRef = state.tableRef;
var tree = (0, _dom2.getTree)(this.dom);
if (!tree) {
return;
}
// when header rows are toggled off - mark sentinels as unobserved
if (!state.isHeaderRowEnabled) {
[this.sentinels.top, this.sentinels.bottom].forEach(function (el) {
if (el) {
delete el.dataset.isObserved;
}
});
}
var isCurrentTableSelected = tableRef === tree.table;
// If current table selected and header row is toggled off, turn off sticky header
if (isCurrentTableSelected && !state.isHeaderRowEnabled && tree) {
this.makeRowHeaderNotLegacySticky(tree.table);
}
this.focused = isCurrentTableSelected;
var wrapper = tree.wrapper;
var tableContainer = wrapper.parentElement;
var tableContentWrapper = tableContainer === null || tableContainer === void 0 ? void 0 : tableContainer.parentElement;
var parentContainer = tableContentWrapper && tableContentWrapper.parentElement;
var isTableInsideLayout = parentContainer && parentContainer.getAttribute('data-layout-content');
if (tableContentWrapper) {
if (isCurrentTableSelected) {
this.colControlsOffset = _consts.tableControlsSpacing;
// move table a little out of the way
// to provide spacing for table controls
if (isTableInsideLayout) {
tableContentWrapper.style.paddingLeft = '11px';
}
} else {
this.colControlsOffset = 0;
if (isTableInsideLayout) {
tableContentWrapper.style.removeProperty('padding-left');
}
}
}
// run after table style changes have been committed
setTimeout(function () {
(0, _dom.syncStickyRowToTable)(tree.table);
}, 0);
}
}, {
key: "updateStickyHeaderWidth",
value: function updateStickyHeaderWidth() {
// table width might have changed, sync that back to sticky row
var tree = (0, _dom2.getTree)(this.dom);
if (!tree) {
return;
}
(0, _dom.syncStickyRowToTable)(tree.table);
}
/**
* Manually refire the intersection observers.
* Useful when the header may have detached from the table.
*/
}, {
key: "refireIntersectionObservers",
value: function refireIntersectionObservers() {
var _this0 = this;
if (this.isLegacySticky) {
[this.sentinels.top, this.sentinels.bottom].forEach(function (el) {
if (el && _this0.intersectionObserver) {
_this0.intersectionObserver.unobserve(el);
_this0.intersectionObserver.observe(el);
}
});
}
}
}, {
key: "makeHeaderRowLegacySticky",
value: function makeHeaderRowLegacySticky(tree, scrollTop) {
var _tbody$firstChild,
_this1 = this;
// If header row height is more than 50% of viewport height don't do this
if (this.isLegacySticky || this.stickyRowHeight && this.stickyRowHeight > window.innerHeight / 2 || this.isInNestedTable) {
return;
}
var table = tree.table,
wrapper = tree.wrapper;
// TODO: ED-16035 - Make sure sticky header is only applied to first row
var tbody = this.dom.parentElement;
var isFirstHeader = tbody === null || tbody === void 0 || (_tbody$firstChild = tbody.firstChild) === null || _tbody$firstChild === void 0 ? void 0 : _tbody$firstChild.isEqualNode(this.dom);
if (!isFirstHeader) {
return;
}
var currentTableTop = this.getCurrentTableTop(tree);
if (!scrollTop) {
scrollTop = (0, _dom2.getTop)(this.editorScrollableElement);
}
var domTop = currentTableTop > 0 ? scrollTop : scrollTop + currentTableTop;
if (!this.isLegacySticky) {
var _this$editorScrollabl;
(0, _dom.syncStickyRowToTable)(table);
this.dom.classList.add('sticky');
table.classList.add(_types.TableCssClassName.TABLE_STICKY);
this.isLegacySticky = true;
/**
* The logic below is not desirable, but acts as a fail safe for scenarios where the sticky header
* detaches from the table. This typically happens during a fast scroll by the user which causes
* the intersection observer logic to not fire as expected.
*/
// Ignored via go/ees005
// eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners
(_this$editorScrollabl = this.editorScrollableElement) === null || _this$editorScrollabl === void 0 || _this$editorScrollabl.addEventListener('scrollend', this.refireIntersectionObservers, {
passive: true,
once: true
});
var fastScrollThresholdMs = 500;
setTimeout(function () {
_this1.refireIntersectionObservers();
}, fastScrollThresholdMs);
}
this.dom.style.top = "0px";
(0, _dom.updateStickyMargins)(table);
this.dom.scrollLeft = wrapper.scrollLeft;
this.emitOn(domTop, this.colControlsOffset);
}
}, {
key: "makeRowHeaderNotLegacySticky",
value: function makeRowHeaderNotLegacySticky(table) {
var isEditorDestroyed = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
if (!this.isLegacySticky || !table || !this.dom) {
return;
}
this.dom.style.removeProperty('width');
this.dom.classList.remove('sticky');
table.classList.remove(_types.TableCssClassName.TABLE_STICKY);
this.isLegacySticky = false;
this.dom.style.top = '';
table.style.removeProperty('margin-top');
this.emitOff(isEditorDestroyed);
}
}, {
key: "getWrapperoffset",
value: function getWrapperoffset() {
var inverse = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
var focusValue = inverse ? !this.focused : this.focused;
return focusValue ? 0 : _consts.tableControlsSpacing;
}
}, {
key: "getWrapperRefTop",
value: function getWrapperRefTop(wrapper) {
return Math.round((0, _dom2.getTop)(wrapper)) + this.getWrapperoffset();
}
}, {
key: "getScrolledTableTop",
value: function getScrolledTableTop(wrapper) {
return this.getWrapperRefTop(wrapper) - this.topPosEditorElement;
}
}, {
key: "getCurrentTableTop",
value: function getCurrentTableTop(tree) {
return this.getScrolledTableTop(tree.wrapper) + tree.table.clientHeight;
}
/* emit external events */
}, {
key: "emitOn",
value: function emitOn(top, padding) {
if (top === this.top && padding === this.padding) {
return;
}
this.top = top;
this.padding = padding;
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
var pos = this.getPos();
if (Number.isFinite(pos)) {
(0, _commands.updateStickyState)({
pos: pos,
top: top,
sticky: true,
padding: padding
})(this.view.state, this.view.dispatch, this.view);
}
}
}, {
key: "emitOff",
value: function emitOff(isEditorDestroyed) {
if (this.top === 0 && this.padding === 0) {
return;
}
this.top = 0;
this.padding = 0;
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
var pos = this.getPos();
if (!isEditorDestroyed && Number.isFinite(pos)) {
(0, _commands.updateStickyState)({
pos: pos,
sticky: false,
top: this.top,
padding: this.padding
})(this.view.state, this.view.dispatch, this.view);
}
}
}]);
}(_TableNodeViewBase.default);