mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(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
This commit is contained in:
parent
f5e3a0a94d
commit
5f182841b9
@ -25,6 +25,7 @@ function AceEditor(options) {
|
|||||||
this.saveValueOnBlurEvent = !(options && (options.saveValueOnBlurEvent === false));
|
this.saveValueOnBlurEvent = !(options && (options.saveValueOnBlurEvent === false));
|
||||||
this.calcSize = (options && options.calcSize) || ((elem, size) => size);
|
this.calcSize = (options && options.calcSize) || ((elem, size) => size);
|
||||||
this.gristDoc = (options && options.gristDoc) || null;
|
this.gristDoc = (options && options.gristDoc) || null;
|
||||||
|
this.editorState = (options && options.editorState) || null;
|
||||||
|
|
||||||
this.editor = null;
|
this.editor = null;
|
||||||
this.editorDom = null;
|
this.editorDom = null;
|
||||||
@ -159,6 +160,15 @@ AceEditor.prototype.adjustContentToWidth = function() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides opportunity to execute some functionality when value in the editor has changed.
|
||||||
|
* Happens every time user types something to the control.
|
||||||
|
*/
|
||||||
|
AceEditor.prototype.onChange = function() {
|
||||||
|
if (this.editorState) this.editorState.set(this.getValue());
|
||||||
|
this.resize();
|
||||||
|
};
|
||||||
|
|
||||||
AceEditor.prototype.setFontSize = function(pxVal) {
|
AceEditor.prototype.setFontSize = function(pxVal) {
|
||||||
this.editor.setFontSize(pxVal);
|
this.editor.setFontSize(pxVal);
|
||||||
this.resize();
|
this.resize();
|
||||||
@ -186,7 +196,7 @@ AceEditor.prototype._setup = function() {
|
|||||||
this.session.setTabSize(2);
|
this.session.setTabSize(2);
|
||||||
this.session.setUseWrapMode(true);
|
this.session.setUseWrapMode(true);
|
||||||
|
|
||||||
this.editor.on('change', this.resize.bind(this));
|
this.editor.on('change', this.onChange.bind(this));
|
||||||
this.editor.$blockScrolling = Infinity;
|
this.editor.$blockScrolling = Infinity;
|
||||||
this.editor.setFontSize(11);
|
this.editor.setFontSize(11);
|
||||||
this.resize();
|
this.resize();
|
||||||
|
@ -440,7 +440,7 @@ export class ActionLog extends dispose.Disposable implements IDomComponent {
|
|||||||
const fieldIndex = viewSection.viewFields().peek().findIndex((f: any) => f.colId.peek() === colId);
|
const fieldIndex = viewSection.viewFields().peek().findIndex((f: any) => f.colId.peek() === colId);
|
||||||
|
|
||||||
// Finally, move cursor position to the section, column (if we found it), and row.
|
// Finally, move cursor position to the section, column (if we found it), and row.
|
||||||
this._gristDoc.moveToCursorPos({rowId, sectionId, fieldIndex});
|
this._gristDoc.moveToCursorPos({rowId, sectionId, fieldIndex}).catch(() => { /* do nothing */});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -230,7 +230,7 @@ _.extend(Base.prototype, BackboneEvents);
|
|||||||
* These commands are common to GridView and DetailView.
|
* These commands are common to GridView and DetailView.
|
||||||
*/
|
*/
|
||||||
BaseView.commonCommands = {
|
BaseView.commonCommands = {
|
||||||
input: function(input) { this.activateEditorAtCursor(input); },
|
input: function(input) { this.activateEditorAtCursor({init: input}); },
|
||||||
editField: function() { this.activateEditorAtCursor(); },
|
editField: function() { this.activateEditorAtCursor(); },
|
||||||
|
|
||||||
insertRecordBefore: function() { this.insertRow(this.cursor.rowIndex()); },
|
insertRecordBefore: function() { this.insertRow(this.cursor.rowIndex()); },
|
||||||
@ -269,7 +269,7 @@ BaseView.prototype.getLoadingDonePromise = function() {
|
|||||||
* @param {String} input: If given, initialize the editor with the given input (rather than the
|
* @param {String} input: If given, initialize the editor with the given input (rather than the
|
||||||
* original content of the cell).
|
* original content of the cell).
|
||||||
*/
|
*/
|
||||||
BaseView.prototype.activateEditorAtCursor = function(input) {
|
BaseView.prototype.activateEditorAtCursor = function(options) {
|
||||||
var builder = this.activeFieldBuilder();
|
var builder = this.activeFieldBuilder();
|
||||||
if (builder.isEditorActive()) {
|
if (builder.isEditorActive()) {
|
||||||
return;
|
return;
|
||||||
@ -286,7 +286,7 @@ BaseView.prototype.activateEditorAtCursor = function(input) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.editRowModel.assign(rowId);
|
this.editRowModel.assign(rowId);
|
||||||
builder.buildEditorDom(this.editRowModel, lazyRow, { 'init': input });
|
builder.buildEditorDom(this.editRowModel, lazyRow, options || {});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
65
app/client/components/CellPosition.ts
Normal file
65
app/client/components/CellPosition.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { CursorPos } from "app/client/components/Cursor";
|
||||||
|
import { DocModel } from "app/client/models/DocModel";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Absolute position of a cell in a document
|
||||||
|
*/
|
||||||
|
export interface CellPosition {
|
||||||
|
sectionId: number;
|
||||||
|
rowId: number;
|
||||||
|
colRef: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if two positions are equal.
|
||||||
|
* @param a First position
|
||||||
|
* @param b Second position
|
||||||
|
*/
|
||||||
|
export function samePosition(a: CellPosition, b: CellPosition) {
|
||||||
|
return a && b && a.colRef == b.colRef &&
|
||||||
|
a.sectionId == b.sectionId &&
|
||||||
|
a.rowId == b.rowId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts cursor position to cell absolute positions. Return null if the conversion is not
|
||||||
|
* possible (if cursor position doesn't have enough information)
|
||||||
|
* @param position Cursor position
|
||||||
|
* @param docModel Document model
|
||||||
|
*/
|
||||||
|
export function fromCursor(position: CursorPos, docModel: DocModel): CellPosition | null {
|
||||||
|
if (!position.sectionId || !position.rowId || position.fieldIndex == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
const section = docModel.viewSections.getRowModel(position.sectionId);
|
||||||
|
const colRef = section.viewFields().peek()[position.fieldIndex]?.colRef.peek();
|
||||||
|
|
||||||
|
const cursorPosition = {
|
||||||
|
rowId: position.rowId,
|
||||||
|
colRef,
|
||||||
|
sectionId: position.sectionId,
|
||||||
|
};
|
||||||
|
|
||||||
|
return cursorPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts cell's absolute position to current cursor position.
|
||||||
|
* @param position Cell's absolute position
|
||||||
|
* @param docModel DocModel
|
||||||
|
*/
|
||||||
|
export function toCursor(position: CellPosition, docModel: DocModel): CursorPos {
|
||||||
|
|
||||||
|
// translate colRef to fieldIndex
|
||||||
|
const fieldIndex = docModel.viewSections.getRowModel(position.sectionId)
|
||||||
|
.viewFields().peek()
|
||||||
|
.findIndex(x => x.colRef.peek() == position.colRef);
|
||||||
|
|
||||||
|
const cursorPosition = {
|
||||||
|
rowId: position.rowId,
|
||||||
|
fieldIndex,
|
||||||
|
sectionId: position.sectionId
|
||||||
|
};
|
||||||
|
|
||||||
|
return cursorPosition;
|
||||||
|
}
|
@ -60,6 +60,8 @@ export class Cursor extends Disposable {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public viewData: LazyArrayModel<BaseRowModel>;
|
public viewData: LazyArrayModel<BaseRowModel>;
|
||||||
|
// observable with current cursor position
|
||||||
|
public currentPosition: ko.Computed<CursorPos>;
|
||||||
|
|
||||||
public rowIndex: ko.Computed<number|null>; // May be null when there are no rows.
|
public rowIndex: ko.Computed<number|null>; // May be null when there are no rows.
|
||||||
public fieldIndex: ko.Observable<number>;
|
public fieldIndex: ko.Observable<number>;
|
||||||
@ -70,12 +72,14 @@ export class Cursor extends Disposable {
|
|||||||
// the rowIndex of the cursor is recalculated to match _rowId. When false, they will
|
// the rowIndex of the cursor is recalculated to match _rowId. When false, they will
|
||||||
// be out of sync.
|
// be out of sync.
|
||||||
private _isLive: ko.Observable<boolean> = ko.observable(true);
|
private _isLive: ko.Observable<boolean> = ko.observable(true);
|
||||||
|
private _sectionId: ko.Computed<number>;
|
||||||
|
|
||||||
constructor(baseView: BaseView, optCursorPos?: CursorPos) {
|
constructor(baseView: BaseView, optCursorPos?: CursorPos) {
|
||||||
super();
|
super();
|
||||||
optCursorPos = optCursorPos || {};
|
optCursorPos = optCursorPos || {};
|
||||||
this.viewData = baseView.viewData;
|
this.viewData = baseView.viewData;
|
||||||
|
|
||||||
|
this._sectionId = this.autoDispose(ko.computed(() => baseView.viewSection.id()))
|
||||||
this._rowId = ko.observable(optCursorPos.rowId || 0);
|
this._rowId = ko.observable(optCursorPos.rowId || 0);
|
||||||
this.rowIndex = this.autoDispose(ko.computed({
|
this.rowIndex = this.autoDispose(ko.computed({
|
||||||
read: () => {
|
read: () => {
|
||||||
@ -97,6 +101,9 @@ export class Cursor extends Disposable {
|
|||||||
|
|
||||||
// On dispose, save the current cursor position to the section model.
|
// On dispose, save the current cursor position to the section model.
|
||||||
this.onDispose(() => { baseView.viewSection.lastCursorPos = this.getCursorPos(); });
|
this.onDispose(() => { baseView.viewSection.lastCursorPos = this.getCursorPos(); });
|
||||||
|
|
||||||
|
// calculate current position
|
||||||
|
this.currentPosition = this.autoDispose(ko.computed(() => this._isLive() ? this.getCursorPos() : {}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the cursor position with rowId, rowIndex, and fieldIndex.
|
// Returns the cursor position with rowId, rowIndex, and fieldIndex.
|
||||||
@ -104,7 +111,8 @@ export class Cursor extends Disposable {
|
|||||||
return {
|
return {
|
||||||
rowId: nullAsUndefined(this._rowId()),
|
rowId: nullAsUndefined(this._rowId()),
|
||||||
rowIndex: nullAsUndefined(this.rowIndex()),
|
rowIndex: nullAsUndefined(this.rowIndex()),
|
||||||
fieldIndex: this.fieldIndex()
|
fieldIndex: this.fieldIndex(),
|
||||||
|
sectionId: this._sectionId()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
119
app/client/components/CursorMonitor.ts
Normal file
119
app/client/components/CursorMonitor.ts
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import { CursorPos } from "app/client/components/Cursor";
|
||||||
|
import { getStorage } from "app/client/lib/localStorageObs";
|
||||||
|
import { IDocPage } from "app/common/gristUrls";
|
||||||
|
import { Disposable } from "grainjs";
|
||||||
|
import { GristDoc } from "app/client/components/GristDoc";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enriched cursor position with a view id
|
||||||
|
*/
|
||||||
|
export type ViewCursorPos = CursorPos & { viewId: number }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component for GristDoc that allows it to keep track of the latest cursor position.
|
||||||
|
* In case, when a document is reloaded abnormally, the latest cursor
|
||||||
|
* position should be restored from a local storage.
|
||||||
|
*/
|
||||||
|
export class CursorMonitor extends Disposable {
|
||||||
|
|
||||||
|
// abstraction to work with local storage
|
||||||
|
private _store: StorageWrapper;
|
||||||
|
// document id that this monitor is attached
|
||||||
|
private _docId: string;
|
||||||
|
// flag that tells if the position was already restored
|
||||||
|
// we track document's view change event, so we only want
|
||||||
|
// to react to that event once
|
||||||
|
private _restored = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
doc: GristDoc,
|
||||||
|
store?: Storage) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this._store = new StorageWrapper(store);
|
||||||
|
this._docId = doc.docId();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When document loads last cursor position should be restored from local storage.
|
||||||
|
*/
|
||||||
|
this._whenDocumentLoadsRestorePosition(doc);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When a cursor position changes, its value is stored in a local storage.
|
||||||
|
*/
|
||||||
|
this._whenCursorHasChangedStoreInMemory(doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _whenCursorHasChangedStoreInMemory(doc: GristDoc) {
|
||||||
|
// whenever current position changes, store it in the memory
|
||||||
|
this.autoDispose(doc.cursorPosition.addListener(pos => {
|
||||||
|
// if current position is not restored yet, don't change it
|
||||||
|
if (!this._restored) return;
|
||||||
|
if (pos) this.storePosition(pos);
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
private _whenDocumentLoadsRestorePosition(doc: GristDoc) {
|
||||||
|
// on view shown
|
||||||
|
this.autoDispose(doc.currentView.addListener(async view => {
|
||||||
|
// if the position was restored for this document do nothing
|
||||||
|
if (this._restored) return;
|
||||||
|
// set that we already restored the position, as some view is shown to the user
|
||||||
|
this._restored = true;
|
||||||
|
// if view wasn't rendered (page is displaying history or code view) do nothing
|
||||||
|
if (!view) return;
|
||||||
|
const viewId = doc.activeViewId.get();
|
||||||
|
const position = this.restoreLastPosition(viewId);
|
||||||
|
if (position) {
|
||||||
|
await doc.recursiveMoveToCursorPos(position, true);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private storePosition(pos: ViewCursorPos) {
|
||||||
|
this._store.update(this._docId, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
private restoreLastPosition(view: IDocPage) {
|
||||||
|
const lastPosition = this._store.read(this._docId);
|
||||||
|
this._store.clear(this._docId);
|
||||||
|
if (lastPosition && lastPosition.position.viewId == view) {
|
||||||
|
return lastPosition.position;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal implementations for working with local storage
|
||||||
|
class StorageWrapper {
|
||||||
|
|
||||||
|
constructor(private storage = getStorage()) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public update(docId: string, position: ViewCursorPos): void {
|
||||||
|
try {
|
||||||
|
const storage = this.storage;
|
||||||
|
const data = { docId, position, timestamp: Date.now() };
|
||||||
|
storage.setItem(this._key(docId), JSON.stringify(data));
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Can't store latest position in storage. Detail error " + e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public clear(docId: string,): void {
|
||||||
|
const storage = this.storage;
|
||||||
|
storage.removeItem(this._key(docId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public read(docId: string): { docId: string; position: ViewCursorPos; } | undefined {
|
||||||
|
const storage = this.storage;
|
||||||
|
const result = storage.getItem(this._key(docId));
|
||||||
|
if (!result) return undefined;
|
||||||
|
return JSON.parse(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _key(docId: string) {
|
||||||
|
return `grist-last-position-${docId}`;
|
||||||
|
}
|
||||||
|
}
|
@ -47,6 +47,10 @@
|
|||||||
outline: 2px dashed var(--grist-color-cursor);
|
outline: 2px dashed var(--grist-color-cursor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.g_record_detail_value.draft {
|
||||||
|
padding-right: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
.detail_row_num {
|
.detail_row_num {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
font-size: var(--grist-x-small-font-size);
|
font-size: var(--grist-x-small-font-size);
|
||||||
|
178
app/client/components/EditorMonitor.ts
Normal file
178
app/client/components/EditorMonitor.ts
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
import { getStorage } from "app/client/lib/localStorageObs";
|
||||||
|
import { Disposable, Emitter, Holder, IDisposableOwner } from "grainjs";
|
||||||
|
import { GristDoc } from "app/client/components/GristDoc";
|
||||||
|
import { FieldEditor, FieldEditorStateEvent } from "app/client/widgets/FieldEditor";
|
||||||
|
import { CellPosition, toCursor } from "app/client/components/CellPosition";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Feature for GristDoc that allows it to keep track of current editor's state.
|
||||||
|
* State is stored in local storage by default.
|
||||||
|
*/
|
||||||
|
export class EditorMonitor extends Disposable {
|
||||||
|
|
||||||
|
// abstraction to work with local storage
|
||||||
|
private _store: EditMemoryStorage;
|
||||||
|
// Holds a listener that is attached to the current view.
|
||||||
|
// It will be cleared after first trigger.
|
||||||
|
private _currentViewListener = Holder.create(this);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
doc: GristDoc,
|
||||||
|
store?: Storage) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
// create store
|
||||||
|
this._store = new EditMemoryStorage(doc.docId(), store);
|
||||||
|
|
||||||
|
// listen to document events to handle view load event
|
||||||
|
this._listenToReload(doc)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Monitors a field editor and updates latest edit position
|
||||||
|
* @param editor Field editor to track
|
||||||
|
*/
|
||||||
|
public monitorEditor(editor: FieldEditor) {
|
||||||
|
// typed helper to connect to the emitter
|
||||||
|
const on = typedListener(this);
|
||||||
|
// When user cancels the edit process, discard the memory of the last edited cell.
|
||||||
|
on(editor.cancelEmitter, (event) => {
|
||||||
|
this._store.clear();
|
||||||
|
});
|
||||||
|
// When saves a cell, discard the memory of the last edited cell.
|
||||||
|
on(editor.saveEmitter, (event) => {
|
||||||
|
this._store.clear();
|
||||||
|
});
|
||||||
|
// When user types in the editor, store its state
|
||||||
|
on(editor.changeEmitter, (event) => {
|
||||||
|
this._store.updateValue(event.position, event.currentState);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When document gets reloaded, restore last cursor position and a state of the editor.
|
||||||
|
* Returns last edited cell position and saved editor state or undefined.
|
||||||
|
*/
|
||||||
|
private _listenToReload(doc: GristDoc) {
|
||||||
|
// subscribe to the current view event on the GristDoc, but make sure that the handler
|
||||||
|
// will be invoked only once
|
||||||
|
let executed = false;
|
||||||
|
|
||||||
|
// on view shown
|
||||||
|
this._currentViewListener.autoDispose(doc.currentView.addListener(async view => {
|
||||||
|
if (executed) {
|
||||||
|
// remove the listener - we can't do it while the listener is actively executing
|
||||||
|
setImmediate(() => this._currentViewListener.clear());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
executed = true;
|
||||||
|
// if view wasn't rendered (page is displaying history or code view) do nothing
|
||||||
|
if (!view) return;
|
||||||
|
const lastEdit = this._restorePosition();
|
||||||
|
if (lastEdit) {
|
||||||
|
// set the cursor at right cell
|
||||||
|
await doc.recursiveMoveToCursorPos(toCursor(lastEdit.position, doc.docModel), true);
|
||||||
|
// activate the editor
|
||||||
|
await doc.activateEditorAtCursor({ state: lastEdit.value });
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// read the value from the storage
|
||||||
|
private _restorePosition() {
|
||||||
|
const entry = this._store.readValue();
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal implementation, not relevant to the main use case
|
||||||
|
|
||||||
|
// typed listener for the Emitter class
|
||||||
|
function typedListener(owner: IDisposableOwner) {
|
||||||
|
return function (emitter: Emitter, clb: (e: FieldEditorStateEvent) => any) {
|
||||||
|
owner.autoDispose(emitter.addListener(clb));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marker for a editor state - each editor can report any data as long as it is serialized
|
||||||
|
type EditorState = any;
|
||||||
|
|
||||||
|
// Schema for value stored in the local storage
|
||||||
|
type LastEditData = {
|
||||||
|
// absolute position for a cell
|
||||||
|
position: CellPosition,
|
||||||
|
// editor's state
|
||||||
|
value: EditorState
|
||||||
|
}
|
||||||
|
|
||||||
|
// Abstraction for working with local storage
|
||||||
|
class EditMemoryStorage {
|
||||||
|
|
||||||
|
private _entry: LastEditData | null = null;
|
||||||
|
private _timestamp = 0;
|
||||||
|
|
||||||
|
constructor(private _docId: string, private storage = getStorage()) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public updateValue(pos: CellPosition, value: EditorState): void {
|
||||||
|
this._entry = { position: pos, value: value };
|
||||||
|
this.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public readValue(): LastEditData | null {
|
||||||
|
this.load();
|
||||||
|
return this._entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public clear(): void {
|
||||||
|
this._entry = null;
|
||||||
|
this.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public timestamp(): number {
|
||||||
|
return this._timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _key() {
|
||||||
|
return `grist-last-edit-${this._docId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected load() {
|
||||||
|
const storage = this.storage;
|
||||||
|
const data = storage.getItem(this._key());
|
||||||
|
this._entry = null;
|
||||||
|
this._timestamp = 0;
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
try {
|
||||||
|
const { entry, timestamp } = JSON.parse(data);
|
||||||
|
if (typeof entry === 'undefined' || typeof timestamp != 'number') {
|
||||||
|
console.error("[EditMemory] Data in local storage has a different structure");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._entry = entry
|
||||||
|
this._timestamp = timestamp;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[EditMemory] Can't deserialize date from local storage");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected save(): void {
|
||||||
|
const storage = this.storage;
|
||||||
|
|
||||||
|
// if entry was removed - clear the storage
|
||||||
|
if (!this._entry) {
|
||||||
|
storage.removeItem(this._key());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this._timestamp = Date.now();
|
||||||
|
const data = { timestamp: this._timestamp, entry: this._entry };
|
||||||
|
storage.setItem(this._key(), JSON.stringify(data));
|
||||||
|
} catch (ex) {
|
||||||
|
console.error("Can't save current edited cell state. Error message: " + ex?.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -424,7 +424,7 @@ GridView.prototype.clearValues = function(selection) {
|
|||||||
|
|
||||||
const options = this._getColumnMenuOptions(selection);
|
const options = this._getColumnMenuOptions(selection);
|
||||||
if (options.isFormula === true) {
|
if (options.isFormula === true) {
|
||||||
this.activateEditorAtCursor('');
|
this.activateEditorAtCursor({ init: ''});
|
||||||
} else {
|
} else {
|
||||||
let clearAction = tableUtil.makeDeleteAction(selection);
|
let clearAction = tableUtil.makeDeleteAction(selection);
|
||||||
if (clearAction) {
|
if (clearAction) {
|
||||||
|
@ -51,6 +51,9 @@ import {IDisposable, Observable, styled} from 'grainjs';
|
|||||||
import * as ko from 'knockout';
|
import * as ko from 'knockout';
|
||||||
import cloneDeepWith = require('lodash/cloneDeepWith');
|
import cloneDeepWith = require('lodash/cloneDeepWith');
|
||||||
import isEqual = require('lodash/isEqual');
|
import isEqual = require('lodash/isEqual');
|
||||||
|
import * as BaseView from 'app/client/components/BaseView';
|
||||||
|
import { CursorMonitor, ViewCursorPos } from "app/client/components/CursorMonitor";
|
||||||
|
import { EditorMonitor } from "app/client/components/EditorMonitor";
|
||||||
|
|
||||||
const G = getBrowserGlobals('document', 'window');
|
const G = getBrowserGlobals('document', 'window');
|
||||||
|
|
||||||
@ -94,6 +97,10 @@ export class GristDoc extends DisposableWithEvents {
|
|||||||
public isReadonly = this.docPageModel.isReadonly;
|
public isReadonly = this.docPageModel.isReadonly;
|
||||||
public isReadonlyKo = toKo(ko, this.isReadonly);
|
public isReadonlyKo = toKo(ko, this.isReadonly);
|
||||||
public comparison: DocStateComparison|null;
|
public comparison: DocStateComparison|null;
|
||||||
|
// component for keeping track of latest cursor position
|
||||||
|
public cursorMonitor: CursorMonitor;
|
||||||
|
// component for keeping track of a cell that is being edited
|
||||||
|
public editorMonitor: EditorMonitor;
|
||||||
|
|
||||||
// Emitter triggered when the main doc area is resized.
|
// Emitter triggered when the main doc area is resized.
|
||||||
public readonly resizeEmitter = this.autoDispose(new Emitter());
|
public readonly resizeEmitter = this.autoDispose(new Emitter());
|
||||||
@ -103,6 +110,12 @@ export class GristDoc extends DisposableWithEvents {
|
|||||||
// most one instance of FieldEditor at any time.
|
// most one instance of FieldEditor at any time.
|
||||||
public readonly fieldEditorHolder = Holder.create(this);
|
public readonly fieldEditorHolder = Holder.create(this);
|
||||||
|
|
||||||
|
// Holds current view that is currently rendered
|
||||||
|
public currentView : Observable<BaseView | null>;
|
||||||
|
|
||||||
|
// Holds current cursor position with a view id
|
||||||
|
public cursorPosition : Computed<ViewCursorPos | undefined>;
|
||||||
|
|
||||||
private _actionLog: ActionLog;
|
private _actionLog: ActionLog;
|
||||||
private _undoStack: UndoStack;
|
private _undoStack: UndoStack;
|
||||||
private _lastOwnActionGroup: ActionGroupWithCursorPos|null = null;
|
private _lastOwnActionGroup: ActionGroupWithCursorPos|null = null;
|
||||||
@ -160,8 +173,8 @@ export class GristDoc extends DisposableWithEvents {
|
|||||||
this.autoDispose(subscribe(urlState().state, async (use, state) => {
|
this.autoDispose(subscribe(urlState().state, async (use, state) => {
|
||||||
if (state.hash) {
|
if (state.hash) {
|
||||||
try {
|
try {
|
||||||
const cursorPos = getCursorPosFromHash(state.hash);
|
const cursorPos = this._getCursorPosFromHash(state.hash);
|
||||||
await this._recursiveMoveToCursorPos(cursorPos, true, state.hash && state.hash.colRef);
|
await this.recursiveMoveToCursorPos(cursorPos, true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
reportError(e);
|
reportError(e);
|
||||||
} finally {
|
} finally {
|
||||||
@ -226,6 +239,45 @@ export class GristDoc extends DisposableWithEvents {
|
|||||||
|
|
||||||
// On window resize, trigger the resizeEmitter to update ViewLayout and individual BaseViews.
|
// On window resize, trigger the resizeEmitter to update ViewLayout and individual BaseViews.
|
||||||
this.autoDispose(dom.onElem(window, 'resize', () => this.resizeEmitter.emit()));
|
this.autoDispose(dom.onElem(window, 'resize', () => this.resizeEmitter.emit()));
|
||||||
|
|
||||||
|
// create current view observer
|
||||||
|
this.currentView = Observable.create<BaseView | null>(this, null);
|
||||||
|
// first create a computed observable for current view
|
||||||
|
const viewInstance = Computed.create(this, (use) => {
|
||||||
|
const section = use(this.viewModel.activeSection);
|
||||||
|
const view = use(section.viewInstance);
|
||||||
|
return view;
|
||||||
|
});
|
||||||
|
// then listen if the view is present, because we still need to wait for it load properly
|
||||||
|
this.autoDispose(viewInstance.addListener(async (view) => {
|
||||||
|
if (!view) return;
|
||||||
|
await view.getLoadingDonePromise();
|
||||||
|
this.currentView.set(view);
|
||||||
|
}))
|
||||||
|
|
||||||
|
// create observable for current cursor position
|
||||||
|
this.cursorPosition = Computed.create<ViewCursorPos | undefined>(this, use => {
|
||||||
|
// get the BaseView
|
||||||
|
const view = use(viewInstance);
|
||||||
|
if (!view) return undefined;
|
||||||
|
// get current viewId
|
||||||
|
const viewId = use(this.activeViewId);
|
||||||
|
if (typeof viewId != 'number') return undefined;
|
||||||
|
// read latest position
|
||||||
|
const currentPosition = use(view.cursor.currentPosition);
|
||||||
|
if (currentPosition) return { ...currentPosition, viewId }
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.cursorMonitor = CursorMonitor.create(this, this);
|
||||||
|
this.editorMonitor = EditorMonitor.create(this, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns current document's id
|
||||||
|
*/
|
||||||
|
public docId() {
|
||||||
|
return this.docPageModel.currentDocId.get()!;
|
||||||
}
|
}
|
||||||
|
|
||||||
public addOptionsTab(label: string, iconElem: any, contentObj: TabContent[], options: TabOptions): IDisposable {
|
public addOptionsTab(label: string, iconElem: any, contentObj: TabContent[], options: TabOptions): IDisposable {
|
||||||
@ -271,7 +323,7 @@ export class GristDoc extends DisposableWithEvents {
|
|||||||
* Switch to the view/section and scroll to the record indicated by cursorPos. If cursorPos is
|
* Switch to the view/section and scroll to the record indicated by cursorPos. If cursorPos is
|
||||||
* null, then moves to a position best suited for optActionGroup (not yet implemented).
|
* null, then moves to a position best suited for optActionGroup (not yet implemented).
|
||||||
*/
|
*/
|
||||||
public moveToCursorPos(cursorPos?: CursorPos, optActionGroup?: ActionGroup): void {
|
public async moveToCursorPos(cursorPos?: CursorPos, optActionGroup?: ActionGroup): Promise<void> {
|
||||||
if (!cursorPos || cursorPos.sectionId == null) {
|
if (!cursorPos || cursorPos.sectionId == null) {
|
||||||
// TODO We could come up with a suitable cursorPos here based on the action itself.
|
// TODO We could come up with a suitable cursorPos here based on the action itself.
|
||||||
// This should only come up if trying to undo/redo after reloading a page (since the cursorPos
|
// This should only come up if trying to undo/redo after reloading a page (since the cursorPos
|
||||||
@ -280,10 +332,14 @@ export class GristDoc extends DisposableWithEvents {
|
|||||||
// place from any action in the action log.
|
// place from any action in the action log.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
this._switchToSectionId(cursorPos.sectionId)
|
const viewInstance = await this._switchToSectionId(cursorPos.sectionId)
|
||||||
.then(viewInstance => (viewInstance && viewInstance.setCursorPos(cursorPos)))
|
if (viewInstance) {
|
||||||
.catch(reportError);
|
viewInstance.setCursorPos(cursorPos);
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
reportError(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -530,7 +586,99 @@ export class GristDoc extends DisposableWithEvents {
|
|||||||
return rulesTable.numRecords() > rulesTable.filterRowIds({permissionsText: '', permissions: 63}).length;
|
return rulesTable.numRecords() > rulesTable.filterRowIds({permissionsText: '', permissions: 63}).length;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getToolContent(tool: typeof RightPanelTool.type): IExtraTool|null {
|
/**
|
||||||
|
* Move to the desired cursor position. If colRef is supplied, the cursor will be
|
||||||
|
* moved to a field with that colRef. Any linked sections that need their cursors
|
||||||
|
* moved in order to achieve the desired outcome are handled recursively.
|
||||||
|
* If setAsActiveSection is true, the section in cursorPos is set as the current
|
||||||
|
* active section.
|
||||||
|
*/
|
||||||
|
public async recursiveMoveToCursorPos(cursorPos: CursorPos, setAsActiveSection: boolean): Promise<void> {
|
||||||
|
try {
|
||||||
|
if (!cursorPos.sectionId) { throw new Error('sectionId required'); }
|
||||||
|
if (!cursorPos.rowId) { throw new Error('rowId required'); }
|
||||||
|
const section = this.docModel.viewSections.getRowModel(cursorPos.sectionId);
|
||||||
|
const srcSection = section.linkSrcSection.peek();
|
||||||
|
if (srcSection.id.peek()) {
|
||||||
|
// We're in a linked section, so we need to recurse to make sure the row we want
|
||||||
|
// will be visible.
|
||||||
|
const linkTargetCol = section.linkTargetCol.peek();
|
||||||
|
let controller: any;
|
||||||
|
if (linkTargetCol.colId.peek()) {
|
||||||
|
const destTable = await this._getTableData(section);
|
||||||
|
controller = destTable.getValue(cursorPos.rowId, linkTargetCol.colId.peek());
|
||||||
|
} else {
|
||||||
|
controller = cursorPos.rowId;
|
||||||
|
}
|
||||||
|
const colId = section.linkSrcCol.peek().colId.peek();
|
||||||
|
let srcRowId: any;
|
||||||
|
const isSrcSummary = srcSection.table.peek().summarySource.peek().id.peek();
|
||||||
|
if (!colId && !isSrcSummary) {
|
||||||
|
// Simple case - source linked by rowId, not a summary.
|
||||||
|
srcRowId = controller;
|
||||||
|
} else {
|
||||||
|
const srcTable = await this._getTableData(srcSection);
|
||||||
|
if (!colId) {
|
||||||
|
// must be a summary -- otherwise dealt with earlier.
|
||||||
|
const destTable = await this._getTableData(section);
|
||||||
|
const filter: { [key: string]: any } = {};
|
||||||
|
for (const c of srcSection.table.peek().columns.peek().peek()) {
|
||||||
|
if (c.summarySourceCol.peek()) {
|
||||||
|
const filterColId = c.summarySource.peek().colId.peek();
|
||||||
|
const destValue = destTable.getValue(cursorPos.rowId, filterColId);
|
||||||
|
filter[filterColId] = destValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const result = srcTable.filterRecords(filter); // Should just have one record, or 0.
|
||||||
|
srcRowId = result[0] && result[0].id;
|
||||||
|
} else {
|
||||||
|
srcRowId = srcTable.findRow(colId, controller);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!srcRowId || typeof srcRowId !== 'number') { throw new Error('cannot trace rowId'); }
|
||||||
|
await this.recursiveMoveToCursorPos({
|
||||||
|
rowId: srcRowId,
|
||||||
|
sectionId: srcSection.id.peek()
|
||||||
|
}, false);
|
||||||
|
}
|
||||||
|
const view: ViewRec = section.view.peek();
|
||||||
|
const viewId = view.getRowId();
|
||||||
|
if (viewId != this.activeViewId.get()) await this.openDocPage(view.getRowId());
|
||||||
|
if (setAsActiveSection) { view.activeSectionId(cursorPos.sectionId); }
|
||||||
|
const fieldIndex = cursorPos.fieldIndex;
|
||||||
|
const viewInstance = await waitObs(section.viewInstance);
|
||||||
|
if (!viewInstance) { throw new Error('view not found'); }
|
||||||
|
// Give any synchronous initial cursor setting a chance to happen.
|
||||||
|
await delay(0);
|
||||||
|
viewInstance.setCursorPos({ ...cursorPos, fieldIndex });
|
||||||
|
// TODO: column selection not working on card/detail view, or getting overridden -
|
||||||
|
// look into it (not a high priority for now since feature not easily discoverable
|
||||||
|
// in this view).
|
||||||
|
} catch (e) {
|
||||||
|
console.debug(`_recursiveMoveToCursorPos(${JSON.stringify(cursorPos)}): ${e}`);
|
||||||
|
throw new UserError('There was a problem finding the desired cell.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens up an editor at cursor position
|
||||||
|
* @param input Optional. Cell's initial value
|
||||||
|
*/
|
||||||
|
public async activateEditorAtCursor(options: { init?: string, state?: any}) {
|
||||||
|
const view = await this.waitForView();
|
||||||
|
view?.activateEditorAtCursor(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Waits for a view to be ready
|
||||||
|
*/
|
||||||
|
private async waitForView() {
|
||||||
|
const view = await waitObs(this.viewModel.activeSection.peek().viewInstance);
|
||||||
|
await view?.getLoadingDonePromise();
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getToolContent(tool: typeof RightPanelTool.type): IExtraTool | null {
|
||||||
switch (tool) {
|
switch (tool) {
|
||||||
case 'docHistory': {
|
case 'docHistory': {
|
||||||
return {icon: 'Log', label: 'Document History', content: this._docHistory};
|
return {icon: 'Log', label: 'Document History', content: this._docHistory};
|
||||||
@ -623,80 +771,6 @@ export class GristDoc extends DisposableWithEvents {
|
|||||||
return waitObs(section.viewInstance);
|
return waitObs(section.viewInstance);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Move to the desired cursor position. If colRef is supplied, the cursor will be
|
|
||||||
* moved to a field with that colRef. Any linked sections that need their cursors
|
|
||||||
* moved in order to achieve the desired outcome are handled recursively.
|
|
||||||
* If setAsActiveSection is true, the section in cursorPos is set as the current
|
|
||||||
* active section.
|
|
||||||
*/
|
|
||||||
private async _recursiveMoveToCursorPos(cursorPos: CursorPos, setAsActiveSection: boolean,
|
|
||||||
colRef?: number): Promise<void> {
|
|
||||||
try {
|
|
||||||
if (!cursorPos.sectionId) { throw new Error('sectionId required'); }
|
|
||||||
if (!cursorPos.rowId) { throw new Error('rowId required'); }
|
|
||||||
const section = this.docModel.viewSections.getRowModel(cursorPos.sectionId);
|
|
||||||
const srcSection = section.linkSrcSection.peek();
|
|
||||||
if (srcSection.id.peek()) {
|
|
||||||
// We're in a linked section, so we need to recurse to make sure the row we want
|
|
||||||
// will be visible.
|
|
||||||
const linkTargetCol = section.linkTargetCol.peek();
|
|
||||||
let controller: any;
|
|
||||||
if (linkTargetCol.colId.peek()) {
|
|
||||||
const destTable = await this._getTableData(section);
|
|
||||||
controller = destTable.getValue(cursorPos.rowId, linkTargetCol.colId.peek());
|
|
||||||
} else {
|
|
||||||
controller = cursorPos.rowId;
|
|
||||||
}
|
|
||||||
const colId = section.linkSrcCol.peek().colId.peek();
|
|
||||||
let srcRowId: any;
|
|
||||||
const isSrcSummary = srcSection.table.peek().summarySource.peek().id.peek();
|
|
||||||
if (!colId && !isSrcSummary) {
|
|
||||||
// Simple case - source linked by rowId, not a summary.
|
|
||||||
srcRowId = controller;
|
|
||||||
} else {
|
|
||||||
const srcTable = await this._getTableData(srcSection);
|
|
||||||
if (!colId) {
|
|
||||||
// must be a summary -- otherwise dealt with earlier.
|
|
||||||
const destTable = await this._getTableData(section);
|
|
||||||
const filter: {[key: string]: any} = {};
|
|
||||||
for (const c of srcSection.table.peek().columns.peek().peek()) {
|
|
||||||
if (c.summarySourceCol.peek()) {
|
|
||||||
const filterColId = c.summarySource.peek().colId.peek();
|
|
||||||
const destValue = destTable.getValue(cursorPos.rowId, filterColId);
|
|
||||||
filter[filterColId] = destValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const result = srcTable.filterRecords(filter); // Should just have one record, or 0.
|
|
||||||
srcRowId = result[0] && result[0].id;
|
|
||||||
} else {
|
|
||||||
srcRowId = srcTable.findRow(colId, controller);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!srcRowId || typeof srcRowId !== 'number') { throw new Error('cannot trace rowId'); }
|
|
||||||
await this._recursiveMoveToCursorPos({
|
|
||||||
rowId: srcRowId,
|
|
||||||
sectionId: srcSection.id.peek()
|
|
||||||
}, false);
|
|
||||||
}
|
|
||||||
const view: ViewRec = section.view.peek();
|
|
||||||
await this.openDocPage(view.getRowId());
|
|
||||||
if (setAsActiveSection) { view.activeSectionId(cursorPos.sectionId); }
|
|
||||||
const fieldIndex = colRef ? section.viewFields().peek().findIndex(f => f.colRef.peek() === colRef) : undefined;
|
|
||||||
const viewInstance = await waitObs(section.viewInstance);
|
|
||||||
if (!viewInstance) { throw new Error('view not found'); }
|
|
||||||
// Give any synchronous initial cursor setting a chance to happen.
|
|
||||||
await delay(0);
|
|
||||||
viewInstance.setCursorPos({...cursorPos, fieldIndex});
|
|
||||||
// TODO: column selection not working on card/detail view, or getting overridden -
|
|
||||||
// look into it (not a high priority for now since feature not easily discoverable
|
|
||||||
// in this view).
|
|
||||||
} catch (e) {
|
|
||||||
console.debug(`_recursiveMoveToCursorPos(${JSON.stringify(cursorPos)}): ${e}`);
|
|
||||||
throw new UserError('There was a problem finding the desired cell.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _getTableData(section: ViewSectionRec): Promise<TableData> {
|
private async _getTableData(section: ViewSectionRec): Promise<TableData> {
|
||||||
const viewInstance = await waitObs(section.viewInstance);
|
const viewInstance = await waitObs(section.viewInstance);
|
||||||
if (!viewInstance) { throw new Error('view not found'); }
|
if (!viewInstance) { throw new Error('view not found'); }
|
||||||
@ -705,13 +779,21 @@ export class GristDoc extends DisposableWithEvents {
|
|||||||
if (!table) { throw new Error('no section table'); }
|
if (!table) { throw new Error('no section table'); }
|
||||||
return table;
|
return table;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a url hash to a cursor position.
|
* Convert a url hash to a cursor position.
|
||||||
*/
|
*/
|
||||||
function getCursorPosFromHash(hash: HashLink): CursorPos {
|
private _getCursorPosFromHash(hash: HashLink): CursorPos {
|
||||||
return { rowId: hash.rowId, sectionId: hash.sectionId };
|
const cursorPos : CursorPos = { rowId: hash.rowId, sectionId: hash.sectionId };
|
||||||
|
if (cursorPos.sectionId != undefined && hash.colRef !== undefined){
|
||||||
|
// translate colRef to a fieldIndex
|
||||||
|
const section = this.docModel.viewSections.getRowModel(cursorPos.sectionId);
|
||||||
|
const fieldIndex = section.viewFields.peek().all()
|
||||||
|
.findIndex(x=> x.colRef.peek() == hash.colRef);
|
||||||
|
if (fieldIndex >= 0) cursorPos.fieldIndex = fieldIndex;
|
||||||
|
}
|
||||||
|
return cursorPos;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function finalizeAnchor() {
|
async function finalizeAnchor() {
|
||||||
|
@ -117,13 +117,13 @@ export class UndoStack extends dispose.Disposable {
|
|||||||
// context where the change was originally made. We jump first immediately to feel more
|
// context where the change was originally made. We jump first immediately to feel more
|
||||||
// responsive, then again when the action is done. The second jump matters more for most
|
// responsive, then again when the action is done. The second jump matters more for most
|
||||||
// changes, but the first is the important one when Undoing an AddRecord.
|
// changes, but the first is the important one when Undoing an AddRecord.
|
||||||
this._gristDoc.moveToCursorPos(ag.cursorPos, ag);
|
this._gristDoc.moveToCursorPos(ag.cursorPos, ag).catch(() => {/* do nothing */})
|
||||||
await this._gristDoc.docComm.applyUserActionsById(
|
await this._gristDoc.docComm.applyUserActionsById(
|
||||||
actionGroups.map(a => a.actionNum),
|
actionGroups.map(a => a.actionNum),
|
||||||
actionGroups.map(a => a.actionHash),
|
actionGroups.map(a => a.actionHash),
|
||||||
isUndo,
|
isUndo,
|
||||||
{ otherId: ag.actionNum });
|
{ otherId: ag.actionNum });
|
||||||
this._gristDoc.moveToCursorPos(ag.cursorPos, ag);
|
this._gristDoc.moveToCursorPos(ag.cursorPos, ag).catch(() => {/* do nothing */})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
err.message = `Failed to apply ${isUndo ? 'undo' : 'redo'} action: ${err.message}`;
|
err.message = `Failed to apply ${isUndo ? 'undo' : 'redo'} action: ${err.message}`;
|
||||||
throw err;
|
throw err;
|
||||||
|
@ -47,6 +47,10 @@
|
|||||||
background-color: var(--grist-color-selection);
|
background-color: var(--grist-color-selection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.field.draft {
|
||||||
|
padding-right: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
.field_clip {
|
.field_clip {
|
||||||
padding: 3px 3px 0px 3px;
|
padding: 3px 3px 0px 3px;
|
||||||
font-family: var(--grist-font-family-data);
|
font-family: var(--grist-font-family-data);
|
||||||
|
6
app/client/declarations.d.ts
vendored
6
app/client/declarations.d.ts
vendored
@ -45,6 +45,11 @@ declare module "app/client/components/BaseView" {
|
|||||||
import {DomArg} from 'grainjs';
|
import {DomArg} from 'grainjs';
|
||||||
import {IOpenController} from 'popweasel';
|
import {IOpenController} from 'popweasel';
|
||||||
|
|
||||||
|
type Options = {
|
||||||
|
init? : string,
|
||||||
|
state? : any
|
||||||
|
}
|
||||||
|
|
||||||
namespace BaseView {}
|
namespace BaseView {}
|
||||||
class BaseView extends Disposable {
|
class BaseView extends Disposable {
|
||||||
public viewSection: ViewSectionRec;
|
public viewSection: ViewSectionRec;
|
||||||
@ -63,6 +68,7 @@ declare module "app/client/components/BaseView" {
|
|||||||
public createFilterMenu(ctl: IOpenController, field: ViewFieldRec, onClose?: () => void): HTMLElement;
|
public createFilterMenu(ctl: IOpenController, field: ViewFieldRec, onClose?: () => void): HTMLElement;
|
||||||
public buildTitleControls(): DomArg;
|
public buildTitleControls(): DomArg;
|
||||||
public getLoadingDonePromise(): Promise<void>;
|
public getLoadingDonePromise(): Promise<void>;
|
||||||
|
public activateEditorAtCursor(options?: Options) : void;
|
||||||
public onResize(): void;
|
public onResize(): void;
|
||||||
public prepareToPrint(onOff: boolean): void;
|
public prepareToPrint(onOff: boolean): void;
|
||||||
public moveEditRowToCursor(): DataRowModel;
|
public moveEditRowToCursor(): DataRowModel;
|
||||||
|
@ -65,6 +65,7 @@ export type IconName = "ChartArea" |
|
|||||||
"Page" |
|
"Page" |
|
||||||
"PanelLeft" |
|
"PanelLeft" |
|
||||||
"PanelRight" |
|
"PanelRight" |
|
||||||
|
"Pencil" |
|
||||||
"PinBig" |
|
"PinBig" |
|
||||||
"PinSmall" |
|
"PinSmall" |
|
||||||
"Pivot" |
|
"Pivot" |
|
||||||
@ -155,6 +156,7 @@ export const IconList: IconName[] = ["ChartArea",
|
|||||||
"Page",
|
"Page",
|
||||||
"PanelLeft",
|
"PanelLeft",
|
||||||
"PanelRight",
|
"PanelRight",
|
||||||
|
"Pencil",
|
||||||
"PinBig",
|
"PinBig",
|
||||||
"PinSmall",
|
"PinSmall",
|
||||||
"Pivot",
|
"Pivot",
|
||||||
|
@ -41,7 +41,7 @@ function DateEditor(options) {
|
|||||||
TextEditor.call(this, _.defaults(options, { placeholder: placeholder }));
|
TextEditor.call(this, _.defaults(options, { placeholder: placeholder }));
|
||||||
|
|
||||||
// Set the edited value, if not explicitly given, to the formatted version of cellValue.
|
// Set the edited value, if not explicitly given, to the formatted version of cellValue.
|
||||||
this.textInput.value = gutil.undefDefault(options.editValue,
|
this.textInput.value = gutil.undef(options.state, options.editValue,
|
||||||
this.formatValue(options.cellValue, this.safeFormat));
|
this.formatValue(options.cellValue, this.safeFormat));
|
||||||
|
|
||||||
// Indicates whether keyboard navigation is active for the datepicker.
|
// Indicates whether keyboard navigation is active for the datepicker.
|
||||||
|
@ -42,10 +42,22 @@ function DateTimeEditor(options) {
|
|||||||
kd.attr('placeholder', moment.tz('0', 'H', this.timezone).format(this._timeFormat)),
|
kd.attr('placeholder', moment.tz('0', 'H', this.timezone).format(this._timeFormat)),
|
||||||
kd.value(this.formatValue(options.cellValue, this._timeFormat)),
|
kd.value(this.formatValue(options.cellValue, this._timeFormat)),
|
||||||
this.commandGroup.attach(),
|
this.commandGroup.attach(),
|
||||||
dom.on('input', () => this._resizeInput())
|
dom.on('input', () => this.onChange())
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// If the edit value is encoded json, use those values as a starting point
|
||||||
|
if (typeof options.state == 'string') {
|
||||||
|
try {
|
||||||
|
const { date, time } = JSON.parse(options.state);
|
||||||
|
this._dateInput.value = date;
|
||||||
|
this._timeInput.value = time;
|
||||||
|
this.onChange();
|
||||||
|
} catch(e) {
|
||||||
|
console.error("DateTimeEditor can't restore its previous state")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dispose.makeDisposable(DateTimeEditor);
|
dispose.makeDisposable(DateTimeEditor);
|
||||||
@ -77,6 +89,18 @@ DateTimeEditor.prototype._setFocus = function(index) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Occurs when user types something into the editor
|
||||||
|
*/
|
||||||
|
DateTimeEditor.prototype.onChange = function() {
|
||||||
|
this._resizeInput();
|
||||||
|
|
||||||
|
// store editor state as an encoded JSON string
|
||||||
|
const date = this._dateInput.value;
|
||||||
|
const time = this._timeInput.value;
|
||||||
|
this.editorState.set(JSON.stringify({ date, time}));
|
||||||
|
}
|
||||||
|
|
||||||
DateTimeEditor.prototype.getCellValue = function() {
|
DateTimeEditor.prototype.getCellValue = function() {
|
||||||
let date = this._dateInput.value;
|
let date = this._dateInput.value;
|
||||||
let time = this._timeInput.value;
|
let time = this._timeInput.value;
|
||||||
|
@ -27,7 +27,8 @@ import * as gristTypes from 'app/common/gristTypes';
|
|||||||
import * as gutil from 'app/common/gutil';
|
import * as gutil from 'app/common/gutil';
|
||||||
import { CellValue } from 'app/plugin/GristData';
|
import { CellValue } from 'app/plugin/GristData';
|
||||||
import { delay } from 'bluebird';
|
import { delay } from 'bluebird';
|
||||||
import { Computed, Disposable, fromKo, dom as grainjsDom, Holder, IDisposable, makeTestId } from 'grainjs';
|
import { Computed, Disposable, fromKo, dom as grainjsDom,
|
||||||
|
Holder, IDisposable, makeTestId } from 'grainjs';
|
||||||
import * as ko from 'knockout';
|
import * as ko from 'knockout';
|
||||||
import * as _ from 'underscore';
|
import * as _ from 'underscore';
|
||||||
|
|
||||||
@ -451,7 +452,8 @@ export class FieldBuilder extends Disposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public buildEditorDom(editRow: DataRowModel, mainRowModel: DataRowModel, options: {
|
public buildEditorDom(editRow: DataRowModel, mainRowModel: DataRowModel, options: {
|
||||||
init?: string
|
init?: string,
|
||||||
|
state?: any
|
||||||
}) {
|
}) {
|
||||||
// If the user attempts to edit a value during transform, finalize (i.e. cancel or execute)
|
// If the user attempts to edit a value during transform, finalize (i.e. cancel or execute)
|
||||||
// the transform.
|
// the transform.
|
||||||
@ -485,6 +487,7 @@ export class FieldBuilder extends Disposable {
|
|||||||
cellElem,
|
cellElem,
|
||||||
editorCtor,
|
editorCtor,
|
||||||
startVal: options.init,
|
startVal: options.init,
|
||||||
|
state : options.state
|
||||||
});
|
});
|
||||||
|
|
||||||
// Put the FieldEditor into a holder in GristDoc too. This way any existing FieldEditor (perhaps
|
// Put the FieldEditor into a holder in GristDoc too. This way any existing FieldEditor (perhaps
|
||||||
|
@ -13,8 +13,9 @@ import {asyncOnce} from "app/common/AsyncCreate";
|
|||||||
import {CellValue} from "app/common/DocActions";
|
import {CellValue} from "app/common/DocActions";
|
||||||
import {isRaisedException} from 'app/common/gristTypes';
|
import {isRaisedException} from 'app/common/gristTypes';
|
||||||
import * as gutil from 'app/common/gutil';
|
import * as gutil from 'app/common/gutil';
|
||||||
import {Disposable, Holder, IDisposable, MultiHolder, Observable} from 'grainjs';
|
import {Disposable, Emitter, Holder, IDisposable, MultiHolder, Observable} from 'grainjs';
|
||||||
import isEqual = require('lodash/isEqual');
|
import isEqual = require('lodash/isEqual');
|
||||||
|
import { CellPosition } from "app/client/components/CellPosition";
|
||||||
|
|
||||||
type IEditorConstructor = typeof NewBaseEditor;
|
type IEditorConstructor = typeof NewBaseEditor;
|
||||||
|
|
||||||
@ -46,7 +47,18 @@ export async function setAndSave(editRow: DataRowModel, field: ViewFieldRec, val
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type FieldEditorStateEvent = {
|
||||||
|
position : CellPosition,
|
||||||
|
currentState : any,
|
||||||
|
type: string
|
||||||
|
}
|
||||||
|
|
||||||
export class FieldEditor extends Disposable {
|
export class FieldEditor extends Disposable {
|
||||||
|
|
||||||
|
public readonly saveEmitter = this.autoDispose(new Emitter());
|
||||||
|
public readonly cancelEmitter = this.autoDispose(new Emitter());
|
||||||
|
public readonly changeEmitter = this.autoDispose(new Emitter());
|
||||||
|
|
||||||
private _gristDoc: GristDoc;
|
private _gristDoc: GristDoc;
|
||||||
private _field: ViewFieldRec;
|
private _field: ViewFieldRec;
|
||||||
private _cursor: Cursor;
|
private _cursor: Cursor;
|
||||||
@ -65,6 +77,7 @@ export class FieldEditor extends Disposable {
|
|||||||
cellElem: Element,
|
cellElem: Element,
|
||||||
editorCtor: IEditorConstructor,
|
editorCtor: IEditorConstructor,
|
||||||
startVal?: string,
|
startVal?: string,
|
||||||
|
state?: any
|
||||||
}) {
|
}) {
|
||||||
super();
|
super();
|
||||||
this._gristDoc = options.gristDoc;
|
this._gristDoc = options.gristDoc;
|
||||||
@ -105,24 +118,30 @@ export class FieldEditor extends Disposable {
|
|||||||
.catch(reportError);
|
.catch(reportError);
|
||||||
},
|
},
|
||||||
fieldEditSaveHere: () => { this._saveEdit().catch(reportError); },
|
fieldEditSaveHere: () => { this._saveEdit().catch(reportError); },
|
||||||
fieldEditCancel: () => { this.dispose(); },
|
fieldEditCancel: () => { this._cancelEdit(); },
|
||||||
prevField: () => { this._saveEdit().then(commands.allCommands.prevField.run).catch(reportError); },
|
prevField: () => { this._saveEdit().then(commands.allCommands.prevField.run).catch(reportError); },
|
||||||
nextField: () => { this._saveEdit().then(commands.allCommands.nextField.run).catch(reportError); },
|
nextField: () => { this._saveEdit().then(commands.allCommands.nextField.run).catch(reportError); },
|
||||||
makeFormula: () => this._makeFormula(),
|
makeFormula: () => this._makeFormula(),
|
||||||
unmakeFormula: () => this._unmakeFormula(),
|
unmakeFormula: () => this._unmakeFormula(),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.rebuildEditor(isFormula, editValue, Number.POSITIVE_INFINITY);
|
const state : any = options.state;
|
||||||
|
|
||||||
|
this.rebuildEditor(isFormula, editValue, Number.POSITIVE_INFINITY, state);
|
||||||
|
|
||||||
if (offerToMakeFormula) {
|
if (offerToMakeFormula) {
|
||||||
this._offerToMakeFormula();
|
this._offerToMakeFormula();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// connect this editor to editor monitor, it will restore this editor
|
||||||
|
// when user or server refreshes the browser
|
||||||
|
this._gristDoc.editorMonitor.monitorEditor(this);
|
||||||
|
|
||||||
setupEditorCleanup(this, this._gristDoc, this._field, this._saveEdit);
|
setupEditorCleanup(this, this._gristDoc, this._field, this._saveEdit);
|
||||||
}
|
}
|
||||||
|
|
||||||
// cursorPos refers to the position of the caret within the editor.
|
// cursorPos refers to the position of the caret within the editor.
|
||||||
public rebuildEditor(isFormula: boolean, editValue: string|undefined, cursorPos: number) {
|
public rebuildEditor(isFormula: boolean, editValue: string|undefined, cursorPos: number, state? : any) {
|
||||||
const editorCtor: IEditorConstructor = isFormula ? FormulaEditor : this._editorCtor;
|
const editorCtor: IEditorConstructor = isFormula ? FormulaEditor : this._editorCtor;
|
||||||
|
|
||||||
const column = this._field.column();
|
const column = this._field.column();
|
||||||
@ -142,11 +161,38 @@ export class FieldEditor extends Disposable {
|
|||||||
formulaError: getFormulaError(this._gristDoc, this._editRow, column),
|
formulaError: getFormulaError(this._gristDoc, this._editRow, column),
|
||||||
editValue,
|
editValue,
|
||||||
cursorPos,
|
cursorPos,
|
||||||
|
state,
|
||||||
commands: this._editCommands,
|
commands: this._editCommands,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// if editor supports live changes, connect it to the change emitter
|
||||||
|
if (editor.editorState) {
|
||||||
|
editor.autoDispose(editor.editorState.addListener((currentState) => {
|
||||||
|
const event : FieldEditorStateEvent = {
|
||||||
|
position : this.cellPosition(),
|
||||||
|
currentState,
|
||||||
|
type : this._field.column.peek().pureType.peek()
|
||||||
|
}
|
||||||
|
this.changeEmitter.emit(event);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
editor.attach(this._cellElem);
|
editor.attach(this._cellElem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// calculate current cell's absolute position
|
||||||
|
private cellPosition() {
|
||||||
|
const rowId = this._editRow.getRowId();
|
||||||
|
const colRef = this._field.colRef.peek();
|
||||||
|
const sectionId = this._field.viewSection.peek().id.peek();
|
||||||
|
const position = {
|
||||||
|
rowId,
|
||||||
|
colRef,
|
||||||
|
sectionId
|
||||||
|
}
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
|
||||||
private _makeFormula() {
|
private _makeFormula() {
|
||||||
const editor = this._editorHolder.get();
|
const editor = this._editorHolder.get();
|
||||||
// On keyPress of "=" on textInput, consider turning the column into a formula.
|
// On keyPress of "=" on textInput, consider turning the column into a formula.
|
||||||
@ -192,6 +238,17 @@ export class FieldEditor extends Disposable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cancels the edit
|
||||||
|
private _cancelEdit() {
|
||||||
|
const event : FieldEditorStateEvent = {
|
||||||
|
position : this.cellPosition(),
|
||||||
|
currentState : this._editorHolder.get()?.editorState?.get(),
|
||||||
|
type : this._field.column.peek().pureType.peek()
|
||||||
|
}
|
||||||
|
this.cancelEmitter.emit(event);
|
||||||
|
this.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
// Returns true if Enter/Shift+Enter should NOT move the cursor, for instance if the current
|
// Returns true if Enter/Shift+Enter should NOT move the cursor, for instance if the current
|
||||||
// record got reordered (i.e. the cursor jumped), or when editing a formula.
|
// record got reordered (i.e. the cursor jumped), or when editing a formula.
|
||||||
private async _doSaveEdit(): Promise<boolean> {
|
private async _doSaveEdit(): Promise<boolean> {
|
||||||
@ -238,6 +295,14 @@ export class FieldEditor extends Disposable {
|
|||||||
waitPromise = setAndSave(this._editRow, this._field, value);
|
waitPromise = setAndSave(this._editRow, this._field, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const event : FieldEditorStateEvent = {
|
||||||
|
position : this.cellPosition(),
|
||||||
|
currentState : this._editorHolder.get()?.editorState?.get(),
|
||||||
|
type : this._field.column.peek().pureType.peek()
|
||||||
|
}
|
||||||
|
this.saveEmitter.emit(event);
|
||||||
|
|
||||||
const cursor = this._cursor;
|
const cursor = this._cursor;
|
||||||
// Deactivate the editor. We are careful to avoid using `this` afterwards.
|
// Deactivate the editor. We are careful to avoid using `this` afterwards.
|
||||||
this.dispose();
|
this.dispose();
|
||||||
|
@ -7,7 +7,7 @@ import {icon} from 'app/client/ui2018/icons';
|
|||||||
import {createMobileButtons, getButtonMargins} from 'app/client/widgets/EditorButtons';
|
import {createMobileButtons, getButtonMargins} from 'app/client/widgets/EditorButtons';
|
||||||
import {EditorPlacement, ISize} from 'app/client/widgets/EditorPlacement';
|
import {EditorPlacement, ISize} from 'app/client/widgets/EditorPlacement';
|
||||||
import {NewBaseEditor, Options} from 'app/client/widgets/NewBaseEditor';
|
import {NewBaseEditor, Options} from 'app/client/widgets/NewBaseEditor';
|
||||||
import {undefDefault} from 'app/common/gutil';
|
import {undef} from 'app/common/gutil';
|
||||||
import {dom, Observable, styled} from 'grainjs';
|
import {dom, Observable, styled} from 'grainjs';
|
||||||
|
|
||||||
// How wide to expand the FormulaEditor when an error is shown in it.
|
// How wide to expand the FormulaEditor when an error is shown in it.
|
||||||
@ -37,12 +37,18 @@ export class FormulaEditor extends NewBaseEditor {
|
|||||||
|
|
||||||
constructor(options: IFormulaEditorOptions) {
|
constructor(options: IFormulaEditorOptions) {
|
||||||
super(options);
|
super(options);
|
||||||
|
|
||||||
|
const initialValue = undef(options.state as string | undefined, options.editValue, String(options.cellValue));
|
||||||
|
// create editor state observable (used by draft and latest position memory)
|
||||||
|
this.editorState = Observable.create(this, initialValue);
|
||||||
|
|
||||||
this._formulaEditor = AceEditor.create({
|
this._formulaEditor = AceEditor.create({
|
||||||
// A bit awkward, but we need to assume calcSize is not used until attach() has been called
|
// A bit awkward, but we need to assume calcSize is not used until attach() has been called
|
||||||
// and _editorPlacement created.
|
// and _editorPlacement created.
|
||||||
calcSize: this._calcSize.bind(this),
|
calcSize: this._calcSize.bind(this),
|
||||||
gristDoc: options.gristDoc,
|
gristDoc: options.gristDoc,
|
||||||
saveValueOnBlurEvent: true,
|
saveValueOnBlurEvent: true,
|
||||||
|
editorState : this.editorState
|
||||||
});
|
});
|
||||||
|
|
||||||
const allCommands = Object.assign({ setCursor: this._onSetCursor }, options.commands);
|
const allCommands = Object.assign({ setCursor: this._onSetCursor }, options.commands);
|
||||||
@ -70,11 +76,16 @@ export class FormulaEditor extends NewBaseEditor {
|
|||||||
aceObj.setHighlightActiveLine(false);
|
aceObj.setHighlightActiveLine(false);
|
||||||
aceObj.getSession().setUseWrapMode(false);
|
aceObj.getSession().setUseWrapMode(false);
|
||||||
aceObj.renderer.setPadding(0);
|
aceObj.renderer.setPadding(0);
|
||||||
const val = undefDefault(options.editValue, String(options.cellValue));
|
const val = initialValue;
|
||||||
const pos = Math.min(options.cursorPos, val.length);
|
const pos = Math.min(options.cursorPos, val.length);
|
||||||
this._formulaEditor.setValue(val, pos);
|
this._formulaEditor.setValue(val, pos);
|
||||||
this._formulaEditor.attachCommandGroup(this._commandGroup);
|
this._formulaEditor.attachCommandGroup(this._commandGroup);
|
||||||
|
|
||||||
|
// enable formula editing if state was passed
|
||||||
|
if (options.state) {
|
||||||
|
options.field.editingFormula(true);
|
||||||
|
}
|
||||||
|
|
||||||
// This catches any change to the value including e.g. via backspace or paste.
|
// This catches any change to the value including e.g. via backspace or paste.
|
||||||
aceObj.once("change", () => options.field.editingFormula(true));
|
aceObj.once("change", () => options.field.editingFormula(true));
|
||||||
})
|
})
|
||||||
|
@ -7,11 +7,14 @@ import {createMobileButtons, getButtonMargins} from 'app/client/widgets/EditorBu
|
|||||||
import {EditorPlacement} from 'app/client/widgets/EditorPlacement';
|
import {EditorPlacement} from 'app/client/widgets/EditorPlacement';
|
||||||
import {NewBaseEditor, Options} from 'app/client/widgets/NewBaseEditor';
|
import {NewBaseEditor, Options} from 'app/client/widgets/NewBaseEditor';
|
||||||
import {CellValue} from "app/common/DocActions";
|
import {CellValue} from "app/common/DocActions";
|
||||||
import {undefDefault} from 'app/common/gutil';
|
import {undef} from 'app/common/gutil';
|
||||||
import {dom} from 'grainjs';
|
import {dom, Observable} from 'grainjs';
|
||||||
|
|
||||||
|
|
||||||
export class NTextEditor extends NewBaseEditor {
|
export class NTextEditor extends NewBaseEditor {
|
||||||
|
// Observable with current editor state (used by drafts or latest edit/position component)
|
||||||
|
public readonly editorState : Observable<string>;
|
||||||
|
|
||||||
protected cellEditorDiv: HTMLElement;
|
protected cellEditorDiv: HTMLElement;
|
||||||
protected textInput: HTMLTextAreaElement;
|
protected textInput: HTMLTextAreaElement;
|
||||||
protected commandGroup: any;
|
protected commandGroup: any;
|
||||||
@ -26,6 +29,11 @@ export class NTextEditor extends NewBaseEditor {
|
|||||||
constructor(options: Options) {
|
constructor(options: Options) {
|
||||||
super(options);
|
super(options);
|
||||||
|
|
||||||
|
const initialValue : string = undef(
|
||||||
|
options.state as string | undefined,
|
||||||
|
options.editValue, String(options.cellValue ?? ""));
|
||||||
|
this.editorState = Observable.create<string>(this, initialValue);
|
||||||
|
|
||||||
this.commandGroup = this.autoDispose(createGroup(options.commands, null, true));
|
this.commandGroup = this.autoDispose(createGroup(options.commands, null, true));
|
||||||
this._alignment = options.field.widgetOptionsJson.peek().alignment || 'left';
|
this._alignment = options.field.widgetOptionsJson.peek().alignment || 'left';
|
||||||
this._dom = dom('div.default_editor',
|
this._dom = dom('div.default_editor',
|
||||||
@ -33,10 +41,9 @@ export class NTextEditor extends NewBaseEditor {
|
|||||||
this._contentSizer = dom('div.celleditor_content_measure'),
|
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.style('text-align', this._alignment),
|
||||||
dom.prop('value', undefDefault(options.editValue, String(options.cellValue ?? ""))),
|
dom.prop('value', initialValue),
|
||||||
this.commandGroup.attach(),
|
this.commandGroup.attach(),
|
||||||
// Resize the textbox whenever user types in it.
|
dom.on('input', () => this.onInput())
|
||||||
dom.on('input', () => this.resizeInput())
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
createMobileButtons(options.commands),
|
createMobileButtons(options.commands),
|
||||||
@ -83,6 +90,18 @@ export class NTextEditor extends NewBaseEditor {
|
|||||||
this._contentSizer.style.maxWidth = Math.ceil(maxSize.width) + 'px';
|
this._contentSizer.style.maxWidth = Math.ceil(maxSize.width) + 'px';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Occurs when user types text in the textarea
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
protected onInput() {
|
||||||
|
// Resize the textbox whenever user types in it.
|
||||||
|
this.resizeInput()
|
||||||
|
|
||||||
|
// notify about current state
|
||||||
|
this.editorState.set(String(this.getTextValue()))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper which resizes textInput to match its content. It relies on having a contentSizer element
|
* 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,
|
* with the same font/size settings as the textInput, and on having `calcSize` helper,
|
||||||
|
@ -21,6 +21,7 @@ export interface Options {
|
|||||||
editValue?: string;
|
editValue?: string;
|
||||||
cursorPos: number;
|
cursorPos: number;
|
||||||
commands: IEditorCommandGroup;
|
commands: IEditorCommandGroup;
|
||||||
|
state? : any;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -54,6 +55,11 @@ export abstract class NewBaseEditor extends Disposable {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current state of the editor. Optional, not all editors will report theirs current state.
|
||||||
|
*/
|
||||||
|
public editorState? : Observable<any>;
|
||||||
|
|
||||||
constructor(protected options: Options) {
|
constructor(protected options: Options) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ import {menuCssClass} from 'app/client/ui2018/menus';
|
|||||||
import {Options} from 'app/client/widgets/NewBaseEditor';
|
import {Options} from 'app/client/widgets/NewBaseEditor';
|
||||||
import {NTextEditor} from 'app/client/widgets/NTextEditor';
|
import {NTextEditor} from 'app/client/widgets/NTextEditor';
|
||||||
import {CellValue} from 'app/common/DocActions';
|
import {CellValue} from 'app/common/DocActions';
|
||||||
import {removePrefix, undefDefault} from 'app/common/gutil';
|
import {removePrefix, undef} from 'app/common/gutil';
|
||||||
import {BaseFormatter} from 'app/common/ValueFormatter';
|
import {BaseFormatter} from 'app/common/ValueFormatter';
|
||||||
import {styled} from 'grainjs';
|
import {styled} from 'grainjs';
|
||||||
|
|
||||||
@ -55,7 +55,7 @@ export class ReferenceEditor extends NTextEditor {
|
|||||||
// Decorate the editor to look like a reference column value (with a "link" icon).
|
// Decorate the editor to look like a reference column value (with a "link" icon).
|
||||||
this.cellEditorDiv.classList.add(cssRefEditor.className);
|
this.cellEditorDiv.classList.add(cssRefEditor.className);
|
||||||
this.cellEditorDiv.appendChild(cssRefEditIcon('FieldReference'));
|
this.cellEditorDiv.appendChild(cssRefEditIcon('FieldReference'));
|
||||||
this.textInput.value = undefDefault(options.editValue, this._idToText(options.cellValue));
|
this.textInput.value = undef(options.state, options.editValue, this._idToText(options.cellValue));
|
||||||
|
|
||||||
const needReload = (options.editValue === undefined && !tableData.isLoaded);
|
const needReload = (options.editValue === undefined && !tableData.isLoaded);
|
||||||
|
|
||||||
@ -64,7 +64,7 @@ export class ReferenceEditor extends NTextEditor {
|
|||||||
docData.fetchTable(refTableId).then(() => {
|
docData.fetchTable(refTableId).then(() => {
|
||||||
if (this.isDisposed()) { return; }
|
if (this.isDisposed()) { return; }
|
||||||
if (needReload && this.textInput.value === '') {
|
if (needReload && this.textInput.value === '') {
|
||||||
this.textInput.value = undefDefault(options.editValue, this._idToText(options.cellValue));
|
this.textInput.value = undef(options.state, options.editValue, this._idToText(options.cellValue));
|
||||||
this.resizeInput();
|
this.resizeInput();
|
||||||
}
|
}
|
||||||
if (this._autocomplete) {
|
if (this._autocomplete) {
|
||||||
|
@ -8,6 +8,7 @@ var commands = require('../components/commands');
|
|||||||
const {testId} = require('app/client/ui2018/cssVars');
|
const {testId} = require('app/client/ui2018/cssVars');
|
||||||
const {createMobileButtons, getButtonMargins} = require('app/client/widgets/EditorButtons');
|
const {createMobileButtons, getButtonMargins} = require('app/client/widgets/EditorButtons');
|
||||||
const {EditorPlacement} = require('app/client/widgets/EditorPlacement');
|
const {EditorPlacement} = require('app/client/widgets/EditorPlacement');
|
||||||
|
const { observable } = require('grainjs');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Required parameters:
|
* Required parameters:
|
||||||
@ -31,6 +32,10 @@ function TextEditor(options) {
|
|||||||
this.options = options;
|
this.options = options;
|
||||||
this.commandGroup = this.autoDispose(commands.createGroup(options.commands, null, true));
|
this.commandGroup = this.autoDispose(commands.createGroup(options.commands, null, true));
|
||||||
this._alignment = options.field.widgetOptionsJson.peek().alignment || 'left';
|
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',
|
this.dom = dom('div.default_editor',
|
||||||
dom('div.celleditor_cursor_editor', dom.testId('TextEditor_editor'),
|
dom('div.celleditor_cursor_editor', dom.testId('TextEditor_editor'),
|
||||||
@ -39,11 +44,11 @@ function TextEditor(options) {
|
|||||||
this.textInput = dom('textarea.celleditor_text_editor',
|
this.textInput = dom('textarea.celleditor_text_editor',
|
||||||
kd.attr('placeholder', options.placeholder || ''),
|
kd.attr('placeholder', options.placeholder || ''),
|
||||||
kd.style('text-align', this._alignment),
|
kd.style('text-align', this._alignment),
|
||||||
kd.value(gutil.undefDefault(options.editValue, String(options.cellValue == null ? "" : options.cellValue))),
|
kd.value(initialValue),
|
||||||
this.commandGroup.attach(),
|
this.commandGroup.attach(),
|
||||||
|
|
||||||
// Resize the textbox whenever user types in it.
|
// Resize the textbox whenever user types in it.
|
||||||
dom.on('input', () => this._resizeInput())
|
dom.on('input', () => this.onChange())
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
createMobileButtons(options.commands),
|
createMobileButtons(options.commands),
|
||||||
@ -85,6 +90,12 @@ TextEditor.prototype.getCellValue = function() {
|
|||||||
return this.textInput.value;
|
return this.textInput.value;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
TextEditor.prototype.onChange = function() {
|
||||||
|
if (this.editorState)
|
||||||
|
this.editorState.set(this.getTextValue());
|
||||||
|
this._resizeInput()
|
||||||
|
}
|
||||||
|
|
||||||
TextEditor.prototype.getTextValue = function() {
|
TextEditor.prototype.getTextValue = function() {
|
||||||
return this.textInput.value;
|
return this.textInput.value;
|
||||||
};
|
};
|
||||||
|
@ -102,6 +102,32 @@ export function undefDefault<T>(x: T|undefined, y: T): T {
|
|||||||
return (x !== void 0) ? x : y;
|
return (x !== void 0) ? x : y;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// for typescript 4
|
||||||
|
// type Undef<T> = T extends [infer A, ...infer B] ? undefined extends A ? NonNullable<A> | Undef<B> : A : unknown;
|
||||||
|
|
||||||
|
type Undef1<T> = T extends [infer A] ?
|
||||||
|
undefined extends A ? NonNullable<A> : A : unknown;
|
||||||
|
|
||||||
|
type Undef2<T> = T extends [infer A, infer B] ?
|
||||||
|
undefined extends A ? NonNullable<A> | Undef1<[B]> : A : Undef1<T>;
|
||||||
|
|
||||||
|
type Undef3<T> = T extends [infer A, infer B, infer C] ?
|
||||||
|
undefined extends A ? NonNullable<A> | Undef2<[B, C]> : A : Undef2<T>;
|
||||||
|
|
||||||
|
type Undef<T> = T extends [infer A, infer B, infer C, infer D] ?
|
||||||
|
undefined extends A ? NonNullable<A> | Undef3<[B, C, D]> : A : Undef3<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the first defined value from the list or unknown.
|
||||||
|
* Use with typed result, so the typescript type checker can provide correct type.
|
||||||
|
*/
|
||||||
|
export function undef<T extends Array<any>>(...list : T): Undef<T> {
|
||||||
|
for(const value of list) {
|
||||||
|
if (value !== undefined) return value;
|
||||||
|
}
|
||||||
|
return undefined as any;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses json and returns the result, or returns defaultVal if parsing fails.
|
* Parses json and returns the result, or returns defaultVal if parsing fails.
|
||||||
*/
|
*/
|
||||||
|
@ -66,6 +66,7 @@
|
|||||||
--icon-Page: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTIsMSBMMiwxNSBMMTQsMTUgTDE0LDEgTDIsMSBaIE0xLjUsMCBMMTQuNSwwIEMxNC43NzYxNDI0LDAgMTUsMC4yMjM4NTc2MjUgMTUsMC41IEwxNSwxNS41IEMxNSwxNS43NzYxNDI0IDE0Ljc3NjE0MjQsMTYgMTQuNSwxNiBMMS41LDE2IEMxLjIyMzg1NzYzLDE2IDEsMTUuNzc2MTQyNCAxLDE1LjUgTDEsMC41IEMxLDAuMjIzODU3NjI1IDEuMjIzODU3NjMsMCAxLjUsMCBaIE05LjUsNCBDOS4yMjM4NTc2Myw0IDksMy43NzYxNDIzNyA5LDMuNSBDOSwzLjIyMzg1NzYzIDkuMjIzODU3NjMsMyA5LjUsMyBMMTEuNSwzIEMxMS43NzYxNDI0LDMgMTIsMy4yMjM4NTc2MyAxMiwzLjUgQzEyLDMuNzc2MTQyMzcgMTEuNzc2MTQyNCw0IDExLjUsNCBMOS41LDQgWiBNOS41LDcgQzkuMjIzODU3NjMsNyA5LDYuNzc2MTQyMzcgOSw2LjUgQzksNi4yMjM4NTc2MyA5LjIyMzg1NzYzLDYgOS41LDYgTDExLjUsNiBDMTEuNzc2MTQyNCw2IDEyLDYuMjIzODU3NjMgMTIsNi41IEMxMiw2Ljc3NjE0MjM3IDExLjc3NjE0MjQsNyAxMS41LDcgTDkuNSw3IFogTTQuNSwxMCBDNC4yMjM4NTc2MywxMCA0LDkuNzc2MTQyMzcgNCw5LjUgQzQsOS4yMjM4NTc2MyA0LjIyMzg1NzYzLDkgNC41LDkgTDExLjUsOSBDMTEuNzc2MTQyNCw5IDEyLDkuMjIzODU3NjMgMTIsOS41IEMxMiw5Ljc3NjE0MjM3IDExLjc3NjE0MjQsMTAgMTEuNSwxMCBMNC41LDEwIFogTTQuNSwxMyBDNC4yMjM4NTc2MywxMyA0LDEyLjc3NjE0MjQgNCwxMi41IEM0LDEyLjIyMzg1NzYgNC4yMjM4NTc2MywxMiA0LjUsMTIgTDExLjUsMTIgQzExLjc3NjE0MjQsMTIgMTIsMTIuMjIzODU3NiAxMiwxMi41IEMxMiwxMi43NzYxNDI0IDExLjc3NjE0MjQsMTMgMTEuNSwxMyBMNC41LDEzIFogTTUsNCBMNSw2IEw3LDYgTDcsNCBMNSw0IFogTTQuNSwzIEw3LjUsMyBDNy43NzYxNDIzNywzIDgsMy4yMjM4NTc2MyA4LDMuNSBMOCw2LjUgQzgsNi43NzYxNDIzNyA3Ljc3NjE0MjM3LDcgNy41LDcgTDQuNSw3IEM0LjIyMzg1NzYzLDcgNCw2Ljc3NjE0MjM3IDQsNi41IEw0LDMuNSBDNCwzLjIyMzg1NzYzIDQuMjIzODU3NjMsMyA0LjUsMyBaIiBmaWxsPSIjMDAwIiBmaWxsLXJ1bGU9Im5vbnplcm8iLz48L3N2Zz4=');
|
--icon-Page: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTIsMSBMMiwxNSBMMTQsMTUgTDE0LDEgTDIsMSBaIE0xLjUsMCBMMTQuNSwwIEMxNC43NzYxNDI0LDAgMTUsMC4yMjM4NTc2MjUgMTUsMC41IEwxNSwxNS41IEMxNSwxNS43NzYxNDI0IDE0Ljc3NjE0MjQsMTYgMTQuNSwxNiBMMS41LDE2IEMxLjIyMzg1NzYzLDE2IDEsMTUuNzc2MTQyNCAxLDE1LjUgTDEsMC41IEMxLDAuMjIzODU3NjI1IDEuMjIzODU3NjMsMCAxLjUsMCBaIE05LjUsNCBDOS4yMjM4NTc2Myw0IDksMy43NzYxNDIzNyA5LDMuNSBDOSwzLjIyMzg1NzYzIDkuMjIzODU3NjMsMyA5LjUsMyBMMTEuNSwzIEMxMS43NzYxNDI0LDMgMTIsMy4yMjM4NTc2MyAxMiwzLjUgQzEyLDMuNzc2MTQyMzcgMTEuNzc2MTQyNCw0IDExLjUsNCBMOS41LDQgWiBNOS41LDcgQzkuMjIzODU3NjMsNyA5LDYuNzc2MTQyMzcgOSw2LjUgQzksNi4yMjM4NTc2MyA5LjIyMzg1NzYzLDYgOS41LDYgTDExLjUsNiBDMTEuNzc2MTQyNCw2IDEyLDYuMjIzODU3NjMgMTIsNi41IEMxMiw2Ljc3NjE0MjM3IDExLjc3NjE0MjQsNyAxMS41LDcgTDkuNSw3IFogTTQuNSwxMCBDNC4yMjM4NTc2MywxMCA0LDkuNzc2MTQyMzcgNCw5LjUgQzQsOS4yMjM4NTc2MyA0LjIyMzg1NzYzLDkgNC41LDkgTDExLjUsOSBDMTEuNzc2MTQyNCw5IDEyLDkuMjIzODU3NjMgMTIsOS41IEMxMiw5Ljc3NjE0MjM3IDExLjc3NjE0MjQsMTAgMTEuNSwxMCBMNC41LDEwIFogTTQuNSwxMyBDNC4yMjM4NTc2MywxMyA0LDEyLjc3NjE0MjQgNCwxMi41IEM0LDEyLjIyMzg1NzYgNC4yMjM4NTc2MywxMiA0LjUsMTIgTDExLjUsMTIgQzExLjc3NjE0MjQsMTIgMTIsMTIuMjIzODU3NiAxMiwxMi41IEMxMiwxMi43NzYxNDI0IDExLjc3NjE0MjQsMTMgMTEuNSwxMyBMNC41LDEzIFogTTUsNCBMNSw2IEw3LDYgTDcsNCBMNSw0IFogTTQuNSwzIEw3LjUsMyBDNy43NzYxNDIzNywzIDgsMy4yMjM4NTc2MyA4LDMuNSBMOCw2LjUgQzgsNi43NzYxNDIzNyA3Ljc3NjE0MjM3LDcgNy41LDcgTDQuNSw3IEM0LjIyMzg1NzYzLDcgNCw2Ljc3NjE0MjM3IDQsNi41IEw0LDMuNSBDNCwzLjIyMzg1NzYzIDQuMjIzODU3NjMsMyA0LjUsMyBaIiBmaWxsPSIjMDAwIiBmaWxsLXJ1bGU9Im5vbnplcm8iLz48L3N2Zz4=');
|
||||||
--icon-PanelLeft: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTMuNzA3MTA2NzgsOC41IEw1Ljg1MzU1MzM5LDEwLjY0NjQ0NjYgQzYuMDQ4ODE1NTQsMTAuODQxNzA4OCA2LjA0ODgxNTU0LDExLjE1ODI5MTIgNS44NTM1NTMzOSwxMS4zNTM1NTM0IEM1LjY1ODI5MTI0LDExLjU0ODgxNTUgNS4zNDE3MDg3NiwxMS41NDg4MTU1IDUuMTQ2NDQ2NjEsMTEuMzUzNTUzNCBMMi4xNDY0NDY2MSw4LjM1MzU1MzM5IEMxLjk1MTE4NDQ2LDguMTU4MjkxMjQgMS45NTExODQ0Niw3Ljg0MTcwODc2IDIuMTQ2NDQ2NjEsNy42NDY0NDY2MSBMNS4xNDY0NDY2MSw0LjY0NjQ0NjYxIEM1LjM0MTcwODc2LDQuNDUxMTg0NDYgNS42NTgyOTEyNCw0LjQ1MTE4NDQ2IDUuODUzNTUzMzksNC42NDY0NDY2MSBDNi4wNDg4MTU1NCw0Ljg0MTcwODc2IDYuMDQ4ODE1NTQsNS4xNTgyOTEyNCA1Ljg1MzU1MzM5LDUuMzUzNTUzMzkgTDMuNzA3MTA2NzgsNy41IEw4LjUsNy41IEM4Ljc3NjE0MjM3LDcuNSA5LDcuNzIzODU3NjMgOSw4IEM5LDguMjc2MTQyMzcgOC43NzYxNDIzNyw4LjUgOC41LDguNSBMMy43MDcxMDY3OCw4LjUgWiBNMTAsMTMuNSBMMTAsMi41IEMxMCwyLjIyMzg1NzYzIDEwLjIyMzg1NzYsMiAxMC41LDIgTDEzLjUsMiBDMTMuNzc2MTQyNCwyIDE0LDIuMjIzODU3NjMgMTQsMi41IEwxNCwxMy41IEMxNCwxMy43NzYxNDI0IDEzLjc3NjE0MjQsMTQgMTMuNSwxNCBMMTAuNSwxNCBDMTAuMjIzODU3NiwxNCAxMCwxMy43NzYxNDI0IDEwLDEzLjUgWiIgZmlsbD0iIzAwMCIgZmlsbC1ydWxlPSJub256ZXJvIi8+PC9zdmc+');
|
--icon-PanelLeft: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTMuNzA3MTA2NzgsOC41IEw1Ljg1MzU1MzM5LDEwLjY0NjQ0NjYgQzYuMDQ4ODE1NTQsMTAuODQxNzA4OCA2LjA0ODgxNTU0LDExLjE1ODI5MTIgNS44NTM1NTMzOSwxMS4zNTM1NTM0IEM1LjY1ODI5MTI0LDExLjU0ODgxNTUgNS4zNDE3MDg3NiwxMS41NDg4MTU1IDUuMTQ2NDQ2NjEsMTEuMzUzNTUzNCBMMi4xNDY0NDY2MSw4LjM1MzU1MzM5IEMxLjk1MTE4NDQ2LDguMTU4MjkxMjQgMS45NTExODQ0Niw3Ljg0MTcwODc2IDIuMTQ2NDQ2NjEsNy42NDY0NDY2MSBMNS4xNDY0NDY2MSw0LjY0NjQ0NjYxIEM1LjM0MTcwODc2LDQuNDUxMTg0NDYgNS42NTgyOTEyNCw0LjQ1MTE4NDQ2IDUuODUzNTUzMzksNC42NDY0NDY2MSBDNi4wNDg4MTU1NCw0Ljg0MTcwODc2IDYuMDQ4ODE1NTQsNS4xNTgyOTEyNCA1Ljg1MzU1MzM5LDUuMzUzNTUzMzkgTDMuNzA3MTA2NzgsNy41IEw4LjUsNy41IEM4Ljc3NjE0MjM3LDcuNSA5LDcuNzIzODU3NjMgOSw4IEM5LDguMjc2MTQyMzcgOC43NzYxNDIzNyw4LjUgOC41LDguNSBMMy43MDcxMDY3OCw4LjUgWiBNMTAsMTMuNSBMMTAsMi41IEMxMCwyLjIyMzg1NzYzIDEwLjIyMzg1NzYsMiAxMC41LDIgTDEzLjUsMiBDMTMuNzc2MTQyNCwyIDE0LDIuMjIzODU3NjMgMTQsMi41IEwxNCwxMy41IEMxNCwxMy43NzYxNDI0IDEzLjc3NjE0MjQsMTQgMTMuNSwxNCBMMTAuNSwxNCBDMTAuMjIzODU3NiwxNCAxMCwxMy43NzYxNDI0IDEwLDEzLjUgWiIgZmlsbD0iIzAwMCIgZmlsbC1ydWxlPSJub256ZXJvIi8+PC9zdmc+');
|
||||||
--icon-PanelRight: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTMuNzA3MTA2NzgsOC41IEw1Ljg1MzU1MzM5LDEwLjY0NjQ0NjYgQzYuMDQ4ODE1NTQsMTAuODQxNzA4OCA2LjA0ODgxNTU0LDExLjE1ODI5MTIgNS44NTM1NTMzOSwxMS4zNTM1NTM0IEM1LjY1ODI5MTI0LDExLjU0ODgxNTUgNS4zNDE3MDg3NiwxMS41NDg4MTU1IDUuMTQ2NDQ2NjEsMTEuMzUzNTUzNCBMMi4xNDY0NDY2MSw4LjM1MzU1MzM5IEMxLjk1MTE4NDQ2LDguMTU4MjkxMjQgMS45NTExODQ0Niw3Ljg0MTcwODc2IDIuMTQ2NDQ2NjEsNy42NDY0NDY2MSBMNS4xNDY0NDY2MSw0LjY0NjQ0NjYxIEM1LjM0MTcwODc2LDQuNDUxMTg0NDYgNS42NTgyOTEyNCw0LjQ1MTE4NDQ2IDUuODUzNTUzMzksNC42NDY0NDY2MSBDNi4wNDg4MTU1NCw0Ljg0MTcwODc2IDYuMDQ4ODE1NTQsNS4xNTgyOTEyNCA1Ljg1MzU1MzM5LDUuMzUzNTUzMzkgTDMuNzA3MTA2NzgsNy41IEw4LjUsNy41IEM4Ljc3NjE0MjM3LDcuNSA5LDcuNzIzODU3NjMgOSw4IEM5LDguMjc2MTQyMzcgOC43NzYxNDIzNyw4LjUgOC41LDguNSBMMy43MDcxMDY3OCw4LjUgWiBNMTAsMTMuNSBMMTAsMi41IEMxMCwyLjIyMzg1NzYzIDEwLjIyMzg1NzYsMiAxMC41LDIgTDEzLjUsMiBDMTMuNzc2MTQyNCwyIDE0LDIuMjIzODU3NjMgMTQsMi41IEwxNCwxMy41IEMxNCwxMy43NzYxNDI0IDEzLjc3NjE0MjQsMTQgMTMuNSwxNCBMMTAuNSwxNCBDMTAuMjIzODU3NiwxNCAxMCwxMy43NzYxNDI0IDEwLDEzLjUgWiIgZmlsbD0iIzAwMCIgZmlsbC1ydWxlPSJub256ZXJvIiB0cmFuc2Zvcm09Im1hdHJpeCgtMSAwIDAgMSAxNiAwKSIvPjwvc3ZnPg==');
|
--icon-PanelRight: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTMuNzA3MTA2NzgsOC41IEw1Ljg1MzU1MzM5LDEwLjY0NjQ0NjYgQzYuMDQ4ODE1NTQsMTAuODQxNzA4OCA2LjA0ODgxNTU0LDExLjE1ODI5MTIgNS44NTM1NTMzOSwxMS4zNTM1NTM0IEM1LjY1ODI5MTI0LDExLjU0ODgxNTUgNS4zNDE3MDg3NiwxMS41NDg4MTU1IDUuMTQ2NDQ2NjEsMTEuMzUzNTUzNCBMMi4xNDY0NDY2MSw4LjM1MzU1MzM5IEMxLjk1MTE4NDQ2LDguMTU4MjkxMjQgMS45NTExODQ0Niw3Ljg0MTcwODc2IDIuMTQ2NDQ2NjEsNy42NDY0NDY2MSBMNS4xNDY0NDY2MSw0LjY0NjQ0NjYxIEM1LjM0MTcwODc2LDQuNDUxMTg0NDYgNS42NTgyOTEyNCw0LjQ1MTE4NDQ2IDUuODUzNTUzMzksNC42NDY0NDY2MSBDNi4wNDg4MTU1NCw0Ljg0MTcwODc2IDYuMDQ4ODE1NTQsNS4xNTgyOTEyNCA1Ljg1MzU1MzM5LDUuMzUzNTUzMzkgTDMuNzA3MTA2NzgsNy41IEw4LjUsNy41IEM4Ljc3NjE0MjM3LDcuNSA5LDcuNzIzODU3NjMgOSw4IEM5LDguMjc2MTQyMzcgOC43NzYxNDIzNyw4LjUgOC41LDguNSBMMy43MDcxMDY3OCw4LjUgWiBNMTAsMTMuNSBMMTAsMi41IEMxMCwyLjIyMzg1NzYzIDEwLjIyMzg1NzYsMiAxMC41LDIgTDEzLjUsMiBDMTMuNzc2MTQyNCwyIDE0LDIuMjIzODU3NjMgMTQsMi41IEwxNCwxMy41IEMxNCwxMy43NzYxNDI0IDEzLjc3NjE0MjQsMTQgMTMuNSwxNCBMMTAuNSwxNCBDMTAuMjIzODU3NiwxNCAxMCwxMy43NzYxNDI0IDEwLDEzLjUgWiIgZmlsbD0iIzAwMCIgZmlsbC1ydWxlPSJub256ZXJvIiB0cmFuc2Zvcm09Im1hdHJpeCgtMSAwIDAgMSAxNiAwKSIvPjwvc3ZnPg==');
|
||||||
|
--icon-Pencil: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9Ii0yIC0yIDIwIDIwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxnIGNsYXNzPSJuYy1pY29uLXdyYXBwZXIiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzIxMjEyMSIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0xMyAuNUwxNS41IDMgNy41IDExIDQgMTIgNSA4LjV6TTExIDIuNUwxMy41IDUiLz48cGF0aCBkPSJNMTMuNSw5LjV2NSBjMCwwLjU1Mi0wLjQ0OCwxLTEsMWgtMTFjLTAuNTUyLDAtMS0wLjQ0OC0xLTF2LTExYzAtMC41NTIsMC40NDgtMSwxLTFoNSIvPjwvZz48L3N2Zz4=');
|
||||||
--icon-PinBig: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTExLjgyOTcyODEsLTcuNjAyODA3MjdlLTEzIEw0LjE5Mjk3NDkxLC03LjU3NjE2MTkyZS0xMyBDMy42MjcyODk0OCwtNy41MzE3NTNlLTEzIDMuMjAzMDI1NDEsMC40MTAyNTY0MSAzLjIwMzAyNTQxLDAuOTU3MjY0OTU3IEMzLjIwMzAyNTQxLDEuNTA0MjczNSAzLjYyNzI4OTQ4LDEuOTE0NTI5OTEgNC4xOTI5NzQ5MSwxLjkxNDUyOTkxIEw1LjA0MTUwMzA0LDEuOTE0NTI5OTEgTDIuODQ5NDcyMDIsOS4wOTQwMTcwOSBMMS40MzUyNTg0Niw5LjA5NDAxNzA5IEMwLjg2OTU3MzAzNiw5LjA5NDAxNzA5IDAuNDQ1MzA4OTY3LDkuNTA0MjczNSAwLjQ0NTMwODk2NywxMC4wNTEyODIxIEMwLjQ0NTMwODk2NywxMC41OTgyOTA2IDAuODY5NTczMDM2LDExLjAwODU0NyAxLjQzNTI1ODQ2LDExLjAwODU0NyBMMy41NTY1Nzg4LDExLjAwODU0NyBMNi45NTA2OTEzNSwxMS4wMDg1NDcgTDcuMDIxNDAyMDMsMTYgTDkuMDAxMzAxMDIsMTYgTDkuMDAxMzAxMDIsMTAuOTQwMTcwOSBMMTQuNjU4MTU1MywxMC45NDAxNzA5IEMxNS4zNjUyNjIxLDEwLjk0MDE3MDkgMTUuNjQ4MTA0OCwxMC4yNTY0MTAzIDE1LjY0ODEwNDgsOS45ODI5MDU5OCBDMTUuNjQ4MTA0OCw5LjQzNTg5NzQ0IDE1LjIyMzg0MDcsOS4wMjU2NDEwMyAxNC42NTgxNTUzLDkuMDI1NjQxMDMgTDEzLjI0Mzk0MTcsOS4wMjU2NDEwMyBMMTEuMTIyNjIxNCwxLjkxNDUyOTkxIEwxMS45NzExNDk1LDEuOTE0NTI5OTEgQzEyLjgxOTY3NzYsMS45MTQ1Mjk5MSAxMi45NjEwOTksMS4yMzA3NjkyMyAxMi45NjEwOTksMC45NTcyNjQ5NTcgQzEyLjg5MDM4ODMsMC40Nzg2MzI0NzkgMTIuMzk1NDEzNiwtNy42NjQ5Nzk3NmUtMTMgMTEuODI5NzI4MSwtNy42MDI4MDcyN2UtMTMgWiBNMTEuMDUxOTEwNyw5LjA5NDAxNzA5IEw0Ljk3MDc5MjM3LDkuMDk0MDE3MDkgTDcuMDkyMTEyNzEsMS45ODI5MDU5OCBMOC45MzA1OTAzNCwxLjk4MjkwNTk4IEwxMS4wNTE5MTA3LDkuMDk0MDE3MDkgWiIgZmlsbD0iIzAwMCIgZmlsbC1ydWxlPSJub256ZXJvIi8+PC9zdmc+');
|
--icon-PinBig: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTExLjgyOTcyODEsLTcuNjAyODA3MjdlLTEzIEw0LjE5Mjk3NDkxLC03LjU3NjE2MTkyZS0xMyBDMy42MjcyODk0OCwtNy41MzE3NTNlLTEzIDMuMjAzMDI1NDEsMC40MTAyNTY0MSAzLjIwMzAyNTQxLDAuOTU3MjY0OTU3IEMzLjIwMzAyNTQxLDEuNTA0MjczNSAzLjYyNzI4OTQ4LDEuOTE0NTI5OTEgNC4xOTI5NzQ5MSwxLjkxNDUyOTkxIEw1LjA0MTUwMzA0LDEuOTE0NTI5OTEgTDIuODQ5NDcyMDIsOS4wOTQwMTcwOSBMMS40MzUyNTg0Niw5LjA5NDAxNzA5IEMwLjg2OTU3MzAzNiw5LjA5NDAxNzA5IDAuNDQ1MzA4OTY3LDkuNTA0MjczNSAwLjQ0NTMwODk2NywxMC4wNTEyODIxIEMwLjQ0NTMwODk2NywxMC41OTgyOTA2IDAuODY5NTczMDM2LDExLjAwODU0NyAxLjQzNTI1ODQ2LDExLjAwODU0NyBMMy41NTY1Nzg4LDExLjAwODU0NyBMNi45NTA2OTEzNSwxMS4wMDg1NDcgTDcuMDIxNDAyMDMsMTYgTDkuMDAxMzAxMDIsMTYgTDkuMDAxMzAxMDIsMTAuOTQwMTcwOSBMMTQuNjU4MTU1MywxMC45NDAxNzA5IEMxNS4zNjUyNjIxLDEwLjk0MDE3MDkgMTUuNjQ4MTA0OCwxMC4yNTY0MTAzIDE1LjY0ODEwNDgsOS45ODI5MDU5OCBDMTUuNjQ4MTA0OCw5LjQzNTg5NzQ0IDE1LjIyMzg0MDcsOS4wMjU2NDEwMyAxNC42NTgxNTUzLDkuMDI1NjQxMDMgTDEzLjI0Mzk0MTcsOS4wMjU2NDEwMyBMMTEuMTIyNjIxNCwxLjkxNDUyOTkxIEwxMS45NzExNDk1LDEuOTE0NTI5OTEgQzEyLjgxOTY3NzYsMS45MTQ1Mjk5MSAxMi45NjEwOTksMS4yMzA3NjkyMyAxMi45NjEwOTksMC45NTcyNjQ5NTcgQzEyLjg5MDM4ODMsMC40Nzg2MzI0NzkgMTIuMzk1NDEzNiwtNy42NjQ5Nzk3NmUtMTMgMTEuODI5NzI4MSwtNy42MDI4MDcyN2UtMTMgWiBNMTEuMDUxOTEwNyw5LjA5NDAxNzA5IEw0Ljk3MDc5MjM3LDkuMDk0MDE3MDkgTDcuMDkyMTEyNzEsMS45ODI5MDU5OCBMOC45MzA1OTAzNCwxLjk4MjkwNTk4IEwxMS4wNTE5MTA3LDkuMDk0MDE3MDkgWiIgZmlsbD0iIzAwMCIgZmlsbC1ydWxlPSJub256ZXJvIi8+PC9zdmc+');
|
||||||
--icon-PinSmall: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTcuNSwxMSBMMi41LDExIEMyLjIyMzg1NzYzLDExIDIsMTAuNzc2MTQyNCAyLDEwLjUgQzIsMTAuMjIzODU3NiAyLjIyMzg1NzYzLDEwIDIuNSwxMCBMNC4yNSwxMCBMNS43NSw0IEw0LjUsNCBDNC4yMjM4NTc2Myw0IDQsMy43NzYxNDIzNyA0LDMuNSBDNCwzLjIyMzg1NzYzIDQuMjIzODU3NjMsMyA0LjUsMyBMMTEuNSwzIEMxMS43NzYxNDI0LDMgMTIsMy4yMjM4NTc2MyAxMiwzLjUgQzEyLDMuNzc2MTQyMzcgMTEuNzc2MTQyNCw0IDExLjUsNCBMMTAuMjUsNCBMMTEuNzUsMTAgTDEzLjUsMTAgQzEzLjc3NjE0MjQsMTAgMTQsMTAuMjIzODU3NiAxNCwxMC41IEMxNCwxMC43NzYxNDI0IDEzLjc3NjE0MjQsMTEgMTMuNSwxMSBMOC41LDExIEw4LjUsMTQgTDcuNSwxNCBMNy41LDExIFogTTUuMjgwNzc2NDEsMTAgTDEwLjcxOTIyMzYsMTAgTDkuMjE5MjIzNTksNCBMNi43ODA3NzY0MSw0IEw1LjI4MDc3NjQxLDEwIFoiIGZpbGw9IiMwMDAiIGZpbGwtcnVsZT0ibm9uemVybyIvPjwvc3ZnPg==');
|
--icon-PinSmall: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTcuNSwxMSBMMi41LDExIEMyLjIyMzg1NzYzLDExIDIsMTAuNzc2MTQyNCAyLDEwLjUgQzIsMTAuMjIzODU3NiAyLjIyMzg1NzYzLDEwIDIuNSwxMCBMNC4yNSwxMCBMNS43NSw0IEw0LjUsNCBDNC4yMjM4NTc2Myw0IDQsMy43NzYxNDIzNyA0LDMuNSBDNCwzLjIyMzg1NzYzIDQuMjIzODU3NjMsMyA0LjUsMyBMMTEuNSwzIEMxMS43NzYxNDI0LDMgMTIsMy4yMjM4NTc2MyAxMiwzLjUgQzEyLDMuNzc2MTQyMzcgMTEuNzc2MTQyNCw0IDExLjUsNCBMMTAuMjUsNCBMMTEuNzUsMTAgTDEzLjUsMTAgQzEzLjc3NjE0MjQsMTAgMTQsMTAuMjIzODU3NiAxNCwxMC41IEMxNCwxMC43NzYxNDI0IDEzLjc3NjE0MjQsMTEgMTMuNSwxMSBMOC41LDExIEw4LjUsMTQgTDcuNSwxNCBMNy41LDExIFogTTUuMjgwNzc2NDEsMTAgTDEwLjcxOTIyMzYsMTAgTDkuMjE5MjIzNTksNCBMNi43ODA3NzY0MSw0IEw1LjI4MDc3NjQxLDEwIFoiIGZpbGw9IiMwMDAiIGZpbGwtcnVsZT0ibm9uemVybyIvPjwvc3ZnPg==');
|
||||||
--icon-Pivot: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTMsMSBMMTMsMSBDMTQuMTA0NTY5NSwxIDE1LDEuODk1NDMwNSAxNSwzIEwxNSwxMyBDMTUsMTQuMTA0NTY5NSAxNC4xMDQ1Njk1LDE1IDEzLDE1IEwzLDE1IEMxLjg5NTQzMDUsMTUgMSwxNC4xMDQ1Njk1IDEsMTMgTDEsMyBDMSwxLjg5NTQzMDUgMS44OTU0MzA1LDEgMywxIFogTTUuMzc1LDQuNSBMNS4zNzUsNC43ODU0NjM0MSBMNy42MzQ2NTAxLDguMDIyNzA3MzIgTDUuMzc1LDExLjA5NDUxMjIgTDUuMzc1LDExLjUgTDEwLjYyNSwxMS41IEwxMC42MjUsMTAuMDMyODE3MSBMOS44MjY0OTkwMSwxMC43MjY0MTQ2IEw2LjkxOTczMTYxLDEwLjcyNjQxNDYgTDYuOTE5NzMxNjEsMTAuNDE2OTYzNCBMOC45MDI4MzMsNy42MTU4NTM2NiBMNi45MTk3MzE2MSw0Ljk5MDg1MzY2IEw5Ljc3NDMxMjEzLDQuOTg3MzUzNjYgTDEwLjYyNSw2LjA5NTMxNzA3IEwxMC42MjUsNC41IEw1LjM3NSw0LjUgWiIgZmlsbD0iIzAwMCIgZmlsbC1ydWxlPSJub256ZXJvIi8+PC9zdmc+');
|
--icon-Pivot: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTMsMSBMMTMsMSBDMTQuMTA0NTY5NSwxIDE1LDEuODk1NDMwNSAxNSwzIEwxNSwxMyBDMTUsMTQuMTA0NTY5NSAxNC4xMDQ1Njk1LDE1IDEzLDE1IEwzLDE1IEMxLjg5NTQzMDUsMTUgMSwxNC4xMDQ1Njk1IDEsMTMgTDEsMyBDMSwxLjg5NTQzMDUgMS44OTU0MzA1LDEgMywxIFogTTUuMzc1LDQuNSBMNS4zNzUsNC43ODU0NjM0MSBMNy42MzQ2NTAxLDguMDIyNzA3MzIgTDUuMzc1LDExLjA5NDUxMjIgTDUuMzc1LDExLjUgTDEwLjYyNSwxMS41IEwxMC42MjUsMTAuMDMyODE3MSBMOS44MjY0OTkwMSwxMC43MjY0MTQ2IEw2LjkxOTczMTYxLDEwLjcyNjQxNDYgTDYuOTE5NzMxNjEsMTAuNDE2OTYzNCBMOC45MDI4MzMsNy42MTU4NTM2NiBMNi45MTk3MzE2MSw0Ljk5MDg1MzY2IEw5Ljc3NDMxMjEzLDQuOTg3MzUzNjYgTDEwLjYyNSw2LjA5NTMxNzA3IEwxMC42MjUsNC41IEw1LjM3NSw0LjUgWiIgZmlsbD0iIzAwMCIgZmlsbC1ydWxlPSJub256ZXJvIi8+PC9zdmc+');
|
||||||
|
9
static/ui-icons/UI/Pencil.svg
Normal file
9
static/ui-icons/UI/Pencil.svg
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="16px" height="16px" viewBox="-2 -2 20 20" version="1.1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<!-- Generator: Sketch 52.5 (67469) - http://www.bohemiancoding.com/sketch -->
|
||||||
|
<title>Icons / UI / Download</title>
|
||||||
|
<desc>Created with Sketch.</desc>
|
||||||
|
<g class="nc-icon-wrapper" stroke-width="1" fill="none" stroke="#212121" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"><polygon points=" 13,0.5 15.5,3 7.5,11 4,12 5,8.5 " stroke="#212121"></polygon> <line x1="11" y1="2.5" x2="13.5" y2="5" stroke="#212121"></line> <path d="M13.5,9.5v5 c0,0.552-0.448,1-1,1h-11c-0.552,0-1-0.448-1-1v-11c0-0.552,0.448-1,1-1h5"></path> </g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 742 B |
@ -342,6 +342,14 @@ export async function resizeColumn(colOptions: IColHeader, deltaPx: number) {
|
|||||||
await waitForServer();
|
await waitForServer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs dbClick
|
||||||
|
* @param cell Element to click
|
||||||
|
*/
|
||||||
|
export async function dbClick(cell: WebElement) {
|
||||||
|
await driver.withActions(a => a.doubleClick(cell));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns {rowNum, col} object representing the position of the cursor in the active view
|
* Returns {rowNum, col} object representing the position of the cursor in the active view
|
||||||
* section. RowNum is a 1-based number as in the row headers, and col is a 0-based index for
|
* section. RowNum is a 1-based number as in the row headers, and col is a 0-based index for
|
||||||
@ -411,6 +419,14 @@ export async function enterFormula(formula: string) {
|
|||||||
await waitForServer();
|
await waitForServer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that formula editor is shown and its value matches the given regexp.
|
||||||
|
*/
|
||||||
|
export async function getFormulaText() {
|
||||||
|
assert.equal(await driver.findWait('.test-formula-editor', 500).isDisplayed(), true);
|
||||||
|
return await driver.find('.code_editor_container').getText();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check that formula editor is shown and its value matches the given regexp.
|
* Check that formula editor is shown and its value matches the given regexp.
|
||||||
*/
|
*/
|
||||||
@ -1311,6 +1327,20 @@ export class Session {
|
|||||||
return doc;
|
return doc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// As for importFixturesDoc, but delete the document at the end of each test.
|
||||||
|
public async tempShortDoc(cleanup: Cleanup, fileName: string, options: ImportOpts = {load: true}) {
|
||||||
|
const doc = await this.importFixturesDoc(fileName, options);
|
||||||
|
const api = this.createHomeApi();
|
||||||
|
if (!noCleanup) {
|
||||||
|
cleanup.addAfterEach(async () => {
|
||||||
|
if (doc.id)
|
||||||
|
await api.deleteDoc(doc.id).catch(noop);
|
||||||
|
doc.id = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
|
||||||
public async tempNewDoc(cleanup: Cleanup, docName: string, {load} = {load: true}) {
|
public async tempNewDoc(cleanup: Cleanup, docName: string, {load} = {load: true}) {
|
||||||
const docId = await createNewDoc(this.settings.name, this.settings.org, this.settings.workspace, docName,
|
const docId = await createNewDoc(this.settings.name, this.settings.org, this.settings.workspace, docName,
|
||||||
{email: this.settings.email});
|
{email: this.settings.email});
|
||||||
|
Loading…
Reference in New Issue
Block a user