mirror of
				https://github.com/gristlabs/grist-core.git
				synced 2025-06-13 20:53:59 +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:
		
							parent
							
								
									8581492912
								
							
						
					
					
						commit
						ea8a59c5e9
					
				@ -1,8 +1,8 @@
 | 
				
			|||||||
 | 
					import ace, {Ace} from 'ace-builds';
 | 
				
			||||||
import {setupAceEditorCompletions} from 'app/client/components/AceEditorCompletions';
 | 
					import {setupAceEditorCompletions} from 'app/client/components/AceEditorCompletions';
 | 
				
			||||||
import {theme} from 'app/client/ui2018/cssVars';
 | 
					import {theme} from 'app/client/ui2018/cssVars';
 | 
				
			||||||
import {Theme} from 'app/common/ThemePrefs';
 | 
					import {Theme} from 'app/common/ThemePrefs';
 | 
				
			||||||
import {getGristConfig} from 'app/common/urlUtils';
 | 
					import {getGristConfig} from 'app/common/urlUtils';
 | 
				
			||||||
import * as ace from 'brace';
 | 
					 | 
				
			||||||
import {Computed, dom, DomArg, Listener, Observable, styled} from 'grainjs';
 | 
					import {Computed, dom, DomArg, Listener, Observable, styled} from 'grainjs';
 | 
				
			||||||
