UNPKG

@atlaskit/editor-plugin-table

Version:

Table plugin for the @atlaskit/editor

749 lines (717 loc) 33.6 kB
"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);