@atlaskit/editor-plugin-show-diff
Version:
ShowDiff plugin for @atlaskit/editor-core
105 lines (100 loc) • 4.68 kB
JavaScript
import { tableMarginTop } from '@atlaskit/editor-common/styles/table';
import { getStickyHeaderHeight } from '@atlaskit/editor-common/table/get-sticky-header-height';
import { fg } from '@atlaskit/platform-feature-flags/fg';
import { isDiffDecoration, SCROLL_TARGET_MARGIN_CSS_PROPERTY } from './decorations/decorationKeys';
import { getWidgetElement } from './decorations/utils/getWidgetElement';
/**
* Extra space above the scrolled-to element so it does not sit flush under the
* viewport edge (helps with sticky table headers, toolbars, etc.).
*
* Implemented with `scroll-margin-top` so we still use the browser’s native
* `scrollIntoView`, which scrolls every relevant scrollport (nested containers
* and the window). A single manual `scrollTop` on one ancestor often misses
* outer scroll or mis-identifies the active scroll container.
*/
const SCROLL_TOP_MARGIN_PX = 100;
function updateScrollTargetMargin(view, targetPos, api) {
var _api$limitedMode, _api$limitedMode$shar;
const isLimitedModeEnabled = Boolean(api === null || api === void 0 ? void 0 : (_api$limitedMode = api.limitedMode) === null || _api$limitedMode === void 0 ? void 0 : (_api$limitedMode$shar = _api$limitedMode.sharedState.currentState()) === null || _api$limitedMode$shar === void 0 ? void 0 : _api$limitedMode$shar.enabled);
const tableHeaderHeight = isLimitedModeEnabled ? undefined : getStickyHeaderHeight(view, targetPos);
view.dom.style.setProperty(SCROLL_TARGET_MARGIN_CSS_PROPERTY, `${tableHeaderHeight ? tableHeaderHeight + tableMarginTop : 0}px`);
}
function scrollToSelection(node) {
// ProseMirror may return a text node; scroll its parent element instead.
const element = node instanceof Element ? node : node === null || node === void 0 ? void 0 : node.parentElement;
if (!(element instanceof HTMLElement)) {
return;
}
const scrollOptions = {
behavior: 'smooth',
block: 'start'
};
if (fg('platform_editor_ai_suggestion_table_scroll_fix')) {
scrollOptions.inline = 'center';
}
if (fg('platform_editor_ai_show_diff_patch_1')) {
element.scrollIntoView(scrollOptions);
return;
}
// scroll-margin is included in scroll-into-view math; it does not change layout.
const previousScrollMarginTop = element.style.scrollMarginTop;
element.style.scrollMarginTop = `${SCROLL_TOP_MARGIN_PX}px`;
try {
element.scrollIntoView(scrollOptions);
} finally {
element.style.scrollMarginTop = previousScrollMarginTop;
}
}
function resolveScrollTarget(view, target) {
var _view$domAtPos;
if (isDiffDecoration(target) && target.spec.decorationType === 'widget') {
return getWidgetElement(target);
}
if (isDiffDecoration(target) && fg('platform_editor_ai_show_diff_patch_2')) {
var _Array$from$find;
return (_Array$from$find = Array.from(view.dom.querySelectorAll('[data-diff-id]')).find(element => element.dataset.diffId === target.spec.diffId)) !== null && _Array$from$find !== void 0 ? _Array$from$find : view.nodeDOM(target.from);
}
const targetNode = view.nodeDOM(target.from);
return targetNode instanceof Element ? targetNode : (_view$domAtPos = view.domAtPos(target.from)) === null || _view$domAtPos === void 0 ? void 0 : _view$domAtPos.node;
}
/**
* Schedules scrolling to the decoration at the given index after the next frame. Defaults to the
* first decoration when no index is provided.
*
* @returns A function that cancels the scheduled `requestAnimationFrame` if it has not run yet.
*/
export const scrollToDecoration = (view, decorations, activeIndex = 0, api) => {
const decoration = decorations[activeIndex];
if (!decoration) {
return () => {};
}
// A grouped decoration spans a whole customer-facing edit, which can visually start with a
// deleted-content widget rather than at the group's `from` position — see `scrollTarget` in
// `getScrollableDecorations`. Only set under `platform_editor_ai_show_diff_patch_1`.
const target = isDiffDecoration(decoration) && decoration.spec.scrollTarget || decoration;
let rafId = null;
let cancelled = false;
const scrollToTarget = () => {
if (view.isDestroyed) {
return;
}
if (fg('platform_editor_ai_show_diff_patch_1')) {
updateScrollTargetMargin(view, target.from, api);
}
scrollToSelection(resolveScrollTarget(view, target));
};
// Called from the plugin's post-DOM update hook; defer measurement until layout can settle.
rafId = requestAnimationFrame(() => {
rafId = null;
if (!cancelled) {
scrollToTarget();
}
});
return () => {
cancelled = true;
if (rafId !== null) {
cancelAnimationFrame(rafId);
rafId = null;
}
};
};