(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:
George Gevoian
2024-04-26 16:34:16 -04:00
parent 34c85757f1
commit 3112433a58
86 changed files with 4221 additions and 1060 deletions

View File

@@ -6,16 +6,10 @@
* https://css-tricks.com/snippets/css/system-font-stack/
*
*/
import {createPausableObs, PausableObservable} from 'app/client/lib/pausableObs';
import {getStorage} from 'app/client/lib/storage';
import {urlState} from 'app/client/models/gristUrlState';
import {getTheme, ProductFlavor} from 'app/client/ui/CustomThemes';
import {Theme, ThemeAppearance} from 'app/common/ThemePrefs';
import {getThemeColors} from 'app/common/Themes';
import {getGristConfig} from 'app/common/urlUtils';
import {Computed, dom, DomElementMethod, makeTestId, Observable, styled, TestId} from 'grainjs';
import {dom, DomElementMethod, makeTestId, Observable, styled, TestId} from 'grainjs';
import debounce = require('lodash/debounce');
import isEqual = require('lodash/isEqual');
import values = require('lodash/values');
const VAR_PREFIX = 'grist';
@@ -1021,51 +1015,6 @@ export function isScreenResizing(): Observable<boolean> {
return _isScreenResizingObs;
}
export function prefersDarkMode(): boolean {
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}
let _prefersDarkModeObs: PausableObservable<boolean>|undefined;
/**
* Returns a singleton observable for whether the user agent prefers dark mode.
*/
export function prefersDarkModeObs(): PausableObservable<boolean> {
if (!_prefersDarkModeObs) {
const query = window.matchMedia('(prefers-color-scheme: dark)');
const obs = createPausableObs<boolean>(null, query.matches);
query.addEventListener('change', event => obs.set(event.matches));
_prefersDarkModeObs = obs;
}
return _prefersDarkModeObs;
}
let _prefersColorSchemeThemeObs: Computed<Theme>|undefined;
/**
* Returns a singleton observable for the Grist theme matching the current
* user agent color scheme preference ("light" or "dark").
*/
export function prefersColorSchemeThemeObs(): Computed<Theme> {
if (!_prefersColorSchemeThemeObs) {
const obs = Computed.create(null, prefersDarkModeObs(), (_use, prefersDarkTheme) => {
if (prefersDarkTheme) {
return {
appearance: 'dark',
colors: getThemeColors('GristDark'),
} as const;
} else {
return {
appearance: 'light',
colors: getThemeColors('GristLight'),
} as const;
}
});
_prefersColorSchemeThemeObs = obs;
}
return _prefersColorSchemeThemeObs;
}
/**
* Attaches the global css properties to the document's root to make them available in the page.
*/
@@ -1081,96 +1030,6 @@ export function attachCssRootVars(productFlavor: ProductFlavor, varsOnly: boolea
document.body.classList.add(`interface-${interfaceStyle}`);
}
export function attachTheme(themeObs: Observable<Theme>) {
// Attach the current theme to the DOM.
attachCssThemeVars(themeObs.get());
// Whenever the theme changes, re-attach it to the DOM.
return themeObs.addListener((newTheme, oldTheme) => {
if (isEqual(newTheme, oldTheme)) { return; }
attachCssThemeVars(newTheme);
});
}
/**
* Attaches theme-related css properties to the theme style element.
*/
function attachCssThemeVars({appearance, colors: themeColors}: Theme) {
// Custom CSS is incompatible with custom themes.
if (getGristConfig().enableCustomCss) { return; }
// Prepare the custom properties needed for applying the theme.
const properties = Object.entries(themeColors)
.map(([name, value]) => `--grist-theme-${name}: ${value};`);
// Include properties for styling the scrollbar.
properties.push(...getCssScrollbarProperties(appearance));
// Include properties for picking an appropriate background image.
properties.push(...getCssThemeBackgroundProperties(appearance));
// Apply the properties to the theme style element.
getOrCreateStyleElement('grist-theme').textContent = `:root {
${properties.join('\n')}
}`;
// Make the browser aware of the color scheme.
document.documentElement.style.setProperty(`color-scheme`, appearance);
// Cache the appearance in local storage; this is currently used to apply a suitable
// background image that's shown while the application is loading.
getStorage().setItem('appearance', appearance);
}
/**
* Gets scrollbar-related css properties that are appropriate for the given `appearance`.
*
* Note: Browser support for customizing scrollbars is still a mixed bag; the bulk of customization
* is non-standard and unsupported by Firefox. If support matures, we could expose some of these in
* custom themes, but for now we'll just go with reasonable presets.
*/
function getCssScrollbarProperties(appearance: ThemeAppearance) {
return [
'--scroll-bar-fg: ' +
(appearance === 'dark' ? '#6B6B6B;' : '#A8A8A8;'),
'--scroll-bar-hover-fg: ' +
(appearance === 'dark' ? '#7B7B7B;' : '#8F8F8F;'),
'--scroll-bar-active-fg: ' +
(appearance === 'dark' ? '#8B8B8B;' : '#7C7C7C;'),
'--scroll-bar-bg: ' +
(appearance === 'dark' ? '#2B2B2B;' : '#F0F0F0;'),
];
}
/**
* Gets background-related css properties that are appropriate for the given `appearance`.
*
* Currently, this sets a property for showing a background image that's visible while a page
* is loading.
*/
function getCssThemeBackgroundProperties(appearance: ThemeAppearance) {
const value = appearance === 'dark'
? 'url("img/prismpattern.png")'
: 'url("img/gplaypattern.png")';
return [`--grist-theme-bg: ${value};`];
}
/**
* Gets or creates a style element in the head of the document with the given `id`.
*
* Useful for grouping CSS values such as theme custom properties without needing to
* pollute the document with in-line styles.
*/
function getOrCreateStyleElement(id: string) {
let style = document.head.querySelector(`#${id}`);
if (style) { return style; }
style = document.createElement('style');
style.setAttribute('id', id);
document.head.append(style);
return style;
}
// A dom method to hide element in print view
export function hideInPrintView(): DomElementMethod {
return cssHideInPrint.cls('');

191
app/client/ui2018/theme.ts Normal file
View File

@@ -0,0 +1,191 @@
import { createPausableObs, PausableObservable } from 'app/client/lib/pausableObs';
import { getStorage } from 'app/client/lib/storage';
import { urlState } from 'app/client/models/gristUrlState';
import { Theme, ThemeAppearance, ThemeColors, ThemePrefs } from 'app/common/ThemePrefs';
import { getThemeColors } from 'app/common/Themes';
import { getGristConfig } from 'app/common/urlUtils';
import { Computed, Observable } from 'grainjs';
import isEqual from 'lodash/isEqual';
const DEFAULT_LIGHT_THEME: Theme = {appearance: 'light', colors: getThemeColors('GristLight')};
const DEFAULT_DARK_THEME: Theme = {appearance: 'dark', colors: getThemeColors('GristDark')};
/**
* A singleton observable for the current user's Grist theme preferences.
*
* Set by `AppModel`, which populates it from `UserPrefs`.
*/
export const gristThemePrefs = Observable.create<ThemePrefs | null>(null, null);
/**
* Returns `true` if the user agent prefers a dark color scheme.
*/
export function prefersColorSchemeDark(): boolean {
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}
let _prefersColorSchemeDarkObs: PausableObservable<boolean> | undefined;
/**
* Returns a singleton observable for whether the user agent prefers a
* dark color scheme.
*/
export function prefersColorSchemeDarkObs(): PausableObservable<boolean> {
if (!_prefersColorSchemeDarkObs) {
const query = window.matchMedia('(prefers-color-scheme: dark)');
const obs = createPausableObs<boolean>(null, query.matches);
query.addEventListener('change', event => obs.set(event.matches));
_prefersColorSchemeDarkObs = obs;
}
return _prefersColorSchemeDarkObs;
}
let _gristThemeObs: Computed<Theme> | undefined;
/**
* A singleton observable for the current Grist theme.
*/
export function gristThemeObs() {
if (!_gristThemeObs) {
_gristThemeObs = Computed.create(null, (use) => {
// Custom CSS is incompatible with custom themes.
if (getGristConfig().enableCustomCss) { return DEFAULT_LIGHT_THEME; }
// If a user's preference is known, return it.
const themePrefs = use(gristThemePrefs);
const userAgentPrefersDarkTheme = use(prefersColorSchemeDarkObs());
if (themePrefs) { return getThemeFromPrefs(themePrefs, userAgentPrefersDarkTheme); }
// Otherwise, fall back to the user agent's preference.
return userAgentPrefersDarkTheme ? DEFAULT_DARK_THEME : DEFAULT_LIGHT_THEME;
});
}
return _gristThemeObs;
}
/**
* Attaches the current theme's CSS variables to the document, and
* re-attaches them whenever the theme changes.
*/
export function attachTheme() {
// Custom CSS is incompatible with custom themes.
if (getGristConfig().enableCustomCss) { return; }
// Attach the current theme's variables to the DOM.
attachCssThemeVars(gristThemeObs().get());
// Whenever the theme changes, re-attach its variables to the DOM.
gristThemeObs().addListener((newTheme, oldTheme) => {
if (isEqual(newTheme, oldTheme)) { return; }
attachCssThemeVars(newTheme);
});
}
/**
* Returns the `Theme` from the given `themePrefs`.
*
* If theme query parameters are present (`themeName`, `themeAppearance`, `themeSyncWithOs`),
* they will take precedence over their respective values in `themePrefs`.
*/
function getThemeFromPrefs(themePrefs: ThemePrefs, userAgentPrefersDarkTheme: boolean): Theme {
let {appearance, syncWithOS} = themePrefs;
const urlParams = urlState().state.get().params;
if (urlParams?.themeAppearance) {
appearance = urlParams?.themeAppearance;
}
if (urlParams?.themeSyncWithOs !== undefined) {
syncWithOS = urlParams?.themeSyncWithOs;
}
if (syncWithOS) {
appearance = userAgentPrefersDarkTheme ? 'dark' : 'light';
}
let nameOrColors = themePrefs.colors[appearance];
if (urlParams?.themeName) {
nameOrColors = urlParams?.themeName;
}
let colors: ThemeColors;
if (typeof nameOrColors === 'string') {
colors = getThemeColors(nameOrColors);
} else {
colors = nameOrColors;
}
return {appearance, colors};
}
function attachCssThemeVars({appearance, colors: themeColors}: Theme) {
// Prepare the custom properties needed for applying the theme.
const properties = Object.entries(themeColors)
.map(([name, value]) => `--grist-theme-${name}: ${value};`);
// Include properties for styling the scrollbar.
properties.push(...getCssThemeScrollbarProperties(appearance));
// Include properties for picking an appropriate background image.
properties.push(...getCssThemeBackgroundProperties(appearance));
// Apply the properties to the theme style element.
getOrCreateStyleElement('grist-theme').textContent = `:root {
${properties.join('\n')}
}`;
// Make the browser aware of the color scheme.
document.documentElement.style.setProperty(`color-scheme`, appearance);
// Cache the appearance in local storage; this is currently used to apply a suitable
// background image that's shown while the application is loading.
getStorage().setItem('appearance', appearance);
}
/**
* Gets scrollbar-related css properties that are appropriate for the given `appearance`.
*
* Note: Browser support for customizing scrollbars is still a mixed bag; the bulk of customization
* is non-standard and unsupported by Firefox. If support matures, we could expose some of these in
* custom themes, but for now we'll just go with reasonable presets.
*/
function getCssThemeScrollbarProperties(appearance: ThemeAppearance) {
return [
'--scroll-bar-fg: ' +
(appearance === 'dark' ? '#6B6B6B;' : '#A8A8A8;'),
'--scroll-bar-hover-fg: ' +
(appearance === 'dark' ? '#7B7B7B;' : '#8F8F8F;'),
'--scroll-bar-active-fg: ' +
(appearance === 'dark' ? '#8B8B8B;' : '#7C7C7C;'),
'--scroll-bar-bg: ' +
(appearance === 'dark' ? '#2B2B2B;' : '#F0F0F0;'),
];
}
/**
* Gets background-related css properties that are appropriate for the given `appearance`.
*
* Currently, this sets a property for showing a background image that's visible while a page
* is loading.
*/
function getCssThemeBackgroundProperties(appearance: ThemeAppearance) {
const value = appearance === 'dark'
? 'url("img/prismpattern.png")'
: 'url("img/gplaypattern.png")';
return [`--grist-theme-bg: ${value};`];
}
/**
* Gets or creates a style element in the head of the document with the given `id`.
*
* Useful for grouping CSS values such as theme custom properties without needing to
* pollute the document with in-line styles.
*/
function getOrCreateStyleElement(id: string) {
let style = document.head.querySelector(`#${id}`);
if (style) { return style; }
style = document.createElement('style');
style.setAttribute('id', id);
document.head.append(style);
return style;
}