@atlaskit/editor-plugin-table
Version:
Table plugin for the @atlaskit/editor
749 lines (717 loc) • 33.6 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 _nesting = require("@atlaskit/editor-common/nesting");
var _nodeVisibility = require("@atlaskit/editor-common/node-visibility");
var _ui = require("@atlaskit/editor-common/ui");
var _utils = require("@atlaskit/editor-prosemirror/utils");
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 TableRow = exports.default = /*#__PURE__*/function (_TableNodeView) {
function TableRow(node, view, getPos, eventDispatcher, api) {
var _api$limitedMode;
var _this;
(0, _classCallCheck2.default)(this, TableRow);
_this = _callSuper(this, TableRow, [node, view, getPos, eventDispatcher]);
(0, _defineProperty2.default)(_this, "cleanup", function () {
if (_this.isStickyHeaderEnabled) {
_this.unsubscribe();
_this.nodeVisibilityObserverCleanupFn && _this.nodeVisibilityObserverCleanupFn();
var tree = (0, _dom2.getTree)(_this.dom);
if (tree) {
_this.makeRowHeaderNotSticky(tree.table, true);
}
_this.emitOff(false);
}
if (_this.tableContainerObserver) {
_this.tableContainerObserver.disconnect();
}
});
(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);
/**
* 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.isSticky) {
_this.dom.classList.add('no-pointer-events');
_this.headerRowMouseScrollEnd();
}
}, HEADER_ROW_SCROLL_THROTTLE_TIMEOUT));
_this.isHeaderRow = (0, _nodes.supportedHeaderRow)(node);
_this.isSticky = false;
var _getPluginState = (0, _pluginFactory.getPluginState)(view.state),
pluginConfig = _getPluginState.pluginConfig;
_this.isStickyHeaderEnabled = !!pluginConfig.stickyHeaders;
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();
}
});
}
return _this;
}
(0, _inherits2.default)(TableRow, _TableNodeView);
return (0, _createClass2.default)(TableRow, [{
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) {
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) {
this.unsubscribe();
this.nodeVisibilityObserverCleanupFn && this.nodeVisibilityObserverCleanupFn();
var tree = (0, _dom2.getTree)(this.dom);
if (tree) {
this.makeRowHeaderNotSticky(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() {
// 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);
}
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();
}
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);
}
// initialize intersection observer to track if table is within scroll area
}, {
key: "initObservers",
value: function initObservers() {
var _this2 = this;
if (!this.dom || this.dom.dataset.isObserved) {
return;
}
this.dom.dataset.isObserved = 'true';
this.createIntersectionObserver();
this.createResizeObserver();
if (!this.intersectionObserver || !this.resizeObserver) {
return;
}
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)(_this2.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() {
_this2.sentinels.top = getSentinelTop();
_this2.sentinels.bottom = getSentinelBottom();
[_this2.sentinels.top, _this2.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
_this2.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
_this2.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 _this2$tableContainer;
observeStickySentinels();
(_this2$tableContainer = _this2.tableContainerObserver) === null || _this2$tableContainer === void 0 || _this2$tableContainer.disconnect();
}
});
var mutatingNode = tableContainer;
if (mutatingNode && _this2.tableContainerObserver) {
_this2.tableContainerObserver.observe(mutatingNode, {
subtree: true,
childList: true
});
}
}
}
});
}
// updating bottom sentinel position if sticky header height changes
// to allocate for new header height
}, {
key: "createResizeObserver",
value: function createResizeObserver() {
var _this3 = this;
this.resizeObserver = new ResizeObserver(function (entries) {
var tree = (0, _dom2.getTree)(_this3.dom);
if (!tree) {
return;
}
var table = tree.table;
entries.forEach(function (entry) {
var _this3$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 === ((_this3$editorScrollab = _this3.editorScrollableElement) === null || _this3$editorScrollab === void 0 ? void 0 : _this3$editorScrollab.className)) {
_this3.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 (_this3.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 - (_this3.stickyRowHeight || 0)) > _consts.stickyHeaderBorderBottomWidth) {
_this3.stickyRowHeight = newHeight;
_this3.sentinels.bottom.style.bottom = "".concat(_consts.tableScrollbarOffset + _consts.stickyRowOffsetTop + newHeight, "px");
(0, _dom.updateStickyMargins)(table);
}
}
});
});
}
}, {
key: "createIntersectionObserver",
value: function createIntersectionObserver() {
var _this4 = this;
this.intersectionObserver = new IntersectionObserver(function (entries, _) {
var _this4$editorScrollab, _this4$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 = (_this4$editorScrollab = _this4.editorScrollableElement) === null || _this4$editorScrollab === void 0 || (_this4$editorScrollab2 = _this4$editorScrollab.getBoundingClientRect) === null || _this4$editorScrollab2 === void 0 ? void 0 : _this4$editorScrollab2.call(_this4$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
_this4.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
_this4.sentinelData[targetKey === 'top' ? 'bottom' : targetKey].rootBounds = rootBounds !== null && rootBounds !== void 0 ? rootBounds : entry.rootBounds;
});
_this4.refreshStickyState();
}, {
threshold: 0,
root: this.editorScrollableElement
});
}
}, {
key: "refreshStickyState",
value: function refreshStickyState() {
var tree = (0, _dom2.getTree)(this.dom);
if (!tree) {
return;
}
var table = tree.table;
var shouldStick = this.shouldSticky();
if (this.isSticky !== 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.makeHeaderRowSticky(tree, rootRect === null || rootRect === void 0 ? void 0 : rootRect.top);
} else {
this.makeRowHeaderNotSticky(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.makeRowHeaderNotSticky(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 _this5 = this;
if (this.isSticky) {
[this.sentinels.top, this.sentinels.bottom].forEach(function (el) {
if (el && _this5.intersectionObserver) {
_this5.intersectionObserver.unobserve(el);
_this5.intersectionObserver.observe(el);
}
});
}
}
}, {
key: "makeHeaderRowSticky",
value: function makeHeaderRowSticky(tree, scrollTop) {
var _tbody$firstChild,
_this6 = this;
// If header row height is more than 50% of viewport height don't do this
if (this.isSticky || 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.isSticky) {
var _this$editorScrollabl;
(0, _dom.syncStickyRowToTable)(table);
this.dom.classList.add('sticky');
table.classList.add(_types.TableCssClassName.TABLE_STICKY);
this.isSticky = 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 () {
_this6.refireIntersectionObservers();
}, fastScrollThresholdMs);
}
this.dom.style.top = "0px";
(0, _dom.updateStickyMargins)(table);
this.dom.scrollLeft = wrapper.scrollLeft;
this.emitOn(domTop, this.colControlsOffset);
}
}, {
key: "makeRowHeaderNotSticky",
value: function makeRowHeaderNotSticky(table) {
var isEditorDestroyed = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
if (!this.isSticky || !table || !this.dom) {
return;
}
this.dom.style.removeProperty('width');
this.dom.classList.remove('sticky');
table.classList.remove(_types.TableCssClassName.TABLE_STICKY);
this.isSticky = 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);