gristlabs_grist-core/app/client/widgets/TextEditor.js
Dmitry S 48e90c4998 (core) Change how formula columns can be converted to data.
Summary:
- No longer convert data columns to formula by typing a leading "=". Instead,
  show a tooltip with a link to click if the conversion was intended.
- No longer convert a formula column to data by deleting its formula. Leave the
  column empty instead.
- Offer the option "Convert formula to data" in column menu for formulas.
- Offer the option to "Clear column"
- If a subset of rows is shown, offer "Clear values" and "Clear entire column".

- Add logic to detect when a view shows a subset of all rows.
- Factor out showTooltip() from showTransientTooltip().

- Add a bunch of test cases to cover various combinations (there are small
  variations in options depending on whether all rows are shown, on whether
  multiple columns are selected, and whether columns include data columns).

Test Plan: Added a bunch of test cases.

Reviewers: paulfitz

Reviewed By: paulfitz

Differential Revision: https://phab.getgrist.com/D2746
2021-03-05 12:42:57 -05:00

119 lines
4.8 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.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.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;