UNPKG

@atlaskit/editor-common

Version:

A package that contains common classes and components for editor and renderer

248 lines (241 loc) • 11.5 kB
import { findDomRefAtPos } from '@atlaskit/editor-prosemirror/utils'; import { CellSelection } from '@atlaskit/editor-tables/cell-selection'; var MAXIMUM_BROWSER_SCROLLBAR_WIDTH = 20; /* Calculates the position of the floating toolbar relative to the selection. This is a re-implementation which closely matches the behaviour on Confluence renderer. The main difference is the popup is always above the selection. Things to consider: - popup is always above the selection - coordinates of head X and getBoundingClientRect() are absolute in client viewport (not including scroll offsets) - popup may appear in '.fabric-editor-popup-scroll-parent' (or body) - we use the toolbarRect to center align toolbar - use wrapperBounds to clamp values - editorView.dom bounds differ to wrapperBounds, convert at the end */ export var calculateToolbarPositionAboveSelection = function calculateToolbarPositionAboveSelection(toolbarTitle) { return function (editorView, nextPos) { var toolbar = document.querySelector("div[aria-label=\"".concat(toolbarTitle, "\"]")); if (!toolbar) { return nextPos; } // scroll wrapper for full page, fall back to document body // Ignored via go/ees007 // eslint-disable-next-line @atlaskit/editor/enforce-todo-comment-format // TODO: look into using getScrollGutterOptions() var scrollWrapper = editorView.dom.closest('.fabric-editor-popup-scroll-parent') || document.body; var wrapperBounds = scrollWrapper.getBoundingClientRect(); var selection = window && window.getSelection(); var range = selection && !selection.isCollapsed && selection.getRangeAt(0); if (!range) { return nextPos; } var toolbarRect = toolbar.getBoundingClientRect(); var _editorView$state$sel = editorView.state.selection, head = _editorView$state$sel.head, anchor = _editorView$state$sel.anchor; var topCoords = editorView.coordsAtPos(Math.min(head, anchor)); var bottomCoords = editorView.coordsAtPos(Math.max(head, anchor) - Math.min(range.endOffset, 1)); var top = (topCoords.top || 0) - toolbarRect.height * 1.5; var left = 0; // If not on the same line if (topCoords.top !== bottomCoords.top) { // selecting downwards if (head > anchor) { left = Math.max(topCoords.right, bottomCoords.right); } else { left = Math.min(topCoords.left, bottomCoords.left); } /* short selection above a long paragraph eg. short {<}heading The purpose of this text is to show the selection range{>}. The horizontal positioning should center around "heading", not where it ends at "range". Note: if it was "head<b>ing</b>" then it would only center around "head". Undesireable but matches the current renderer. */ var cliffPosition = range.getClientRects()[0]; if (cliffPosition.right < left) { left = cliffPosition.left + cliffPosition.width / 2; } } else { // Otherwise center on the single line selection left = topCoords.left + (bottomCoords.right - topCoords.left) / 2; } left -= toolbarRect.width / 2; // Place toolbar below selection if not sufficient space above if (top < wrapperBounds.top) { var _getCoordsBelowSelect = getCoordsBelowSelection(bottomCoords, toolbarRect); top = _getCoordsBelowSelect.top; left = _getCoordsBelowSelect.left; } // remap positions from browser document to wrapperBounds return { top: top - wrapperBounds.top + scrollWrapper.scrollTop, left: Math.max(0, left - wrapperBounds.left) }; }; }; export var calculateToolbarPositionTrackHead = function calculateToolbarPositionTrackHead(toolbarTitle) { return function (editorView, nextPos) { var toolbar = document.querySelector("div[aria-label=\"".concat(toolbarTitle, "\"]")); if (!toolbar) { return nextPos; } // scroll wrapper for full page, fall back to document body // Ignored via go/ees007 // eslint-disable-next-line @atlaskit/editor/enforce-todo-comment-format // TODO: look into using getScrollGutterOptions() var scrollWrapper = editorView.dom.closest('.fabric-editor-popup-scroll-parent') || document.body; var wrapperBounds = scrollWrapper.getBoundingClientRect(); var selection = window && window.getSelection(); var moreRovoOptionsButton = document.querySelector('button[aria-label="More Rovo options"], [aria-label="More Rovo options"]'); var isMoreRovoOptionsButtonVisible = !!moreRovoOptionsButton && moreRovoOptionsButton instanceof HTMLElement && !!moreRovoOptionsButton.offsetParent; var range = null; if (isMoreRovoOptionsButtonVisible) { if (selection && selection.getRangeAt && selection.rangeCount > 0) { var maybeRange = selection.getRangeAt(0); if (maybeRange instanceof Range) { range = maybeRange; } } } else { if (selection && !selection.isCollapsed && selection.getRangeAt && selection.rangeCount > 0) { var _maybeRange = selection.getRangeAt(0); if (_maybeRange instanceof Range) { range = _maybeRange; } } } if (!range) { return nextPos; } var toolbarRect = toolbar.getBoundingClientRect(); var _editorView$state$sel2 = editorView.state.selection, head = _editorView$state$sel2.head, anchor = _editorView$state$sel2.anchor; var topCoords = editorView.coordsAtPos(Math.min(head, anchor)); var bottomCoords = editorView.coordsAtPos(Math.max(head, anchor) - Math.min(range.endOffset, 1)); var top; // If not the same line, display toolbar below. if (head > anchor && topCoords.top !== bottomCoords.top) { // We are taking the previous pos to the maxium, so avoid end of line positions // returning the next line's rect. top = (bottomCoords.top || 0) + toolbarRect.height / 1.15; } else { top = (topCoords.top || 0) - toolbarRect.height * 1.5; } var left = (head > anchor ? bottomCoords.right : topCoords.left) - toolbarRect.width / 2; // Place toolbar below selection if not sufficient space above if (top < wrapperBounds.top) { var _getCoordsBelowSelect2 = getCoordsBelowSelection(bottomCoords, toolbarRect); top = _getCoordsBelowSelect2.top; left = _getCoordsBelowSelect2.left; } var leftCoord = Math.max(0, left - wrapperBounds.left); if (leftCoord + toolbarRect.width > wrapperBounds.width) { var _scrollbarWidth = MAXIMUM_BROWSER_SCROLLBAR_WIDTH; leftCoord = Math.max(0, wrapperBounds.width - (toolbarRect.width + _scrollbarWidth)); } // remap positions from browser document to wrapperBounds return { top: top - wrapperBounds.top + scrollWrapper.scrollTop, left: leftCoord }; }; }; /** * Returns the coordintes at the bottom the selection. */ var getCoordsBelowSelection = function getCoordsBelowSelection(bottomCoords, toolbarRect) { return { top: (bottomCoords.top || 0) + toolbarRect.height / 1.15, left: bottomCoords.right - toolbarRect.width / 2 }; }; var cellSelectionToolbarOffsetTop = 10; var scrollbarWidth = 20; var offsetTopOnColumnSelection = 4; export var calculateToolbarPositionOnCellSelection = function calculateToolbarPositionOnCellSelection(toolbarTitle) { return function (editorView, nextPos) { var toolbar = document.querySelector("div[aria-label=\"".concat(toolbarTitle, "\"]")); if (!toolbar) { return nextPos; } var selection = editorView.state.selection; if (!(selection instanceof CellSelection)) { return nextPos; } var $anchorCell = selection.$anchorCell, $headCell = selection.$headCell; var domAtPos = editorView.domAtPos.bind(editorView); var anchorCellDOM = findDomRefAtPos($anchorCell.pos, domAtPos); var headCellDOM = findDomRefAtPos($headCell.pos, domAtPos); if (!(anchorCellDOM instanceof HTMLElement) || !(headCellDOM instanceof HTMLElement)) { return nextPos; } var anchorCellRect = anchorCellDOM.getBoundingClientRect(); var headCellRect = headCellDOM.getBoundingClientRect(); var toolbarRect = toolbar.getBoundingClientRect(); var top; if (headCellRect.top <= anchorCellRect.top) { // Display Selection toolbar at the top of the selection top = headCellRect.top - toolbarRect.height - cellSelectionToolbarOffsetTop; } else { // Display Selection toolbar at the bottom of the selection top = headCellRect.bottom + cellSelectionToolbarOffsetTop; } // scroll wrapper for full page, fall back to document body // Ignored via go/ees007 // eslint-disable-next-line @atlaskit/editor/enforce-todo-comment-format // TODO: look into using getScrollGutterOptions() var scrollWrapper = editorView.dom.closest('.fabric-editor-popup-scroll-parent') || document.body; var wrapperBounds = scrollWrapper.getBoundingClientRect(); // Place toolbar below selection if not sufficient space above if (top < wrapperBounds.top && headCellRect.top <= anchorCellRect.top) { top = anchorCellRect.bottom + cellSelectionToolbarOffsetTop; } var left; if (headCellRect.left < anchorCellRect.left) { left = headCellRect.left; } else if (headCellRect.left === anchorCellRect.left) { left = headCellRect.left + headCellRect.width / 2; } else { left = headCellRect.right; } // If a user selected multiple columns via clicking on a drag handle // (clicking first on the left column and then shift clicking on the right column), // the $headcell stays in place and $anchorcell changes position. If they clicked on the right column // and then shift clicked on the left, the $headCell will change while $anchor stays in place. // Where is no way to know if user was dragging to select the cells or clicking on the drag handle. // So if all cells in columns are selected, we will align the Text Formatting toolbar // relative to center of the selected area. if (selection.isColSelection()) { if (headCellRect.left < anchorCellRect.left) { left = headCellRect.left + (anchorCellRect.right - headCellRect.left) / 2; } else if (headCellRect.left === anchorCellRect.left) { left = left; } else { left = anchorCellRect.left + (headCellRect.right - anchorCellRect.left) / 2; } // When column is selected, adjust top position to ensure that the toolbar does not // touch column drag handle top = top - offsetTopOnColumnSelection; } var adjustedLeft = Math.max(0, left - toolbarRect.width / 2 - wrapperBounds.left); if (adjustedLeft + toolbarRect.width > wrapperBounds.width) { adjustedLeft = Math.max(0, wrapperBounds.width - (toolbarRect.width + scrollbarWidth)); } // If the toolbar is to be placed at the bottom of the selection (it happens when // there is no sufficient space above) and the bottom of the selection is not in the view, // place the toolbar at the bottom of the visible part of selection if (selection.isColSelection() && top + toolbarRect.height > wrapperBounds.bottom) { top = top - (top - wrapperBounds.bottom) - (toolbarRect.height + 2 * cellSelectionToolbarOffsetTop); } return { top: top - wrapperBounds.top + scrollWrapper.scrollTop, left: adjustedLeft }; }; };