mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) Readonly editors
Summary: Grist should not prevent read-only viewers from opening cell editors since they usually provide much more information than is visible in a cell. Every editor was enhanced with a read-only mode that provides the same information available for an editor but doesn't allow to change the underlying data. Test Plan: Browser tests Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D2842
This commit is contained in:
parent
d3dc910784
commit
698c9d4e40
@ -26,6 +26,7 @@ function AceEditor(options) {
|
||||
this.calcSize = (options && options.calcSize) || ((elem, size) => size);
|
||||
this.gristDoc = (options && options.gristDoc) || null;
|
||||
this.editorState = (options && options.editorState) || null;
|
||||
this._readonly = options.readonly || false;
|
||||
|
||||
this.editor = null;
|
||||
this.editorDom = null;
|
||||
@ -112,6 +113,11 @@ AceEditor.prototype.attachCommandGroup = function(commandGroup) {
|
||||
_.each(commandGroup.knownKeys, (command, key) => {
|
||||
this.editor.commands.addCommand({
|
||||
name: command,
|
||||
// We are setting readonly as true to enable all commands
|
||||
// in a readonly mode.
|
||||
// Because FieldEditor in readonly mode will rewire all commands that
|
||||
// modify state, we are safe to enable them.
|
||||
readOnly: this._readonly,
|
||||
bindKey: {
|
||||
win: key,
|
||||
mac: key,
|
||||
|
@ -278,16 +278,12 @@ BaseView.prototype.activateEditorAtCursor = function(options) {
|
||||
// LazyArrayModel row model which is also used to build the cell dom. Needed since
|
||||
// it may be used as a key to retrieve the cell dom, which is useful for editor placement.
|
||||
var lazyRow = this.getRenderedRowModel(rowId);
|
||||
if (builder.field.disableEditData() || this.gristDoc.isReadonly.get()) {
|
||||
builder.flashCursorReadOnly(lazyRow);
|
||||
} else {
|
||||
if (!lazyRow) {
|
||||
// TODO scroll into view. For now, just don't activate the editor.
|
||||
return;
|
||||
}
|
||||
this.editRowModel.assign(rowId);
|
||||
builder.buildEditorDom(this.editRowModel, lazyRow, options || {});
|
||||
if (!lazyRow) {
|
||||
// TODO scroll into view. For now, just don't activate the editor.
|
||||
return;
|
||||
}
|
||||
this.editRowModel.assign(rowId);
|
||||
builder.buildEditorDom(this.editRowModel, lazyRow, options || {});
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -58,6 +58,9 @@ export class EditorMonitor extends Disposable {
|
||||
// will be invoked only once
|
||||
let executed = false;
|
||||
|
||||
// don't restore on readonly mode
|
||||
if (doc.isReadonly.get()) { return; }
|
||||
|
||||
// on view shown
|
||||
this._currentViewListener.autoDispose(doc.currentView.addListener(async view => {
|
||||
if (executed) {
|
||||
|
@ -35,6 +35,7 @@ export interface ITokenFieldOptions {
|
||||
acOptions?: IAutocompleteOptions<IToken & ACItem>;
|
||||
openAutocompleteOnFocus?: boolean;
|
||||
styles?: ITokenFieldStyles;
|
||||
readonly?: boolean;
|
||||
|
||||
// Allows overriding how tokens are copied to the clipboard, or retrieved from it.
|
||||
// By default, tokens are placed into clipboard as text/plain comma-separated token labels, with
|
||||
@ -89,7 +90,13 @@ export class TokenField extends Disposable {
|
||||
this.autoDispose(this._tokens.addListener(this._recordUndo.bind(this)));
|
||||
|
||||
// Use overridden styles if any were provided.
|
||||
const {cssTokenField, cssToken, cssInputWrapper, cssTokenInput, cssDeleteButton, cssDeleteIcon} =
|
||||
const {
|
||||
cssTokenField,
|
||||
cssToken,
|
||||
cssInputWrapper,
|
||||
cssTokenInput,
|
||||
cssDeleteButton,
|
||||
cssDeleteIcon} =
|
||||
{...tokenFieldStyles, ..._options.styles};
|
||||
|
||||
function stop(ev: Event) {
|
||||
@ -101,15 +108,18 @@ export class TokenField extends Disposable {
|
||||
{tabIndex: '-1'},
|
||||
dom.forEach(this._tokens, (t) =>
|
||||
cssToken(this._options.renderToken(t.token),
|
||||
cssDeleteButton(cssDeleteIcon('CrossSmall'), testId('tokenfield-delete')),
|
||||
dom.cls('selected', (use) => use(this._selection).has(t)),
|
||||
dom.on('click', (ev) => this._onTokenClick(ev, t)),
|
||||
dom.on('mousedown', (ev) => this._onMouseDown(ev, t)),
|
||||
_options.readonly ? null : [
|
||||
cssDeleteButton(cssDeleteIcon('CrossSmall'), testId('tokenfield-delete')),
|
||||
dom.on('click', (ev) => this._onTokenClick(ev, t)),
|
||||
dom.on('mousedown', (ev) => this._onMouseDown(ev, t))
|
||||
],
|
||||
testId('tokenfield-token')
|
||||
),
|
||||
),
|
||||
cssInputWrapper(
|
||||
this._textInput = cssTokenInput(
|
||||
dom.boolAttr("readonly", this._options.readonly ?? false),
|
||||
dom.on('focus', this._onInputFocus.bind(this)),
|
||||
dom.on('blur', () => { this._acHolder.clear(); }),
|
||||
(this._acOptions ?
|
||||
@ -174,6 +184,8 @@ export class TokenField extends Disposable {
|
||||
|
||||
// Open the autocomplete dropdown, if autocomplete was configured in the options.
|
||||
private _openAutocomplete() {
|
||||
// don't open dropdown in a readonly mode
|
||||
if (this._options.readonly) { return; }
|
||||
if (this._acOptions && this._acHolder.isEmpty()) {
|
||||
Autocomplete.create(this._acHolder, this._textInput, this._acOptions);
|
||||
}
|
||||
|
@ -130,6 +130,22 @@ function attr(attrName, valueOrFunc) {
|
||||
}
|
||||
exports.attr = attr;
|
||||
|
||||
/**
|
||||
* Sets or removes a boolean attribute of a DOM element. According to the spec, empty string is a
|
||||
* valid true value for the attribute, and the false value is indicated by the attribute's absence.
|
||||
* @param {String} attrName The name of the attribute to bind, e.g. 'href'.
|
||||
* @param {Object} valueOrFunc An observable, a constant, or a function for a computed observable.
|
||||
*/
|
||||
function boolAttr(attrName, valueOrFunc) {
|
||||
return makeBinding(valueOrFunc, function(elem, value) {
|
||||
if (!value) {
|
||||
elem.removeAttribute(attrName);
|
||||
} else {
|
||||
elem.setAttribute(attrName, '');
|
||||
}
|
||||
});
|
||||
}
|
||||
exports.boolAttr = boolAttr;
|
||||
|
||||
/**
|
||||
* Keeps the style property `property` of a DOM element in sync with an observable value.
|
||||
|
@ -149,16 +149,18 @@ export class AttachmentsEditor extends NewBaseEditor {
|
||||
testId('pw-download')
|
||||
),
|
||||
),
|
||||
cssButton(cssButtonIcon('FieldAttachment'), 'Add',
|
||||
dom.on('click', () => this._select()),
|
||||
testId('pw-add')
|
||||
),
|
||||
dom.maybe(this._selected, selected =>
|
||||
cssButton(cssButtonIcon('Remove'), 'Delete',
|
||||
dom.on('click', () => this._remove()),
|
||||
testId('pw-remove')
|
||||
this.options.readonly ? null : [
|
||||
cssButton(cssButtonIcon('FieldAttachment'), 'Add',
|
||||
dom.on('click', () => this._select()),
|
||||
testId('pw-add')
|
||||
),
|
||||
)
|
||||
dom.maybe(this._selected, () =>
|
||||
cssButton(cssButtonIcon('Remove'), 'Delete',
|
||||
dom.on('click', () => this._remove()),
|
||||
testId('pw-remove')
|
||||
),
|
||||
)
|
||||
]
|
||||
),
|
||||
cssCloseButton(cssBigIcon('CrossBig'), dom.on('click', () => ctl.close()),
|
||||
testId('pw-close')),
|
||||
@ -172,12 +174,12 @@ export class AttachmentsEditor extends NewBaseEditor {
|
||||
dom.hide(use => !use(this._attachments).length || use(this._index) === use(this._attachments).length - 1),
|
||||
dom.on('click', () => this._moveIndex(1))
|
||||
),
|
||||
dom.domComputed(this._selected, selected => renderContent(selected)),
|
||||
dom.domComputed(this._selected, selected => renderContent(selected, this.options.readonly)),
|
||||
|
||||
// Drag-over logic
|
||||
(elem: HTMLElement) => dragOverClass(elem, cssDropping.className),
|
||||
cssDragArea(cssWarning('Drop files here to attach')),
|
||||
dom.on('drop', ev => this._upload(ev.dataTransfer!.files)),
|
||||
cssDragArea(this.options.readonly ? null : cssWarning('Drop files here to attach')),
|
||||
this.options.readonly ? null : dom.on('drop', ev => this._upload(ev.dataTransfer!.files)),
|
||||
testId('pw-modal')
|
||||
];
|
||||
}
|
||||
@ -235,10 +237,10 @@ function isInEditor(ev: KeyboardEvent): boolean {
|
||||
return (ev.target as HTMLElement).tagName === 'INPUT';
|
||||
}
|
||||
|
||||
function renderContent(att: Attachment|null): HTMLElement {
|
||||
function renderContent(att: Attachment|null, readonly: boolean): HTMLElement {
|
||||
const commonArgs = [cssContent.cls(''), testId('pw-attachment-content')];
|
||||
if (!att) {
|
||||
return cssWarning('No attachments', cssDetails('Drop files here to attach.'), ...commonArgs);
|
||||
return cssWarning('No attachments', readonly ? null : cssDetails('Drop files here to attach.'), ...commonArgs);
|
||||
} else if (att.hasPreview) {
|
||||
return dom('img', dom.attr('src', att.url), ...commonArgs);
|
||||
} else if (att.fileType.startsWith('video/')) {
|
||||
|
@ -18,4 +18,7 @@ CheckBoxEditor.skipEditor = function(typedVal, cellVal) {
|
||||
}
|
||||
}
|
||||
|
||||
// For documentation, see NewBaseEditor.ts
|
||||
CheckBoxEditor.supportsReadonly = function() { return false; }
|
||||
|
||||
module.exports = CheckBoxEditor;
|
||||
|
@ -13,7 +13,7 @@ function ChoiceEditor(options) {
|
||||
this.choices = options.field.widgetOptionsJson.peek().choices || [];
|
||||
|
||||
// Add autocomplete if there are any choices to select from
|
||||
if (this.choices.length > 0) {
|
||||
if (this.choices.length > 0 && !options.readonly) {
|
||||
|
||||
autocomplete(this.textInput, this.choices, {
|
||||
allowNothingSelected: true,
|
||||
|
@ -70,10 +70,13 @@ export class ChoiceListEditor extends NewBaseEditor {
|
||||
createToken: label => new ChoiceItem(label, !choiceSet.has(label)),
|
||||
acOptions,
|
||||
openAutocompleteOnFocus: true,
|
||||
readonly : options.readonly,
|
||||
styles: {cssTokenField, cssToken, cssDeleteButton, cssDeleteIcon},
|
||||
});
|
||||
|
||||
this._dom = dom('div.default_editor',
|
||||
dom.cls("readonly_editor", options.readonly),
|
||||
dom.cls(cssReadonlyStyle.className, options.readonly),
|
||||
this.cellEditorDiv = cssCellEditor(testId('widget-text-editor'),
|
||||
this._contentSizer = cssContentSizer(),
|
||||
elem => this._tokenField.attach(elem),
|
||||
@ -281,3 +284,8 @@ const cssChoiceList = styled('div', `
|
||||
z-index: 1001;
|
||||
box-shadow: 0 0px 8px 0 rgba(38,38,51,0.6)
|
||||
`);
|
||||
|
||||
const cssReadonlyStyle = styled('div', `
|
||||
padding-left: 16px;
|
||||
background: white;
|
||||
`);
|
||||
|
@ -35,60 +35,75 @@ function DateEditor(options) {
|
||||
// Strip moment format string to remove markers unsupported by the datepicker.
|
||||
this.safeFormat = DateEditor.parseMomentToSafe(this.dateFormat);
|
||||
|
||||
this._readonly = options.readonly;
|
||||
|
||||
// Use the default local timezone to format the placeholder date.
|
||||
let defaultTimezone = moment.tz.guess();
|
||||
let placeholder = moment.tz(defaultTimezone).format(this.safeFormat);
|
||||
if (options.readonly) {
|
||||
// clear placeholder for readonly mode
|
||||
placeholder = null;
|
||||
}
|
||||
TextEditor.call(this, _.defaults(options, { placeholder: placeholder }));
|
||||
|
||||
const isValid = _.isNumber(options.cellValue);
|
||||
const formatted = this.formatValue(options.cellValue, this.safeFormat);
|
||||
// Formatted value will be empty if a cell contains an error,
|
||||
// but for a readonly mode we actually want to show what user typed
|
||||
// into the cell.
|
||||
const readonlyValue = isValid ? formatted : options.cellValue;
|
||||
const cellValue = options.readonly ? readonlyValue : formatted;
|
||||
|
||||
// Set the edited value, if not explicitly given, to the formatted version of cellValue.
|
||||
this.textInput.value = gutil.undef(options.state, options.editValue,
|
||||
this.formatValue(options.cellValue, this.safeFormat));
|
||||
this.textInput.value = gutil.undef(options.state, options.editValue, cellValue);
|
||||
|
||||
// Indicates whether keyboard navigation is active for the datepicker.
|
||||
this._keyboardNav = false;
|
||||
if (!options.readonly) {
|
||||
// Indicates whether keyboard navigation is active for the datepicker.
|
||||
this._keyboardNav = false;
|
||||
|
||||
// Attach the datepicker.
|
||||
this._datePickerWidget = $(this.textInput).datepicker({
|
||||
keyboardNavigation: false,
|
||||
forceParse: false,
|
||||
todayHighlight: true,
|
||||
todayBtn: 'linked',
|
||||
// Convert the stripped format string to one suitable for the datepicker.
|
||||
format: DateEditor.parseSafeToCalendar(this.safeFormat)
|
||||
});
|
||||
this.autoDisposeCallback(() => this._datePickerWidget.datepicker('remove'));
|
||||
// Attach the datepicker.
|
||||
this._datePickerWidget = $(this.textInput).datepicker({
|
||||
keyboardNavigation: false,
|
||||
forceParse: false,
|
||||
todayHighlight: true,
|
||||
todayBtn: 'linked',
|
||||
// Convert the stripped format string to one suitable for the datepicker.
|
||||
format: DateEditor.parseSafeToCalendar(this.safeFormat)
|
||||
});
|
||||
this.autoDisposeCallback(() => this._datePickerWidget.datepicker('remove'));
|
||||
|
||||
// NOTE: Datepicker interferes with normal enter and escape functionality. Add an event handler
|
||||
// to the DatePicker to prevent interference with normal behavior.
|
||||
this._datePickerWidget.on('keydown', e => {
|
||||
// If enter or escape is pressed, destroy the datepicker and re-dispatch the event.
|
||||
if (e.keyCode === 13 || e.keyCode === 27) {
|
||||
this._datePickerWidget.datepicker('remove');
|
||||
// The current target of the event will be the textarea.
|
||||
setTimeout(() => e.currentTarget.dispatchEvent(e.originalEvent), 0);
|
||||
}
|
||||
});
|
||||
// NOTE: Datepicker interferes with normal enter and escape functionality. Add an event handler
|
||||
// to the DatePicker to prevent interference with normal behavior.
|
||||
this._datePickerWidget.on('keydown', e => {
|
||||
// If enter or escape is pressed, destroy the datepicker and re-dispatch the event.
|
||||
if (e.keyCode === 13 || e.keyCode === 27) {
|
||||
this._datePickerWidget.datepicker('remove');
|
||||
// The current target of the event will be the textarea.
|
||||
setTimeout(() => e.currentTarget.dispatchEvent(e.originalEvent), 0);
|
||||
}
|
||||
});
|
||||
|
||||
// When the up/down arrow is pressed, modify the datepicker options to take control of
|
||||
// the arrow keys for date selection.
|
||||
let datepickerCommands = Object.assign({}, options.commands, {
|
||||
datepickerFocus: () => { this._allowKeyboardNav(true); }
|
||||
});
|
||||
this._datepickerCommands = this.autoDispose(commands.createGroup(datepickerCommands, this, true));
|
||||
// When the up/down arrow is pressed, modify the datepicker options to take control of
|
||||
// the arrow keys for date selection.
|
||||
let datepickerCommands = Object.assign({}, options.commands, {
|
||||
datepickerFocus: () => { this._allowKeyboardNav(true); }
|
||||
});
|
||||
this._datepickerCommands = this.autoDispose(commands.createGroup(datepickerCommands, this, true));
|
||||
|
||||
this._datePickerWidget.on('show', () => {
|
||||
// A workaround to allow clicking in the datepicker without losing focus.
|
||||
dom(document.querySelector('.datepicker'),
|
||||
kd.attr('tabIndex', 0), // allows datepicker to gain focus
|
||||
kd.toggleClass('clipboard_focus', true) // tells clipboard to not steal focus from us
|
||||
);
|
||||
// Attach command group to the input to allow switching keyboard focus to the datepicker.
|
||||
dom(this.textInput,
|
||||
// If the user inputs text into the textbox, take keyboard focus from the datepicker.
|
||||
dom.on('input', () => { this._allowKeyboardNav(false); }),
|
||||
this._datepickerCommands.attach()
|
||||
);
|
||||
});
|
||||
this._datePickerWidget.on('show', () => {
|
||||
// A workaround to allow clicking in the datepicker without losing focus.
|
||||
dom(document.querySelector('.datepicker'),
|
||||
kd.attr('tabIndex', 0), // allows datepicker to gain focus
|
||||
kd.toggleClass('clipboard_focus', true) // tells clipboard to not steal focus from us
|
||||
);
|
||||
// Attach command group to the input to allow switching keyboard focus to the datepicker.
|
||||
dom(this.textInput,
|
||||
// If the user inputs text into the textbox, take keyboard focus from the datepicker.
|
||||
dom.on('input', () => { this._allowKeyboardNav(false); }),
|
||||
this._datepickerCommands.attach()
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
dispose.makeDisposable(DateEditor);
|
||||
|
@ -7,6 +7,7 @@ const kd = require('../lib/koDom');
|
||||
const DateEditor = require('./DateEditor');
|
||||
const gutil = require('app/common/gutil');
|
||||
const { parseDate } = require('app/common/parseDate');
|
||||
const TextEditor = require('./TextEditor');
|
||||
|
||||
/**
|
||||
* DateTimeEditor - Editor for DateTime type. Includes a dropdown datepicker.
|
||||
@ -18,10 +19,14 @@ function DateTimeEditor(options) {
|
||||
|
||||
// Adjust the command group.
|
||||
var origCommands = options.commands;
|
||||
options.commands = Object.assign({}, origCommands, {
|
||||
prevField: () => this._focusIndex() === 1 ? this._setFocus(0) : origCommands.prevField(),
|
||||
nextField: () => this._focusIndex() === 0 ? this._setFocus(1) : origCommands.nextField(),
|
||||
});
|
||||
|
||||
// don't modify navigation for readonly mode
|
||||
if (!options.readonly) {
|
||||
options.commands = Object.assign({}, origCommands, {
|
||||
prevField: () => this._focusIndex() === 1 ? this._setFocus(0) : origCommands.prevField(),
|
||||
nextField: () => this._focusIndex() === 0 ? this._setFocus(1) : origCommands.nextField(),
|
||||
});
|
||||
}
|
||||
|
||||
// Call the superclass.
|
||||
DateEditor.call(this, options);
|
||||
@ -32,20 +37,37 @@ function DateTimeEditor(options) {
|
||||
// modifies that to be two side-by-side textareas.
|
||||
this._dateSizer = this.contentSizer; // For consistency with _timeSizer and _timeInput.
|
||||
this._dateInput = this.textInput;
|
||||
dom(this.dom, kd.toggleClass('celleditor_datetime', true));
|
||||
dom(this.dom.firstChild, kd.toggleClass('celleditor_datetime_editor', true));
|
||||
this.dom.appendChild(
|
||||
dom('div.celleditor_cursor_editor.celleditor_datetime_editor',
|
||||
this._timeSizer = dom('div.celleditor_content_measure'),
|
||||
this._timeInput = dom('textarea.celleditor_text_editor',
|
||||
// Use a placeholder of 12:00am, since that is the autofill time value.
|
||||
kd.attr('placeholder', moment.tz('0', 'H', this.timezone).format(this._timeFormat)),
|
||||
kd.value(this.formatValue(options.cellValue, this._timeFormat)),
|
||||
this.commandGroup.attach(),
|
||||
dom.on('input', () => this.onChange())
|
||||
|
||||
const isValid = _.isNumber(options.cellValue);
|
||||
const formatted = this.formatValue(options.cellValue, this._timeFormat);
|
||||
// Use a placeholder of 12:00am, since that is the autofill time value.
|
||||
const placeholder = moment.tz('0', 'H', this.timezone).format(this._timeFormat);
|
||||
|
||||
// for readonly
|
||||
if (options.readonly) {
|
||||
if (!isValid) {
|
||||
// do nothing - DateEditor will show correct error
|
||||
} else {
|
||||
// append time format or a placeholder
|
||||
const time = (formatted || placeholder);
|
||||
const sep = time ? ' ' : '';
|
||||
this.textInput.value = this.textInput.value + sep + time;
|
||||
}
|
||||
} else {
|
||||
dom(this.dom, kd.toggleClass('celleditor_datetime', true));
|
||||
dom(this.dom.firstChild, kd.toggleClass('celleditor_datetime_editor', true));
|
||||
this.dom.appendChild(
|
||||
dom('div.celleditor_cursor_editor.celleditor_datetime_editor',
|
||||
this._timeSizer = dom('div.celleditor_content_measure'),
|
||||
this._timeInput = dom('textarea.celleditor_text_editor',
|
||||
kd.attr('placeholder', placeholder),
|
||||
kd.value(formatted),
|
||||
this.commandGroup.attach(),
|
||||
dom.on('input', () => this.onChange())
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
// If the edit value is encoded json, use those values as a starting point
|
||||
if (typeof options.state == 'string') {
|
||||
@ -65,6 +87,9 @@ _.extend(DateTimeEditor.prototype, DateEditor.prototype);
|
||||
|
||||
DateTimeEditor.prototype.setSizerLimits = function() {
|
||||
var maxSize = this.editorPlacement.calcSize({width: Infinity, height: Infinity}, {calcOnly: true});
|
||||
if (this.options.readonly) {
|
||||
return;
|
||||
}
|
||||
this._dateSizer.style.maxWidth =
|
||||
this._timeSizer.style.maxWidth = Math.ceil(maxSize.width / 2 - 6) + 'px';
|
||||
};
|
||||
@ -118,6 +143,12 @@ DateTimeEditor.prototype.getCellValue = function() {
|
||||
* Overrides the resizing function in TextEditor.
|
||||
*/
|
||||
DateTimeEditor.prototype._resizeInput = function() {
|
||||
|
||||
// for readonly field, we will use logic from a super class
|
||||
if (this.options.readonly) {
|
||||
TextEditor.prototype._resizeInput.call(this);
|
||||
return;
|
||||
}
|
||||
// Use the size calculation provided in options.calcSize (that takes into account cell size and
|
||||
// screen size), with both date and time parts as the input. The resulting size is applied to
|
||||
// the parent (containing date + time), with date and time each expanding or shrinking from the
|
||||
|
@ -21,14 +21,14 @@ import { DiffBox } from 'app/client/widgets/DiffBox';
|
||||
import { buildErrorDom } from 'app/client/widgets/ErrorDom';
|
||||
import { FieldEditor, openSideFormulaEditor, saveWithoutEditor } from 'app/client/widgets/FieldEditor';
|
||||
import { NewAbstractWidget } from 'app/client/widgets/NewAbstractWidget';
|
||||
import { NewBaseEditor } from "app/client/widgets/NewBaseEditor";
|
||||
import * as UserType from 'app/client/widgets/UserType';
|
||||
import * as UserTypeImpl from 'app/client/widgets/UserTypeImpl';
|
||||
import * as gristTypes from 'app/common/gristTypes';
|
||||
import * as gutil from 'app/common/gutil';
|
||||
import { CellValue } from 'app/plugin/GristData';
|
||||
import { delay } from 'bluebird';
|
||||
import { Computed, Disposable, fromKo, dom as grainjsDom,
|
||||
Holder, IDisposable, makeTestId } from 'grainjs';
|
||||
Holder, IDisposable, makeTestId, toKo } from 'grainjs';
|
||||
import * as ko from 'knockout';
|
||||
import * as _ from 'underscore';
|
||||
|
||||
@ -77,6 +77,7 @@ export class FieldBuilder extends Disposable {
|
||||
private readonly _fieldEditorHolder: Holder<IDisposable>;
|
||||
private readonly _widgetCons: ko.Computed<{create: (...args: any[]) => NewAbstractWidget}>;
|
||||
private readonly _docModel: DocModel;
|
||||
private readonly _readonly: Computed<boolean>;
|
||||
|
||||
public constructor(public readonly gristDoc: GristDoc, public readonly field: ViewFieldRec,
|
||||
private _cursor: Cursor) {
|
||||
@ -88,6 +89,8 @@ export class FieldBuilder extends Disposable {
|
||||
|
||||
this._readOnlyPureType = ko.pureComputed(() => this.field.column().pureType());
|
||||
|
||||
this._readonly = Computed.create(this, (use) => use(gristDoc.isReadonly) || use(field.disableEditData));
|
||||
|
||||
// Observable with a list of available types.
|
||||
this._availableTypes = Computed.create(this, (use) => {
|
||||
const isFormula = use(this.origColumn.isFormula);
|
||||
@ -407,11 +410,13 @@ export class FieldBuilder extends Disposable {
|
||||
}
|
||||
}, this).extend({ deferred: true })).onlyNotifyUnequal();
|
||||
|
||||
|
||||
return (elem: Element) => {
|
||||
this._rowMap.set(row, elem);
|
||||
dom(elem,
|
||||
dom.autoDispose(widgetObs),
|
||||
kd.cssClass(this.field.formulaCssClass),
|
||||
kd.toggleClass("readonly", toKo(ko, this._readonly)),
|
||||
kd.maybe(isSelected, () => dom('div.selected_cursor',
|
||||
kd.toggleClass('active_cursor', isActive)
|
||||
)),
|
||||
@ -426,28 +431,6 @@ export class FieldBuilder extends Disposable {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Flash the cursor in the given row briefly to indicate that editing in this cell is disabled.
|
||||
*/
|
||||
public async flashCursorReadOnly(mainRow: DataRowModel) {
|
||||
const mainCell = this._rowMap.get(mainRow);
|
||||
// Abort if a cell is not found (i.e. if this is a ChartView)
|
||||
if (!mainCell) { return; }
|
||||
const elem = mainCell.querySelector('.active_cursor');
|
||||
if (elem && !elem.classList.contains('cursor_read_only')) {
|
||||
elem.classList.add('cursor_read_only');
|
||||
const div = elem.appendChild(dom('div.cursor_read_only_lock.glyphicon.glyphicon-lock'));
|
||||
try {
|
||||
await delay(200);
|
||||
elem.classList.add('cursor_read_only_fade');
|
||||
await delay(400);
|
||||
} finally {
|
||||
elem.classList.remove('cursor_read_only', 'cursor_read_only_fade');
|
||||
elem.removeChild(div);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public buildEditorDom(editRow: DataRowModel, mainRowModel: DataRowModel, options: {
|
||||
init?: string,
|
||||
state?: any
|
||||
@ -459,7 +442,16 @@ export class FieldBuilder extends Disposable {
|
||||
return;
|
||||
}
|
||||
|
||||
const editorCtor = UserTypeImpl.getEditorConstructor(this.options().widget, this._readOnlyPureType());
|
||||
// If this is censored value, don't open up the editor, unless it is a formula field.
|
||||
const cell = editRow.cells[this.field.colId()];
|
||||
const value = cell && cell();
|
||||
if (gristTypes.isCensored(value) && !this.origColumn.isFormula.peek()) {
|
||||
this._fieldEditorHolder.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
const editorCtor: typeof NewBaseEditor =
|
||||
UserTypeImpl.getEditorConstructor(this.options().widget, this._readOnlyPureType());
|
||||
// constructor may be null for a read-only non-formula field, though not today.
|
||||
if (!editorCtor) {
|
||||
// Actually, we only expect buildEditorDom() to be called when isEditorActive() is false (i.e.
|
||||
@ -468,7 +460,13 @@ export class FieldBuilder extends Disposable {
|
||||
return;
|
||||
}
|
||||
|
||||
if (saveWithoutEditor(editorCtor, editRow, this.field, options.init)) {
|
||||
// if editor doesn't support readonly mode, don't show it
|
||||
if (this._readonly.get() && editorCtor.supportsReadonly && !editorCtor.supportsReadonly()) {
|
||||
this._fieldEditorHolder.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._readonly.get() && saveWithoutEditor(editorCtor, editRow, this.field, options.init)) {
|
||||
this._fieldEditorHolder.clear();
|
||||
return;
|
||||
}
|
||||
@ -483,8 +481,9 @@ export class FieldBuilder extends Disposable {
|
||||
editRow,
|
||||
cellElem,
|
||||
editorCtor,
|
||||
startVal: options.init,
|
||||
state : options.state
|
||||
state: options.state,
|
||||
startVal: this._readonly.get() ? undefined : options.init, // don't start with initial value
|
||||
readonly: this._readonly.get() // readonly for editor will not be observable
|
||||
});
|
||||
|
||||
// Put the FieldEditor into a holder in GristDoc too. This way any existing FieldEditor (perhaps
|
||||
|
@ -74,6 +74,7 @@ export class FieldEditor extends Disposable {
|
||||
private _saveEdit = asyncOnce(() => this._doSaveEdit());
|
||||
private _editorHasChanged = false;
|
||||
private _isFormula = false;
|
||||
private _readonly = false;
|
||||
|
||||
constructor(options: {
|
||||
gristDoc: GristDoc,
|
||||
@ -83,7 +84,8 @@ export class FieldEditor extends Disposable {
|
||||
cellElem: Element,
|
||||
editorCtor: IEditorConstructor,
|
||||
startVal?: string,
|
||||
state?: any
|
||||
state?: any,
|
||||
readonly: boolean
|
||||
}) {
|
||||
super();
|
||||
this._gristDoc = options.gristDoc;
|
||||
@ -92,6 +94,7 @@ export class FieldEditor extends Disposable {
|
||||
this._editRow = options.editRow;
|
||||
this._editorCtor = options.editorCtor;
|
||||
this._cellElem = options.cellElem;
|
||||
this._readonly = options.readonly;
|
||||
|
||||
const startVal = options.startVal;
|
||||
let offerToMakeFormula = false;
|
||||
@ -99,7 +102,7 @@ export class FieldEditor extends Disposable {
|
||||
const column = this._field.column();
|
||||
this._isFormula = column.isRealFormula.peek();
|
||||
let editValue: string|undefined = startVal;
|
||||
if (startVal && gutil.startsWith(startVal, '=')) {
|
||||
if (!options.readonly && startVal && gutil.startsWith(startVal, '=')) {
|
||||
if (this._isFormula || this._field.column().isEmpty()) {
|
||||
// If we typed '=' on an empty column, convert it to a formula. If on a formula column,
|
||||
// start editing ignoring the initial '='.
|
||||
@ -131,9 +134,24 @@ export class FieldEditor extends Disposable {
|
||||
unmakeFormula: () => this._unmakeFormula(),
|
||||
};
|
||||
|
||||
const state: any = options.state;
|
||||
// for readonly editor rewire commands, most of this also could be
|
||||
// done by just overriding the saveEdit method, but this is more clearer
|
||||
if (options.readonly) {
|
||||
this._editCommands.fieldEditSave = () => {
|
||||
// those two lines are tightly coupled - without disposing first
|
||||
// it will run itself in a loop. But this is needed for a GridView
|
||||
// which navigates to the next row on save.
|
||||
this._editCommands.fieldEditCancel();
|
||||
commands.allCommands.fieldEditSave.run();
|
||||
};
|
||||
this._editCommands.fieldEditSaveHere = this._editCommands.fieldEditCancel;
|
||||
this._editCommands.prevField = () => { this._cancelEdit(); commands.allCommands.prevField.run(); };
|
||||
this._editCommands.nextField = () => { this._cancelEdit(); commands.allCommands.nextField.run(); };
|
||||
this._editCommands.makeFormula = () => true; /* don't stop propagation */
|
||||
this._editCommands.unmakeFormula = () => true;
|
||||
}
|
||||
|
||||
this.rebuildEditor(editValue, Number.POSITIVE_INFINITY, state);
|
||||
this.rebuildEditor(editValue, Number.POSITIVE_INFINITY, options.state);
|
||||
|
||||
if (offerToMakeFormula) {
|
||||
this._offerToMakeFormula();
|
||||
@ -143,7 +161,12 @@ export class FieldEditor extends Disposable {
|
||||
// when user or server refreshes the browser
|
||||
this._gristDoc.editorMonitor.monitorEditor(this);
|
||||
|
||||
setupEditorCleanup(this, this._gristDoc, this._field, this._saveEdit);
|
||||
// for readonly field we don't need to do anything special
|
||||
if (!options.readonly) {
|
||||
setupEditorCleanup(this, this._gristDoc, this._field, this._saveEdit);
|
||||
} else {
|
||||
setupReadonlyEditorCleanup(this, this._gristDoc, this._field, () => this._cancelEdit());
|
||||
}
|
||||
}
|
||||
|
||||
// cursorPos refers to the position of the caret within the editor.
|
||||
@ -166,10 +189,16 @@ export class FieldEditor extends Disposable {
|
||||
cellValue = cellCurrentValue;
|
||||
}
|
||||
|
||||
// Enter formula-editing mode (e.g. click-on-column inserts its ID) only if we are opening the
|
||||
// editor by typing into it (and overriding previous formula). In other cases (e.g. double-click),
|
||||
// we defer this mode until the user types something.
|
||||
this._field.editingFormula(this._isFormula && editValue !== undefined);
|
||||
const error = getFormulaError(this._gristDoc, this._editRow, column);
|
||||
|
||||
// For readonly mode use the default behavior of Formula Editor
|
||||
// TODO: cleanup this flag - it gets modified in too many places
|
||||
if (!this._readonly){
|
||||
// Enter formula-editing mode (e.g. click-on-column inserts its ID) only if we are opening the
|
||||
// editor by typing into it (and overriding previous formula). In other cases (e.g. double-click),
|
||||
// we defer this mode until the user types something.
|
||||
this._field.editingFormula(this._isFormula && editValue !== undefined);
|
||||
}
|
||||
|
||||
this._editorHasChanged = false;
|
||||
// Replace the item in the Holder with a new one, disposing the previous one.
|
||||
@ -177,11 +206,12 @@ export class FieldEditor extends Disposable {
|
||||
gristDoc: this._gristDoc,
|
||||
field: this._field,
|
||||
cellValue,
|
||||
formulaError: getFormulaError(this._gristDoc, this._editRow, column),
|
||||
formulaError: error,
|
||||
editValue,
|
||||
cursorPos,
|
||||
state,
|
||||
commands: this._editCommands,
|
||||
readonly : this._readonly
|
||||
}));
|
||||
|
||||
// if editor supports live changes, connect it to the change emitter
|
||||
@ -268,6 +298,7 @@ export class FieldEditor extends Disposable {
|
||||
|
||||
// Cancels the edit
|
||||
private _cancelEdit() {
|
||||
if (this.isDisposed()) { return; }
|
||||
const event: FieldEditorStateEvent = {
|
||||
position : this.cellPosition(),
|
||||
wasModified : this._editorHasChanged,
|
||||
@ -380,6 +411,7 @@ export function openSideFormulaEditor(options: {
|
||||
cursorPos: Number.POSITIVE_INFINITY, // Position of the caret within the editor.
|
||||
commands: editCommands,
|
||||
cssClass: 'formula_editor_sidepane',
|
||||
readonly : false
|
||||
});
|
||||
editor.attach(refElem);
|
||||
|
||||
@ -389,6 +421,20 @@ export function openSideFormulaEditor(options: {
|
||||
return holder;
|
||||
}
|
||||
|
||||
/**
|
||||
* For an readonly editor, set up its cleanup:
|
||||
* - canceling on click-away (when focus returns to Grist "clipboard" element)
|
||||
*/
|
||||
function setupReadonlyEditorCleanup(
|
||||
owner: MultiHolder, gristDoc: GristDoc, field: ViewFieldRec, cancelEdit: () => any
|
||||
) {
|
||||
// Whenever focus returns to the Clipboard component, close the editor by saving the value.
|
||||
gristDoc.app.on('clipboard_focus', cancelEdit);
|
||||
owner.onDispose(() => {
|
||||
field.editingFormula(false);
|
||||
gristDoc.app.off('clipboard_focus', cancelEdit);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* For an active editor, set up its cleanup:
|
||||
|
@ -47,11 +47,15 @@ export class FormulaEditor extends NewBaseEditor {
|
||||
// and _editorPlacement created.
|
||||
calcSize: this._calcSize.bind(this),
|
||||
gristDoc: options.gristDoc,
|
||||
saveValueOnBlurEvent: true,
|
||||
editorState : this.editorState
|
||||
saveValueOnBlurEvent: !options.readonly,
|
||||
editorState : this.editorState,
|
||||
readonly: options.readonly
|
||||
});
|
||||
|
||||
const allCommands = Object.assign({ setCursor: this._onSetCursor }, options.commands);
|
||||
const allCommands = !options.readonly
|
||||
? Object.assign({ setCursor: this._onSetCursor }, options.commands)
|
||||
// for readonly mode don't grab cursor when clicked away - just move the cursor
|
||||
: options.commands;
|
||||
this._commandGroup = this.autoDispose(createGroup(allCommands, this, options.field.editingFormula));
|
||||
|
||||
const hideErrDetails = Observable.create(null, true);
|
||||
@ -59,6 +63,8 @@ export class FormulaEditor extends NewBaseEditor {
|
||||
|
||||
this.autoDispose(this._formulaEditor);
|
||||
this._dom = dom('div.default_editor',
|
||||
// switch border shadow
|
||||
dom.cls("readonly_editor", options.readonly),
|
||||
createMobileButtons(options.commands),
|
||||
options.cssClass ? dom.cls(options.cssClass) : null,
|
||||
|
||||
@ -70,7 +76,10 @@ export class FormulaEditor extends NewBaseEditor {
|
||||
dom('div.formula_editor.formula_field_edit', testId('formula-editor'),
|
||||
// We don't always enter editing mode immediately, e.g. not on double-clicking a cell.
|
||||
// In those cases, we'll switch as soon as the user types or clicks into the editor.
|
||||
dom.on('mousedown', () => options.field.editingFormula(true)),
|
||||
dom.on('mousedown', () => {
|
||||
// but don't do it when this is a readonly mode
|
||||
options.field.editingFormula(true);
|
||||
}),
|
||||
this._formulaEditor.buildDom((aceObj: any) => {
|
||||
aceObj.setFontSize(11);
|
||||
aceObj.setHighlightActiveLine(false);
|
||||
@ -82,10 +91,13 @@ export class FormulaEditor extends NewBaseEditor {
|
||||
this._formulaEditor.attachCommandGroup(this._commandGroup);
|
||||
|
||||
// enable formula editing if state was passed
|
||||
if (options.state) {
|
||||
if (options.state || options.readonly) {
|
||||
options.field.editingFormula(true);
|
||||
}
|
||||
|
||||
if (options.readonly) {
|
||||
this._formulaEditor.enable(false);
|
||||
aceObj.gotoLine(0, 0); // By moving, ace editor won't highlight anything
|
||||
}
|
||||
// This catches any change to the value including e.g. via backspace or paste.
|
||||
aceObj.once("change", () => options.field.editingFormula(true));
|
||||
})
|
||||
@ -147,8 +159,11 @@ export class FormulaEditor extends NewBaseEditor {
|
||||
|
||||
// TODO: update regexes to unicode?
|
||||
private _onSetCursor(row: DataRowModel, col: ViewFieldRec) {
|
||||
|
||||
if (!col) { return; } // if clicked on row header, no col to insert
|
||||
|
||||
if (this.options.readonly) { return; }
|
||||
|
||||
const aceObj = this._formulaEditor.getEditor();
|
||||
|
||||
if (!aceObj.selection.isEmpty()) { // If text selected, replace whole selection
|
||||
|
@ -10,7 +10,6 @@ import {CellValue} from "app/common/DocActions";
|
||||
import {undef} from 'app/common/gutil';
|
||||
import {dom, Observable} from 'grainjs';
|
||||
|
||||
|
||||
export class NTextEditor extends NewBaseEditor {
|
||||
// Observable with current editor state (used by drafts or latest edit/position component)
|
||||
public readonly editorState: Observable<string>;
|
||||
@ -36,12 +35,18 @@ export class NTextEditor extends NewBaseEditor {
|
||||
|
||||
this.commandGroup = this.autoDispose(createGroup(options.commands, null, true));
|
||||
this._alignment = options.field.widgetOptionsJson.peek().alignment || 'left';
|
||||
this._dom = dom('div.default_editor',
|
||||
this.cellEditorDiv = dom('div.celleditor_cursor_editor', testId('widget-text-editor'),
|
||||
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'),
|
||||
this._contentSizer = dom('div.celleditor_content_measure'),
|
||||
this.textInput = dom('textarea', dom.cls('celleditor_text_editor'),
|
||||
this.textInput = dom('textarea',
|
||||
dom.cls('celleditor_text_editor'),
|
||||
dom.style('text-align', this._alignment),
|
||||
dom.prop('value', initialValue),
|
||||
dom.boolAttr('readonly', options.readonly),
|
||||
this.commandGroup.attach(),
|
||||
dom.on('input', () => this.onInput())
|
||||
)
|
||||
|
@ -22,6 +22,7 @@ export interface Options {
|
||||
cursorPos: number;
|
||||
commands: IEditorCommandGroup;
|
||||
state?: any;
|
||||
readonly: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -55,6 +56,13 @@ export abstract class NewBaseEditor extends Disposable {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if editor supports readonly mode (default: true)
|
||||
*/
|
||||
public static supportsReadonly(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Current state of the editor. Optional, not all editors will report theirs current state.
|
||||
*/
|
||||
|
@ -53,8 +53,12 @@ export class ReferenceEditor extends NTextEditor {
|
||||
this._visibleCol = vcol.colId() || 'id';
|
||||
|
||||
// Decorate the editor to look like a reference column value (with a "link" icon).
|
||||
this.cellEditorDiv.classList.add(cssRefEditor.className);
|
||||
this.cellEditorDiv.appendChild(cssRefEditIcon('FieldReference'));
|
||||
// But not on readonly mode - here we will reuse default decoration
|
||||
if (!options.readonly) {
|
||||
this.cellEditorDiv.classList.add(cssRefEditor.className);
|
||||
this.cellEditorDiv.appendChild(cssRefEditIcon('FieldReference'));
|
||||
}
|
||||
|
||||
this.textInput.value = undef(options.state, options.editValue, this._idToText(options.cellValue));
|
||||
|
||||
const needReload = (options.editValue === undefined && !tableData.isLoaded);
|
||||
@ -80,6 +84,8 @@ export class ReferenceEditor extends NTextEditor {
|
||||
|
||||
public attach(cellElem: Element): void {
|
||||
super.attach(cellElem);
|
||||
// don't create autocomplete for readonly mode
|
||||
if (this.options.readonly) { return; }
|
||||
this._autocomplete = this.autoDispose(new Autocomplete<ICellItem>(this.textInput, {
|
||||
menuCssClass: menuCssClass + ' ' + cssRefList.className,
|
||||
search: this._doSearch.bind(this),
|
||||
|
@ -31,10 +31,10 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.formula_field::before {
|
||||
.formula_field::before, .formula_field_edit::before {
|
||||
background-color: #D0D0D0;
|
||||
}
|
||||
.formula_field_edit::before {
|
||||
.formula_field_edit:not(.readonly)::before {
|
||||
background-color: var(--grist-color-cursor);
|
||||
}
|
||||
.formula_field.invalid::before {
|
||||
@ -45,6 +45,10 @@
|
||||
background-color: var(--grist-color-cursor);
|
||||
color: #ffb6c1;
|
||||
}
|
||||
|
||||
.readonly_editor .formula_field_edit::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.invalid-text input {
|
||||
|
@ -7,6 +7,32 @@
|
||||
box-shadow: 0 0 3px 2px var(--grist-color-cursor);
|
||||
}
|
||||
|
||||
.readonly_editor {
|
||||
box-shadow: 0 0 3px 2px var(--grist-color-slate);
|
||||
}
|
||||
|
||||
/* make room for lock icon */
|
||||
.readonly_editor .celleditor_cursor_editor .celleditor_text_editor,
|
||||
.readonly_editor .celleditor_cursor_editor .celleditor_content_measure {
|
||||
padding-left: 18px;
|
||||
}
|
||||
|
||||
.readonly_editor::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
margin: 4px 3px 0 3px;
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
background-color: #D0D0D0;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
-webkit-mask-position: center;
|
||||
-webkit-mask-size: contain;
|
||||
-webkit-mask-image: var(--icon-Lock);
|
||||
}
|
||||
|
||||
|
||||
.formula_editor {
|
||||
background-color: white;
|
||||
padding: 4px 0 2px 21px;
|
||||
|
@ -8,7 +8,7 @@ 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');
|
||||
const {observable} = require('grainjs');
|
||||
|
||||
/**
|
||||
* Required parameters:
|
||||
@ -38,12 +38,14 @@ function TextEditor(options) {
|
||||
this.editorState = this.autoDispose(observable(initialValue));
|
||||
|
||||
this.dom = dom('div.default_editor',
|
||||
kd.toggleClass("readonly_editor", options.readonly),
|
||||
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.boolAttr('readonly', options.readonly),
|
||||
kd.value(initialValue),
|
||||
this.commandGroup.attach(),
|
||||
|
||||
|
@ -331,6 +331,12 @@ export async function getColumnNames() {
|
||||
.filter(name => name !== '+');
|
||||
}
|
||||
|
||||
export async function getCardFieldLabels() {
|
||||
const section = await driver.findWait('.active_section', 4000);
|
||||
const labels = await section.findAll(".g_record_detail_label", el => el.getText());
|
||||
return labels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize the given grid column by a given number of pixels.
|
||||
*/
|
||||
@ -371,6 +377,12 @@ export async function getCursorPosition() {
|
||||
// This must be a detail view, and we just got the info we need.
|
||||
return {rowNum: parseInt(rowNum, 10), col: colName};
|
||||
} else {
|
||||
// We might be on a single card record
|
||||
const counter = await section.findAll(".grist-single-record__menu__count");
|
||||
if (counter.length) {
|
||||
const cardRow = (await counter[0].getText()).split(' OF ')[0];
|
||||
return { rowNum : parseInt(cardRow), col: colName };
|
||||
}
|
||||
// Otherwise, it's a grid view, and we need to use indices to look up the info.
|
||||
const gridRows = await section.findAll('.gridview_data_row_num');
|
||||
const gridRowNum = await gridRows[rowIndex].getText();
|
||||
|
Loading…
Reference in New Issue
Block a user