gristlabs_grist-core/app/client/widgets/TextEditor.js
Jarosław Sadziński 5f182841b9 (core) Document keeps track of latest cursor position and latest editor value and is able to restore them when it is reloaded.
Summary: Grist document, when reloaded, is able to restore the latest cursor position and the editor state.

Test Plan: Browser test were created.

Reviewers: dsagal

Reviewed By: dsagal

Subscribers: paulfitz

Differential Revision: https://phab.getgrist.com/D2808
2021-05-18 10:36:59 +02:00

130 lines
5.2 KiB
JavaScript

var _ = require('underscore');
var gutil = require('app/common/gutil');
var dom = require('../lib/dom');
var kd = require('../lib/koDom');
var dispose = require('../lib/dispose');
var BaseEditor = require('./BaseEditor');
var commands = require('../components/commands');
const {testId} = require('app/client/ui2018/cssVars');
const {createMobileButtons, getButtonMargins} = require('app/client/widgets/EditorButtons');
const {EditorPlacement} = require('app/client/widgets/EditorPlacement');
const { observable } = require('grainjs');
/**
* Required parameters:
* @param {RowModel} options.field: ViewSectionField (i.e. column) being edited.
* @param {Object} options.cellValue: The value in the underlying cell being edited.
* @param {String} options.editValue: String to be edited, or undefined to use cellValue.
* @param {Number} options.cursorPos: The initial position where to place the cursor.
* @param {Object} options.commands: Object mapping command names to functions, to enable as part
* of the command group that should be activated while the editor exists.
*
* Optional parameters:
* @param {String} options.placeholder: Optional placeholder for the textarea.
*
* TextEditor exposes the following members, which derived classes may use:
* @member {Object} this.options: Options as passed into the constructor.
* @member {Node} this.dom: The DOM element for the editor.
* @member {Node} this.textInput: The textarea element of the editor (contained within this.dom).
* @member {Object} this.commandGroup: The CommandGroup created from options.commands.
*/
function TextEditor(options) {
this.options = options;
this.commandGroup = this.autoDispose(commands.createGroup(options.commands, null, true));
this._alignment = options.field.widgetOptionsJson.peek().alignment || 'left';
// calculate initial value (state, requested edited value or a current cell value)
const initialValue = gutil.undef(options.state, options.editValue, String(options.cellValue == null ? "" : options.cellValue));
// create observable with current state
this.editorState = this.autoDispose(observable(initialValue));
this.dom = dom('div.default_editor',
dom('div.celleditor_cursor_editor', dom.testId('TextEditor_editor'),
testId('widget-text-editor'), // new-style testId matches NTextEditor, for more uniform tests.
this.contentSizer = dom('div.celleditor_content_measure'),
this.textInput = dom('textarea.celleditor_text_editor',
kd.attr('placeholder', options.placeholder || ''),
kd.style('text-align', this._alignment),
kd.value(initialValue),
this.commandGroup.attach(),
// Resize the textbox whenever user types in it.
dom.on('input', () => this.onChange())
)
),
createMobileButtons(options.commands),
);
}
dispose.makeDisposable(TextEditor);
_.extend(TextEditor.prototype, BaseEditor.prototype);
TextEditor.prototype.attach = function(cellElem) {
// Attach the editor dom to page DOM.
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));
this.setSizerLimits();
// Once the editor is attached to DOM, resize it to content, focus, and set cursor.
this._resizeInput();
this.textInput.focus();
var pos = Math.min(this.options.cursorPos, this.textInput.value.length);
this.textInput.setSelectionRange(pos, pos);
};
TextEditor.prototype.getDom = function() {
return this.dom;
};
TextEditor.prototype.setSizerLimits = function() {
// 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';
};
TextEditor.prototype.getCellValue = function() {
return this.textInput.value;
};
TextEditor.prototype.onChange = function() {
if (this.editorState)
this.editorState.set(this.getTextValue());
this._resizeInput()
}
TextEditor.prototype.getTextValue = function() {
return this.textInput.value;
};
TextEditor.prototype.getCursorPos = function() {
return this.textInput.selectionStart;
};
/**
* 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.
*/
TextEditor.prototype._resizeInput = function() {
var 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';
var 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') {
rect.width += 16;
}
var size = this.editorPlacement.calcSizeWithPadding(textInput, rect);
textInput.style.width = size.width + 'px';
textInput.style.height = size.height + 'px';
};
module.exports = TextEditor;