UNPKG

@atlaskit/editor-plugin-table

Version:

Table plugin for the @atlaskit/editor

986 lines (951 loc) 45.5 kB
import _classCallCheck from "@babel/runtime/helpers/classCallCheck"; import _createClass from "@babel/runtime/helpers/createClass"; import _possibleConstructorReturn from "@babel/runtime/helpers/possibleConstructorReturn"; import _getPrototypeOf from "@babel/runtime/helpers/getPrototypeOf"; import _inherits from "@babel/runtime/helpers/inherits"; import _defineProperty from "@babel/runtime/helpers/defineProperty"; function _callSuper(t, o, e) { return o = _getPrototypeOf(o), _possibleConstructorReturn(t, _isNativeReflectConstruct() ? Reflect.construct(o, e || [], _getPrototypeOf(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; })(); } import debounce from 'lodash/debounce'; import throttle from 'lodash/throttle'; import { ACTION_SUBJECT_ID, ACTION_SUBJECT, EVENT_TYPE, TABLE_ACTION } from '@atlaskit/editor-common/analytics'; import { getParentOfTypeCount } from '@atlaskit/editor-common/nesting'; import { nodeVisibilityManager } from '@atlaskit/editor-common/node-visibility'; import { tableMarginTop } from '@atlaskit/editor-common/styles'; import { findOverflowScrollParent } from '@atlaskit/editor-common/ui'; import { findParentNodeClosestToPos } from '@atlaskit/editor-prosemirror/utils'; import { fg } from '@atlaskit/platform-feature-flags'; import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals'; import { INITIAL_STATIC_VIEWPORT_HEIGHT } from '../pm-plugins/editor-content-area-height'; 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 { areAllRectsZero, 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 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 = /*#__PURE__*/function (_ref) { function TableRowNativeStickyWithFallback(node, view, getPos, eventDispatcher, api) { var _api$limitedMode; var _this; _classCallCheck(this, TableRowNativeStickyWithFallback); _this = _callSuper(this, TableRowNativeStickyWithFallback, [node, view, getPos, eventDispatcher]); _defineProperty(_this, "cleanup", function () { var _this$onEditorContent, _this2; if (_this.isStickyHeaderEnabled) { _this.unsubscribe(); _this.nodeVisibilityObserverCleanupFn && _this.nodeVisibilityObserverCleanupFn(); var tree = 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); }); _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); _defineProperty(_this, "hasScrolledSinceLoad", false); _defineProperty(_this, "disableNativeSticky", false); /** * Methods */ _defineProperty(_this, "headerRowMouseScrollEnd", debounce(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 _defineProperty(_this, "headerRowMouseScroll", throttle(function () { if (_this.isLegacySticky) { _this.dom.classList.add('no-pointer-events'); _this.headerRowMouseScrollEnd(); } }, HEADER_ROW_SCROLL_THROTTLE_TIMEOUT)); _defineProperty(_this, "toggleDisableNativeSticky", function (headerHeight, viewportHeight) { if (!_this.disableNativeSticky && headerHeight > viewportHeight * 0.5) { _this.disableNativeSticky = true; if (_this.isNativeSticky === undefined) { _this.dom.classList.remove(ClassName.NATIVE_STICKY); } } if (_this.disableNativeSticky && headerHeight <= viewportHeight * 0.5) { _this.disableNativeSticky = false; } }); _this.isHeaderRow = supportedHeaderRow(node); _this.isLegacySticky = false; var _getPluginState = 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 = getParentOfTypeCount(view.state.schema.nodes.table)(view.state.doc.resolve(pos)) > 1; } if (_this.isHeaderRow) { _this.dom.setAttribute('data-vc-nvs', 'true'); var _nodeVisibilityManage = nodeVisibilityManager(view.dom), observe = _nodeVisibilityManage.observe; _this.nodeVisibilityObserverCleanupFn = observe({ element: _this.contentDOM, onFirstVisible: function onFirstVisible() { _this.subscribeWhenRowVisible(); } }); } if (_this.isHeaderRow && _this.isStickyHeaderEnabled && 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; } _inherits(TableRowNativeStickyWithFallback, _ref); return _createClass(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 = supportedHeaderRow(node); if (this.isHeaderRow !== newNodeIsHeaderRow) { if (!newNodeIsHeaderRow && this.isHeaderRow) { var _this$dom$closest; (_this$dom$closest = this.dom.closest(".".concat(ClassName.TABLE_NODE_WRAPPER))) === null || _this$dom$closest === void 0 || _this$dom$closest.classList.remove(ClassName.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; 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 = 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 = findOverflowScrollParent(this.view.dom) || window; if (this.editorScrollableElement) { this.initObservers(); this.topPosEditorElement = getTop(this.editorScrollableElement); if (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(ClassName.TABLE_NODE_WRAPPER)); if (tableWrapper && tableWrapper instanceof HTMLElement && (!areAllRectsZero(entry) && expValEquals('platform_editor_table_sticky_header_patch_10', 'isEnabled', true) || !expValEquals('platform_editor_table_sticky_header_patch_10', 'isEnabled', true))) { if (entry.isIntersecting) { tableWrapper.classList.add(ClassName.TABLE_NODE_WRAPPER_NO_OVERFLOW); _this3.dom.classList.add(ClassName.NATIVE_STICKY); _this3.isNativeSticky = true; } else { tableWrapper.classList.remove(ClassName.TABLE_NODE_WRAPPER_NO_OVERFLOW); _this3.dom.classList.remove(ClassName.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(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 }); } }, { 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(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); } }, { key: "initOverflowObserver", value: function initOverflowObserver() { var _this4 = this; var tableWrapper = this.dom.closest(".".concat(ClassName.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 && fg('platform_editor_table_sticky_header_patch_5')) { _this4.overflowObserverEntries = entries; return; } if (entry.isIntersecting) { if (fg('platform_editor_table_sticky_header_patch_4')) { observer.root.classList.add(ClassName.TABLE_NODE_WRAPPER_NO_OVERFLOW); if (!_this4.disableNativeSticky) { _this4.dom.classList.add(ClassName.NATIVE_STICKY); } } else { observer.root.classList.add(ClassName.TABLE_NODE_WRAPPER_NO_OVERFLOW); _this4.dom.classList.add(ClassName.NATIVE_STICKY); } _this4.isNativeSticky = true; } else { if (fg('platform_editor_table_sticky_header_patch_4')) { observer.root.classList.remove(ClassName.TABLE_NODE_WRAPPER_NO_OVERFLOW); _this4.dom.classList.remove(ClassName.NATIVE_STICKY); } else { observer.root.classList.remove(ClassName.TABLE_NODE_WRAPPER_NO_OVERFLOW); _this4.dom.classList.remove(ClassName.NATIVE_STICKY); } _this4.isNativeSticky = false; } _this4.refreshLegacyStickyState(); if (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: TABLE_ACTION.STICKY_HEADER_METHOD_TOGGLED, actionSubject: ACTION_SUBJECT.TABLE, actionSubjectId: ACTION_SUBJECT_ID.TABLE_STICKY_HEADER, eventType: 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(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(ClassName.TABLE_CONTAINER)); if (entry.intersectionRect.top === ((_entry$rootBounds = entry.rootBounds) === null || _entry$rootBounds === void 0 ? void 0 : _entry$rootBounds.top) && (!_this5.disableNativeSticky || !fg('platform_editor_table_sticky_header_patch_4'))) { _this5.dom.classList.add(ClassName.NATIVE_STICKY_ACTIVE); if (tableContainer && tableContainer instanceof HTMLElement) { tableContainer.dataset.tableHeaderIsStuck = 'true'; } } else { _this5.dom.classList.remove(ClassName.NATIVE_STICKY_ACTIVE); if (tableContainer && tableContainer instanceof HTMLElement) { if (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 (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 (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 (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 = getTree(_this6.dom)) === null || _getTree === void 0 ? void 0 : _getTree.wrapper.closest(".".concat(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(ClassName.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(ClassName.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(ClassName.NATIVE_STICKY); } if (_this7.disableNativeSticky === false) { _this7.dom.classList.add(ClassName.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 = 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)) > stickyHeaderBorderBottomWidth) { _this8.stickyRowHeight = newHeight; _this8.sentinels.bottom.style.bottom = "".concat(tableScrollbarOffset + stickyRowOffsetTop + newHeight, "px"); updateTableMargin(table); } if (fg('platform_editor_table_sticky_header_patch_4')) { var _this8$editorContentA; var viewportHeight = (_this8$editorContentA = _this8.editorContentAreaHeight) !== null && _this8$editorContentA !== void 0 ? _this8$editorContentA : 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(ClassName.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 = 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 = 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 = 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 = 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 () { syncStickyRowToTable(tree.table); }, 0); } }, { key: "updateStickyHeaderWidth", value: function updateStickyHeaderWidth() { // table width might have changed, sync that back to sticky row var 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. */ }, { 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 = getTop(this.editorScrollableElement); } var domTop = currentTableTop > 0 ? scrollTop : scrollTop + currentTableTop; if (!this.isLegacySticky) { var _this$editorScrollabl; syncStickyRowToTable(table); this.dom.classList.add('sticky'); table.classList.add(ClassName.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"; updateTableMargin(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(ClassName.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 : tableControlsSpacing; } }, { key: "getWrapperRefTop", value: function getWrapperRefTop(wrapper) { return Math.round(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)) { 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)) { updateStickyState({ pos: pos, sticky: false, top: this.top, padding: this.padding })(this.view.state, this.view.dispatch, this.view); } } }]); }(TableNodeView); export { TableRowNativeStickyWithFallback as default };