import {Disposable, dom, Emitter} from 'grainjs'; export interface ISize { width: number; height: number; } interface ISizeOpts { // Don't reposition the editor as part of the size calculation. calcOnly?: boolean; } export interface IMargins { top: number; bottom: number; left: number; right: number; } export type IRect = ISize & IMargins; // edgeMargin is how many pixels to leave before the edge of the browser window by default. // This is added to margins that may be passed into the constructor. const edgeMargin = 12; // How large the editor can get when it needs to shift to the left or upwards. const maxShiftWidth = 560; const maxShiftHeight = 400; /** * This class implements the placement and sizing of the cell editor, such as TextEditor and * FormulaEditor. These try to match the size and position of the cell being edited, expanding * when needed. * * This class also takes care of attaching the editor DOM and destroying it on disposal. */ export class EditorPlacement extends Disposable { public readonly onReposition = this.autoDispose(new Emitter()); private _editorRoot: HTMLElement; private _maxRect: IRect; private _cellRect: IRect; private _margins: IMargins; // - editorDom is the DOM to attach. It gets destroyed when EditorPlacement is disposed. // - cellElem is the cell being mirrored by the editor; the editor generally expands to match // the size of the cell. // - margins may be given to add to the default edgeMargin, to increase distance to edges of the window. constructor(editorDom: HTMLElement, private _cellElem: Element, options: {margins?: IMargins} = {}) { super(); this._margins = { top: (options.margins?.top || 0) + edgeMargin, bottom: (options.margins?.bottom || 0) + edgeMargin, left: (options.margins?.left || 0) + edgeMargin, right: (options.margins?.right || 0) + edgeMargin, }; // Initialize _maxRect and _cellRect used for sizing the editor. We don't re-measure them // while typing (e.g. OK to scroll the view away from the editor), but we re-measure them on // window resize, which is only a normal occurrence on Android when virtual keyboard is shown. this._maxRect = document.body.getBoundingClientRect(); this._cellRect = rectWithoutBorders(this._cellElem); this.autoDispose(dom.onElem(window, 'resize', () => { this._maxRect = document.body.getBoundingClientRect(); this._cellRect = rectWithoutBorders(this._cellElem); this.onReposition.emit(); })); const editorRoot = this._editorRoot = dom('div.cell_editor', editorDom); // To hide from the user the incorrectly-sized element, we set visibility to hidden, and // reset it in _calcEditorSize() as soon as we have the sizes. editorRoot.style.visibility = 'hidden'; document.body.appendChild(editorRoot); this.onDispose(() => { // When the editor is destroyed, destroy and remove its DOM. dom.domDispose(editorRoot); editorRoot.remove(); }); } /** * Calculate the size of the full editor and shift the editor if needed to give it more space. * The position and size are applied to the editor unless {calcOnly: true} option is given. */ public calcSize(desiredSize: ISize, options: ISizeOpts = {}): ISize { const maxRect = this._maxRect; const margin = this._margins; const noShiftMaxWidth = maxRect.right - margin.right - this._cellRect.left; const maxWidth = Math.min(maxRect.width - margin.left - margin.right, Math.max(maxShiftWidth, noShiftMaxWidth)); const width = Math.min(maxWidth, Math.max(this._cellRect.width, desiredSize.width)); const left = Math.max(margin.left, Math.min(this._cellRect.left - maxRect.left, maxRect.width - margin.right - width)); const noShiftMaxHeight = maxRect.bottom - margin.bottom - this._cellRect.top; const maxHeight = Math.min(maxRect.height - margin.top - margin.bottom, Math.max(maxShiftHeight, noShiftMaxHeight)); const height = Math.min(maxHeight, Math.max(this._cellRect.height, desiredSize.height)); const top = Math.max(margin.top, Math.min(this._cellRect.top - maxRect.top, maxRect.height - margin.bottom - height)); // To hide from the user the split second before things are sized correctly, we set visibility // to hidden until we can get the sizes. As soon as sizes are available, restore visibility. if (!options.calcOnly) { Object.assign(this._editorRoot.style, { visibility: 'visible', left: left + 'px', top: top + 'px', // Set the width (but not the height) of the outer container explicitly to accommodate the // particular setup where a formula may include error details below -- these should // stretch to the calculated width (so need an explicit value), but may be dynamic in // height. (This feels hacky, but solves the problem.) width: width + 'px', }); } return {width, height}; } /** * Calculate the size for the editable part of the editor, given in elem. This assumes that the * size of the full editor differs from the editable part only in constant padding. The full * editor may be shifted as part of this call. */ public calcSizeWithPadding(elem: HTMLElement, desiredElemSize: ISize, options: ISizeOpts = {}): ISize { const rootRect = this._editorRoot.getBoundingClientRect(); const elemRect = elem.getBoundingClientRect(); const heightDelta = rootRect.height - elemRect.height; const widthDelta = rootRect.width - elemRect.width; const {width, height} = this.calcSize({ width: desiredElemSize.width + widthDelta, height: desiredElemSize.height + heightDelta, }, options); return { width: width - widthDelta, height: height - heightDelta, }; } } // Get the bounding rect of elem excluding borders. This allows the editor to match cellElem more // closely which is more visible in case of DetailView. function rectWithoutBorders(elem: Element): IRect { const rect = elem.getBoundingClientRect(); const style = getComputedStyle(elem, null); const bTop = parseFloat(style.getPropertyValue('border-top-width')); const bRight = parseFloat(style.getPropertyValue('border-right-width')); const bBottom = parseFloat(style.getPropertyValue('border-bottom-width')); const bLeft = parseFloat(style.getPropertyValue('border-left-width')); return { width: rect.width - bLeft - bRight, height: rect.height - bTop - bBottom, top: rect.top + bTop, bottom: rect.bottom - bBottom, left: rect.left + bLeft, right: rect.right - bRight, }; }