gristlabs_grist-core/app/client/widgets/EditorPlacement.ts
Dmitry S 4e805a4d9c (core) Fix sizing of tracebacks in formula errors, to make it scrollable
Summary: When traceback is present, give it 64px, or more if available, or less if less is needed. If less space is available than needed, the traceback will scroll within its allocated area.

Test Plan: The test FieldEditorSizing which tests basic sizing still passes; details with different size of formula and traceback were tested manually.

Reviewers: jarek

Reviewed By: jarek

Subscribers: jarek

Differential Revision: https://phab.getgrist.com/D3531
2022-07-21 09:56:52 -04:00

164 lines
6.6 KiB
TypeScript

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',
'max-height': maxHeight + '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,
};
}