import debounce from 'lodash/debounce';
 | 
					import debounce from 'lodash/debounce';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -13,13 +13,13 @@ export interface ACLFormulaOptions {
 | 
				
			|||||||
  placeholder: DomArg;
 | 
					  placeholder: DomArg;
 | 
				
			||||||
  setValue: (value: string) => void;
 | 
					  setValue: (value: string) => void;
 | 
				
			||||||
  getSuggestions: (prefix: string) => string[];
 | 
					  getSuggestions: (prefix: string) => string[];
 | 
				
			||||||
  customiseEditor?: (editor: ace.Editor) => void;
 | 
					  customiseEditor?: (editor: Ace.Editor) => void;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function aclFormulaEditor(options: ACLFormulaOptions) {
 | 
					export function aclFormulaEditor(options: ACLFormulaOptions) {
 | 
				
			||||||
  // Create an element and an editor within it.
 | 
					  // Create an element and an editor within it.
 | 
				
			||||||
  const editorElem = dom('div');
 | 
					  const editorElem = dom('div');
 | 
				
			||||||
  const editor: ace.Editor = ace.edit(editorElem);
 | 
					  const editor: Ace.Editor = ace.edit(editorElem);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Set various editor options.
 | 
					  // Set various editor options.
 | 
				
			||||||
  function setAceTheme(gristTheme: Theme) {
 | 
					  function setAceTheme(gristTheme: Theme) {
 | 
				
			||||||
@ -40,7 +40,7 @@ export function aclFormulaEditor(options: ACLFormulaOptions) {
 | 
				
			|||||||
  editor.renderer.setShowGutter(false);       // Default line numbers to hidden
 | 
					  editor.renderer.setShowGutter(false);       // Default line numbers to hidden
 | 
				
			||||||
  editor.renderer.setPadding(5);
 | 
					  editor.renderer.setPadding(5);
 | 
				
			||||||
  editor.renderer.setScrollMargin(4, 4, 0, 0);
 | 
					  editor.renderer.setScrollMargin(4, 4, 0, 0);
 | 
				
			||||||
  editor.$blockScrolling = Infinity;
 | 
					  (editor as any).$blockScrolling = Infinity;
 | 
				
			||||||
  editor.setReadOnly(options.readOnly);
 | 
					  editor.setReadOnly(options.readOnly);
 | 
				
			||||||
  editor.setFontSize('12');
 | 
					  editor.setFontSize('12');
 | 
				
			||||||
  editor.setHighlightActiveLine(false);
 | 
					  editor.setHighlightActiveLine(false);
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,7 @@
 | 
				
			|||||||
 | 
					.ace_editor {
 | 
				
			||||||
 | 
					  background-color: var(--grist-theme-ace-editor-bg, white);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.ace_grist_link_hidden {
 | 
					.ace_grist_link_hidden {
 | 
				
			||||||
  display: none;
 | 
					  display: none;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -14,6 +18,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
.ace_editor.ace_autocomplete .ace_completion-highlight {
 | 
					.ace_editor.ace_autocomplete .ace_completion-highlight {
 | 
				
			||||||
  color: var(--grist-theme-ace-autocomplete-highlighted-fg, #000) !important;
 | 
					  color: var(--grist-theme-ace-autocomplete-highlighted-fg, #000) !important;
 | 
				
			||||||
 | 
					  text-shadow: 0 0 0.01em;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.ace_editor.ace_autocomplete .ace_completion-highlight.ace_grist_link {
 | 
					.ace_editor.ace_autocomplete .ace_completion-highlight.ace_grist_link {
 | 
				
			||||||
@ -41,3 +46,8 @@
 | 
				
			|||||||
.ace_editor.ace_autocomplete .ace_marker-layer .ace_active-line {
 | 
					.ace_editor.ace_autocomplete .ace_marker-layer .ace_active-line {
 | 
				
			||||||
  background-color: var(--grist-theme-ace-autocomplete-active-line-bg, #CAD6FA) !important;
 | 
					  background-color: var(--grist-theme-ace-autocomplete-active-line-bg, #CAD6FA) !important;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.ace_autocomplete .ace_line .ace_ {
 | 
				
			||||||
 | 
					  /* Ace collapses whitespace by default, which breaks alignment changes made in AceEditorCompletions.ts. */
 | 
				
			||||||
 | 
					  white-space: pre !important;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,10 +1,11 @@
 | 
				
			|||||||
var ace = require('brace');
 | 
					var ace = require('ace-builds');
 | 
				
			||||||
var _ = require('underscore');
 | 
					var _ = require('underscore');
 | 
				
			||||||
// Used to load python language settings and color themes
 | 
					// ace-builds also has a minified build (src-min-noconflict), but we don't
 | 
				
			||||||
require('brace/mode/python');
 | 
					// use it since webpack already handles minification.
 | 
				
			||||||
require('brace/theme/chrome');
 | 
					require('ace-builds/src-noconflict/mode-python');
 | 
				
			||||||
require('brace/theme/dracula');
 | 
					require('ace-builds/src-noconflict/theme-chrome');
 | 
				
			||||||
require('brace/ext/language_tools');
 | 
					require('ace-builds/src-noconflict/theme-dracula');
 | 
				
			||||||
 | 
					require('ace-builds/src-noconflict/ext-language_tools');
 | 
				
			||||||
var {setupAceEditorCompletions} = require('./AceEditorCompletions');
 | 
					var {setupAceEditorCompletions} = require('./AceEditorCompletions');
 | 
				
			||||||
var {getGristConfig} = require('../../common/urlUtils');
 | 
					var {getGristConfig} = require('../../common/urlUtils');
 | 
				
			||||||
var dom = require('../lib/dom');
 | 
					var dom = require('../lib/dom');
 | 
				
			||||||
@ -291,7 +292,7 @@ AceEditor.prototype._setAceTheme = function(gristTheme) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
let _RangeConstructor = null; //singleton, load it lazily
 | 
					let _RangeConstructor = null; //singleton, load it lazily
 | 
				
			||||||
AceEditor.makeRange = function(a, b, c, d) {
 | 
					AceEditor.makeRange = function(a, b, c, d) {
 | 
				
			||||||
  _RangeConstructor = _RangeConstructor || ace.acequire('ace/range').Range;
 | 
					  _RangeConstructor = _RangeConstructor || ace.require('ace/range').Range;
 | 
				
			||||||
  return new _RangeConstructor(a, b, c, d);
 | 
					  return new _RangeConstructor(a, b, c, d);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,14 +1,14 @@
 | 
				
			|||||||
 | 
					import ace, {Ace} from 'ace-builds';
 | 
				
			||||||
import {ISuggestionWithValue} from 'app/common/ActiveDocAPI';
 | 
					import {ISuggestionWithValue} from 'app/common/ActiveDocAPI';
 | 
				
			||||||
import {commonUrls} from 'app/common/gristUrls';
 | 
					import {commonUrls} from 'app/common/gristUrls';
 | 
				
			||||||
import * as ace from 'brace';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface ICompletionOptions {
 | 
					export interface ICompletionOptions {
 | 
				
			||||||
  getSuggestions(prefix: string): Promise<ISuggestionWithValue[]>;
 | 
					  getSuggestions(prefix: string): Promise<ISuggestionWithValue[]>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const completionOptions = new WeakMap<ace.Editor, ICompletionOptions>();
 | 
					const completionOptions = new WeakMap<Ace.Editor, ICompletionOptions>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function setupAceEditorCompletions(editor: ace.Editor, options: ICompletionOptions) {
 | 
					export function setupAceEditorCompletions(editor: Ace.Editor, options: ICompletionOptions) {
 | 
				
			||||||
  initCustomCompleter();
 | 
					  initCustomCompleter();
 | 
				
			||||||
  completionOptions.set(editor, options);
 | 
					  completionOptions.set(editor, options);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -17,7 +17,7 @@ export function setupAceEditorCompletions(editor: ace.Editor, options: ICompleti
 | 
				
			|||||||
  // It is important for autoSelect to be off so that hitting enter doesn't automatically
 | 
					  // It is important for autoSelect to be off so that hitting enter doesn't automatically
 | 
				
			||||||
  // use a suggestion, a change of behavior that doesn't seem particularly desirable and
 | 
					  // use a suggestion, a change of behavior that doesn't seem particularly desirable and
 | 
				
			||||||
  // which also breaks several existing tests.
 | 
					  // which also breaks several existing tests.
 | 
				
			||||||
  const {Autocomplete} = ace.acequire('ace/autocomplete'); // lives in brace/ext/language_tools
 | 
					  const {Autocomplete} = ace.require('ace/autocomplete');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const completer = new Autocomplete();
 | 
					  const completer = new Autocomplete();
 | 
				
			||||||
  completer.autoSelect = false;
 | 
					  completer.autoSelect = false;
 | 
				
			||||||
@ -69,7 +69,7 @@ export function setupAceEditorCompletions(editor: ace.Editor, options: ICompleti
 | 
				
			|||||||
  // it adds to body even when it detaches itself. Ace's AutoCompleter doesn't expose any
 | 
					  // it adds to body even when it detaches itself. Ace's AutoCompleter doesn't expose any
 | 
				
			||||||
  // interface for this, so this takes some hacking. (One reason for this is that Ace seems to
 | 
					  // interface for this, so this takes some hacking. (One reason for this is that Ace seems to
 | 
				
			||||||
  // expect that a single AutoCompleter would be used for all editor instances.)
 | 
					  // expect that a single AutoCompleter would be used for all editor instances.)
 | 
				
			||||||
  editor.on('destroy', () => {
 | 
					  editor.on('destroy' as any, () => {
 | 
				
			||||||
    if (completer.editor) {
 | 
					    if (completer.editor) {
 | 
				
			||||||
      completer.detach();
 | 
					      completer.detach();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -91,10 +91,10 @@ function initCustomCompleter() {
 | 
				
			|||||||
  const prefixMatchRegex = /\w+\.(?:lookupRecords|lookupOne)\([\w.$\u00A2-\uFFFF]*$|[\w.$\u00A2-\uFFFF]+$/;
 | 
					  const prefixMatchRegex = /\w+\.(?:lookupRecords|lookupOne)\([\w.$\u00A2-\uFFFF]*$|[\w.$\u00A2-\uFFFF]+$/;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Monkey-patch getCompletionPrefix. This is based on the source code in
 | 
					  // Monkey-patch getCompletionPrefix. This is based on the source code in
 | 
				
			||||||
  // node_modules/brace/ext/language_tools.js, simplified to do the one thing we want here (since
 | 
					  // node_modules/ace-builds/src-noconflict/ext-language_tools.js, simplified to do the one thing
 | 
				
			||||||
  // the original method's generality doesn't help us here).
 | 
					  // we want here (since the original method's generality doesn't help us here).
 | 
				
			||||||
  const util = ace.acequire('ace/autocomplete/util');   // lives in brace/ext/language_tools
 | 
					  const util = ace.require('ace/autocomplete/util');
 | 
				
			||||||
  util.getCompletionPrefix = function getCompletionPrefix(this: any, editor: ace.Editor) {
 | 
					  util.getCompletionPrefix = function getCompletionPrefix(this: any, editor: Ace.Editor) {
 | 
				
			||||||
    const pos = editor.getCursorPosition();
 | 
					    const pos = editor.getCursorPosition();
 | 
				
			||||||
    const line = editor.session.getLine(pos.row);
 | 
					    const line = editor.session.getLine(pos.row);
 | 
				
			||||||
    const match = line.slice(0, pos.column).match(prefixMatchRegex);
 | 
					    const match = line.slice(0, pos.column).match(prefixMatchRegex);
 | 
				
			||||||
@ -102,14 +102,14 @@ function initCustomCompleter() {
 | 
				
			|||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Add some autocompletion with partial access to document
 | 
					  // Add some autocompletion with partial access to document
 | 
				
			||||||
  const aceLanguageTools = ace.acequire('ace/ext/language_tools');
 | 
					  const aceLanguageTools = ace.require('ace/ext/language_tools');
 | 
				
			||||||
  aceLanguageTools.setCompleters([]);
 | 
					  aceLanguageTools.setCompleters([]);
 | 
				
			||||||
  aceLanguageTools.addCompleter({
 | 
					  aceLanguageTools.addCompleter({
 | 
				
			||||||
    // For autocompletion we ship text to the sandbox and run standard completion there.
 | 
					    // For autocompletion we ship text to the sandbox and run standard completion there.
 | 
				
			||||||
    async getCompletions(
 | 
					    async getCompletions(
 | 
				
			||||||
      editor: ace.Editor,
 | 
					      editor: Ace.Editor,
 | 
				
			||||||
      session: ace.IEditSession,
 | 
					      session: Ace.EditSession,
 | 
				
			||||||
      pos: ace.Position,
 | 
					      pos: Ace.Position,
 | 
				
			||||||
      prefix: string,
 | 
					      prefix: string,
 | 
				
			||||||
      callback: any
 | 
					      callback: any
 | 
				
			||||||
    ) {
 | 
					    ) {
 | 
				
			||||||
@ -120,12 +120,13 @@ function initCustomCompleter() {
 | 
				
			|||||||
      // in the case where one function is being switched with another. Since we normally
 | 
					      // in the case where one function is being switched with another. Since we normally
 | 
				
			||||||
      // append a "(" when completing such suggestions, we need to be careful not to do
 | 
					      // append a "(" when completing such suggestions, we need to be careful not to do
 | 
				
			||||||
      // so if a "(" is already present. One way to do this in ACE is to check if the
 | 
					      // so if a "(" is already present. One way to do this in ACE is to check if the
 | 
				
			||||||
      // current token is an identifier, and the next token is a lparen; if both are true,
 | 
					      // current token is a function/identifier, and the next token is a lparen; if both are
 | 
				
			||||||
      // we skip appending a "(" to each suggestion.
 | 
					      // true, we skip appending a "(" to each suggestion.
 | 
				
			||||||
      const wordRange = session.getWordRange(pos.row, pos.column);
 | 
					      const wordRange = session.getWordRange(pos.row, pos.column);
 | 
				
			||||||
      const token = session.getTokenAt(pos.row, wordRange.end.column) as TokenInfo;
 | 
					      const token = session.getTokenAt(pos.row, wordRange.end.column) as Ace.Token;
 | 
				
			||||||
      const nextToken = session.getTokenAt(pos.row, wordRange.end.column + 1) as TokenInfo|null;
 | 
					      const nextToken = session.getTokenAt(pos.row, wordRange.end.column + 1);
 | 
				
			||||||
      const isRenamingFunc = token.type === 'identifier' && nextToken?.type === 'paren.lparen';
 | 
					      const isRenamingFunc = ['function.support', 'identifier'].includes(token.type)
 | 
				
			||||||
 | 
					        && nextToken?.type === 'paren.lparen';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const suggestions = await options.getSuggestions(prefix);
 | 
					      const suggestions = await options.getSuggestions(prefix);
 | 
				
			||||||
      // ACE autocompletions are very poorly documented. This is somewhat helpful:
 | 
					      // ACE autocompletions are very poorly documented. This is somewhat helpful:
 | 
				
			||||||
@ -209,7 +210,8 @@ interface AceSuggestion {
 | 
				
			|||||||
 * them to look like links, and handle clicks to open the destination URL.
 | 
					 * them to look like links, and handle clicks to open the destination URL.
 | 
				
			||||||
 *
 | 
					 *
 | 
				
			||||||
 * This implementation relies a lot on the details of the implementation in
 | 
					 * This implementation relies a lot on the details of the implementation in
 | 
				
			||||||
 * node_modules/brace/ext/language_tools.js. Updates to brace module may easily break it.
 | 
					 * node_modules/ace-builds/src-noconflict/ext-language_tools.js. Updates to ace-builds module may
 | 
				
			||||||
 | 
					 * easily break it.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
function aceCompleterAddHelpLinks(completer: any) {
 | 
					function aceCompleterAddHelpLinks(completer: any) {
 | 
				
			||||||
  // Replace the $init function in order to intercept the creation of the autocomplete popup.
 | 
					  // Replace the $init function in order to intercept the creation of the autocomplete popup.
 | 
				
			||||||
@ -239,11 +241,7 @@ function customizeAceCompleterPopup(completer: any, popup: any) {
 | 
				
			|||||||
  });
 | 
					  });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface TokenInfo extends ace.TokenInfo {
 | 
					function retokenizeAceCompleterRow(rowData: AceSuggestion, tokens: Ace.Token[]): Ace.Token[] {
 | 
				
			||||||
  type: string;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function retokenizeAceCompleterRow(rowData: AceSuggestion, tokens: TokenInfo[]): TokenInfo[] {
 | 
					 | 
				
			||||||
  if (!(rowData.funcname || rowData.example)) {
 | 
					  if (!(rowData.funcname || rowData.example)) {
 | 
				
			||||||
    // Not a special completion, pass through the result of ACE's original tokenizing.
 | 
					    // Not a special completion, pass through the result of ACE's original tokenizing.
 | 
				
			||||||
    return tokens;
 | 
					    return tokens;
 | 
				
			||||||
 | 
				
			|||||||
@ -29,6 +29,7 @@ export function documentCursor(type: 'ns-resize' | 'grabbing'): IDisposable {
 | 
				
			|||||||
export function movable<T>(options: {
 | 
					export function movable<T>(options: {
 | 
				
			||||||
  onMove: (dx: number, dy: number, state: T) => void,
 | 
					  onMove: (dx: number, dy: number, state: T) => void,
 | 
				
			||||||
  onStart: () => T,
 | 
					  onStart: () => T,
 | 
				
			||||||
 | 
					  onEnd?: () => void,
 | 
				
			||||||
}) {
 | 
					}) {
 | 
				
			||||||
  return (el: HTMLElement) => {
 | 
					  return (el: HTMLElement) => {
 | 
				
			||||||
    // Remember the initial position of the mouse.
 | 
					    // Remember the initial position of the mouse.
 | 
				
			||||||
@ -53,6 +54,7 @@ export function movable<T>(options: {
 | 
				
			|||||||
        options.onMove(dx, dy, state);
 | 
					        options.onMove(dx, dy, state);
 | 
				
			||||||
      }));
 | 
					      }));
 | 
				
			||||||
      owner.autoDispose(dom.onElem(document, 'mouseup', () => {
 | 
					      owner.autoDispose(dom.onElem(document, 'mouseup', () => {
 | 
				
			||||||
 | 
					        options.onEnd?.();
 | 
				
			||||||
        holder.clear();
 | 
					        holder.clear();
 | 
				
			||||||
      }));
 | 
					      }));
 | 
				
			||||||
      owner.autoDispose(documentCursor('ns-resize'));
 | 
					      owner.autoDispose(documentCursor('ns-resize'));
 | 
				
			||||||
 | 
				
			|||||||
@ -1,14 +1,15 @@
 | 
				
			|||||||
 | 
					import * as ace from 'ace-builds';
 | 
				
			||||||
import {theme, vars} from 'app/client/ui2018/cssVars';
 | 
					import {theme, vars} from 'app/client/ui2018/cssVars';
 | 
				
			||||||
import {Theme} from 'app/common/ThemePrefs';
 | 
					import {Theme} from 'app/common/ThemePrefs';
 | 
				
			||||||
import {getGristConfig} from 'app/common/urlUtils';
 | 
					import {getGristConfig} from 'app/common/urlUtils';
 | 
				
			||||||
import * as ace from 'brace';
 | 
					 | 
				
			||||||
import {BindableValue, Computed, dom, DomElementArg, Observable, styled, subscribeElem} from 'grainjs';
 | 
					import {BindableValue, Computed, dom, DomElementArg, Observable, styled, subscribeElem} from 'grainjs';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// tslint:disable:no-var-requires
 | 
					// ace-builds also has a minified build (src-min-noconflict), but we don't
 | 
				
			||||||
require('brace/ext/static_highlight');
 | 
					// use it since webpack already handles minification.
 | 
				
			||||||
require("brace/mode/python");
 | 
					require('ace-builds/src-noconflict/ext-static_highlight');
 | 
				
			||||||
require("brace/theme/chrome");
 | 
					require('ace-builds/src-noconflict/mode-python');
 | 
				
			||||||
require('brace/theme/dracula');
 | 
					require('ace-builds/src-noconflict/theme-chrome');
 | 
				
			||||||
 | 
					require('ace-builds/src-noconflict/theme-dracula');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface ICodeOptions {
 | 
					export interface ICodeOptions {
 | 
				
			||||||
  gristTheme: Computed<Theme>;
 | 
					  gristTheme: Computed<Theme>;
 | 
				
			||||||
@ -22,10 +23,11 @@ export function buildHighlightedCode(
 | 
				
			|||||||
  const {gristTheme, placeholder, maxLines} = options;
 | 
					  const {gristTheme, placeholder, maxLines} = options;
 | 
				
			||||||
  const {enableCustomCss} = getGristConfig();
 | 
					  const {enableCustomCss} = getGristConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const highlighter = ace.acequire('ace/ext/static_highlight');
 | 
					  const highlighter = ace.require('ace/ext/static_highlight');
 | 
				
			||||||
  const PythonMode = ace.acequire('ace/mode/python').Mode;
 | 
					  const PythonMode = ace.require('ace/mode/python').Mode;
 | 
				
			||||||
  const chrome = ace.acequire('ace/theme/chrome');
 | 
					  const aceDom = ace.require('ace/lib/dom');
 | 
				
			||||||
  const dracula = ace.acequire('ace/theme/dracula');
 | 
					  const chrome = ace.require('ace/theme/chrome');
 | 
				
			||||||
 | 
					  const dracula = ace.require('ace/theme/dracula');
 | 
				
			||||||
  const mode = new PythonMode();
 | 
					  const mode = new PythonMode();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const codeText = Observable.create(null, '');
 | 
					  const codeText = Observable.create(null, '');
 | 
				
			||||||
@ -33,21 +35,37 @@ export function buildHighlightedCode(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  function updateHighlightedCode(elem: HTMLElement) {
 | 
					  function updateHighlightedCode(elem: HTMLElement) {
 | 
				
			||||||
    let text = codeText.get();
 | 
					    let text = codeText.get();
 | 
				
			||||||
    if (text) {
 | 
					    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 {
 | 
					 | 
				
			||||||
      elem.textContent = placeholder || '';
 | 
					      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(
 | 
					  return cssHighlightedCode(
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,7 @@ import {GristDoc} from 'app/client/components/GristDoc';
 | 
				
			|||||||
import {logTelemetryEvent} from 'app/client/lib/telemetry';
 | 
					import {logTelemetryEvent} from 'app/client/lib/telemetry';
 | 
				
			||||||
import {getWelcomeHomeUrl, urlState} from 'app/client/models/gristUrlState';
 | 
					import {getWelcomeHomeUrl, urlState} from 'app/client/models/gristUrlState';
 | 
				
			||||||
import {renderer} from 'app/client/ui/DocTutorialRenderer';
 | 
					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 {sanitizeHTML} from 'app/client/ui/sanitizeHTML';
 | 
				
			||||||
import {hoverTooltip, setHoverTooltip} from 'app/client/ui/tooltips';
 | 
					import {hoverTooltip, setHoverTooltip} from 'app/client/ui/tooltips';
 | 
				
			||||||
import {basicButton, primaryButton} from 'app/client/ui2018/buttons';
 | 
					import {basicButton, primaryButton} from 'app/client/ui2018/buttons';
 | 
				
			||||||
@ -26,8 +26,6 @@ interface DocTutorialSlide {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const testId = makeTestId('test-doc-tutorial-');
 | 
					const testId = makeTestId('test-doc-tutorial-');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const TOOLTIP_KEY = 'docTutorialTooltip';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export class DocTutorial extends FloatingPopup {
 | 
					export class DocTutorial extends FloatingPopup {
 | 
				
			||||||
  private _appModel = this._gristDoc.docPageModel.appModel;
 | 
					  private _appModel = this._gristDoc.docPageModel.appModel;
 | 
				
			||||||
  private _currentDoc = this._gristDoc.docPageModel.currentDoc.get();
 | 
					  private _currentDoc = this._gristDoc.docPageModel.currentDoc.get();
 | 
				
			||||||
@ -47,7 +45,10 @@ export class DocTutorial extends FloatingPopup {
 | 
				
			|||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor(private _gristDoc: GristDoc) {
 | 
					  constructor(private _gristDoc: GristDoc) {
 | 
				
			||||||
    super({stopClickPropagationOnMove: true});
 | 
					    super({
 | 
				
			||||||
 | 
					      minimizable: true,
 | 
				
			||||||
 | 
					      stopClickPropagationOnMove: true,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public async start() {
 | 
					  public async start() {
 | 
				
			||||||
@ -102,7 +103,7 @@ export class DocTutorial extends FloatingPopup {
 | 
				
			|||||||
          return [
 | 
					          return [
 | 
				
			||||||
              cssFooterButtonsLeft(
 | 
					              cssFooterButtonsLeft(
 | 
				
			||||||
              cssPopupFooterButton(icon('Undo'),
 | 
					              cssPopupFooterButton(icon('Undo'),
 | 
				
			||||||
                hoverTooltip('Restart Tutorial', {key: TOOLTIP_KEY}),
 | 
					                hoverTooltip('Restart Tutorial', {key: FLOATING_POPUP_TOOLTIP_KEY}),
 | 
				
			||||||
                dom.on('click', () => this._restartTutorial()),
 | 
					                dom.on('click', () => this._restartTutorial()),
 | 
				
			||||||
                testId('popup-restart'),
 | 
					                testId('popup-restart'),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
@ -111,7 +112,7 @@ export class DocTutorial extends FloatingPopup {
 | 
				
			|||||||
              range(slides.length).map((i) => cssProgressBarDot(
 | 
					              range(slides.length).map((i) => cssProgressBarDot(
 | 
				
			||||||
                hoverTooltip(slides[i].slideTitle, {
 | 
					                hoverTooltip(slides[i].slideTitle, {
 | 
				
			||||||
                  closeOnClick: false,
 | 
					                  closeOnClick: false,
 | 
				
			||||||
                  key: TOOLTIP_KEY,
 | 
					                  key: FLOATING_POPUP_TOOLTIP_KEY,
 | 
				
			||||||
                }),
 | 
					                }),
 | 
				
			||||||
                cssProgressBarDot.cls('-current', i === slideIndex),
 | 
					                cssProgressBarDot.cls('-current', i === slideIndex),
 | 
				
			||||||
                i === slideIndex ? null : dom.on('click', () => this._changeSlide(i)),
 | 
					                i === slideIndex ? null : dom.on('click', () => this._changeSlide(i)),
 | 
				
			||||||
@ -315,7 +316,7 @@ export class DocTutorial extends FloatingPopup {
 | 
				
			|||||||
          img.src = img.src;
 | 
					          img.src = img.src;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          setHoverTooltip(img, 'Click to expand', {
 | 
					          setHoverTooltip(img, 'Click to expand', {
 | 
				
			||||||
            key: TOOLTIP_KEY,
 | 
					            key: FLOATING_POPUP_TOOLTIP_KEY,
 | 
				
			||||||
            modifiers: {
 | 
					            modifiers: {
 | 
				
			||||||
              flip: {
 | 
					              flip: {
 | 
				
			||||||
                boundariesElement: 'scrollParent',
 | 
					                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 SaveHandler = (column: ColumnRec, formula: string) => Promise<void>;
 | 
				
			||||||
type BuildEditor = (
 | 
					
 | 
				
			||||||
  cellElem: Element,
 | 
					type BuildEditor = (options: BuildEditorOptions) => void;
 | 
				
			||||||
  editValue?: string,
 | 
					 | 
				
			||||||
  onSave?: SaveHandler,
 | 
					 | 
				
			||||||
  onCancel?: () => void) => void;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function buildFormulaConfig(
 | 
					export function buildFormulaConfig(
 | 
				
			||||||
  owner: MultiHolder, origColumn: ColumnRec, gristDoc: GristDoc, buildEditor: BuildEditor
 | 
					  owner: MultiHolder, origColumn: ColumnRec, gristDoc: GristDoc, buildEditor: BuildEditor
 | 
				
			||||||
@ -315,14 +325,14 @@ export function buildFormulaConfig(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  const errorMessage = createFormulaErrorObs(owner, gristDoc, origColumn);
 | 
					  const errorMessage = createFormulaErrorObs(owner, gristDoc, origColumn);
 | 
				
			||||||
  // Helper that will create different flavors for formula builder.
 | 
					  // Helper that will create different flavors for formula builder.
 | 
				
			||||||
  const formulaBuilder = (onSave: SaveHandler) => [
 | 
					  const formulaBuilder = (onSave: SaveHandler, canDetach?: boolean) => [
 | 
				
			||||||
    cssRow(formulaField = buildFormula(
 | 
					    cssRow(formulaField = buildFormula(
 | 
				
			||||||
      origColumn,
 | 
					      origColumn,
 | 
				
			||||||
      buildEditor,
 | 
					      buildEditor,
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
        gristTheme: gristDoc.currentTheme,
 | 
					        gristTheme: gristDoc.currentTheme,
 | 
				
			||||||
        placeholder: t("Enter formula"),
 | 
					 | 
				
			||||||
        disabled: disableOtherActions,
 | 
					        disabled: disableOtherActions,
 | 
				
			||||||
 | 
					        canDetach,
 | 
				
			||||||
        onSave,
 | 
					        onSave,
 | 
				
			||||||
        onCancel: clearState,
 | 
					        onCancel: clearState,
 | 
				
			||||||
      })),
 | 
					      })),
 | 
				
			||||||
@ -386,7 +396,7 @@ export function buildFormulaConfig(
 | 
				
			|||||||
        // If data column is or wants to be a trigger formula:
 | 
					        // If data column is or wants to be a trigger formula:
 | 
				
			||||||
        dom.maybe((use) => use(maybeTrigger) || use(origColumn.hasTriggerFormula), () => [
 | 
					        dom.maybe((use) => use(maybeTrigger) || use(origColumn.hasTriggerFormula), () => [
 | 
				
			||||||
          cssLabel(t("TRIGGER FORMULA")),
 | 
					          cssLabel(t("TRIGGER FORMULA")),
 | 
				
			||||||
          formulaBuilder(onSaveConvertToTrigger),
 | 
					          formulaBuilder(onSaveConvertToTrigger, false),
 | 
				
			||||||
          dom.create(buildFormulaTriggers, origColumn, {
 | 
					          dom.create(buildFormulaTriggers, origColumn, {
 | 
				
			||||||
            disabled: disableOtherActions,
 | 
					            disabled: disableOtherActions,
 | 
				
			||||||
            notTrigger: maybeTrigger,
 | 
					            notTrigger: maybeTrigger,
 | 
				
			||||||
@ -411,8 +421,8 @@ export function buildFormulaConfig(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
interface BuildFormulaOptions {
 | 
					interface BuildFormulaOptions {
 | 
				
			||||||
  gristTheme: Computed<Theme>;
 | 
					  gristTheme: Computed<Theme>;
 | 
				
			||||||
  placeholder: string;
 | 
					 | 
				
			||||||
  disabled: Observable<boolean>;
 | 
					  disabled: Observable<boolean>;
 | 
				
			||||||
 | 
					  canDetach?: boolean;
 | 
				
			||||||
  onSave?: SaveHandler;
 | 
					  onSave?: SaveHandler;
 | 
				
			||||||
  onCancel?: () => void;
 | 
					  onCancel?: () => void;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -422,8 +432,8 @@ function buildFormula(
 | 
				
			|||||||
  buildEditor: BuildEditor,
 | 
					  buildEditor: BuildEditor,
 | 
				
			||||||
  options: BuildFormulaOptions
 | 
					  options: BuildFormulaOptions
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
  const {gristTheme, placeholder, disabled, onSave, onCancel} = options;
 | 
					  const {gristTheme, disabled, canDetach = true, onSave, onCancel} = options;
 | 
				
			||||||
  return cssFieldFormula(column.formula, {gristTheme, placeholder, maxLines: 2},
 | 
					  return cssFieldFormula(column.formula, {gristTheme, maxLines: 2},
 | 
				
			||||||
    dom.cls('formula_field_sidepane'),
 | 
					    dom.cls('formula_field_sidepane'),
 | 
				
			||||||
    cssFieldFormula.cls('-disabled', disabled),
 | 
					    cssFieldFormula.cls('-disabled', disabled),
 | 
				
			||||||
    cssFieldFormula.cls('-disabled-icon', use => !use(column.formula)),
 | 
					    cssFieldFormula.cls('-disabled-icon', use => !use(column.formula)),
 | 
				
			||||||
@ -431,7 +441,13 @@ function buildFormula(
 | 
				
			|||||||
    {tabIndex: '-1'},
 | 
					    {tabIndex: '-1'},
 | 
				
			||||||
    // Focus event use used by a user to edit an existing formula.
 | 
					    // Focus event use used by a user to edit an existing formula.
 | 
				
			||||||
    // It can also be triggered manually to open up the editor.
 | 
					    // 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 {documentCursor} from 'app/client/lib/popupUtils';
 | 
				
			||||||
import {hoverTooltip} from 'app/client/ui/tooltips';
 | 
					import {hoverTooltip} from 'app/client/ui/tooltips';
 | 
				
			||||||
import {isNarrowScreen, isNarrowScreenObs, theme, vars} from 'app/client/ui2018/cssVars';
 | 
					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 {icon} from 'app/client/ui2018/icons';
 | 
				
			||||||
import {Disposable, dom, DomContents, DomElementArg,
 | 
					import {Disposable, dom, DomContents, DomElementArg,
 | 
				
			||||||
        IDisposable, makeTestId, Observable, styled} from 'grainjs';
 | 
					        IDisposable, makeTestId, Observable, styled} from 'grainjs';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const POPUP_INITIAL_PADDING_PX = 16;
 | 
					const POPUP_INITIAL_PADDING_PX = 16;
 | 
				
			||||||
const POPUP_MIN_HEIGHT = 300;
 | 
					const POPUP_DEFAULT_MIN_HEIGHT = 300;
 | 
				
			||||||
const POPUP_MAX_HEIGHT = 711;
 | 
					const POPUP_MAX_HEIGHT = 711;
 | 
				
			||||||
const POPUP_HEADER_HEIGHT = 30;
 | 
					const POPUP_HEADER_HEIGHT = 30;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const t = makeT('FloatingPopup');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const testId = makeTestId('test-floating-popup-');
 | 
					const testId = makeTestId('test-floating-popup-');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const FLOATING_POPUP_TOOLTIP_KEY = 'floatingPopupTooltip';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface PopupOptions {
 | 
					export interface PopupOptions {
 | 
				
			||||||
  title?: () => DomContents;
 | 
					  title?: () => DomContents;
 | 
				
			||||||
  content?: () => DomContents;
 | 
					  content?: () => DomContents;
 | 
				
			||||||
  onClose?: () => void;
 | 
					  onClose?: () => void;
 | 
				
			||||||
  closeButton?: boolean;
 | 
					  closeButton?: boolean;
 | 
				
			||||||
 | 
					  closeButtonIcon?: IconName;
 | 
				
			||||||
  closeButtonHover?: () => DomContents;
 | 
					  closeButtonHover?: () => DomContents;
 | 
				
			||||||
 | 
					  minimizable?: boolean;
 | 
				
			||||||
  autoHeight?: boolean;
 | 
					  autoHeight?: boolean;
 | 
				
			||||||
 | 
					  /** Minimum height in pixels. */
 | 
				
			||||||
 | 
					  minHeight?: number;
 | 
				
			||||||
  /** Defaults to false. */
 | 
					  /** Defaults to false. */
 | 
				
			||||||
  stopClickPropagationOnMove?: boolean;
 | 
					  stopClickPropagationOnMove?: boolean;
 | 
				
			||||||
 | 
					  initialPosition?: [left: number, top: number];
 | 
				
			||||||
  args?: DomElementArg[];
 | 
					  args?: DomElementArg[];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class FloatingPopup extends Disposable {
 | 
					export class FloatingPopup extends Disposable {
 | 
				
			||||||
  protected _isMinimized = Observable.create(this, false);
 | 
					  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 _isFinishingMove = false;
 | 
				
			||||||
  private _popupElement: HTMLElement | null = null;
 | 
					  private _popupElement: HTMLElement | null = null;
 | 
				
			||||||
  private _popupMinimizeButtonElement: HTMLElement | null = null;
 | 
					  private _popupMinimizeButtonElement: HTMLElement | null = null;
 | 
				
			||||||
@ -71,7 +85,7 @@ export class FloatingPopup extends Disposable {
 | 
				
			|||||||
    this.autoDispose(isNarrowScreenObs().addListener(() => this._repositionPopup()));
 | 
					    this.autoDispose(isNarrowScreenObs().addListener(() => this._repositionPopup()));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.onDispose(() => {
 | 
					    this.onDispose(() => {
 | 
				
			||||||
      this._closePopup();
 | 
					      this._disposePopup();
 | 
				
			||||||
      this._cursorGrab?.dispose();
 | 
					      this._cursorGrab?.dispose();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@ -79,18 +93,22 @@ export class FloatingPopup extends Disposable {
 | 
				
			|||||||
  public showPopup() {
 | 
					  public showPopup() {
 | 
				
			||||||
    this._popupElement = this._buildPopup();
 | 
					    this._popupElement = this._buildPopup();
 | 
				
			||||||
    document.body.appendChild(this._popupElement);
 | 
					    document.body.appendChild(this._popupElement);
 | 
				
			||||||
    const topPaddingPx = getTopPopupPaddingPx();
 | 
					
 | 
				
			||||||
    const initialLeft = document.body.offsetWidth - this._popupElement.offsetWidth - POPUP_INITIAL_PADDING_PX;
 | 
					    const {initialPosition} = this._options;
 | 
				
			||||||
    const initialTop = document.body.offsetHeight - this._popupElement.offsetHeight - topPaddingPx;
 | 
					    if (initialPosition) {
 | 
				
			||||||
    this._popupElement.style.left = `${initialLeft}px`;
 | 
					      this._setPosition(initialPosition);
 | 
				
			||||||
    this._popupElement.style.top = `${initialTop}px`;
 | 
					      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() {
 | 
					  protected _closePopup() {
 | 
				
			||||||
    if (!this._popupElement) { return; }
 | 
					    if (!this._closable) { return; }
 | 
				
			||||||
    document.body.removeChild(this._popupElement);
 | 
					
 | 
				
			||||||
    dom.domDispose(this._popupElement);
 | 
					    this._disposePopup();
 | 
				
			||||||
    this._popupElement = null;
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  protected _buildTitle(): DomContents {
 | 
					  protected _buildTitle(): DomContents {
 | 
				
			||||||
@ -105,6 +123,21 @@ export class FloatingPopup extends Disposable {
 | 
				
			|||||||
    return this._options.args ?? [];
 | 
					    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() {
 | 
					  private _rememberPosition() {
 | 
				
			||||||
    this._initialLeft = this._popupElement!.offsetLeft;
 | 
					    this._initialLeft = this._popupElement!.offsetLeft;
 | 
				
			||||||
    this._initialTop = this._popupElement!.offsetTop;
 | 
					    this._initialTop = this._popupElement!.offsetTop;
 | 
				
			||||||
@ -151,7 +184,7 @@ export class FloatingPopup extends Disposable {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      // First just how much we can resize the popup.
 | 
					      // First just how much we can resize the popup.
 | 
				
			||||||
      let minTop = this._initialBottom - POPUP_MAX_HEIGHT;
 | 
					      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).
 | 
					      // Now how far we can move top (leave at least some padding for mobile).
 | 
				
			||||||
      minTop = Math.max(minTop, getTopPopupPaddingPx());
 | 
					      minTop = Math.max(minTop, getTopPopupPaddingPx());
 | 
				
			||||||
@ -250,6 +283,8 @@ export class FloatingPopup extends Disposable {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _minimizeOrMaximize() {
 | 
					  private _minimizeOrMaximize() {
 | 
				
			||||||
 | 
					    if (!this._minimizable) { return; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this._isMinimized.set(!this._isMinimized.get());
 | 
					    this._isMinimized.set(!this._isMinimized.get());
 | 
				
			||||||
    this._repositionPopup();
 | 
					    this._repositionPopup();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@ -258,6 +293,7 @@ export class FloatingPopup extends Disposable {
 | 
				
			|||||||
    const body = cssPopup(
 | 
					    const body = cssPopup(
 | 
				
			||||||
      {tabIndex: '-1'},
 | 
					      {tabIndex: '-1'},
 | 
				
			||||||
      cssPopup.cls('-auto', this._options.autoHeight ?? false),
 | 
					      cssPopup.cls('-auto', this._options.autoHeight ?? false),
 | 
				
			||||||
 | 
					      dom.style('min-height', `${this._minHeight}px`),
 | 
				
			||||||
      cssPopupHeader(
 | 
					      cssPopupHeader(
 | 
				
			||||||
        cssBottomHandle(testId('move-handle')),
 | 
					        cssBottomHandle(testId('move-handle')),
 | 
				
			||||||
        dom.maybe(use => !use(this._isMinimized), () => {
 | 
					        dom.maybe(use => !use(this._isMinimized), () => {
 | 
				
			||||||
@ -277,10 +313,12 @@ export class FloatingPopup extends Disposable {
 | 
				
			|||||||
            // center the title.
 | 
					            // center the title.
 | 
				
			||||||
            cssPopupButtons(
 | 
					            cssPopupButtons(
 | 
				
			||||||
              cssPopupHeaderButton(
 | 
					              cssPopupHeaderButton(
 | 
				
			||||||
                icon('Maximize')
 | 
					                icon('Maximize'),
 | 
				
			||||||
 | 
					                dom.show(this._minimizable),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
              !this._options.closeButton ? null : cssPopupHeaderButton(
 | 
					              cssPopupHeaderButton(
 | 
				
			||||||
                icon('CrossBig'),
 | 
					                icon('CrossBig'),
 | 
				
			||||||
 | 
					                dom.show(this._closable),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
              dom.style('visibility', 'hidden'),
 | 
					              dom.style('visibility', 'hidden'),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
@ -291,17 +329,23 @@ export class FloatingPopup extends Disposable {
 | 
				
			|||||||
            cssPopupButtons(
 | 
					            cssPopupButtons(
 | 
				
			||||||
              this._popupMinimizeButtonElement = cssPopupHeaderButton(
 | 
					              this._popupMinimizeButtonElement = cssPopupHeaderButton(
 | 
				
			||||||
                isMinimized ? icon('Maximize'): icon('Minimize'),
 | 
					                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.on('click', () => this._minimizeOrMaximize()),
 | 
				
			||||||
 | 
					                dom.show(this._minimizable),
 | 
				
			||||||
                testId('minimize-maximize'),
 | 
					                testId('minimize-maximize'),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
              !this._options.closeButton ? null : cssPopupHeaderButton(
 | 
					              cssPopupHeaderButton(
 | 
				
			||||||
                icon('CrossBig'),
 | 
					                icon(this._options.closeButtonIcon ?? 'CrossBig'),
 | 
				
			||||||
 | 
					                this._options.closeButtonHover && hoverTooltip(this._options.closeButtonHover(), {
 | 
				
			||||||
 | 
					                  key: FLOATING_POPUP_TOOLTIP_KEY,
 | 
				
			||||||
 | 
					                }),
 | 
				
			||||||
                dom.on('click', () => {
 | 
					                dom.on('click', () => {
 | 
				
			||||||
                  this._options.onClose?.() ?? this._closePopup();
 | 
					                  this._options.onClose?.() ?? this._closePopup();
 | 
				
			||||||
                }),
 | 
					                }),
 | 
				
			||||||
 | 
					                dom.show(this._closable),
 | 
				
			||||||
                testId('close'),
 | 
					                testId('close'),
 | 
				
			||||||
                this._options.closeButtonHover && hoverTooltip(this._options.closeButtonHover())
 | 
					 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
              // Disable dragging when a button in the header is clicked.
 | 
					              // Disable dragging when a button in the header is clicked.
 | 
				
			||||||
              dom.on('mousedown', ev => ev.stopPropagation()),
 | 
					              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 = `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_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', `
 | 
					const cssPopup = styled('div.floating-popup', `
 | 
				
			||||||
  position: fixed;
 | 
					  position: fixed;
 | 
				
			||||||
@ -374,7 +420,6 @@ const cssPopup = styled('div.floating-popup', `
 | 
				
			|||||||
  --height: ${POPUP_MAX_HEIGHT}px;
 | 
					  --height: ${POPUP_MAX_HEIGHT}px;
 | 
				
			||||||
  height: ${POPUP_HEIGHT};
 | 
					  height: ${POPUP_HEIGHT};
 | 
				
			||||||
  width: ${POPUP_WIDTH};
 | 
					  width: ${POPUP_WIDTH};
 | 
				
			||||||
  min-height: ${POPUP_MIN_HEIGHT}px;
 | 
					 | 
				
			||||||
  background-color: ${theme.popupBg};
 | 
					  background-color: ${theme.popupBg};
 | 
				
			||||||
  box-shadow: 0 2px 18px 0 ${theme.popupInnerShadow}, 0 0 1px 0 ${theme.popupOuterShadow};
 | 
					  box-shadow: 0 2px 18px 0 ${theme.popupInnerShadow}, 0 0 1px 0 ${theme.popupOuterShadow};
 | 
				
			||||||
  outline: unset;
 | 
					  outline: unset;
 | 
				
			||||||
 | 
				
			|||||||
@ -23,12 +23,13 @@ import * as imports from 'app/client/lib/imports';
 | 
				
			|||||||
import {makeT} from 'app/client/lib/localization';
 | 
					import {makeT} from 'app/client/lib/localization';
 | 
				
			||||||
import {createSessionObs} from 'app/client/lib/sessionObs';
 | 
					import {createSessionObs} from 'app/client/lib/sessionObs';
 | 
				
			||||||
import {reportError} from 'app/client/models/AppModel';
 | 
					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 {GridOptions} from 'app/client/ui/GridOptions';
 | 
				
			||||||
import {attachPageWidgetPicker, IPageWidget, toPageWidget} from 'app/client/ui/PageWidgetPicker';
 | 
					import {attachPageWidgetPicker, IPageWidget, toPageWidget} from 'app/client/ui/PageWidgetPicker';
 | 
				
			||||||
import {linkId, selectBy} from 'app/client/ui/selectBy';
 | 
					import {linkId, selectBy} from 'app/client/ui/selectBy';
 | 
				
			||||||
import {CustomSectionConfig} from 'app/client/ui/CustomSectionConfig';
 | 
					import {CustomSectionConfig} from 'app/client/ui/CustomSectionConfig';
 | 
				
			||||||
import {buildDescriptionConfig} from 'app/client/ui/DescriptionConfig';
 | 
					import {buildDescriptionConfig} from 'app/client/ui/DescriptionConfig';
 | 
				
			||||||
 | 
					import {BuildEditorOptions} from 'app/client/ui/FieldConfig';
 | 
				
			||||||
import {cssLabel} from 'app/client/ui/RightPanelStyles';
 | 
					import {cssLabel} from 'app/client/ui/RightPanelStyles';
 | 
				
			||||||
import {VisibleFieldsConfig} from 'app/client/ui/VisibleFieldsConfig';
 | 
					import {VisibleFieldsConfig} from 'app/client/ui/VisibleFieldsConfig';
 | 
				
			||||||
import {IWidgetType, widgetTypes} from 'app/client/ui/widgetTypes';
 | 
					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.
 | 
					  // Helper to activate the side-pane formula editor over the given HTML element.
 | 
				
			||||||
  private _activateFormulaEditor(
 | 
					  private _activateFormulaEditor(options: BuildEditorOptions) {
 | 
				
			||||||
    // 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) {
 | 
					 | 
				
			||||||
    const vsi = this._gristDoc.viewModel.activeSection().viewInstance();
 | 
					    const vsi = this._gristDoc.viewModel.activeSection().viewInstance();
 | 
				
			||||||
    if (!vsi) { return; }
 | 
					    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) {
 | 
					  private _buildPageWidgetContent(_owner: MultiHolder) {
 | 
				
			||||||
 | 
				
			|||||||
@ -143,6 +143,7 @@ export const vars = {
 | 
				
			|||||||
  floatingPopupZIndex: new CustomProp('floating-popup-z-index', '1002'),
 | 
					  floatingPopupZIndex: new CustomProp('floating-popup-z-index', '1002'),
 | 
				
			||||||
  tutorialModalZIndex: new CustomProp('tutorial-modal-z-index', '1003'),
 | 
					  tutorialModalZIndex: new CustomProp('tutorial-modal-z-index', '1003'),
 | 
				
			||||||
  pricingModalZIndex: new CustomProp('pricing-modal-z-index', '1004'),
 | 
					  pricingModalZIndex: new CustomProp('pricing-modal-z-index', '1004'),
 | 
				
			||||||
 | 
					  floatingPopupMenuZIndex: new CustomProp('floating-popup-menu-z-index', '1004'),
 | 
				
			||||||
  notificationZIndex: new CustomProp('notification-z-index', '1100'),
 | 
					  notificationZIndex: new CustomProp('notification-z-index', '1100'),
 | 
				
			||||||
  browserCheckZIndex: new CustomProp('browser-check-z-index', '5000'),
 | 
					  browserCheckZIndex: new CustomProp('browser-check-z-index', '5000'),
 | 
				
			||||||
  tooltipZIndex: new CustomProp('tooltip-z-index', '5000'),
 | 
					  tooltipZIndex: new CustomProp('tooltip-z-index', '5000'),
 | 
				
			||||||
@ -686,9 +687,6 @@ export const theme = {
 | 
				
			|||||||
  cellBg: new CustomProp('theme-cell-bg', undefined, '#FFFFFF00'),
 | 
					  cellBg: new CustomProp('theme-cell-bg', undefined, '#FFFFFF00'),
 | 
				
			||||||
  cellZebraBg: new CustomProp('theme-cell-zebra-bg', undefined, '#F8F8F8'),
 | 
					  cellZebraBg: new CustomProp('theme-cell-zebra-bg', undefined, '#F8F8F8'),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /* Formula Editor */
 | 
					 | 
				
			||||||
  formulaEditorBg: new CustomProp('theme-formula-editor-bg', undefined, 'white'),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /* Charts */
 | 
					  /* Charts */
 | 
				
			||||||
  chartFg: new CustomProp('theme-chart-fg', undefined, '#444'),
 | 
					  chartFg: new CustomProp('theme-chart-fg', undefined, '#444'),
 | 
				
			||||||
  chartBg: new CustomProp('theme-chart-bg', undefined, '#fff'),
 | 
					  chartBg: new CustomProp('theme-chart-bg', undefined, '#fff'),
 | 
				
			||||||
@ -736,7 +734,8 @@ export const theme = {
 | 
				
			|||||||
    colors.lightGreen),
 | 
					    colors.lightGreen),
 | 
				
			||||||
  tutorialsPopupBoxBg: new CustomProp('theme-tutorials-popup-box-bg', undefined, '#F5F5F5'),
 | 
					  tutorialsPopupBoxBg: new CustomProp('theme-tutorials-popup-box-bg', undefined, '#F5F5F5'),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /* Ace Autocomplete */
 | 
					  /* Ace */
 | 
				
			||||||
 | 
					  aceEditorBg: new CustomProp('theme-ace-editor-bg', undefined, 'white'),
 | 
				
			||||||
  aceAutocompletePrimaryFg: new CustomProp('theme-ace-autocomplete-primary-fg', undefined, '#444'),
 | 
					  aceAutocompletePrimaryFg: new CustomProp('theme-ace-autocomplete-primary-fg', undefined, '#444'),
 | 
				
			||||||
  aceAutocompleteSecondaryFg: new CustomProp('theme-ace-autocomplete-secondary-fg', undefined,
 | 
					  aceAutocompleteSecondaryFg: new CustomProp('theme-ace-autocomplete-secondary-fg', undefined,
 | 
				
			||||||
    '#8f8f8f'),
 | 
					    '#8f8f8f'),
 | 
				
			||||||
@ -797,6 +796,14 @@ export const theme = {
 | 
				
			|||||||
  loginPageBg: new CustomProp('theme-login-page-bg', undefined, 'white'),
 | 
					  loginPageBg: new CustomProp('theme-login-page-bg', undefined, 'white'),
 | 
				
			||||||
  loginPageBackdrop: new CustomProp('theme-login-page-backdrop', undefined, '#F5F8FA'),
 | 
					  loginPageBackdrop: new CustomProp('theme-login-page-backdrop', undefined, '#F5F8FA'),
 | 
				
			||||||
  loginPageLine: new CustomProp('theme-login-page-line', undefined, colors.lightGrey),
 | 
					  loginPageLine: new CustomProp('theme-login-page-line', undefined, colors.lightGrey),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /* Formula Assistant */
 | 
				
			||||||
 | 
					  formulaAssistantHeaderBg: new CustomProp(
 | 
				
			||||||
 | 
					    'theme-formula-assistant-header-bg', undefined, colors.lightGrey),
 | 
				
			||||||
 | 
					  formulaAssistantBorder: new CustomProp(
 | 
				
			||||||
 | 
					    'theme-formula-assistant-border', undefined, colors.darkGrey),
 | 
				
			||||||
 | 
					  formulaAssistantPreformattedTextBg: new CustomProp(
 | 
				
			||||||
 | 
					    'theme-formula-assistant-preformatted-text-bg', undefined, colors.lightGrey),
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const cssColors = values(colors).map(v => v.decl()).join('\n');
 | 
					const cssColors = values(colors).map(v => v.decl()).join('\n');
 | 
				
			||||||
 | 
				
			|||||||
@ -775,12 +775,16 @@ export class FieldBuilder extends Disposable {
 | 
				
			|||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Open the formula editor in the side pane. It will be positioned over refElem.
 | 
					   * Open the formula editor in the side pane. It will be positioned over refElem.
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  public openSideFormulaEditor(
 | 
					  public openSideFormulaEditor(options: {
 | 
				
			||||||
    editRow: DataRowModel,
 | 
					    editRow: DataRowModel,
 | 
				
			||||||
    refElem: Element,
 | 
					    refElem: Element,
 | 
				
			||||||
 | 
					    canDetach: boolean,
 | 
				
			||||||
    editValue?: string,
 | 
					    editValue?: string,
 | 
				
			||||||
    onSave?: (column: ColumnRec, formula: string) => Promise<void>,
 | 
					    onSave?: (column: ColumnRec, formula: string) => Promise<void>,
 | 
				
			||||||
    onCancel?: () => void) {
 | 
					    onCancel?: () => void
 | 
				
			||||||
 | 
					  }) {
 | 
				
			||||||
 | 
					    const {editRow, refElem, canDetach, editValue, onSave, onCancel} = options;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Remember position when the popup was opened.
 | 
					    // Remember position when the popup was opened.
 | 
				
			||||||
    const position = this.gristDoc.cursorPosition.get();
 | 
					    const position = this.gristDoc.cursorPosition.get();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -838,14 +842,18 @@ export class FieldBuilder extends Disposable {
 | 
				
			|||||||
      editRow,
 | 
					      editRow,
 | 
				
			||||||
      refElem,
 | 
					      refElem,
 | 
				
			||||||
      editValue,
 | 
					      editValue,
 | 
				
			||||||
      canDetach: true,
 | 
					      canDetach,
 | 
				
			||||||
      onSave,
 | 
					      onSave,
 | 
				
			||||||
      onCancel
 | 
					      onCancel
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // And now create the floating editor itself. It is just a floating wrapper that will grab the dom
 | 
					    // And now create the floating editor itself. It is just a floating wrapper that will grab the dom
 | 
				
			||||||
    // from the editor and show it in the popup. It also overrides various parts of Grist to make smoother experience.
 | 
					    // from the editor and show it in the popup. It also overrides various parts of Grist to make smoother experience.
 | 
				
			||||||
    const floatingExtension = FloatingEditor.create(formulaEditor, floatController, this.gristDoc);
 | 
					    const floatingExtension = FloatingEditor.create(formulaEditor, floatController, {
 | 
				
			||||||
 | 
					      gristDoc: this.gristDoc,
 | 
				
			||||||
 | 
					      refElem,
 | 
				
			||||||
 | 
					      placement: 'overlapping',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Add editor to document holder - this will prevent multiple formula editor instances.
 | 
					    // Add editor to document holder - this will prevent multiple formula editor instances.
 | 
				
			||||||
    this.gristDoc.fieldEditorHolder.autoDispose(formulaEditor);
 | 
					    this.gristDoc.fieldEditorHolder.autoDispose(formulaEditor);
 | 
				
			||||||
 | 
				
			|||||||
@ -159,7 +159,11 @@ export class FieldEditor extends Disposable {
 | 
				
			|||||||
    this.rebuildEditor(editValue, Number.POSITIVE_INFINITY, options.state);
 | 
					    this.rebuildEditor(editValue, Number.POSITIVE_INFINITY, options.state);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Create a floating editor, which will be used to display the editor in a popup.
 | 
					    // Create a floating editor, which will be used to display the editor in a popup.
 | 
				
			||||||
    this.floatingEditor = FloatingEditor.create(this, this, this._gristDoc);
 | 
					    this.floatingEditor = FloatingEditor.create(this, this, {
 | 
				
			||||||
 | 
					      gristDoc: this._gristDoc,
 | 
				
			||||||
 | 
					      refElem: this._cellElem,
 | 
				
			||||||
 | 
					      placement: 'adjacent',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (offerToMakeFormula) {
 | 
					    if (offerToMakeFormula) {
 | 
				
			||||||
      this._offerToMakeFormula();
 | 
					      this._offerToMakeFormula();
 | 
				
			||||||
@ -318,6 +322,10 @@ export class FieldEditor extends Disposable {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  private _unmakeFormula() {
 | 
					  private _unmakeFormula() {
 | 
				
			||||||
    const editor = this._editorHolder.get();
 | 
					    const editor = this._editorHolder.get();
 | 
				
			||||||
 | 
					    if (editor instanceof FormulaEditor && editor.isDetached.get()) {
 | 
				
			||||||
 | 
					      return true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Only convert to data if we are undoing a to-formula conversion. To convert formula to
 | 
					    // Only convert to data if we are undoing a to-formula conversion. To convert formula to
 | 
				
			||||||
    // data, use column menu option, or delete the formula first (which makes the column "empty").
 | 
					    // data, use column menu option, or delete the formula first (which makes the column "empty").
 | 
				
			||||||
    if (editor && this._field.editingFormula.peek() && editor.getCursorPos() === 0 &&
 | 
					    if (editor && this._field.editingFormula.peek() && editor.getCursorPos() === 0 &&
 | 
				
			||||||
 | 
				
			|||||||
@ -2,24 +2,57 @@ import * as commands from 'app/client/components/commands';
 | 
				
			|||||||
import {GristDoc} from 'app/client/components/GristDoc';
 | 
					import {GristDoc} from 'app/client/components/GristDoc';
 | 
				
			||||||
import {detachNode} from 'app/client/lib/dom';
 | 
					import {detachNode} from 'app/client/lib/dom';
 | 
				
			||||||
import {FocusLayer} from 'app/client/lib/FocusLayer';
 | 
					import {FocusLayer} from 'app/client/lib/FocusLayer';
 | 
				
			||||||
import {FloatingPopup} from 'app/client/ui/FloatingPopup';
 | 
					import {makeT} from 'app/client/lib/localization';
 | 
				
			||||||
 | 
					import {FLOATING_POPUP_MAX_WIDTH_PX, FloatingPopup} from 'app/client/ui/FloatingPopup';
 | 
				
			||||||
import {theme} from 'app/client/ui2018/cssVars';
 | 
					import {theme} from 'app/client/ui2018/cssVars';
 | 
				
			||||||
import {icon} from 'app/client/ui2018/icons';
 | 
					import {icon} from 'app/client/ui2018/icons';
 | 
				
			||||||
import {Disposable, dom, Holder, IDisposableOwner, IDomArgs,
 | 
					import {Disposable, dom, Holder, IDisposableOwner, IDomArgs,
 | 
				
			||||||
        makeTestId, MultiHolder, Observable, styled} from 'grainjs';
 | 
					        makeTestId, MultiHolder, Observable, styled} from 'grainjs';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const t = makeT('FloatingEditor');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const testId = makeTestId('test-floating-editor-');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface IFloatingOwner extends IDisposableOwner {
 | 
					export interface IFloatingOwner extends IDisposableOwner {
 | 
				
			||||||
  detach(): HTMLElement;
 | 
					  detach(): HTMLElement;
 | 
				
			||||||
  attach(content: HTMLElement): Promise<void>|void;
 | 
					  attach(content: HTMLElement): Promise<void>|void;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const testId = makeTestId('test-floating-editor-');
 | 
					export interface FloatingEditorOptions {
 | 
				
			||||||
 | 
					  gristDoc: GristDoc;
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * The element that `placement` should be relative to.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  refElem?: Element;
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * How to position the editor.
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * If "overlapping", the editor will be positioned on top of `refElem`, anchored
 | 
				
			||||||
 | 
					   * to its top-left corner.
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * If "adjacent", the editor will be positioned to the left or right of `refElem`,
 | 
				
			||||||
 | 
					   * depending on available space.
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * If "fixed", the editor will be positioned in the bottom-right corner of the
 | 
				
			||||||
 | 
					   * viewport.
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * Defaults to "fixed".
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  placement?: 'overlapping' | 'adjacent' | 'fixed';
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class FloatingEditor extends Disposable {
 | 
					export class FloatingEditor extends Disposable {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public active = Observable.create<boolean>(this, false);
 | 
					  public active = Observable.create<boolean>(this, false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor(private _fieldEditor: IFloatingOwner, private _gristDoc: GristDoc) {
 | 
					  private _gristDoc = this._options.gristDoc;
 | 
				
			||||||
 | 
					  private _placement = this._options.placement ?? 'fixed';
 | 
				
			||||||
 | 
					  private _refElem = this._options.refElem;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor(
 | 
				
			||||||
 | 
					    private _fieldEditor: IFloatingOwner,
 | 
				
			||||||
 | 
					    private _options: FloatingEditorOptions
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
    super();
 | 
					    super();
 | 
				
			||||||
    this.autoDispose(commands.createGroup({
 | 
					    this.autoDispose(commands.createGroup({
 | 
				
			||||||
      detachEditor: this.createPopup.bind(this),
 | 
					      detachEditor: this.createPopup.bind(this),
 | 
				
			||||||
@ -52,7 +85,8 @@ export class FloatingEditor extends Disposable {
 | 
				
			|||||||
                                                    // detach it on close.
 | 
					                                                    // detach it on close.
 | 
				
			||||||
        title: () => title, // We are not reactive yet
 | 
					        title: () => title, // We are not reactive yet
 | 
				
			||||||
        closeButton: true,  // Show the close button with a hover
 | 
					        closeButton: true,  // Show the close button with a hover
 | 
				
			||||||
        closeButtonHover: () => 'Return to cell',
 | 
					        closeButtonIcon: 'Minimize',
 | 
				
			||||||
 | 
					        closeButtonHover: () => t('Collapse Editor'),
 | 
				
			||||||
        onClose: async () => {
 | 
					        onClose: async () => {
 | 
				
			||||||
          const layer = FocusLayer.create(null, { defaultFocusElem: document.activeElement as any});
 | 
					          const layer = FocusLayer.create(null, { defaultFocusElem: document.activeElement as any});
 | 
				
			||||||
          try {
 | 
					          try {
 | 
				
			||||||
@ -63,6 +97,8 @@ export class FloatingEditor extends Disposable {
 | 
				
			|||||||
            layer.dispose();
 | 
					            layer.dispose();
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        minHeight: 550,
 | 
				
			||||||
 | 
					        initialPosition: this._getInitialPosition(),
 | 
				
			||||||
        args: [testId('popup')]
 | 
					        args: [testId('popup')]
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      // Set a public flag that we are active.
 | 
					      // Set a public flag that we are active.
 | 
				
			||||||
@ -78,6 +114,38 @@ export class FloatingEditor extends Disposable {
 | 
				
			|||||||
      tempOwner.dispose();
 | 
					      tempOwner.dispose();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private _getInitialPosition(): [number, number] | undefined {
 | 
				
			||||||
 | 
					    if (!this._refElem || this._placement === 'fixed') {
 | 
				
			||||||
 | 
					      return undefined;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const refElem = this._refElem as HTMLElement;
 | 
				
			||||||
 | 
					    const refElemBoundingRect = refElem.getBoundingClientRect();
 | 
				
			||||||
 | 
					    if (this._placement === 'overlapping') {
 | 
				
			||||||
 | 
					      // Anchor the floating editor to the top-left corner of the refElement.
 | 
				
			||||||
 | 
					      return [
 | 
				
			||||||
 | 
					        refElemBoundingRect.left,
 | 
				
			||||||
 | 
					        refElemBoundingRect.top,
 | 
				
			||||||
 | 
					      ];
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      if (window.innerWidth - refElemBoundingRect.right >= FLOATING_POPUP_MAX_WIDTH_PX) {
 | 
				
			||||||
 | 
					        // If there's enough space to the right of refElement, position the
 | 
				
			||||||
 | 
					        // floating editor there.
 | 
				
			||||||
 | 
					        return [
 | 
				
			||||||
 | 
					          refElemBoundingRect.right,
 | 
				
			||||||
 | 
					          refElemBoundingRect.top,
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        // Otherwise position it to the left of refElement; note that it may still
 | 
				
			||||||
 | 
					        // overlap if there isn't enough space on this side either.
 | 
				
			||||||
 | 
					        return [
 | 
				
			||||||
 | 
					          refElemBoundingRect.left - FLOATING_POPUP_MAX_WIDTH_PX,
 | 
				
			||||||
 | 
					          refElemBoundingRect.top,
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function createDetachedIcon(...args: IDomArgs<HTMLDivElement>) {
 | 
					export function createDetachedIcon(...args: IDomArgs<HTMLDivElement>) {
 | 
				
			||||||
 | 
				
			|||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -8,12 +8,14 @@ import {ColumnRec} from 'app/client/models/DocModel';
 | 
				
			|||||||
import {ViewFieldRec} from 'app/client/models/entities/ViewFieldRec';
 | 
					import {ViewFieldRec} from 'app/client/models/entities/ViewFieldRec';
 | 
				
			||||||
import {reportError} from 'app/client/models/errors';
 | 
					import {reportError} from 'app/client/models/errors';
 | 
				
			||||||
import {GRIST_FORMULA_ASSISTANT} from 'app/client/models/features';
 | 
					import {GRIST_FORMULA_ASSISTANT} from 'app/client/models/features';
 | 
				
			||||||
import {colors, testId, theme} from 'app/client/ui2018/cssVars';
 | 
					import {hoverTooltip} from 'app/client/ui/tooltips';
 | 
				
			||||||
 | 
					import {textButton} from 'app/client/ui2018/buttons';
 | 
				
			||||||
 | 
					import {colors, testId, theme, vars} from 'app/client/ui2018/cssVars';
 | 
				
			||||||
import {icon} from 'app/client/ui2018/icons';
 | 
					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 {createDetachedIcon} from 'app/client/widgets/FloatingEditor';
 | 
					import {createDetachedIcon} from 'app/client/widgets/FloatingEditor';
 | 
				
			||||||
import {buildRobotIcon, FormulaAssistant} from 'app/client/widgets/FormulaAssistant';
 | 
					import {FormulaAssistant} from 'app/client/widgets/FormulaAssistant';
 | 
				
			||||||
import {NewBaseEditor, Options} from 'app/client/widgets/NewBaseEditor';
 | 
					import {NewBaseEditor, Options} from 'app/client/widgets/NewBaseEditor';
 | 
				
			||||||
import {asyncOnce} from 'app/common/AsyncCreate';
 | 
					import {asyncOnce} from 'app/common/AsyncCreate';
 | 
				
			||||||
import {CellValue} from 'app/common/DocActions';
 | 
					import {CellValue} from 'app/common/DocActions';
 | 
				
			||||||
@ -55,6 +57,8 @@ export class FormulaEditor extends NewBaseEditor {
 | 
				
			|||||||
  private _dom: HTMLElement;
 | 
					  private _dom: HTMLElement;
 | 
				
			||||||
  private _editorPlacement!: EditorPlacement;
 | 
					  private _editorPlacement!: EditorPlacement;
 | 
				
			||||||
  private _placementHolder = Holder.create(this);
 | 
					  private _placementHolder = Holder.create(this);
 | 
				
			||||||
 | 
					  private _canDetach: boolean;
 | 
				
			||||||
 | 
					  private _isEmpty: Computed<boolean>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor(options: IFormulaEditorOptions) {
 | 
					  constructor(options: IFormulaEditorOptions) {
 | 
				
			||||||
    super(options);
 | 
					    super(options);
 | 
				
			||||||
@ -65,6 +69,8 @@ export class FormulaEditor extends NewBaseEditor {
 | 
				
			|||||||
    // create editor state observable (used by draft and latest position memory)
 | 
					    // create editor state observable (used by draft and latest position memory)
 | 
				
			||||||
    this.editorState = Observable.create(this, initialValue);
 | 
					    this.editorState = Observable.create(this, initialValue);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this._isEmpty = Computed.create(this, this.editorState, (_use, state) => state === '');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    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.
 | 
				
			||||||
@ -101,8 +107,7 @@ export class FormulaEditor extends NewBaseEditor {
 | 
				
			|||||||
        return true;
 | 
					        return true;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      // Else invoke regular command.
 | 
					      // Else invoke regular command.
 | 
				
			||||||
      commands.allCommands[name]?.run();
 | 
					      return commands.allCommands[name]?.run() ?? false;
 | 
				
			||||||
      return false;
 | 
					 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
    const detachedCommands = this.autoDispose(commands.createGroup({
 | 
					    const detachedCommands = this.autoDispose(commands.createGroup({
 | 
				
			||||||
      nextField: passThrough('nextField'),
 | 
					      nextField: passThrough('nextField'),
 | 
				
			||||||
@ -140,11 +145,17 @@ export class FormulaEditor extends NewBaseEditor {
 | 
				
			|||||||
    // the DOM to update before resizing.
 | 
					    // the DOM to update before resizing.
 | 
				
			||||||
    this.autoDispose(errorDetails.addListener(() => setTimeout(this.resize.bind(this), 0)));
 | 
					    this.autoDispose(errorDetails.addListener(() => setTimeout(this.resize.bind(this), 0)));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const canDetach = GRIST_FORMULA_ASSISTANT().get() &&  options.canDetach && !options.readonly;
 | 
					    this._canDetach = Boolean(GRIST_FORMULA_ASSISTANT().get() && options.canDetach && !options.readonly);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.autoDispose(this._formulaEditor);
 | 
					    this.autoDispose(this._formulaEditor);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Show placeholder text when the formula is blank.
 | 
				
			||||||
 | 
					    this._isEmpty.addListener(() => this._updateEditorPlaceholder());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Update the placeholder text when expanding or collapsing the editor.
 | 
				
			||||||
 | 
					    this.isDetached.addListener(() => this._updateEditorPlaceholder());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this._dom = cssFormulaEditor(
 | 
					    this._dom = cssFormulaEditor(
 | 
				
			||||||
      buildRobotIcon(),
 | 
					 | 
				
			||||||
      // switch border shadow
 | 
					      // switch border shadow
 | 
				
			||||||
      dom.cls("readonly_editor", options.readonly),
 | 
					      dom.cls("readonly_editor", options.readonly),
 | 
				
			||||||
      createMobileButtons(options.commands),
 | 
					      createMobileButtons(options.commands),
 | 
				
			||||||
@ -173,7 +184,10 @@ export class FormulaEditor extends NewBaseEditor {
 | 
				
			|||||||
        ev.preventDefault();
 | 
					        ev.preventDefault();
 | 
				
			||||||
        this.focus();
 | 
					        this.focus();
 | 
				
			||||||
      }),
 | 
					      }),
 | 
				
			||||||
      canDetach ? createDetachedIcon(dom.hide(this.isDetached)) : null,
 | 
					      !this._canDetach ? null : createDetachedIcon(
 | 
				
			||||||
 | 
					        hoverTooltip(t('Expand Editor')),
 | 
				
			||||||
 | 
					        dom.hide(this.isDetached),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
      cssFormulaEditor.cls('-detached', this.isDetached),
 | 
					      cssFormulaEditor.cls('-detached', this.isDetached),
 | 
				
			||||||
      dom('div.formula_editor.formula_field_edit', testId('formula-editor'),
 | 
					      dom('div.formula_editor.formula_field_edit', testId('formula-editor'),
 | 
				
			||||||
        this._formulaEditor.buildDom((aceObj: any) => {
 | 
					        this._formulaEditor.buildDom((aceObj: any) => {
 | 
				
			||||||
@ -198,6 +212,11 @@ export class FormulaEditor extends NewBaseEditor {
 | 
				
			|||||||
          aceObj.once("change", () => {
 | 
					          aceObj.once("change", () => {
 | 
				
			||||||
            editingFormula?.(true);
 | 
					            editingFormula?.(true);
 | 
				
			||||||
          });
 | 
					          });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (val === '') {
 | 
				
			||||||
 | 
					            // Show placeholder text if the formula is blank.
 | 
				
			||||||
 | 
					            this._updateEditorPlaceholder();
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      dom.maybe(options.formulaError, () => [
 | 
					      dom.maybe(options.formulaError, () => [
 | 
				
			||||||
@ -305,6 +324,40 @@ export class FormulaEditor extends NewBaseEditor {
 | 
				
			|||||||
    return this._dom;
 | 
					    return this._dom;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private _updateEditorPlaceholder() {
 | 
				
			||||||
 | 
					    const editor = this._formulaEditor.getEditor();
 | 
				
			||||||
 | 
					    const shouldShowPlaceholder = editor.session.getValue().length === 0;
 | 
				
			||||||
 | 
					    const placeholderNode = editor.renderer.emptyMessageNode;
 | 
				
			||||||
 | 
					    if (placeholderNode) {
 | 
				
			||||||
 | 
					      // Remove the current placeholder if one is present.
 | 
				
			||||||
 | 
					      editor.renderer.scroller.removeChild(placeholderNode);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!shouldShowPlaceholder) {
 | 
				
			||||||
 | 
					      editor.renderer.emptyMessageNode = null;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      editor.renderer.emptyMessageNode = cssFormulaPlaceholder(
 | 
				
			||||||
 | 
					        !this._canDetach || this.isDetached.get()
 | 
				
			||||||
 | 
					          ? t('Enter formula.')
 | 
				
			||||||
 | 
					          : t('Enter formula or {{button}}.', {
 | 
				
			||||||
 | 
					            button: cssUseAssistantButton(
 | 
				
			||||||
 | 
					              t('use AI Assistant'),
 | 
				
			||||||
 | 
					              dom.on('click', (ev) => this._handleUseAssistantButtonClick(ev)),
 | 
				
			||||||
 | 
					              testId('formula-editor-use-ai-assistant'),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          }),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      editor.renderer.scroller.appendChild(editor.renderer.emptyMessageNode);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    this._formulaEditor.resize();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private _handleUseAssistantButtonClick(ev: MouseEvent) {
 | 
				
			||||||
 | 
					    ev.stopPropagation();
 | 
				
			||||||
 | 
					    ev.preventDefault();
 | 
				
			||||||
 | 
					    commands.allCommands.detachEditor.run();
 | 
				
			||||||
 | 
					    commands.allCommands.activateAssistant.run();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _calcSize(elem: HTMLElement, desiredElemSize: ISize) {
 | 
					  private _calcSize(elem: HTMLElement, desiredElemSize: ISize) {
 | 
				
			||||||
    if (this.isDetached.get()) {
 | 
					    if (this.isDetached.get()) {
 | 
				
			||||||
      // If we are detached, we will stop autosizing.
 | 
					      // If we are detached, we will stop autosizing.
 | 
				
			||||||
@ -313,6 +366,16 @@ export class FormulaEditor extends NewBaseEditor {
 | 
				
			|||||||
        width: 0
 | 
					        width: 0
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const placeholder: HTMLElement | undefined = this._formulaEditor.getEditor().renderer.emptyMessageNode;
 | 
				
			||||||
 | 
					    if (placeholder) {
 | 
				
			||||||
 | 
					      // If we are showing the placeholder, fit it all on the same line.
 | 
				
			||||||
 | 
					      return this._editorPlacement.calcSizeWithPadding(elem, {
 | 
				
			||||||
 | 
					        width: placeholder.scrollWidth,
 | 
				
			||||||
 | 
					        height: placeholder.scrollHeight,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const errorBox: HTMLElement|null = this._dom.querySelector('.error_details');
 | 
					    const errorBox: HTMLElement|null = this._dom.querySelector('.error_details');
 | 
				
			||||||
    const errorBoxStartHeight = errorBox?.getBoundingClientRect().height || 0;
 | 
					    const errorBoxStartHeight = errorBox?.getBoundingClientRect().height || 0;
 | 
				
			||||||
    const errorBoxDesiredHeight = errorBox?.scrollHeight || 0;
 | 
					    const errorBoxDesiredHeight = errorBox?.scrollHeight || 0;
 | 
				
			||||||
@ -652,6 +715,9 @@ const cssCollapseIcon = styled(icon, `
 | 
				
			|||||||
  margin: -3px 4px 0 4px;
 | 
					  margin: -3px 4px 0 4px;
 | 
				
			||||||
  --icon-color: ${colors.slate};
 | 
					  --icon-color: ${colors.slate};
 | 
				
			||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					  position: sticky;
 | 
				
			||||||
 | 
					  top: 0px;
 | 
				
			||||||
 | 
					  flex-shrink: 0;
 | 
				
			||||||
`);
 | 
					`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const cssError = styled('div', `
 | 
					export const cssError = styled('div', `
 | 
				
			||||||
@ -666,11 +732,15 @@ const cssFormulaEditor = styled('div.default_editor.formula_editor_wrapper', `
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
  &-detached .formula_editor {
 | 
					  &-detached .formula_editor {
 | 
				
			||||||
    flex-grow: 1;
 | 
					    flex-grow: 1;
 | 
				
			||||||
 | 
					    min-height: 100px;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &-detached .error_msg, &-detached .error_details {
 | 
					  &-detached .error_msg, &-detached .error_details {
 | 
				
			||||||
    flex-grow: 0;
 | 
					    max-height: 100px;
 | 
				
			||||||
    flex-shrink: 1;
 | 
					    flex-shrink: 0;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &-detached .error_msg {
 | 
				
			||||||
    cursor: default;
 | 
					    cursor: default;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -683,12 +753,14 @@ const cssFormulaEditor = styled('div.default_editor.formula_editor_wrapper', `
 | 
				
			|||||||
    height: 100% !important;
 | 
					    height: 100% !important;
 | 
				
			||||||
    width: 100% !important;
 | 
					    width: 100% !important;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					`);
 | 
				
			||||||
  .floating-popup .formula_editor {
 | 
					
 | 
				
			||||||
    min-height: 100px;
 | 
					const cssFormulaPlaceholder = styled('div', `
 | 
				
			||||||
  }
 | 
					  color: ${theme.lightText};
 | 
				
			||||||
 | 
					  font-style: italic;
 | 
				
			||||||
  .floating-popup .error_details {
 | 
					  white-space: nowrap;
 | 
				
			||||||
    min-height: 100px;
 | 
					`);
 | 
				
			||||||
  }
 | 
					
 | 
				
			||||||
 | 
					const cssUseAssistantButton = styled(textButton, `
 | 
				
			||||||
 | 
					  font-size: ${vars.smallFontSize};
 | 
				
			||||||
`);
 | 
					`);
 | 
				
			||||||
 | 
				
			|||||||
@ -42,8 +42,8 @@
 | 
				
			|||||||
/* Make overflow hidden, since editor might be 1 pixel bigger due to fix for devices
 | 
					/* Make overflow hidden, since editor might be 1 pixel bigger due to fix for devices
 | 
				
			||||||
 * with different pixel ratio */
 | 
					 * with different pixel ratio */
 | 
				
			||||||
.formula_editor {
 | 
					.formula_editor {
 | 
				
			||||||
  background-color: var(--grist-theme-formula-editor-bg, white);
 | 
					  background-color: var(--grist-theme-ace-editor-bg, white);
 | 
				
			||||||
  padding: 4px 0 2px 21px;
 | 
					  padding: 4px 4px 2px 21px;
 | 
				
			||||||
  z-index: 10;
 | 
					  z-index: 10;
 | 
				
			||||||
  overflow: hidden;
 | 
					  overflow: hidden;
 | 
				
			||||||
  flex: none;
 | 
					  flex: none;
 | 
				
			||||||
@ -109,12 +109,14 @@
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.error_msg {
 | 
					.error_msg {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
  background-color: #ffb6c1;
 | 
					  background-color: #ffb6c1;
 | 
				
			||||||
  padding: 4px;
 | 
					  padding: 4px;
 | 
				
			||||||
  color: black;
 | 
					  color: black;
 | 
				
			||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
  white-space: pre-wrap;
 | 
					  white-space: pre-wrap;
 | 
				
			||||||
  flex: none;
 | 
					  flex: none;
 | 
				
			||||||
 | 
					  overflow: auto;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.error_details {
 | 
					.error_details {
 | 
				
			||||||
 | 
				
			|||||||
@ -333,7 +333,6 @@ export const ThemeColors = t.iface([], {
 | 
				
			|||||||
  "cell-fg": "string",
 | 
					  "cell-fg": "string",
 | 
				
			||||||
  "cell-bg": "string",
 | 
					  "cell-bg": "string",
 | 
				
			||||||
  "cell-zebra-bg": "string",
 | 
					  "cell-zebra-bg": "string",
 | 
				
			||||||
  "formula-editor-bg": "string",
 | 
					 | 
				
			||||||
  "chart-fg": "string",
 | 
					  "chart-fg": "string",
 | 
				
			||||||
  "chart-bg": "string",
 | 
					  "chart-bg": "string",
 | 
				
			||||||
  "chart-legend-bg": "string",
 | 
					  "chart-legend-bg": "string",
 | 
				
			||||||
@ -359,6 +358,7 @@ export const ThemeColors = t.iface([], {
 | 
				
			|||||||
  "tutorials-popup-border": "string",
 | 
					  "tutorials-popup-border": "string",
 | 
				
			||||||
  "tutorials-popup-header-fg": "string",
 | 
					  "tutorials-popup-header-fg": "string",
 | 
				
			||||||
  "tutorials-popup-box-bg": "string",
 | 
					  "tutorials-popup-box-bg": "string",
 | 
				
			||||||
 | 
					  "ace-editor-bg": "string",
 | 
				
			||||||
  "ace-autocomplete-primary-fg": "string",
 | 
					  "ace-autocomplete-primary-fg": "string",
 | 
				
			||||||
  "ace-autocomplete-secondary-fg": "string",
 | 
					  "ace-autocomplete-secondary-fg": "string",
 | 
				
			||||||
  "ace-autocomplete-highlighted-fg": "string",
 | 
					  "ace-autocomplete-highlighted-fg": "string",
 | 
				
			||||||
@ -391,6 +391,9 @@ export const ThemeColors = t.iface([], {
 | 
				
			|||||||
  "login-page-bg": "string",
 | 
					  "login-page-bg": "string",
 | 
				
			||||||
  "login-page-backdrop": "string",
 | 
					  "login-page-backdrop": "string",
 | 
				
			||||||
  "login-page-line": "string",
 | 
					  "login-page-line": "string",
 | 
				
			||||||
 | 
					  "formula-assistant-header-bg": "string",
 | 
				
			||||||
 | 
					  "formula-assistant-border": "string",
 | 
				
			||||||
 | 
					  "formula-assistant-preformatted-text-bg": "string",
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const exportedTypeSuite: t.ITypeSuite = {
 | 
					const exportedTypeSuite: t.ITypeSuite = {
 | 
				
			||||||
 | 
				
			|||||||
@ -436,9 +436,6 @@ export interface ThemeColors {
 | 
				
			|||||||
  'cell-bg': string;
 | 
					  'cell-bg': string;
 | 
				
			||||||
  'cell-zebra-bg': string;
 | 
					  'cell-zebra-bg': string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /* Formula Editor */
 | 
					 | 
				
			||||||
  'formula-editor-bg': string;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /* Charts */
 | 
					  /* Charts */
 | 
				
			||||||
  'chart-fg': string;
 | 
					  'chart-fg': string;
 | 
				
			||||||
  'chart-bg': string;
 | 
					  'chart-bg': string;
 | 
				
			||||||
@ -472,7 +469,8 @@ export interface ThemeColors {
 | 
				
			|||||||
  'tutorials-popup-header-fg': string;
 | 
					  'tutorials-popup-header-fg': string;
 | 
				
			||||||
  'tutorials-popup-box-bg': string;
 | 
					  'tutorials-popup-box-bg': string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /* Ace Autocomplete */
 | 
					  /* Ace */
 | 
				
			||||||
 | 
					  'ace-editor-bg': string;
 | 
				
			||||||
  'ace-autocomplete-primary-fg': string;
 | 
					  'ace-autocomplete-primary-fg': string;
 | 
				
			||||||
  'ace-autocomplete-secondary-fg': string;
 | 
					  'ace-autocomplete-secondary-fg': string;
 | 
				
			||||||
  'ace-autocomplete-highlighted-fg': string;
 | 
					  'ace-autocomplete-highlighted-fg': string;
 | 
				
			||||||
@ -511,6 +509,11 @@ export interface ThemeColors {
 | 
				
			|||||||
  'login-page-bg': string;
 | 
					  'login-page-bg': string;
 | 
				
			||||||
  'login-page-backdrop': string;
 | 
					  'login-page-backdrop': string;
 | 
				
			||||||
  'login-page-line': string;
 | 
					  'login-page-line': string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /* Formula Assistant */
 | 
				
			||||||
 | 
					  'formula-assistant-header-bg': string;
 | 
				
			||||||
 | 
					  'formula-assistant-border': string;
 | 
				
			||||||
 | 
					  'formula-assistant-preformatted-text-bg': string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const ThemePrefsChecker = createCheckers(ThemePrefsTI).ThemePrefs as CheckerT<ThemePrefs>;
 | 
					export const ThemePrefsChecker = createCheckers(ThemePrefsTI).ThemePrefs as CheckerT<ThemePrefs>;
 | 
				
			||||||
 | 
				
			|||||||
@ -415,9 +415,6 @@ export const GristDark: ThemeColors = {
 | 
				
			|||||||
  'cell-bg': '#32323F',
 | 
					  'cell-bg': '#32323F',
 | 
				
			||||||
  'cell-zebra-bg': '#262633',
 | 
					  'cell-zebra-bg': '#262633',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /* Formula Editor */
 | 
					 | 
				
			||||||
  'formula-editor-bg': '#282A36',
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /* Charts */
 | 
					  /* Charts */
 | 
				
			||||||
  'chart-fg': '#A4A4A4',
 | 
					  'chart-fg': '#A4A4A4',
 | 
				
			||||||
  'chart-bg': '#32323F',
 | 
					  'chart-bg': '#32323F',
 | 
				
			||||||
@ -451,7 +448,8 @@ export const GristDark: ThemeColors = {
 | 
				
			|||||||
  'tutorials-popup-header-fg': '#FFFFFF',
 | 
					  'tutorials-popup-header-fg': '#FFFFFF',
 | 
				
			||||||
  'tutorials-popup-box-bg': '#57575F',
 | 
					  'tutorials-popup-box-bg': '#57575F',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /* Ace Autocomplete */
 | 
					  /* Ace */
 | 
				
			||||||
 | 
					  'ace-editor-bg': '#32323F',
 | 
				
			||||||
  'ace-autocomplete-primary-fg': '#EFEFEF',
 | 
					  'ace-autocomplete-primary-fg': '#EFEFEF',
 | 
				
			||||||
  'ace-autocomplete-secondary-fg': '#A4A4A4',
 | 
					  'ace-autocomplete-secondary-fg': '#A4A4A4',
 | 
				
			||||||
  'ace-autocomplete-highlighted-fg': '#FFFFFF',
 | 
					  'ace-autocomplete-highlighted-fg': '#FFFFFF',
 | 
				
			||||||
@ -490,4 +488,9 @@ export const GristDark: ThemeColors = {
 | 
				
			|||||||
  'login-page-bg': '#32323F',
 | 
					  'login-page-bg': '#32323F',
 | 
				
			||||||
  'login-page-backdrop': '#404150',
 | 
					  'login-page-backdrop': '#404150',
 | 
				
			||||||
  'login-page-line': '#57575F',
 | 
					  'login-page-line': '#57575F',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /* Formula Assistant */
 | 
				
			||||||
 | 
					  'formula-assistant-header-bg': '#262633',
 | 
				
			||||||
 | 
					  'formula-assistant-border': '#69697D',
 | 
				
			||||||
 | 
					  'formula-assistant-preformatted-text-bg': '#262633',
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -415,9 +415,6 @@ export const GristLight: ThemeColors = {
 | 
				
			|||||||
  'cell-bg': 'white',
 | 
					  'cell-bg': 'white',
 | 
				
			||||||
  'cell-zebra-bg': '#F8F8F8',
 | 
					  'cell-zebra-bg': '#F8F8F8',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /* Formula Editor */
 | 
					 | 
				
			||||||
  'formula-editor-bg': 'white',
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /* Charts */
 | 
					  /* Charts */
 | 
				
			||||||
  'chart-fg': '#444',
 | 
					  'chart-fg': '#444',
 | 
				
			||||||
  'chart-bg': '#fff',
 | 
					  'chart-bg': '#fff',
 | 
				
			||||||
@ -451,7 +448,8 @@ export const GristLight: ThemeColors = {
 | 
				
			|||||||
  'tutorials-popup-header-fg': '#FFFFFF',
 | 
					  'tutorials-popup-header-fg': '#FFFFFF',
 | 
				
			||||||
  'tutorials-popup-box-bg': '#F5F5F5',
 | 
					  'tutorials-popup-box-bg': '#F5F5F5',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /* Ace Autocomplete */
 | 
					  /* Ace */
 | 
				
			||||||
 | 
					  'ace-editor-bg': 'white',
 | 
				
			||||||
  'ace-autocomplete-primary-fg': '#444',
 | 
					  'ace-autocomplete-primary-fg': '#444',
 | 
				
			||||||
  'ace-autocomplete-secondary-fg': '#8F8F8F',
 | 
					  'ace-autocomplete-secondary-fg': '#8F8F8F',
 | 
				
			||||||
  'ace-autocomplete-highlighted-fg': '#000',
 | 
					  'ace-autocomplete-highlighted-fg': '#000',
 | 
				
			||||||
@ -490,4 +488,9 @@ export const GristLight: ThemeColors = {
 | 
				
			|||||||
  'login-page-bg': 'white',
 | 
					  'login-page-bg': 'white',
 | 
				
			||||||
  'login-page-backdrop': '#F5F8FA',
 | 
					  'login-page-backdrop': '#F5F8FA',
 | 
				
			||||||
  'login-page-line': '#F7F7F7',
 | 
					  'login-page-line': '#F7F7F7',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /* Formula Assistant */
 | 
				
			||||||
 | 
					  'formula-assistant-header-bg': '#F7F7F7',
 | 
				
			||||||
 | 
					  'formula-assistant-border': '#D9D9D9',
 | 
				
			||||||
 | 
					  'formula-assistant-preformatted-text-bg': '#F7F7F7',
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -116,13 +116,13 @@
 | 
				
			|||||||
    "@gristlabs/sqlite3": "5.1.4-grist.8",
 | 
					    "@gristlabs/sqlite3": "5.1.4-grist.8",
 | 
				
			||||||
    "@popperjs/core": "2.3.3",
 | 
					    "@popperjs/core": "2.3.3",
 | 
				
			||||||
    "accept-language-parser": "1.5.0",
 | 
					    "accept-language-parser": "1.5.0",
 | 
				
			||||||
 | 
					    "ace-builds": "1.23.3",
 | 
				
			||||||
    "async-mutex": "0.2.4",
 | 
					    "async-mutex": "0.2.4",
 | 
				
			||||||
    "axios": "0.21.2",
 | 
					    "axios": "0.21.2",
 | 
				
			||||||
    "backbone": "1.3.3",
 | 
					    "backbone": "1.3.3",
 | 
				
			||||||
    "bootstrap": "3.3.5",
 | 
					    "bootstrap": "3.3.5",
 | 
				
			||||||
    "bootstrap-datepicker": "1.9.0",
 | 
					    "bootstrap-datepicker": "1.9.0",
 | 
				
			||||||
    "bowser": "2.7.0",
 | 
					    "bowser": "2.7.0",
 | 
				
			||||||
    "brace": "0.11.1",
 | 
					 | 
				
			||||||
    "collect-js-deps": "^0.1.1",
 | 
					    "collect-js-deps": "^0.1.1",
 | 
				
			||||||
    "color-convert": "2.0.1",
 | 
					    "color-convert": "2.0.1",
 | 
				
			||||||
    "commander": "9.3.0",
 | 
					    "commander": "9.3.0",
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										10
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								yarn.lock
									
									
									
									
									
								
							@ -1262,6 +1262,11 @@ accepts@~1.3.5:
 | 
				
			|||||||
    mime-types "~2.1.24"
 | 
					    mime-types "~2.1.24"
 | 
				
			||||||
    negotiator "0.6.2"
 | 
					    negotiator "0.6.2"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ace-builds@1.23.3:
 | 
				
			||||||
 | 
					  version "1.23.3"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/ace-builds/-/ace-builds-1.23.3.tgz#0e9a18194b3e8a29a724ffa11900bf1fdc6fe8ef"
 | 
				
			||||||
 | 
					  integrity sha512-TTWtmCQCaMZyTALMBDeQGu2VD9N6ijVZPYwz+CrpuS1XxYVdWPuvK+7b0ORVlIVXLJBnIf8D6dpP8iTO1elemA==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
acorn-class-fields@^0.3.7:
 | 
					acorn-class-fields@^0.3.7:
 | 
				
			||||||
  version "0.3.7"
 | 
					  version "0.3.7"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/acorn-class-fields/-/acorn-class-fields-0.3.7.tgz#a35122f3cc6ad2bb33b1857e79215677fcfdd720"
 | 
					  resolved "https://registry.yarnpkg.com/acorn-class-fields/-/acorn-class-fields-0.3.7.tgz#a35122f3cc6ad2bb33b1857e79215677fcfdd720"
 | 
				
			||||||
@ -1871,11 +1876,6 @@ brace-expansion@^2.0.1:
 | 
				
			|||||||
  dependencies:
 | 
					  dependencies:
 | 
				
			||||||
    balanced-match "^1.0.0"
 | 
					    balanced-match "^1.0.0"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
brace@0.11.1:
 | 
					 | 
				
			||||||
  version "0.11.1"
 | 
					 | 
				
			||||||
  resolved "https://registry.npmjs.org/brace/-/brace-0.11.1.tgz"
 | 
					 | 
				
			||||||
  integrity sha1-SJb8ydVE7vRfS7dmDbMg07N5/lg=
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
braces@^3.0.2, braces@~3.0.2:
 | 
					braces@^3.0.2, braces@~3.0.2:
 | 
				
			||||||
  version "3.0.2"
 | 
					  version "3.0.2"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
 | 
					  resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user