mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Implement AI Assistant UI V2
Summary: Implements the latest design of the Formula AI Assistant. Also switches out brace to the latest build of ace. Test Plan: Browser tests. Reviewers: jarek Reviewed By: jarek Subscribers: jarek Differential Revision: https://phab.getgrist.com/D3949
This commit is contained in:
@@ -1,14 +1,15 @@
|
||||
import * as ace from 'ace-builds';
|
||||
import {theme, vars} from 'app/client/ui2018/cssVars';
|
||||
import {Theme} from 'app/common/ThemePrefs';
|
||||
import {getGristConfig} from 'app/common/urlUtils';
|
||||
import * as ace from 'brace';
|
||||
import {BindableValue, Computed, dom, DomElementArg, Observable, styled, subscribeElem} from 'grainjs';
|
||||
|
||||
// tslint:disable:no-var-requires
|
||||
require('brace/ext/static_highlight');
|
||||
require("brace/mode/python");
|
||||
require("brace/theme/chrome");
|
||||
require('brace/theme/dracula');
|
||||
// ace-builds also has a minified build (src-min-noconflict), but we don't
|
||||
// use it since webpack already handles minification.
|
||||
require('ace-builds/src-noconflict/ext-static_highlight');
|
||||
require('ace-builds/src-noconflict/mode-python');
|
||||
require('ace-builds/src-noconflict/theme-chrome');
|
||||
require('ace-builds/src-noconflict/theme-dracula');
|
||||
|
||||
export interface ICodeOptions {
|
||||
gristTheme: Computed<Theme>;
|
||||
@@ -22,10 +23,11 @@ export function buildHighlightedCode(
|
||||
const {gristTheme, placeholder, maxLines} = options;
|
||||
const {enableCustomCss} = getGristConfig();
|
||||
|
||||
const highlighter = ace.acequire('ace/ext/static_highlight');
|
||||
const PythonMode = ace.acequire('ace/mode/python').Mode;
|
||||
const chrome = ace.acequire('ace/theme/chrome');
|
||||
const dracula = ace.acequire('ace/theme/dracula');
|
||||
const highlighter = ace.require('ace/ext/static_highlight');
|
||||
const PythonMode = ace.require('ace/mode/python').Mode;
|
||||
const aceDom = ace.require('ace/lib/dom');
|
||||
const chrome = ace.require('ace/theme/chrome');
|
||||
const dracula = ace.require('ace/theme/dracula');
|
||||
const mode = new PythonMode();
|
||||
|
||||
const codeText = Observable.create(null, '');
|
||||
@@ -33,21 +35,37 @@ export function buildHighlightedCode(
|
||||
|
||||
function updateHighlightedCode(elem: HTMLElement) {
|
||||
let text = codeText.get();
|
||||
if (text) {
|
||||
if (maxLines) {
|
||||
// If requested, trim to maxLines, and add an ellipsis at the end.
|
||||
// (Long lines are also truncated with an ellpsis via text-overflow style.)
|
||||
const lines = text.split(/\n/);
|
||||
if (lines.length > maxLines) {
|
||||
text = lines.slice(0, maxLines).join("\n") + " \u2026"; // Ellipsis
|
||||
}
|
||||
}
|
||||
|
||||
const aceTheme = codeTheme.get().appearance === 'dark' && !enableCustomCss ? dracula : chrome;
|
||||
elem.innerHTML = highlighter.render(text, mode, aceTheme, 1, true).html;
|
||||
} else {
|
||||
if (!text) {
|
||||
elem.textContent = placeholder || '';
|
||||
return;
|
||||
}
|
||||
|
||||
if (maxLines) {
|
||||
// If requested, trim to maxLines, and add an ellipsis at the end.
|
||||
// (Long lines are also truncated with an ellpsis via text-overflow style.)
|
||||
const lines = text.split(/\n/);
|
||||
if (lines.length > maxLines) {
|
||||
text = lines.slice(0, maxLines).join("\n") + " \u2026"; // Ellipsis
|
||||
}
|
||||
}
|
||||
|
||||
let aceThemeName: 'chrome' | 'dracula';
|
||||
let aceTheme: any;
|
||||
if (codeTheme.get().appearance === 'dark' && !enableCustomCss) {
|
||||
aceThemeName = 'dracula';
|
||||
aceTheme = dracula;
|
||||
} else {
|
||||
aceThemeName = 'chrome';
|
||||
aceTheme = chrome;
|
||||
}
|
||||
|
||||
// Rendering highlighted code gives you back the HTML to insert into the DOM, as well
|
||||
// as the CSS styles needed to apply the theme. The latter typically isn't included in
|
||||
// the document until an Ace editor is opened, so we explicitly import it here to avoid
|
||||
// leaving highlighted code blocks without a theme applied.
|
||||
const {html, css} = highlighter.render(text, mode, aceTheme, 1, true);
|
||||
elem.innerHTML = html;
|
||||
aceDom.importCssString(css, `${aceThemeName}-highlighted-code`);
|
||||
}
|
||||
|
||||
return cssHighlightedCode(
|
||||
|
||||
@@ -2,7 +2,7 @@ import {GristDoc} from 'app/client/components/GristDoc';
|
||||
import {logTelemetryEvent} from 'app/client/lib/telemetry';
|
||||
import {getWelcomeHomeUrl, urlState} from 'app/client/models/gristUrlState';
|
||||
import {renderer} from 'app/client/ui/DocTutorialRenderer';
|
||||
import {cssPopupBody, FloatingPopup} from 'app/client/ui/FloatingPopup';
|
||||
import {cssPopupBody, FLOATING_POPUP_TOOLTIP_KEY, FloatingPopup} from 'app/client/ui/FloatingPopup';
|
||||
import {sanitizeHTML} from 'app/client/ui/sanitizeHTML';
|
||||
import {hoverTooltip, setHoverTooltip} from 'app/client/ui/tooltips';
|
||||
import {basicButton, primaryButton} from 'app/client/ui2018/buttons';
|
||||
@@ -26,8 +26,6 @@ interface DocTutorialSlide {
|
||||
|
||||
const testId = makeTestId('test-doc-tutorial-');
|
||||
|
||||
const TOOLTIP_KEY = 'docTutorialTooltip';
|
||||
|
||||
export class DocTutorial extends FloatingPopup {
|
||||
private _appModel = this._gristDoc.docPageModel.appModel;
|
||||
private _currentDoc = this._gristDoc.docPageModel.currentDoc.get();
|
||||
@@ -47,7 +45,10 @@ export class DocTutorial extends FloatingPopup {
|
||||
});
|
||||
|
||||
constructor(private _gristDoc: GristDoc) {
|
||||
super({stopClickPropagationOnMove: true});
|
||||
super({
|
||||
minimizable: true,
|
||||
stopClickPropagationOnMove: true,
|
||||
});
|
||||
}
|
||||
|
||||
public async start() {
|
||||
@@ -102,7 +103,7 @@ export class DocTutorial extends FloatingPopup {
|
||||
return [
|
||||
cssFooterButtonsLeft(
|
||||
cssPopupFooterButton(icon('Undo'),
|
||||
hoverTooltip('Restart Tutorial', {key: TOOLTIP_KEY}),
|
||||
hoverTooltip('Restart Tutorial', {key: FLOATING_POPUP_TOOLTIP_KEY}),
|
||||
dom.on('click', () => this._restartTutorial()),
|
||||
testId('popup-restart'),
|
||||
),
|
||||
@@ -111,7 +112,7 @@ export class DocTutorial extends FloatingPopup {
|
||||
range(slides.length).map((i) => cssProgressBarDot(
|
||||
hoverTooltip(slides[i].slideTitle, {
|
||||
closeOnClick: false,
|
||||
key: TOOLTIP_KEY,
|
||||
key: FLOATING_POPUP_TOOLTIP_KEY,
|
||||
}),
|
||||
cssProgressBarDot.cls('-current', i === slideIndex),
|
||||
i === slideIndex ? null : dom.on('click', () => this._changeSlide(i)),
|
||||
@@ -315,7 +316,7 @@ export class DocTutorial extends FloatingPopup {
|
||||
img.src = img.src;
|
||||
|
||||
setHoverTooltip(img, 'Click to expand', {
|
||||
key: TOOLTIP_KEY,
|
||||
key: FLOATING_POPUP_TOOLTIP_KEY,
|
||||
modifiers: {
|
||||
flip: {
|
||||
boundariesElement: 'scrollParent',
|
||||
|
||||
@@ -89,12 +89,22 @@ export function buildNameConfig(
|
||||
];
|
||||
}
|
||||
|
||||
export interface BuildEditorOptions {
|
||||
// Element to attach to.
|
||||
refElem: Element;
|
||||
// Should the detach button be shown?
|
||||
canDetach: boolean;
|
||||
// Simulate user typing on the cell - open editor with an initial value.
|
||||
editValue?: string;
|
||||
// Custom save handler.
|
||||
onSave?: SaveHandler;
|
||||
// Custom cancel handler.
|
||||
onCancel?: () => void;
|
||||
}
|
||||
|
||||
type SaveHandler = (column: ColumnRec, formula: string) => Promise<void>;
|
||||
type BuildEditor = (
|
||||
cellElem: Element,
|
||||
editValue?: string,
|
||||
onSave?: SaveHandler,
|
||||
onCancel?: () => void) => void;
|
||||
|
||||
type BuildEditor = (options: BuildEditorOptions) => void;
|
||||
|
||||
export function buildFormulaConfig(
|
||||
owner: MultiHolder, origColumn: ColumnRec, gristDoc: GristDoc, buildEditor: BuildEditor
|
||||
@@ -315,14 +325,14 @@ export function buildFormulaConfig(
|
||||
|
||||
const errorMessage = createFormulaErrorObs(owner, gristDoc, origColumn);
|
||||
// Helper that will create different flavors for formula builder.
|
||||
const formulaBuilder = (onSave: SaveHandler) => [
|
||||
const formulaBuilder = (onSave: SaveHandler, canDetach?: boolean) => [
|
||||
cssRow(formulaField = buildFormula(
|
||||
origColumn,
|
||||
buildEditor,
|
||||
{
|
||||
gristTheme: gristDoc.currentTheme,
|
||||
placeholder: t("Enter formula"),
|
||||
disabled: disableOtherActions,
|
||||
canDetach,
|
||||
onSave,
|
||||
onCancel: clearState,
|
||||
})),
|
||||
@@ -386,7 +396,7 @@ export function buildFormulaConfig(
|
||||
// If data column is or wants to be a trigger formula:
|
||||
dom.maybe((use) => use(maybeTrigger) || use(origColumn.hasTriggerFormula), () => [
|
||||
cssLabel(t("TRIGGER FORMULA")),
|
||||
formulaBuilder(onSaveConvertToTrigger),
|
||||
formulaBuilder(onSaveConvertToTrigger, false),
|
||||
dom.create(buildFormulaTriggers, origColumn, {
|
||||
disabled: disableOtherActions,
|
||||
notTrigger: maybeTrigger,
|
||||
@@ -411,8 +421,8 @@ export function buildFormulaConfig(
|
||||
|
||||
interface BuildFormulaOptions {
|
||||
gristTheme: Computed<Theme>;
|
||||
placeholder: string;
|
||||
disabled: Observable<boolean>;
|
||||
canDetach?: boolean;
|
||||
onSave?: SaveHandler;
|
||||
onCancel?: () => void;
|
||||
}
|
||||
@@ -422,8 +432,8 @@ function buildFormula(
|
||||
buildEditor: BuildEditor,
|
||||
options: BuildFormulaOptions
|
||||
) {
|
||||
const {gristTheme, placeholder, disabled, onSave, onCancel} = options;
|
||||
return cssFieldFormula(column.formula, {gristTheme, placeholder, maxLines: 2},
|
||||
const {gristTheme, disabled, canDetach = true, onSave, onCancel} = options;
|
||||
return cssFieldFormula(column.formula, {gristTheme, maxLines: 2},
|
||||
dom.cls('formula_field_sidepane'),
|
||||
cssFieldFormula.cls('-disabled', disabled),
|
||||
cssFieldFormula.cls('-disabled-icon', use => !use(column.formula)),
|
||||
@@ -431,7 +441,13 @@ function buildFormula(
|
||||
{tabIndex: '-1'},
|
||||
// Focus event use used by a user to edit an existing formula.
|
||||
// It can also be triggered manually to open up the editor.
|
||||
dom.on('focus', (_, elem) => buildEditor(elem, undefined, onSave, onCancel)),
|
||||
dom.on('focus', (_, refElem) => buildEditor({
|
||||
refElem,
|
||||
editValue: undefined,
|
||||
canDetach,
|
||||
onSave,
|
||||
onCancel,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,31 +1,45 @@
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {documentCursor} from 'app/client/lib/popupUtils';
|
||||
import {hoverTooltip} from 'app/client/ui/tooltips';
|
||||
import {isNarrowScreen, isNarrowScreenObs, theme, vars} from 'app/client/ui2018/cssVars';
|
||||
import {IconName} from 'app/client/ui2018/IconList';
|
||||
import {icon} from 'app/client/ui2018/icons';
|
||||
import {Disposable, dom, DomContents, DomElementArg,
|
||||
IDisposable, makeTestId, Observable, styled} from 'grainjs';
|
||||
|
||||
const POPUP_INITIAL_PADDING_PX = 16;
|
||||
const POPUP_MIN_HEIGHT = 300;
|
||||
const POPUP_DEFAULT_MIN_HEIGHT = 300;
|
||||
const POPUP_MAX_HEIGHT = 711;
|
||||
const POPUP_HEADER_HEIGHT = 30;
|
||||
|
||||
const t = makeT('FloatingPopup');
|
||||
|
||||
const testId = makeTestId('test-floating-popup-');
|
||||
|
||||
export const FLOATING_POPUP_TOOLTIP_KEY = 'floatingPopupTooltip';
|
||||
|
||||
export interface PopupOptions {
|
||||
title?: () => DomContents;
|
||||
content?: () => DomContents;
|
||||
onClose?: () => void;
|
||||
closeButton?: boolean;
|
||||
closeButtonIcon?: IconName;
|
||||
closeButtonHover?: () => DomContents;
|
||||
minimizable?: boolean;
|
||||
autoHeight?: boolean;
|
||||
/** Minimum height in pixels. */
|
||||
minHeight?: number;
|
||||
/** Defaults to false. */
|
||||
stopClickPropagationOnMove?: boolean;
|
||||
initialPosition?: [left: number, top: number];
|
||||
args?: DomElementArg[];
|
||||
}
|
||||
|
||||
export class FloatingPopup extends Disposable {
|
||||
protected _isMinimized = Observable.create(this, false);
|
||||
private _closable = this._options.closeButton ?? false;
|
||||
private _minimizable = this._options.minimizable ?? false;
|
||||
private _minHeight = this._options.minHeight ?? POPUP_DEFAULT_MIN_HEIGHT;
|
||||
private _isFinishingMove = false;
|
||||
private _popupElement: HTMLElement | null = null;
|
||||
private _popupMinimizeButtonElement: HTMLElement | null = null;
|
||||
@@ -71,7 +85,7 @@ export class FloatingPopup extends Disposable {
|
||||
this.autoDispose(isNarrowScreenObs().addListener(() => this._repositionPopup()));
|
||||
|
||||
this.onDispose(() => {
|
||||
this._closePopup();
|
||||
this._disposePopup();
|
||||
this._cursorGrab?.dispose();
|
||||
});
|
||||
}
|
||||
@@ -79,18 +93,22 @@ export class FloatingPopup extends Disposable {
|
||||
public showPopup() {
|
||||
this._popupElement = this._buildPopup();
|
||||
document.body.appendChild(this._popupElement);
|
||||
const topPaddingPx = getTopPopupPaddingPx();
|
||||
const initialLeft = document.body.offsetWidth - this._popupElement.offsetWidth - POPUP_INITIAL_PADDING_PX;
|
||||
const initialTop = document.body.offsetHeight - this._popupElement.offsetHeight - topPaddingPx;
|
||||
this._popupElement.style.left = `${initialLeft}px`;
|
||||
this._popupElement.style.top = `${initialTop}px`;
|
||||
|
||||
const {initialPosition} = this._options;
|
||||
if (initialPosition) {
|
||||
this._setPosition(initialPosition);
|
||||
this._repositionPopup();
|
||||
} else {
|
||||
const left = document.body.offsetWidth - this._popupElement.offsetWidth - POPUP_INITIAL_PADDING_PX;
|
||||
const top = document.body.offsetHeight - this._popupElement.offsetHeight - getTopPopupPaddingPx();
|
||||
this._setPosition([left, top]);
|
||||
}
|
||||
}
|
||||
|
||||
protected _closePopup() {
|
||||
if (!this._popupElement) { return; }
|
||||
document.body.removeChild(this._popupElement);
|
||||
dom.domDispose(this._popupElement);
|
||||
this._popupElement = null;
|
||||
if (!this._closable) { return; }
|
||||
|
||||
this._disposePopup();
|
||||
}
|
||||
|
||||
protected _buildTitle(): DomContents {
|
||||
@@ -105,6 +123,21 @@ export class FloatingPopup extends Disposable {
|
||||
return this._options.args ?? [];
|
||||
}
|
||||
|
||||
private _disposePopup() {
|
||||
if (!this._popupElement) { return; }
|
||||
|
||||
document.body.removeChild(this._popupElement);
|
||||
dom.domDispose(this._popupElement);
|
||||
this._popupElement = null;
|
||||
}
|
||||
|
||||
private _setPosition([left, top]: [left: number, top: number]) {
|
||||
if (!this._popupElement) { return; }
|
||||
|
||||
this._popupElement.style.left = `${left}px`;
|
||||
this._popupElement.style.top = `${top}px`;
|
||||
}
|
||||
|
||||
private _rememberPosition() {
|
||||
this._initialLeft = this._popupElement!.offsetLeft;
|
||||
this._initialTop = this._popupElement!.offsetTop;
|
||||
@@ -151,7 +184,7 @@ export class FloatingPopup extends Disposable {
|
||||
|
||||
// First just how much we can resize the popup.
|
||||
let minTop = this._initialBottom - POPUP_MAX_HEIGHT;
|
||||
let maxTop = this._initialBottom - POPUP_MIN_HEIGHT;
|
||||
let maxTop = this._initialBottom - this._minHeight;
|
||||
|
||||
// Now how far we can move top (leave at least some padding for mobile).
|
||||
minTop = Math.max(minTop, getTopPopupPaddingPx());
|
||||
@@ -250,6 +283,8 @@ export class FloatingPopup extends Disposable {
|
||||
}
|
||||
|
||||
private _minimizeOrMaximize() {
|
||||
if (!this._minimizable) { return; }
|
||||
|
||||
this._isMinimized.set(!this._isMinimized.get());
|
||||
this._repositionPopup();
|
||||
}
|
||||
@@ -258,6 +293,7 @@ export class FloatingPopup extends Disposable {
|
||||
const body = cssPopup(
|
||||
{tabIndex: '-1'},
|
||||
cssPopup.cls('-auto', this._options.autoHeight ?? false),
|
||||
dom.style('min-height', `${this._minHeight}px`),
|
||||
cssPopupHeader(
|
||||
cssBottomHandle(testId('move-handle')),
|
||||
dom.maybe(use => !use(this._isMinimized), () => {
|
||||
@@ -277,10 +313,12 @@ export class FloatingPopup extends Disposable {
|
||||
// center the title.
|
||||
cssPopupButtons(
|
||||
cssPopupHeaderButton(
|
||||
icon('Maximize')
|
||||
icon('Maximize'),
|
||||
dom.show(this._minimizable),
|
||||
),
|
||||
!this._options.closeButton ? null : cssPopupHeaderButton(
|
||||
cssPopupHeaderButton(
|
||||
icon('CrossBig'),
|
||||
dom.show(this._closable),
|
||||
),
|
||||
dom.style('visibility', 'hidden'),
|
||||
),
|
||||
@@ -291,17 +329,23 @@ export class FloatingPopup extends Disposable {
|
||||
cssPopupButtons(
|
||||
this._popupMinimizeButtonElement = cssPopupHeaderButton(
|
||||
isMinimized ? icon('Maximize'): icon('Minimize'),
|
||||
hoverTooltip(isMinimized ? 'Maximize' : 'Minimize', {key: 'docTutorialTooltip'}),
|
||||
hoverTooltip(isMinimized ? t('Maximize') : t('Minimize'), {
|
||||
key: FLOATING_POPUP_TOOLTIP_KEY,
|
||||
}),
|
||||
dom.on('click', () => this._minimizeOrMaximize()),
|
||||
dom.show(this._minimizable),
|
||||
testId('minimize-maximize'),
|
||||
),
|
||||
!this._options.closeButton ? null : cssPopupHeaderButton(
|
||||
icon('CrossBig'),
|
||||
cssPopupHeaderButton(
|
||||
icon(this._options.closeButtonIcon ?? 'CrossBig'),
|
||||
this._options.closeButtonHover && hoverTooltip(this._options.closeButtonHover(), {
|
||||
key: FLOATING_POPUP_TOOLTIP_KEY,
|
||||
}),
|
||||
dom.on('click', () => {
|
||||
this._options.onClose?.() ?? this._closePopup();
|
||||
}),
|
||||
dom.show(this._closable),
|
||||
testId('close'),
|
||||
this._options.closeButtonHover && hoverTooltip(this._options.closeButtonHover())
|
||||
),
|
||||
// Disable dragging when a button in the header is clicked.
|
||||
dom.on('mousedown', ev => ev.stopPropagation()),
|
||||
@@ -362,7 +406,9 @@ function getTopPopupPaddingPx(): number {
|
||||
|
||||
const POPUP_HEIGHT = `min(var(--height), calc(100% - (2 * ${POPUP_INITIAL_PADDING_PX}px)))`;
|
||||
const POPUP_HEIGHT_MOBILE = `min(var(--height), calc(100% - (2 * ${POPUP_INITIAL_PADDING_PX}px) - (2 * 50px)))`;
|
||||
const POPUP_WIDTH = `min(436px, calc(100% - (2 * ${POPUP_INITIAL_PADDING_PX}px)))`;
|
||||
|
||||
export const FLOATING_POPUP_MAX_WIDTH_PX = 436;
|
||||
const POPUP_WIDTH = `min(${FLOATING_POPUP_MAX_WIDTH_PX}px, calc(100% - (2 * ${POPUP_INITIAL_PADDING_PX}px)))`;
|
||||
|
||||
const cssPopup = styled('div.floating-popup', `
|
||||
position: fixed;
|
||||
@@ -374,7 +420,6 @@ const cssPopup = styled('div.floating-popup', `
|
||||
--height: ${POPUP_MAX_HEIGHT}px;
|
||||
height: ${POPUP_HEIGHT};
|
||||
width: ${POPUP_WIDTH};
|
||||
min-height: ${POPUP_MIN_HEIGHT}px;
|
||||
background-color: ${theme.popupBg};
|
||||
box-shadow: 0 2px 18px 0 ${theme.popupInnerShadow}, 0 0 1px 0 ${theme.popupOuterShadow};
|
||||
outline: unset;
|
||||
|
||||
@@ -23,12 +23,13 @@ import * as imports from 'app/client/lib/imports';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {createSessionObs} from 'app/client/lib/sessionObs';
|
||||
import {reportError} from 'app/client/models/AppModel';
|
||||
import {ColumnRec, ViewSectionRec} from 'app/client/models/DocModel';
|
||||
import {ViewSectionRec} from 'app/client/models/DocModel';
|
||||
import {GridOptions} from 'app/client/ui/GridOptions';
|
||||
import {attachPageWidgetPicker, IPageWidget, toPageWidget} from 'app/client/ui/PageWidgetPicker';
|
||||
import {linkId, selectBy} from 'app/client/ui/selectBy';
|
||||
import {CustomSectionConfig} from 'app/client/ui/CustomSectionConfig';
|
||||
import {buildDescriptionConfig} from 'app/client/ui/DescriptionConfig';
|
||||
import {BuildEditorOptions} from 'app/client/ui/FieldConfig';
|
||||
import {cssLabel} from 'app/client/ui/RightPanelStyles';
|
||||
import {VisibleFieldsConfig} from 'app/client/ui/VisibleFieldsConfig';
|
||||
import {IWidgetType, widgetTypes} from 'app/client/ui/widgetTypes';
|
||||
@@ -295,19 +296,20 @@ export class RightPanel extends Disposable {
|
||||
}
|
||||
|
||||
// Helper to activate the side-pane formula editor over the given HTML element.
|
||||
private _activateFormulaEditor(
|
||||
// Element to attach to.
|
||||
refElem: Element,
|
||||
// Simulate user typing on the cell - open editor with an initial value.
|
||||
editValue?: string,
|
||||
// Custom save handler.
|
||||
onSave?: (column: ColumnRec, formula: string) => Promise<void>,
|
||||
// Custom cancel handler.
|
||||
onCancel?: () => void) {
|
||||
private _activateFormulaEditor(options: BuildEditorOptions) {
|
||||
const vsi = this._gristDoc.viewModel.activeSection().viewInstance();
|
||||
if (!vsi) { return; }
|
||||
const editRowModel = vsi.moveEditRowToCursor();
|
||||
return vsi.activeFieldBuilder.peek().openSideFormulaEditor(editRowModel, refElem, editValue, onSave, onCancel);
|
||||
|
||||
const {refElem, editValue, canDetach, onSave, onCancel} = options;
|
||||
const editRow = vsi.moveEditRowToCursor();
|
||||
return vsi.activeFieldBuilder.peek().openSideFormulaEditor({
|
||||
editRow,
|
||||
refElem,
|
||||
canDetach,
|
||||
editValue,
|
||||
onSave,
|
||||
onCancel,
|
||||
});
|
||||
}
|
||||
|
||||
private _buildPageWidgetContent(_owner: MultiHolder) {
|
||||
|
||||
Reference in New Issue
Block a user