import { Bounds, Boxes } from '@ephox/alloy';
import { InlineContent } from '@ephox/bridge';
import { Optional } from '@ephox/katamari';
import { Scroll, SelectorFind, SugarBody, SugarElement, SugarNode, Traverse, WindowVisualViewport } from '@ephox/sugar';
import Editor from 'tinymce/core/api/Editor';
import * as Options from '../../api/Options';
import { UiFactoryBackstageShared } from '../../backstage/Backstage';
// The "threshold" here is the amount of overlap. To make the overlap check
// be more permissive (return true for 'almost' an overlap), use a negative
// threshold value
const isVerticalOverlap = (a: Bounds, b: Bounds, threshold: number): boolean =>
b.bottom - a.y >= threshold && a.bottom - b.y >= threshold;
const getRangeRect = (rng: Range): DOMRect => {
const rect = rng.getBoundingClientRect();
// Some ranges (eg
| ) will return a 0x0 rect, so we'll need to calculate it from the leaf instead
if (rect.height <= 0 && rect.width <= 0) {
const leaf = Traverse.leaf(SugarElement.fromDom(rng.startContainer), rng.startOffset).element;
const elm = SugarNode.isText(leaf) ? Traverse.parent(leaf) : Optional.some(leaf);
return elm.filter(SugarNode.isElement)
.map((e) => e.dom.getBoundingClientRect())
// We have nothing valid, so just fallback to the original rect
.getOr(rect);
} else {
return rect;
}
};
const getSelectionBounds = (editor: Editor): Bounds => {
const rng = editor.selection.getRng();
const rect = getRangeRect(rng);
if (editor.inline) {
const scroll = Scroll.get();
return Boxes.bounds(scroll.left + rect.left, scroll.top + rect.top, rect.width, rect.height);
} else {
// Translate to the top level document, as rect is relative to the iframe viewport
const bodyPos = Boxes.absolute(SugarElement.fromDom(editor.getBody()));
return Boxes.bounds(bodyPos.x + rect.left, bodyPos.y + rect.top, rect.width, rect.height);
}
};
const getAnchorElementBounds = (editor: Editor, lastElement: Optional>): Bounds =>
lastElement
.filter((elem): elem is SugarElement => SugarBody.inBody(elem) && SugarNode.isHTMLElement(elem))
.map(Boxes.absolute)
.getOrThunk(() => getSelectionBounds(editor));
const getHorizontalBounds = (contentAreaBox: Bounds, viewportBounds: Bounds, margin: number): { x: number; width: number } => {
const x = Math.max(contentAreaBox.x + margin, viewportBounds.x);
const right = Math.min(contentAreaBox.right - margin, viewportBounds.right);
return { x, width: right - x };
};
const getVerticalBounds = (
editor: Editor,
contentAreaBox: Bounds,
viewportBounds: Bounds,
isToolbarLocationTop: boolean,
toolbarType: InlineContent.ContextPosition,
margin: number
): { y: number; bottom: number } => {
const container = SugarElement.fromDom(editor.getContainer());
const header = SelectorFind.descendant(container, '.tox-editor-header').getOr(container);
const headerBox = Boxes.box(header);
const isToolbarBelowContentArea = headerBox.y >= contentAreaBox.bottom;
const isToolbarAbove = isToolbarLocationTop && !isToolbarBelowContentArea;
// Scenario toolbar top & inline: Bottom of the header -> Bottom of the viewport
if (editor.inline && isToolbarAbove) {
return {
y: Math.max(headerBox.bottom + margin, viewportBounds.y),
bottom: viewportBounds.bottom
};
}
// Scenario toolbar top & inline: Top of the viewport -> Top of the header
if (editor.inline && !isToolbarAbove) {
return {
y: viewportBounds.y,
bottom: Math.min(headerBox.y - margin, viewportBounds.bottom)
};
}
// Allow line based context toolbar to overlap the statusbar
const containerBounds = toolbarType === 'line' ? Boxes.box(container) : contentAreaBox;
// Scenario toolbar bottom & Iframe: Bottom of the header -> Bottom of the editor container
if (isToolbarAbove) {
return {
y: Math.max(headerBox.bottom + margin, viewportBounds.y),
bottom: Math.min(containerBounds.bottom - margin, viewportBounds.bottom)
};
}
// Scenario toolbar bottom & Iframe: Top of the editor container -> Top of the header
return {
y: Math.max(containerBounds.y + margin, viewportBounds.y),
bottom: Math.min(headerBox.y - margin, viewportBounds.bottom)
};
};
const getContextToolbarBounds = (
editor: Editor,
sharedBackstage: UiFactoryBackstageShared,
toolbarType: InlineContent.ContextPosition,
margin: number = 0
): Bounds => {
const viewportBounds = WindowVisualViewport.getBounds(window);
const contentAreaBox = Boxes.box(SugarElement.fromDom(editor.getContentAreaContainer()));
const toolbarOrMenubarEnabled = Options.isMenubarEnabled(editor) || Options.isToolbarEnabled(editor) || Options.isMultipleToolbars(editor);
const { x, width } = getHorizontalBounds(contentAreaBox, viewportBounds, margin);
// Create bounds that lets the context toolbar overflow outside the content area, but remains in the viewport
if (editor.inline && !toolbarOrMenubarEnabled) {
return Boxes.bounds(x, viewportBounds.y, width, viewportBounds.height);
} else {
const isToolbarTop = sharedBackstage.header.isPositionedAtTop();
const { y, bottom } = getVerticalBounds(editor, contentAreaBox, viewportBounds, isToolbarTop, toolbarType, margin);
return Boxes.bounds(x, y, width, bottom - y);
}
};
export {
getContextToolbarBounds,
getAnchorElementBounds,
getSelectionBounds,
isVerticalOverlap
};