2021-02-04 03:17:17 +00:00
|
|
|
import {Disposable, dom, Emitter} from 'grainjs';
|
2020-10-02 15:10:00 +00:00
|
|
|
|
|
|
|
export interface ISize {
|
|
|
|
width: number;
|
|
|
|
height: number;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface ISizeOpts {
|
|
|
|
// Don't reposition the editor as part of the size calculation.
|
|
|
|
calcOnly?: boolean;
|
|
|
|
}
|
|
|
|
|
2021-02-04 03:17:17 +00:00
|
|
|
export interface IMargins {
|
|
|
|
top: number;
|
|
|
|
bottom: number;
|
|
|
|
left: number;
|
|
|
|
right: number;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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.
|
2020-10-02 15:10:00 +00:00
|
|
|
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 {
|
2021-02-04 03:17:17 +00:00
|
|
|
public readonly onReposition = this.autoDispose(new Emitter());
|
|
|
|
|
2020-10-02 15:10:00 +00:00
|
|
|
private _editorRoot: HTMLElement;
|
2021-02-04 03:17:17 +00:00
|
|
|
private _maxRect: ClientRect|DOMRect;
|
|
|
|
private _cellRect: ClientRect|DOMRect;
|
|
|
|
private _margins: IMargins;
|
2020-10-02 15:10:00 +00:00
|
|
|
|
|
|
|
// - editorDom is the DOM to attach. It gets destroyed when EditorPlacement is disposed.
|
2021-02-04 03:17:17 +00:00
|
|
|
// - 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} = {}) {
|
2020-10-02 15:10:00 +00:00
|
|
|
super();
|
|
|
|
|
2021-02-04 03:17:17 +00:00
|
|
|
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();
|
|
|
|
}));
|
|
|
|
|
2020-10-02 15:10:00 +00:00
|
|
|
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 {
|
2021-02-04 03:17:17 +00:00
|
|
|
const maxRect = this._maxRect;
|
|
|
|
const margin = this._margins;
|
2020-10-02 15:10:00 +00:00
|
|
|
|
2021-02-04 03:17:17 +00:00
|
|
|
const noShiftMaxWidth = maxRect.right - margin.right - this._cellRect.left;
|
|
|
|
const maxWidth = Math.min(maxRect.width - margin.left - margin.right, Math.max(maxShiftWidth, noShiftMaxWidth));
|
2020-10-02 15:10:00 +00:00
|
|
|
const width = Math.min(maxWidth, Math.max(this._cellRect.width, desiredSize.width));
|
2021-02-04 03:17:17 +00:00
|
|
|
const left = Math.max(margin.left,
|
|
|
|
Math.min(this._cellRect.left - maxRect.left, maxRect.width - margin.right - width));
|
2020-10-02 15:10:00 +00:00
|
|
|
|
2021-02-04 03:17:17 +00:00
|
|
|
const noShiftMaxHeight = maxRect.bottom - margin.bottom - this._cellRect.top;
|
|
|
|
const maxHeight = Math.min(maxRect.height - margin.top - margin.bottom, Math.max(maxShiftHeight, noShiftMaxHeight));
|
2020-10-02 15:10:00 +00:00
|
|
|
const height = Math.min(maxHeight, Math.max(this._cellRect.height, desiredSize.height));
|
2021-02-04 03:17:17 +00:00
|
|
|
const top = Math.max(margin.top,
|
|
|
|
Math.min(this._cellRect.top - maxRect.top, maxRect.height - margin.bottom - height));
|
2020-10-02 15:10:00 +00:00
|
|
|
|
|
|
|
// 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,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
2021-02-04 03:17:17 +00:00
|
|
|
|
|
|
|
// 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): ClientRect {
|
|
|
|
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,
|
|
|
|
};
|
|
|
|
}
|