gristlabs_grist-core/app/client/widgets/TextEditor.js
Dmitry S 7284644313 (core) Add support for editing on mobile.
Summary:
- Add custom handling for dblclick on mobile, to allow focusing editor.
- In place of Clipboard.js, use a FocusLayer with document.body as the default focus element.
- Set maximum-scale on iOS viewport to prevent auto-zoom.
- Reposition the editor on window resize when editing a cell, which is a normal
  occurrence on Android when virtual keyboard is shown.
- Add Save/Cancel icon-buttons next to cell editor on mobile.

Test Plan: Tested manually on Safari / FF on iPhone, and on Chrome on Android emulator.

Reviewers: paulfitz

Reviewed By: paulfitz

Differential Revision: https://phab.getgrist.com/D2721
2021-02-03 23:10:51 -05:00

115 lines
4.7 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');
/**
* 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';
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(gutil.undefDefault(options.editValue, String(options.cellValue))),
this.commandGroup.attach(),
// Resize the textbox whenever user types in it.
dom.on('input', () => this._resizeInput())
)
),
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.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.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;