UNPKG

@atlaskit/editor-plugin-table

Version:

Table plugin for the @atlaskit/editor

688 lines (654 loc) 28.8 kB
import _defineProperty from "@babel/runtime/helpers/defineProperty"; import debounce from 'lodash/debounce'; import throttle from 'lodash/throttle'; import { getParentOfTypeCount } from '@atlaskit/editor-common/nesting'; import { nodeVisibilityManager } from '@atlaskit/editor-common/node-visibility'; import { findOverflowScrollParent } from '@atlaskit/editor-common/ui'; import { findParentNodeClosestToPos } from '@atlaskit/editor-prosemirror/utils'; import { getPluginState } from '../pm-plugins/plugin-factory'; import { pluginKey as tablePluginKey } from '../pm-plugins/plugin-key'; import { updateStickyState } from '../pm-plugins/sticky-headers/commands'; import { syncStickyRowToTable, updateStickyMargins as updateTableMargin } from '../pm-plugins/table-resizing/utils/dom'; import { getTop, getTree } from '../pm-plugins/utils/dom'; import { supportedHeaderRow } from '../pm-plugins/utils/nodes'; import { TableCssClassName as ClassName, TableCssClassName } from '../types'; import { stickyHeaderBorderBottomWidth, stickyRowOffsetTop, tableControlsSpacing, tableScrollbarOffset } from '../ui/consts'; import TableNodeView from './TableNodeViewBase'; // limit scroll event calls const 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. const HEADER_ROW_SCROLL_RESET_DEBOUNCE_TIMEOUT = 400; export default class TableRow extends TableNodeView { constructor(node, view, getPos, eventDispatcher, api) { var _api$limitedMode, _api$limitedMode$shar, _api$limitedMode$shar2; super(node, view, getPos, eventDispatcher); _defineProperty(this, "cleanup", () => { if (this.isStickyHeaderEnabled) { this.unsubscribe(); this.nodeVisibilityObserverCleanupFn && this.nodeVisibilityObserverCleanupFn(); const tree = getTree(this.dom); if (tree) { this.makeRowHeaderNotSticky(tree.table, true); } this.emitOff(false); } if (this.tableContainerObserver) { this.tableContainerObserver.disconnect(); } }); _defineProperty(this, "colControlsOffset", 0); _defineProperty(this, "focused", false); _defineProperty(this, "topPosEditorElement", 0); _defineProperty(this, "sentinels", {}); _defineProperty(this, "sentinelData", { top: { isIntersecting: false, boundingClientRect: null, rootBounds: null }, bottom: { isIntersecting: false, boundingClientRect: null, rootBounds: null } }); _defineProperty(this, "listening", false); _defineProperty(this, "padding", 0); _defineProperty(this, "top", 0); /** * Methods */ _defineProperty(this, "headerRowMouseScrollEnd", debounce(() => { 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 _defineProperty(this, "headerRowMouseScroll", throttle(() => { if (this.isSticky) { this.dom.classList.add('no-pointer-events'); this.headerRowMouseScrollEnd(); } }, HEADER_ROW_SCROLL_THROTTLE_TIMEOUT)); this.isHeaderRow = supportedHeaderRow(node); this.isSticky = false; const { pluginConfig } = getPluginState(view.state); this.isStickyHeaderEnabled = !!pluginConfig.stickyHeaders; if (api !== null && api !== void 0 && (_api$limitedMode = api.limitedMode) !== null && _api$limitedMode !== void 0 && (_api$limitedMode$shar = _api$limitedMode.sharedState.currentState()) !== null && _api$limitedMode$shar !== void 0 && (_api$limitedMode$shar2 = _api$limitedMode$shar.limitedModePluginKey.getState(view.state)) !== null && _api$limitedMode$shar2 !== void 0 && _api$limitedMode$shar2.documentSizeBreachesThreshold) { this.isStickyHeaderEnabled = false; // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners document.addEventListener('limited-mode-activated', this.cleanup); } const pos = this.getPos(); this.isInNestedTable = false; if (pos) { this.isInNestedTable = getParentOfTypeCount(view.state.schema.nodes.table)(view.state.doc.resolve(pos)) > 1; } if (this.isHeaderRow) { this.dom.setAttribute('data-vc-nvs', 'true'); const { observe } = nodeVisibilityManager(view.dom); this.nodeVisibilityObserverCleanupFn = observe({ element: this.contentDOM, onFirstVisible: () => { this.subscribeWhenRowVisible(); } }); } } subscribeWhenRowVisible() { if (this.listening) { return; } this.dom.setAttribute('data-header-row', 'true'); if (this.isStickyHeaderEnabled) { this.subscribe(); } } /** * Variables */ /** * Methods: Nodeview Lifecycle */ // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-explicit-any update(node, ..._args) { // 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 const newNodeIsHeaderRow = 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) { const tbody = this.dom.parentElement; const table = tbody && tbody.parentElement; syncStickyRowToTable(table); } return true; } destroy() { if (this.isStickyHeaderEnabled) { this.unsubscribe(); this.nodeVisibilityObserverCleanupFn && this.nodeVisibilityObserverCleanupFn(); const tree = 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(); } } 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 * */ const 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. */ const isNodeInsertion = mutationRecord.type === 'childList' && mutationRecord.target.nodeName === 'TR' && mutationRecord.addedNodes.length; if (isTableSelection || isNodeInsertion) { return false; } return true; } subscribe() { // Ignored via go/ees005 // eslint-disable-next-line @atlaskit/editor/no-as-casting this.editorScrollableElement = findOverflowScrollParent(this.view.dom) || window; if (this.editorScrollableElement) { this.initObservers(); this.topPosEditorElement = 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(tablePluginKey.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 }); } 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(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(tablePluginKey.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 initObservers() { 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(() => { const getTableContainer = () => { var _getTree; return (_getTree = getTree(this.dom)) === null || _getTree === void 0 ? void 0 : _getTree.wrapper.closest(`.${TableCssClassName.NODEVIEW_WRAPPER}`); }; // we expect tree to be defined after animation frame let tableContainer = getTableContainer(); if (tableContainer) { const getSentinelTop = () => tableContainer && // Ignored via go/ees005 // eslint-disable-next-line @atlaskit/editor/no-as-casting tableContainer.getElementsByClassName(ClassName.TABLE_STICKY_SENTINEL_TOP).item(0); const 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. const bottomSentinels = tableContainer && tableContainer.getElementsByClassName(ClassName.TABLE_STICKY_SENTINEL_BOTTOM); return ( // eslint-disable-next-line @atlaskit/editor/no-as-casting bottomSentinels && bottomSentinels.item(bottomSentinels.length - 1) ); }; const sentinelsInDom = () => getSentinelTop() !== null && getSentinelBottom() !== null; const observeStickySentinels = () => { this.sentinels.top = getSentinelTop(); this.sentinels.bottom = getSentinelBottom(); [this.sentinels.top, this.sentinels.bottom].forEach(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 this.intersectionObserver.observe(el); } }); }; const 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 this.tableContainerObserver = new MutationObserver(() => { // 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 _this$tableContainerO; observeStickySentinels(); (_this$tableContainerO = this.tableContainerObserver) === null || _this$tableContainerO === void 0 ? void 0 : _this$tableContainerO.disconnect(); } }); const mutatingNode = tableContainer; if (mutatingNode && this.tableContainerObserver) { this.tableContainerObserver.observe(mutatingNode, { subtree: true, childList: true }); } } } }); } // updating bottom sentinel position if sticky header height changes // to allocate for new header height createResizeObserver() { this.resizeObserver = new ResizeObserver(entries => { const tree = getTree(this.dom); if (!tree) { return; } const { table } = tree; entries.forEach(entry => { var _this$editorScrollabl; // 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 === ((_this$editorScrollabl = this.editorScrollableElement) === null || _this$editorScrollabl === void 0 ? void 0 : _this$editorScrollabl.className)) { this.updateStickyHeaderWidth(); } else { const newHeight = entry.contentRect ? entry.contentRect.height : // Ignored via go/ees005 // eslint-disable-next-line @atlaskit/editor/no-as-casting entry.target.offsetHeight; if (this.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 - (this.stickyRowHeight || 0)) > stickyHeaderBorderBottomWidth) { this.stickyRowHeight = newHeight; this.sentinels.bottom.style.bottom = `${tableScrollbarOffset + stickyRowOffsetTop + newHeight}px`; updateTableMargin(table); } } }); }); } createIntersectionObserver() { this.intersectionObserver = new IntersectionObserver((entries, _) => { var _this$editorScrollabl2, _this$editorScrollabl3; // 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. const rootBounds = (_this$editorScrollabl2 = this.editorScrollableElement) === null || _this$editorScrollabl2 === void 0 ? void 0 : (_this$editorScrollabl3 = _this$editorScrollabl2.getBoundingClientRect) === null || _this$editorScrollabl3 === void 0 ? void 0 : _this$editorScrollabl3.call(_this$editorScrollabl2); entries.forEach(entry => { const { target, isIntersecting, boundingClientRect } = entry; // This observer only every looks at the top/bottom sentinels, we can assume if it's not one then it's the other. const targetKey = target.classList.contains(ClassName.TABLE_STICKY_SENTINEL_TOP) ? 'top' : 'bottom'; // Cache the latest sentinel information this.sentinelData[targetKey] = { isIntersecting, boundingClientRect, rootBounds: rootBounds !== null && rootBounds !== void 0 ? rootBounds : entry.rootBounds }; // Keep the other sentinels rootBounds in sync with this latest one this.sentinelData[targetKey === 'top' ? 'bottom' : targetKey].rootBounds = rootBounds !== null && rootBounds !== void 0 ? rootBounds : entry.rootBounds; }); this.refreshStickyState(); }, { threshold: 0, root: this.editorScrollableElement }); } refreshStickyState() { const tree = getTree(this.dom); if (!tree) { return; } const { table } = tree; const 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. const 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); } } } shouldSticky() { if ( // is Safari navigator.userAgent.includes('AppleWebKit') && !navigator.userAgent.includes('Chrome')) { const pos = this.getPos(); if (typeof pos === 'number') { const $tableRowPos = this.view.state.doc.resolve(pos); // layout -> layout column -> table -> table row if ($tableRowPos.depth >= 3) { var _findParentNodeCloses; const isInsideLayout = (_findParentNodeCloses = findParentNodeClosestToPos($tableRowPos, node => { return node.type.name === 'layoutColumn'; })) === null || _findParentNodeCloses === void 0 ? void 0 : _findParentNodeCloses.node; if (isInsideLayout) { return false; } } } } return this.isHeaderSticky(); } 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. */ const { top: sentinelTop, bottom: sentinelBottom } = this.sentinelData; // The rootRect is kept in sync across sentinels so it doesn't matter which one we use. const 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. const sentinelTopY = sentinelTop.boundingClientRect.bottom; const sentinelBottomY = sentinelBottom.boundingClientRect.top; // If header row height is more than 50% of viewport height don't do this const isRowHeaderTooLarge = this.stickyRowHeight && this.stickyRowHeight > window.innerHeight * 0.5; const isTopSentinelAboveScrollArea = !sentinelTop.isIntersecting && sentinelTopY <= rootBounds.top; const 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. const isTopSentinelAboveBottomSentinel = sentinelTopY < sentinelBottomY; return isTopSentinelAboveScrollArea && isBottomSentinelInOrBelowScrollArea && isTopSentinelAboveBottomSentinel && !isRowHeaderTooLarge; } /* receive external events */ onTablePluginState(state) { const tableRef = state.tableRef; const tree = 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(el => { if (el) { delete el.dataset.isObserved; } }); } const 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; const { wrapper } = tree; const tableContainer = wrapper.parentElement; const tableContentWrapper = tableContainer === null || tableContainer === void 0 ? void 0 : tableContainer.parentElement; const parentContainer = tableContentWrapper && tableContentWrapper.parentElement; const isTableInsideLayout = parentContainer && parentContainer.getAttribute('data-layout-content'); if (tableContentWrapper) { if (isCurrentTableSelected) { this.colControlsOffset = 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(() => { syncStickyRowToTable(tree.table); }, 0); } updateStickyHeaderWidth() { // table width might have changed, sync that back to sticky row const tree = getTree(this.dom); if (!tree) { return; } syncStickyRowToTable(tree.table); } /** * Manually refire the intersection observers. * Useful when the header may have detached from the table. */ refireIntersectionObservers() { if (this.isSticky) { [this.sentinels.top, this.sentinels.bottom].forEach(el => { if (el && this.intersectionObserver) { this.intersectionObserver.unobserve(el); this.intersectionObserver.observe(el); } }); } } makeHeaderRowSticky(tree, scrollTop) { var _tbody$firstChild; // 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; } const { table, wrapper } = tree; // TODO: ED-16035 - Make sure sticky header is only applied to first row const tbody = this.dom.parentElement; const isFirstHeader = tbody === null || tbody === void 0 ? void 0 : (_tbody$firstChild = tbody.firstChild) === null || _tbody$firstChild === void 0 ? void 0 : _tbody$firstChild.isEqualNode(this.dom); if (!isFirstHeader) { return; } const currentTableTop = this.getCurrentTableTop(tree); if (!scrollTop) { scrollTop = getTop(this.editorScrollableElement); } const domTop = currentTableTop > 0 ? scrollTop : scrollTop + currentTableTop; if (!this.isSticky) { var _this$editorScrollabl4; syncStickyRowToTable(table); this.dom.classList.add('sticky'); table.classList.add(ClassName.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$editorScrollabl4 = this.editorScrollableElement) === null || _this$editorScrollabl4 === void 0 ? void 0 : _this$editorScrollabl4.addEventListener('scrollend', this.refireIntersectionObservers, { passive: true, once: true }); const fastScrollThresholdMs = 500; setTimeout(() => { this.refireIntersectionObservers(); }, fastScrollThresholdMs); } this.dom.style.top = `0px`; updateTableMargin(table); this.dom.scrollLeft = wrapper.scrollLeft; this.emitOn(domTop, this.colControlsOffset); } makeRowHeaderNotSticky(table, isEditorDestroyed = false) { if (!this.isSticky || !table || !this.dom) { return; } this.dom.style.removeProperty('width'); this.dom.classList.remove('sticky'); table.classList.remove(ClassName.TABLE_STICKY); this.isSticky = false; this.dom.style.top = ''; table.style.removeProperty('margin-top'); this.emitOff(isEditorDestroyed); } getWrapperoffset(inverse = false) { const focusValue = inverse ? !this.focused : this.focused; return focusValue ? 0 : tableControlsSpacing; } getWrapperRefTop(wrapper) { return Math.round(getTop(wrapper)) + this.getWrapperoffset(); } getScrolledTableTop(wrapper) { return this.getWrapperRefTop(wrapper) - this.topPosEditorElement; } getCurrentTableTop(tree) { return this.getScrolledTableTop(tree.wrapper) + tree.table.clientHeight; } /* emit external events */ 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 const pos = this.getPos(); if (Number.isFinite(pos)) { updateStickyState({ pos, top, sticky: true, padding })(this.view.state, this.view.dispatch, this.view); } } 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 const pos = this.getPos(); if (!isEditorDestroyed && Number.isFinite(pos)) { updateStickyState({ pos, sticky: false, top: this.top, padding: this.padding })(this.view.state, this.view.dispatch, this.view); } } }