mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Add dropdown conditions
Summary: Dropdown conditions let you specify a predicate formula that's used to filter choices and references in their respective autocomplete dropdown menus. Test Plan: Python and browser tests (WIP). Reviewers: jarek, paulfitz Reviewed By: jarek Subscribers: dsagal, paulfitz Differential Revision: https://phab.getgrist.com/D4235
This commit is contained in:
@@ -7,10 +7,10 @@ require('ace-builds/src-noconflict/theme-chrome');
|
||||
require('ace-builds/src-noconflict/theme-dracula');
|
||||
require('ace-builds/src-noconflict/ext-language_tools');
|
||||
var {setupAceEditorCompletions} = require('./AceEditorCompletions');
|
||||
var {getGristConfig} = require('../../common/urlUtils');
|
||||
var dom = require('../lib/dom');
|
||||
var dispose = require('../lib/dispose');
|
||||
var modelUtil = require('../models/modelUtil');
|
||||
var {gristThemeObs} = require('../ui2018/theme');
|
||||
|
||||
/**
|
||||
* A class to help set up the ace editor with standard formatting and convenience functions
|
||||
@@ -28,10 +28,9 @@ function AceEditor(options) {
|
||||
this.observable = options.observable || null;
|
||||
this.saveValueOnBlurEvent = !(options.saveValueOnBlurEvent === false);
|
||||
this.calcSize = options.calcSize || ((_elem, size) => size);
|
||||
this.gristDoc = options.gristDoc || null;
|
||||
this.column = options.column || null;
|
||||
this.editorState = options.editorState || null;
|
||||
this._readonly = options.readonly || false;
|
||||
this._getSuggestions = options.getSuggestions || null;
|
||||
|
||||
this.editor = null;
|
||||
this.editorDom = null;
|
||||
@@ -185,19 +184,8 @@ AceEditor.prototype.setFontSize = function(pxVal) {
|
||||
AceEditor.prototype._setup = function() {
|
||||
// Standard editor setup
|
||||
this.editor = this.autoDisposeWith('destroy', ace.edit(this.editorDom));
|
||||
if (this.gristDoc && this.column) {
|
||||
const getSuggestions = (prefix) => {
|
||||
const section = this.gristDoc.viewModel.activeSection();
|
||||
// If section is disposed or is pointing to an empty row, don't try to autocomplete.
|
||||
if (!section?.getRowId()) {
|
||||
return [];
|
||||
}
|
||||
const tableId = section.table().tableId();
|
||||
const columnId = this.column.colId();
|
||||
const rowId = section.activeRowId();
|
||||
return this.gristDoc.docComm.autocomplete(prefix, tableId, columnId, rowId);
|
||||
};
|
||||
setupAceEditorCompletions(this.editor, {getSuggestions});
|
||||
if (this._getSuggestions) {
|
||||
setupAceEditorCompletions(this.editor, {getSuggestions: this._getSuggestions});
|
||||
}
|
||||
this.editor.setOptions({
|
||||
enableLiveAutocompletion: true, // use autocompletion without needing special activation.
|
||||
@@ -205,13 +193,10 @@ AceEditor.prototype._setup = function() {
|
||||
this.session = this.editor.getSession();
|
||||
this.session.setMode('ace/mode/python');
|
||||
|
||||
const gristTheme = this.gristDoc?.currentTheme;
|
||||
this._setAceTheme(gristTheme?.get());
|
||||
if (!getGristConfig().enableCustomCss && gristTheme) {
|
||||
this.autoDispose(gristTheme.addListener((theme) => {
|
||||
this._setAceTheme(theme);
|
||||
}));
|
||||
}
|
||||
this._setAceTheme(gristThemeObs().get());
|
||||
this.autoDispose(gristThemeObs().addListener((newTheme) => {
|
||||
this._setAceTheme(newTheme);
|
||||
}));
|
||||
|
||||
// Default line numbers to hidden
|
||||
this.editor.renderer.setShowGutter(false);
|
||||
@@ -283,10 +268,9 @@ AceEditor.prototype._getContentHeight = function() {
|
||||
return Math.max(1, this.session.getScreenLength()) * this.editor.renderer.lineHeight;
|
||||
};
|
||||
|
||||
AceEditor.prototype._setAceTheme = function(gristTheme) {
|
||||
const {enableCustomCss} = getGristConfig();
|
||||
const gristAppearance = gristTheme?.appearance;
|
||||
const aceTheme = gristAppearance === 'dark' && !enableCustomCss ? 'dracula' : 'chrome';
|
||||
AceEditor.prototype._setAceTheme = function(newTheme) {
|
||||
const {appearance} = newTheme;
|
||||
const aceTheme = appearance === 'dark' ? 'dracula' : 'chrome';
|
||||
this.editor.setTheme(`ace/theme/${aceTheme}`);
|
||||
};
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import {cssFieldEntry, cssFieldLabel, IField, VisibleFieldsConfig } from 'app/cl
|
||||
import {IconName} from 'app/client/ui2018/IconList';
|
||||
import {squareCheckbox} from 'app/client/ui2018/checkbox';
|
||||
import {theme, vars} from 'app/client/ui2018/cssVars';
|
||||
import {gristThemeObs} from 'app/client/ui2018/theme';
|
||||
import {cssDragger} from 'app/client/ui2018/draggableList';
|
||||
import {icon} from 'app/client/ui2018/icons';
|
||||
import {IOptionFull, linkSelect, menu, menuItem, menuText, select} from 'app/client/ui2018/menus';
|
||||
@@ -229,7 +230,7 @@ export class ChartView extends Disposable {
|
||||
this.listenTo(this.sortedRows, 'rowNotify', this._update);
|
||||
this.autoDispose(this.sortedRows.getKoArray().subscribe(this._update));
|
||||
this.autoDispose(this._formatterComp.subscribe(this._update));
|
||||
this.autoDispose(this.gristDoc.currentTheme.addListener(() => this._update()));
|
||||
this.autoDispose(gristThemeObs().addListener(() => this._update()));
|
||||
}
|
||||
|
||||
public prepareToPrint(onOff: boolean) {
|
||||
@@ -387,8 +388,7 @@ export class ChartView extends Disposable {
|
||||
}
|
||||
|
||||
private _getPlotlyTheme(): Partial<Layout> {
|
||||
const appModel = this.gristDoc.docPageModel.appModel;
|
||||
const {colors} = appModel.currentTheme.get();
|
||||
const {colors} = gristThemeObs().get();
|
||||
return {
|
||||
paper_bgcolor: colors['chart-bg'],
|
||||
plot_bgcolor: colors['chart-bg'],
|
||||
|
||||
@@ -91,9 +91,9 @@ export class ColumnTransform extends Disposable {
|
||||
protected buildEditorDom(optInit?: string) {
|
||||
if (!this.editor) {
|
||||
this.editor = this.autoDispose(AceEditor.create({
|
||||
gristDoc: this.gristDoc,
|
||||
observable: this.transformColumn.formula,
|
||||
saveValueOnBlurEvent: false,
|
||||
// TODO: set `getSuggestions` (see `FormulaEditor.ts` for an example).
|
||||
}));
|
||||
}
|
||||
return this.editor.buildDom((aceObj: any) => {
|
||||
|
||||
@@ -321,7 +321,7 @@ export class CustomView extends Disposable {
|
||||
}),
|
||||
new MinimumLevel(AccessLevel.none)); // none access is enough
|
||||
frame.useEvents(
|
||||
ThemeNotifier.create(frame, this.gristDoc.currentTheme),
|
||||
ThemeNotifier.create(frame),
|
||||
new MinimumLevel(AccessLevel.none));
|
||||
},
|
||||
onElem: (iframe) => onFrameFocus(iframe, () => {
|
||||
|
||||
197
app/client/components/DropdownConditionConfig.ts
Normal file
197
app/client/components/DropdownConditionConfig.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
import {buildDropdownConditionEditor} from 'app/client/components/DropdownConditionEditor';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {ViewFieldRec} from 'app/client/models/DocModel';
|
||||
import {cssLabel, cssRow} from 'app/client/ui/RightPanelStyles';
|
||||
import {withInfoTooltip} from 'app/client/ui/tooltips';
|
||||
import {textButton } from 'app/client/ui2018/buttons';
|
||||
import {testId, theme} from 'app/client/ui2018/cssVars';
|
||||
import {ISuggestionWithValue} from 'app/common/ActiveDocAPI';
|
||||
import {getPredicateFormulaProperties} from 'app/common/PredicateFormula';
|
||||
import {Computed, Disposable, dom, Observable, styled} from 'grainjs';
|
||||
|
||||
const t = makeT('DropdownConditionConfig');
|
||||
|
||||
/**
|
||||
* Right panel configuration for dropdown conditions.
|
||||
*
|
||||
* Contains an instance of `DropdownConditionEditor`, the class responsible
|
||||
* for setting dropdown conditions.
|
||||
*/
|
||||
export class DropdownConditionConfig extends Disposable {
|
||||
private _text = Computed.create(this, use => {
|
||||
const dropdownCondition = use(this._field.dropdownCondition);
|
||||
if (!dropdownCondition) { return ''; }
|
||||
|
||||
return dropdownCondition.text;
|
||||
});
|
||||
|
||||
private _saveError = Observable.create<string | null>(this, null);
|
||||
|
||||
private _properties = Computed.create(this, use => {
|
||||
const dropdownCondition = use(this._field.dropdownCondition);
|
||||
if (!dropdownCondition?.parsed) { return null; }
|
||||
|
||||
return getPredicateFormulaProperties(JSON.parse(dropdownCondition.parsed));
|
||||
});
|
||||
|
||||
private _column = Computed.create(this, use => use(this._field.column));
|
||||
|
||||
private _columns = Computed.create(this, use => use(use(use(this._column).table).visibleColumns));
|
||||
|
||||
private _refColumns = Computed.create(this, use => {
|
||||
const refTable = use(use(this._column).refTable);
|
||||
if (!refTable) { return null; }
|
||||
|
||||
return use(refTable.visibleColumns);
|
||||
});
|
||||
|
||||
private _propertiesError = Computed.create<string | null>(this, use => {
|
||||
const properties = use(this._properties);
|
||||
if (!properties) { return null; }
|
||||
|
||||
const {recColIds = [], choiceColIds = []} = properties;
|
||||
const columns = use(this._columns);
|
||||
const validRecColIds = new Set(columns.map((({colId}) => use(colId))));
|
||||
const invalidRecColIds = recColIds.filter(colId => !validRecColIds.has(colId));
|
||||
if (invalidRecColIds.length > 0) {
|
||||
return t('Invalid columns: {{colIds}}', {colIds: invalidRecColIds.join(', ')});
|
||||
}
|
||||
|
||||
const refColumns = use(this._refColumns);
|
||||
if (refColumns) {
|
||||
const validChoiceColIds = new Set(['id', ...refColumns.map((({colId}) => use(colId)))]);
|
||||
const invalidChoiceColIds = choiceColIds.filter(colId => !validChoiceColIds.has(colId));
|
||||
if (invalidChoiceColIds.length > 0) {
|
||||
return t('Invalid columns: {{colIds}}', {colIds: invalidChoiceColIds.join(', ')});
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
private _error = Computed.create<string | null>(this, (use) => {
|
||||
const maybeSaveError = use(this._saveError);
|
||||
if (maybeSaveError) { return maybeSaveError; }
|
||||
|
||||
const maybeCompiled = use(this._field.dropdownConditionCompiled);
|
||||
if (maybeCompiled?.kind === 'failure') { return maybeCompiled.error; }
|
||||
|
||||
const maybePropertiesError = use(this._propertiesError);
|
||||
if (maybePropertiesError) { return maybePropertiesError; }
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
private _disabled = Computed.create(this, use =>
|
||||
use(this._field.disableModify) ||
|
||||
use(use(this._column).disableEditData) ||
|
||||
use(this._field.config.multiselect)
|
||||
);
|
||||
|
||||
private _isEditingCondition = Observable.create(this, false);
|
||||
|
||||
private _isRefField = Computed.create(this, (use) =>
|
||||
['Ref', 'RefList'].includes(use(use(this._column).pureType)));
|
||||
|
||||
private _tooltip = Computed.create(this, use => use(this._isRefField)
|
||||
? 'setRefDropdownCondition'
|
||||
: 'setChoiceDropdownCondition');
|
||||
|
||||
private _editorElement: HTMLElement;
|
||||
|
||||
constructor(private _field: ViewFieldRec) {
|
||||
super();
|
||||
|
||||
this.autoDispose(this._text.addListener(() => {
|
||||
this._saveError.set('');
|
||||
}));
|
||||
}
|
||||
|
||||
public buildDom() {
|
||||
return [
|
||||
dom.maybe((use) => !(use(this._isEditingCondition) || Boolean(use(this._text))), () => [
|
||||
cssSetDropdownConditionRow(
|
||||
dom.domComputed(use => withInfoTooltip(
|
||||
textButton(
|
||||
t('Set dropdown condition'),
|
||||
dom.on('click', () => {
|
||||
this._isEditingCondition.set(true);
|
||||
setTimeout(() => this._editorElement.focus(), 0);
|
||||
}),
|
||||
dom.prop('disabled', this._disabled),
|
||||
testId('field-set-dropdown-condition'),
|
||||
),
|
||||
use(this._tooltip),
|
||||
)),
|
||||
),
|
||||
]),
|
||||
dom.maybe((use) => use(this._isEditingCondition) || Boolean(use(this._text)), () => [
|
||||
cssLabel(t('Dropdown Condition')),
|
||||
cssRow(
|
||||
dom.create(buildDropdownConditionEditor,
|
||||
{
|
||||
value: this._text,
|
||||
disabled: this._disabled,
|
||||
getAutocompleteSuggestions: () => this._getAutocompleteSuggestions(),
|
||||
onSave: async (value) => {
|
||||
try {
|
||||
const widgetOptions = this._field.widgetOptionsJson.peek();
|
||||
if (value.trim() === '') {
|
||||
delete widgetOptions.dropdownCondition;
|
||||
} else {
|
||||
widgetOptions.dropdownCondition = {text: value};
|
||||
}
|
||||
await this._field.widgetOptionsJson.setAndSave(widgetOptions);
|
||||
} catch (e) {
|
||||
if (e?.code === 'ACL_DENY') {
|
||||
reportError(e);
|
||||
} else {
|
||||
this._saveError.set(e.message.replace(/^\[Sandbox\]/, '').trim());
|
||||
}
|
||||
}
|
||||
},
|
||||
onDispose: () => {
|
||||
this._isEditingCondition.set(false);
|
||||
},
|
||||
},
|
||||
(el) => { this._editorElement = el; },
|
||||
testId('field-dropdown-condition'),
|
||||
),
|
||||
),
|
||||
dom.maybe(this._error, (error) => cssRow(
|
||||
cssDropdownConditionError(error), testId('field-dropdown-condition-error')),
|
||||
),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
private _getAutocompleteSuggestions(): ISuggestionWithValue[] {
|
||||
const variables = ['choice'];
|
||||
const refColumns = this._refColumns.get();
|
||||
if (refColumns) {
|
||||
variables.push('choice.id', ...refColumns.map(({colId}) => `choice.${colId.peek()}`));
|
||||
}
|
||||
const columns = this._columns.get();
|
||||
variables.push(
|
||||
...columns.map(({colId}) => `$${colId.peek()}`),
|
||||
...columns.map(({colId}) => `rec.${colId.peek()}`),
|
||||
);
|
||||
|
||||
const suggestions = [
|
||||
'and', 'or', 'not', 'in', 'is', 'True', 'False', 'None',
|
||||
'OWNER', 'EDITOR', 'VIEWER',
|
||||
...variables,
|
||||
];
|
||||
return suggestions.map(suggestion => [suggestion, null]);
|
||||
}
|
||||
}
|
||||
|
||||
const cssSetDropdownConditionRow = styled(cssRow, `
|
||||
margin-top: 16px;
|
||||
`);
|
||||
|
||||
const cssDropdownConditionError = styled('div', `
|
||||
color: ${theme.errorText};
|
||||
margin-top: 4px;
|
||||
width: 100%;
|
||||
`);
|
||||
234
app/client/components/DropdownConditionEditor.ts
Normal file
234
app/client/components/DropdownConditionEditor.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
import * as AceEditor from 'app/client/components/AceEditor';
|
||||
import {createGroup} from 'app/client/components/commands';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {buildHighlightedCode} from 'app/client/ui/CodeHighlight';
|
||||
import {theme} from 'app/client/ui2018/cssVars';
|
||||
import {createMobileButtons, getButtonMargins} from 'app/client/widgets/EditorButtons';
|
||||
import {EditorPlacement, ISize} from 'app/client/widgets/EditorPlacement';
|
||||
import {initializeAceOptions} from 'app/client/widgets/FormulaEditor';
|
||||
import {IEditorCommandGroup} from 'app/client/widgets/NewBaseEditor';
|
||||
import {ISuggestionWithValue} from 'app/common/ActiveDocAPI';
|
||||
import {
|
||||
Computed,
|
||||
Disposable,
|
||||
dom,
|
||||
DomElementArg,
|
||||
Holder,
|
||||
IDisposableOwner,
|
||||
Observable,
|
||||
styled,
|
||||
} from 'grainjs';
|
||||
|
||||
const t = makeT('DropdownConditionEditor');
|
||||
|
||||
interface BuildDropdownConditionEditorOptions {
|
||||
value: Computed<string>;
|
||||
disabled: Computed<boolean>;
|
||||
onSave(value: string): Promise<void>;
|
||||
onDispose(): void;
|
||||
getAutocompleteSuggestions(prefix: string): ISuggestionWithValue[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an editor for dropdown conditions.
|
||||
*
|
||||
* Dropdown conditions are client-evaluated predicate formulas used to filter
|
||||
* items shown in autocomplete dropdowns for Choice and Reference type columns.
|
||||
*
|
||||
* Unlike Python formulas, dropdown conditions only support a very limited set of
|
||||
* features. They are a close relative of ACL formulas, sharing the same underlying
|
||||
* parser and compiler.
|
||||
*
|
||||
* See `sandbox/grist/predicate_formula.py` and `app/common/PredicateFormula.ts` for
|
||||
* more details on parsing and compiling, respectively.
|
||||
*/
|
||||
export function buildDropdownConditionEditor(
|
||||
owner: IDisposableOwner,
|
||||
options: BuildDropdownConditionEditorOptions,
|
||||
...args: DomElementArg[]
|
||||
) {
|
||||
const {value, disabled, onSave, onDispose, getAutocompleteSuggestions} = options;
|
||||
return dom.create(buildHighlightedCode,
|
||||
value,
|
||||
{maxLines: 1},
|
||||
dom.cls(cssDropdownConditionField.className),
|
||||
dom.cls('disabled'),
|
||||
cssDropdownConditionField.cls('-disabled', disabled),
|
||||
{tabIndex: '-1'},
|
||||
dom.on('focus', (_, refElem) => openDropdownConditionEditor(owner, {
|
||||
refElem,
|
||||
value,
|
||||
onSave,
|
||||
onDispose,
|
||||
getAutocompleteSuggestions,
|
||||
})),
|
||||
...args,
|
||||
);
|
||||
}
|
||||
|
||||
function openDropdownConditionEditor(owner: IDisposableOwner, options: {
|
||||
refElem: Element;
|
||||
value: Computed<string>;
|
||||
onSave: (value: string) => Promise<void>;
|
||||
onDispose: () => void;
|
||||
getAutocompleteSuggestions(prefix: string): ISuggestionWithValue[];
|
||||
}) {
|
||||
const {refElem, value, onSave, onDispose, getAutocompleteSuggestions} = options;
|
||||
|
||||
const saveAndDispose = async () => {
|
||||
const editorValue = editor.getValue();
|
||||
if (editorValue !== value.get()) {
|
||||
await onSave(editorValue);
|
||||
}
|
||||
if (editor.isDisposed()) { return; }
|
||||
|
||||
editor.dispose();
|
||||
};
|
||||
|
||||
const commands: IEditorCommandGroup = {
|
||||
fieldEditCancel: () => editor.dispose(),
|
||||
fieldEditSaveHere: () => editor.blur(),
|
||||
fieldEditSave: () => editor.blur(),
|
||||
};
|
||||
|
||||
const editor = DropdownConditionEditor.create(owner, {
|
||||
editValue: value.get(),
|
||||
commands,
|
||||
onBlur: saveAndDispose,
|
||||
getAutocompleteSuggestions,
|
||||
});
|
||||
editor.attach(refElem);
|
||||
editor.onDispose(() => onDispose());
|
||||
}
|
||||
|
||||
interface DropdownConditionEditorOptions {
|
||||
editValue: string;
|
||||
commands: IEditorCommandGroup;
|
||||
onBlur(): Promise<void>;
|
||||
getAutocompleteSuggestions(prefix: string): ISuggestionWithValue[];
|
||||
}
|
||||
|
||||
class DropdownConditionEditor extends Disposable {
|
||||
private _aceEditor: any;
|
||||
private _dom: HTMLElement;
|
||||
private _editorPlacement!: EditorPlacement;
|
||||
private _placementHolder = Holder.create(this);
|
||||
private _isEmpty: Computed<boolean>;
|
||||
|
||||
constructor(private _options: DropdownConditionEditorOptions) {
|
||||
super();
|
||||
|
||||
const initialValue = _options.editValue;
|
||||
const editorState = Observable.create(this, initialValue);
|
||||
|
||||
this._aceEditor = this.autoDispose(AceEditor.create({
|
||||
calcSize: this._calcSize.bind(this),
|
||||
editorState,
|
||||
getSuggestions: _options.getAutocompleteSuggestions,
|
||||
}));
|
||||
|
||||
this._isEmpty = Computed.create(this, editorState, (_use, state) => state === '');
|
||||
this.autoDispose(this._isEmpty.addListener(() => this._updateEditorPlaceholder()));
|
||||
|
||||
const commandGroup = this.autoDispose(createGroup({
|
||||
..._options.commands,
|
||||
}, this, true));
|
||||
|
||||
this._dom = cssDropdownConditionEditorWrapper(
|
||||
cssDropdownConditionEditor(
|
||||
createMobileButtons(_options.commands),
|
||||
this._aceEditor.buildDom((aceObj: any) => {
|
||||
initializeAceOptions(aceObj);
|
||||
const val = initialValue;
|
||||
const pos = val.length;
|
||||
this._aceEditor.setValue(val, pos);
|
||||
this._aceEditor.attachCommandGroup(commandGroup);
|
||||
if (val === '') {
|
||||
this._updateEditorPlaceholder();
|
||||
}
|
||||
})
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public attach(cellElem: Element): void {
|
||||
this._editorPlacement = EditorPlacement.create(this._placementHolder, this._dom, cellElem, {
|
||||
margins: getButtonMargins(),
|
||||
});
|
||||
this.autoDispose(this._editorPlacement.onReposition.addListener(this._aceEditor.resize, this._aceEditor));
|
||||
this._aceEditor.onAttach();
|
||||
this._updateEditorPlaceholder();
|
||||
this._aceEditor.resize();
|
||||
this._aceEditor.getEditor().focus();
|
||||
this._aceEditor.getEditor().on('blur', () => this._options.onBlur());
|
||||
}
|
||||
|
||||
public getValue(): string {
|
||||
return this._aceEditor.getValue();
|
||||
}
|
||||
|
||||
public blur() {
|
||||
this._aceEditor.getEditor().blur();
|
||||
}
|
||||
|
||||
private _updateEditorPlaceholder() {
|
||||
const editor = this._aceEditor.getEditor();
|
||||
const shouldShowPlaceholder = editor.session.getValue().length === 0;
|
||||
if (editor.renderer.emptyMessageNode) {
|
||||
// Remove the current placeholder if one is present.
|
||||
editor.renderer.scroller.removeChild(editor.renderer.emptyMessageNode);
|
||||
}
|
||||
if (!shouldShowPlaceholder) {
|
||||
editor.renderer.emptyMessageNode = null;
|
||||
} else {
|
||||
editor.renderer.emptyMessageNode = cssDropdownConditionPlaceholder(t('Enter condition.'));
|
||||
editor.renderer.scroller.appendChild(editor.renderer.emptyMessageNode);
|
||||
}
|
||||
}
|
||||
|
||||
private _calcSize(elem: HTMLElement, desiredElemSize: ISize) {
|
||||
const placeholder: HTMLElement | undefined = this._aceEditor.getEditor().renderer.emptyMessageNode;
|
||||
if (placeholder) {
|
||||
return this._editorPlacement.calcSizeWithPadding(elem, {
|
||||
width: placeholder.scrollWidth,
|
||||
height: placeholder.scrollHeight,
|
||||
});
|
||||
} else {
|
||||
return this._editorPlacement.calcSizeWithPadding(elem, {
|
||||
width: desiredElemSize.width,
|
||||
height: desiredElemSize.height,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const cssDropdownConditionField = styled('div', `
|
||||
flex: auto;
|
||||
cursor: pointer;
|
||||
margin-top: 4px;
|
||||
|
||||
&-disabled {
|
||||
opacity: 0.4;
|
||||
pointer-events: none;
|
||||
}
|
||||
`);
|
||||
|
||||
const cssDropdownConditionEditorWrapper = styled('div.default_editor.formula_editor_wrapper', `
|
||||
border-radius: 3px;
|
||||
`);
|
||||
|
||||
const cssDropdownConditionEditor = styled('div', `
|
||||
background-color: ${theme.aceEditorBg};
|
||||
padding: 5px;
|
||||
z-index: 10;
|
||||
overflow: hidden;
|
||||
flex: none;
|
||||
min-height: 22px;
|
||||
border-radius: 3px;
|
||||
`);
|
||||
|
||||
const cssDropdownConditionPlaceholder = styled('div', `
|
||||
color: ${theme.lightText};
|
||||
font-style: italic;
|
||||
white-space: nowrap;
|
||||
`);
|
||||
@@ -192,8 +192,6 @@ export class GristDoc extends DisposableWithEvents {
|
||||
// Holder for the popped up formula editor.
|
||||
public readonly formulaPopup = Holder.create(this);
|
||||
|
||||
public readonly currentTheme = this.docPageModel.appModel.currentTheme;
|
||||
|
||||
public get docApi() {
|
||||
return this.docPageModel.appModel.api.getDocAPI(this.docPageModel.currentDocId.get()!);
|
||||
}
|
||||
@@ -238,7 +236,6 @@ export class GristDoc extends DisposableWithEvents {
|
||||
untrustedContentOrigin: app.topAppModel.getUntrustedContentOrigin(),
|
||||
docComm: this.docComm,
|
||||
clientScope: app.clientScope,
|
||||
theme: this.currentTheme,
|
||||
});
|
||||
|
||||
// Maintain the MetaRowModel for the global document info, including docId and peers.
|
||||
|
||||
@@ -1345,8 +1345,9 @@ export class Importer extends DisposableWithEvents {
|
||||
const column = use(field.column);
|
||||
return use(column.formula);
|
||||
});
|
||||
const codeOptions = {gristTheme: this._gristDoc.currentTheme, placeholder: 'Skip', maxLines: 1};
|
||||
return cssFieldFormula(formula, codeOptions,
|
||||
const codeOptions = {placeholder: 'Skip', maxLines: 1};
|
||||
return dom.create(buildHighlightedCode, formula, codeOptions,
|
||||
dom.cls(cssFieldFormula.className),
|
||||
dom.cls('disabled'),
|
||||
dom.cls('formula_field_sidepane'),
|
||||
{tabIndex: '-1'},
|
||||
@@ -1701,7 +1702,7 @@ const cssColumnMatchRow = styled('div', `
|
||||
}
|
||||
`);
|
||||
|
||||
const cssFieldFormula = styled(buildHighlightedCode, `
|
||||
const cssFieldFormula = styled('div', `
|
||||
flex: auto;
|
||||
cursor: pointer;
|
||||
margin-top: 1px;
|
||||
|
||||
@@ -2,7 +2,7 @@ import {CustomView} from 'app/client/components/CustomView';
|
||||
import {DataRowModel} from 'app/client/models/DataRowModel';
|
||||
import DataTableModel from 'app/client/models/DataTableModel';
|
||||
import {ViewSectionRec} from 'app/client/models/DocModel';
|
||||
import {prefersDarkMode, prefersDarkModeObs} from 'app/client/ui2018/cssVars';
|
||||
import {prefersColorSchemeDark, prefersColorSchemeDarkObs} from 'app/client/ui2018/theme';
|
||||
import {dom} from 'grainjs';
|
||||
|
||||
type RowId = number|'new';
|
||||
@@ -45,7 +45,7 @@ export async function printViewSection(layout: any, viewSection: ViewSectionRec)
|
||||
// is Grist temporarily reverting to the light theme until the print dialog is dismissed.
|
||||
// As a workaround, we'll temporarily pause our listener, and unpause after the print dialog
|
||||
// is dismissed.
|
||||
prefersDarkModeObs().pause();
|
||||
prefersColorSchemeDarkObs().pause();
|
||||
|
||||
// Hide all layout boxes that do NOT contain the section to be printed.
|
||||
layout?.forEachBox((box: any) => {
|
||||
@@ -87,10 +87,10 @@ export async function printViewSection(layout: any, viewSection: ViewSectionRec)
|
||||
prepareToPrint(false);
|
||||
}
|
||||
delete (window as any).afterPrintCallback;
|
||||
prefersDarkModeObs().pause(false);
|
||||
prefersColorSchemeDarkObs().pause(false);
|
||||
|
||||
// This may have changed while window.print() was blocking.
|
||||
prefersDarkModeObs().set(prefersDarkMode());
|
||||
prefersColorSchemeDarkObs().set(prefersColorSchemeDark());
|
||||
});
|
||||
|
||||
// Running print on a timeout makes it possible to test printing using selenium, and doesn't
|
||||
|
||||
@@ -7,11 +7,11 @@ import {get as getBrowserGlobals} from 'app/client/lib/browserGlobals';
|
||||
import {makeTestId} from 'app/client/lib/domUtils';
|
||||
import {ColumnRec, ViewSectionRec} from 'app/client/models/DocModel';
|
||||
import {reportError} from 'app/client/models/errors';
|
||||
import {gristThemeObs} from 'app/client/ui2018/theme';
|
||||
import {AccessLevel, ICustomWidget, isSatisfied, matchWidget} from 'app/common/CustomWidget';
|
||||
import {DisposableWithEvents} from 'app/common/DisposableWithEvents';
|
||||
import {BulkColValues, fromTableDataAction, RowRecord} from 'app/common/DocActions';
|
||||
import {extractInfoFromColType, reencodeAsAny} from 'app/common/gristTypes';
|
||||
import {Theme} from 'app/common/ThemePrefs';
|
||||
import {getGristConfig} from 'app/common/urlUtils';
|
||||
import {
|
||||
AccessTokenOptions, CursorPos, CustomSectionAPI, FetchSelectedOptions, GristDocAPI, GristView,
|
||||
@@ -696,10 +696,10 @@ export class ConfigNotifier extends BaseEventSource {
|
||||
* Notifies about theme changes. Exposed in the API as `onThemeChange`.
|
||||
*/
|
||||
export class ThemeNotifier extends BaseEventSource {
|
||||
constructor(private _theme: Computed<Theme>) {
|
||||
constructor() {
|
||||
super();
|
||||
this.autoDispose(
|
||||
this._theme.addListener((newTheme, oldTheme) => {
|
||||
gristThemeObs().addListener((newTheme, oldTheme) => {
|
||||
if (isEqual(newTheme, oldTheme)) { return; }
|
||||
|
||||
this._update();
|
||||
@@ -715,7 +715,7 @@ export class ThemeNotifier extends BaseEventSource {
|
||||
if (this.isDisposed()) { return; }
|
||||
|
||||
this._notify({
|
||||
theme: this._theme.get(),
|
||||
theme: gristThemeObs().get(),
|
||||
fromReady,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user