2020-10-02 15:10:00 +00:00
|
|
|
/**
|
|
|
|
* This is a copy of TextEditor.js, converted to typescript.
|
|
|
|
*/
|
|
|
|
import {createGroup} from 'app/client/components/commands';
|
|
|
|
import {testId} from 'app/client/ui2018/cssVars';
|
2021-02-04 03:17:17 +00:00
|
|
|
import {createMobileButtons, getButtonMargins} from 'app/client/widgets/EditorButtons';
|
2021-11-05 21:08:20 +00:00
|
|
|
import {EditorPlacement, ISize} from 'app/client/widgets/EditorPlacement';
|
2020-10-02 15:10:00 +00:00
|
|
|
import {NewBaseEditor, Options} from 'app/client/widgets/NewBaseEditor';
|
|
|
|
import {CellValue} from "app/common/DocActions";
|
2021-05-17 14:05:49 +00:00
|
|
|
import {undef} from 'app/common/gutil';
|
|
|
|
import {dom, Observable} from 'grainjs';
|
2020-10-02 15:10:00 +00:00
|
|
|
|
|
|
|
export class NTextEditor extends NewBaseEditor {
|
2021-05-17 14:05:49 +00:00
|
|
|
// Observable with current editor state (used by drafts or latest edit/position component)
|
2021-05-23 17:43:11 +00:00
|
|
|
public readonly editorState: Observable<string>;
|
2021-05-17 14:05:49 +00:00
|
|
|
|
2020-10-02 15:10:00 +00:00
|
|
|
protected cellEditorDiv: HTMLElement;
|
|
|
|
protected textInput: HTMLTextAreaElement;
|
|
|
|
protected commandGroup: any;
|
|
|
|
|
|
|
|
private _dom: HTMLElement;
|
|
|
|
private _editorPlacement: EditorPlacement;
|
|
|
|
private _contentSizer: HTMLElement;
|
|
|
|
private _alignment: string;
|
|
|
|
|
|
|
|
// Note: TextEditor supports also options.placeholder for use by derived classes, but this is
|
|
|
|
// easy to apply to this.textInput without needing a separate option.
|
|
|
|
constructor(options: Options) {
|
|
|
|
super(options);
|
|
|
|
|
2021-05-23 17:43:11 +00:00
|
|
|
const initialValue: string = undef(
|
2021-05-17 14:05:49 +00:00
|
|
|
options.state as string | undefined,
|
|
|
|
options.editValue, String(options.cellValue ?? ""));
|
|
|
|
this.editorState = Observable.create<string>(this, initialValue);
|
|
|
|
|
2020-10-02 15:10:00 +00:00
|
|
|
this.commandGroup = this.autoDispose(createGroup(options.commands, null, true));
|
|
|
|
this._alignment = options.field.widgetOptionsJson.peek().alignment || 'left';
|
2021-06-17 16:41:07 +00:00
|
|
|
this._dom =
|
|
|
|
dom('div.default_editor',
|
|
|
|
// add readonly class
|
|
|
|
dom.cls("readonly_editor", options.readonly),
|
|
|
|
this.cellEditorDiv = dom('div.celleditor_cursor_editor',
|
|
|
|
testId('widget-text-editor'),
|
2020-10-02 15:10:00 +00:00
|
|
|
this._contentSizer = dom('div.celleditor_content_measure'),
|
2021-06-17 16:41:07 +00:00
|
|
|
this.textInput = dom('textarea',
|
|
|
|
dom.cls('celleditor_text_editor'),
|
2020-10-02 15:10:00 +00:00
|
|
|
dom.style('text-align', this._alignment),
|
2021-05-17 14:05:49 +00:00
|
|
|
dom.prop('value', initialValue),
|
2021-06-17 16:41:07 +00:00
|
|
|
dom.boolAttr('readonly', options.readonly),
|
2020-10-02 15:10:00 +00:00
|
|
|
this.commandGroup.attach(),
|
2021-05-17 14:05:49 +00:00
|
|
|
dom.on('input', () => this.onInput())
|
2020-10-02 15:10:00 +00:00
|
|
|
)
|
2021-02-04 03:17:17 +00:00
|
|
|
),
|
|
|
|
createMobileButtons(options.commands),
|
2020-10-02 15:10:00 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-02-04 03:17:17 +00:00
|
|
|
public attach(cellElem: Element): void {
|
2020-10-02 15:10:00 +00:00
|
|
|
// Attach the editor dom to page DOM.
|
2021-02-04 03:17:17 +00:00
|
|
|
this._editorPlacement = EditorPlacement.create(this, this._dom, cellElem, {margins: getButtonMargins()});
|
|
|
|
|
|
|
|
// Reposition the editor if needed for external reasons (in practice, window resize).
|
|
|
|
this.autoDispose(this._editorPlacement.onReposition.addListener(this.resizeInput, this));
|
|
|
|
|
2020-10-02 15:10:00 +00:00
|
|
|
this.setSizerLimits();
|
|
|
|
|
|
|
|
// Once the editor is attached to DOM, resize it to content, focus, and set cursor.
|
|
|
|
this.resizeInput();
|
|
|
|
this.textInput.focus();
|
|
|
|
const pos = Math.min(this.options.cursorPos, this.textInput.value.length);
|
|
|
|
this.textInput.setSelectionRange(pos, pos);
|
|
|
|
}
|
|
|
|
|
2021-03-05 15:17:07 +00:00
|
|
|
public getDom(): HTMLElement {
|
|
|
|
return this._dom;
|
|
|
|
}
|
|
|
|
|
2020-10-02 15:10:00 +00:00
|
|
|
public getCellValue(): CellValue {
|
2021-12-07 22:37:53 +00:00
|
|
|
const valueParser = this.options.field.createValueParser();
|
2021-11-01 15:48:08 +00:00
|
|
|
return valueParser(this.getTextValue());
|
2020-10-02 15:10:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public getTextValue() {
|
|
|
|
return this.textInput.value;
|
|
|
|
}
|
|
|
|
|
|
|
|
public getCursorPos() {
|
|
|
|
return this.textInput.selectionStart;
|
|
|
|
}
|
|
|
|
|
|
|
|
public setSizerLimits() {
|
|
|
|
// Set the max width of the sizer to the max we could possibly grow to, so that it knows to wrap
|
|
|
|
// once we reach it.
|
|
|
|
const maxSize = this._editorPlacement.calcSizeWithPadding(this.textInput,
|
|
|
|
{width: Infinity, height: Infinity}, {calcOnly: true});
|
|
|
|
this._contentSizer.style.maxWidth = Math.ceil(maxSize.width) + 'px';
|
|
|
|
}
|
|
|
|
|
2021-05-17 14:05:49 +00:00
|
|
|
/**
|
|
|
|
* Occurs when user types text in the textarea
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
protected onInput() {
|
|
|
|
// Resize the textbox whenever user types in it.
|
2021-05-23 17:43:11 +00:00
|
|
|
this.resizeInput();
|
2021-05-17 14:05:49 +00:00
|
|
|
|
|
|
|
// notify about current state
|
2021-05-23 17:43:11 +00:00
|
|
|
this.editorState.set(String(this.getTextValue()));
|
2021-05-17 14:05:49 +00:00
|
|
|
}
|
|
|
|
|
2020-10-02 15:10:00 +00:00
|
|
|
/**
|
|
|
|
* Helper which resizes textInput to match its content. It relies on having a contentSizer element
|
|
|
|
* with the same font/size settings as the textInput, and on having `calcSize` helper,
|
|
|
|
* which is provided by the EditorPlacement class.
|
|
|
|
*/
|
|
|
|
protected resizeInput() {
|
|
|
|
const textInput = this.textInput;
|
|
|
|
// \u200B is a zero-width space; it is used so the textbox will expand vertically
|
|
|
|
// on newlines, but it does not add any width.
|
|
|
|
this._contentSizer.textContent = textInput.value + '\u200B';
|
|
|
|
const rect = this._contentSizer.getBoundingClientRect();
|
|
|
|
|
|
|
|
// Allow for a bit of extra space after the cursor (only desirable when text is left-aligned).
|
|
|
|
if (this._alignment === "left") {
|
|
|
|
// Modifiable in modern browsers: https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect
|
|
|
|
rect.width += 16;
|
|
|
|
}
|
|
|
|
|
|
|
|
const size = this._editorPlacement.calcSizeWithPadding(textInput, rect);
|
|
|
|
textInput.style.width = size.width + 'px';
|
|
|
|
textInput.style.height = size.height + 'px';
|
2021-11-05 21:08:20 +00:00
|
|
|
|
|
|
|
// Scrollbars are first visible (as the content get larger), but resizing should hide them (if there is enough
|
|
|
|
// space), but this doesn't work in Chrome on Windows or Ubuntu (but works on Mac). Here if scrollbars are visible,
|
|
|
|
// but we got same enough spaces, we will force browser to check the available space once more time.
|
|
|
|
if (enoughSpace(rect, size) && hasScroll(textInput)) {
|
|
|
|
textInput.style.overflow = "hidden";
|
|
|
|
textInput.clientHeight; // just access metrics is enough to repaint
|
|
|
|
textInput.style.overflow = "auto";
|
|
|
|
}
|
2020-10-02 15:10:00 +00:00
|
|
|
}
|
|
|
|
}
|
2021-11-05 21:08:20 +00:00
|
|
|
|
|
|
|
function enoughSpace(requested: ISize, received: ISize) {
|
|
|
|
return requested.width <= received.width && requested.height <= received.height;
|
|
|
|
}
|
|
|
|
|
|
|
|
function hasScroll(el: HTMLTextAreaElement) {
|
|
|
|
// This is simple check for dimensions, scrollbar will appear when scrollHeight > clientHeight
|
|
|
|
return el.scrollHeight > el.clientHeight || el.scrollWidth > el.clientWidth;
|
|
|
|
}
|