mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
ecf242c6c6
Summary: Update for the admin page to show the latest available version information. - Latest version is read from docs.getgrist.com by default - It sends basic information (installationId, deployment type, and version) - Checks are done only on the page itself - The actual request is routed through the API (to avoid CORS) Test Plan: Added new test Reviewers: paulfitz Reviewed By: paulfitz Subscribers: paulfitz Differential Revision: https://phab.getgrist.com/D4238
248 lines
6.7 KiB
TypeScript
248 lines
6.7 KiB
TypeScript
/**
|
|
* UI 2018 Checkboxes
|
|
*
|
|
* Includes:
|
|
* - squareCheckbox
|
|
* - circleCheckbox
|
|
* - labeledSquareCheckbox
|
|
* - labeledCircleCheckbox
|
|
*
|
|
* Checkboxes support passing in DomElementArgs, which can be used to register click handlers, set
|
|
* the disabled property, and other HTML <input> element behaviors.
|
|
*
|
|
* Examples:
|
|
* squareCheckbox(observable(true)),
|
|
* labeledSquareCheckbox(observable(false), 'Include other values', dom.prop('disabled', true)),
|
|
*/
|
|
|
|
import {testId, theme} from 'app/client/ui2018/cssVars';
|
|
import {Computed, dom, DomArg, DomContents, DomElementArg, Observable, styled} from 'grainjs';
|
|
|
|
export const cssLabel = styled('label', `
|
|
position: relative;
|
|
display: inline-flex;
|
|
min-width: 0px;
|
|
margin-bottom: 0px;
|
|
flex-shrink: 0;
|
|
|
|
outline: none;
|
|
user-select: none;
|
|
|
|
--color: ${theme.checkboxBorder};
|
|
&:hover {
|
|
--color: ${theme.checkboxBorderHover};
|
|
}
|
|
`);
|
|
|
|
|
|
|
|
// TODO: the !important markings are to trump bootstrap, and should be removed when it's gone.
|
|
export const cssCheckboxSquare = styled('input', `
|
|
-webkit-appearance: none;
|
|
-moz-appearance: none;
|
|
margin: 0 !important;
|
|
padding: 0;
|
|
|
|
flex-shrink: 0;
|
|
|
|
display: inline-block;
|
|
width: 16px;
|
|
height: 16px;
|
|
outline: none !important;
|
|
|
|
--radius: 3px;
|
|
|
|
&:checked:enabled, &:indeterminate:enabled {
|
|
--color: ${theme.controlPrimaryBg};
|
|
}
|
|
|
|
&:disabled {
|
|
--color: ${theme.checkboxDisabledBg};
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
&::before, &::after {
|
|
content: '';
|
|
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
|
|
height: 16px;
|
|
width: 16px;
|
|
|
|
box-sizing: border-box;
|
|
border: 1px solid var(--color);
|
|
border-radius: var(--radius);
|
|
}
|
|
|
|
&:checked::before, &:disabled::before, &:indeterminate::before {
|
|
background-color: var(--color);
|
|
}
|
|
|
|
&:not(:checked):indeterminate::after {
|
|
-webkit-mask-image: var(--icon-Minus);
|
|
}
|
|
|
|
&:not(:disabled)::after {
|
|
background-color: ${theme.checkboxBg};
|
|
}
|
|
|
|
&:checked::after, &:indeterminate::after {
|
|
content: '';
|
|
position: absolute;
|
|
height: 16px;
|
|
width: 16px;
|
|
-webkit-mask-image: var(--icon-Tick);
|
|
-webkit-mask-size: contain;
|
|
-webkit-mask-position: center;
|
|
-webkit-mask-repeat: no-repeat;
|
|
background-color: ${theme.controlPrimaryFg};
|
|
}
|
|
`);
|
|
|
|
export const cssCheckboxCircle = styled(cssCheckboxSquare, `
|
|
--radius: 100%;
|
|
`);
|
|
|
|
export const cssLabelText = styled('span', `
|
|
margin-left: 8px;
|
|
color: ${theme.text};
|
|
font-weight: initial; /* negate bootstrap */
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
line-height: 16px;
|
|
`);
|
|
|
|
type CheckboxArg = DomArg<HTMLInputElement>;
|
|
|
|
function checkbox(
|
|
obs: Observable<boolean>, cssCheckbox: typeof cssCheckboxSquare,
|
|
label: DomArg, right: boolean, ...domArgs: CheckboxArg[]
|
|
) {
|
|
const field = cssCheckbox(
|
|
{ type: 'checkbox' },
|
|
dom.prop('checked', obs),
|
|
dom.on('change', (ev, el) => obs.set(el.checked)),
|
|
...domArgs
|
|
);
|
|
const text = label ? cssLabelText(label) : null;
|
|
if (right) {
|
|
return cssReversedLabel([text, cssInlineRelative(field)]);
|
|
}
|
|
return cssLabel(field, text);
|
|
}
|
|
|
|
export function squareCheckbox(obs: Observable<boolean>, ...domArgs: CheckboxArg[]) {
|
|
return checkbox(obs, cssCheckboxSquare, '', false, ...domArgs);
|
|
}
|
|
|
|
export function circleCheckbox(obs: Observable<boolean>, ...domArgs: CheckboxArg[]) {
|
|
return checkbox(obs, cssCheckboxCircle, '', false, ...domArgs);
|
|
}
|
|
|
|
export function labeledSquareCheckbox(obs: Observable<boolean>, label: DomArg, ...domArgs: CheckboxArg[]) {
|
|
return checkbox(obs, cssCheckboxSquare, label, false, ...domArgs);
|
|
}
|
|
|
|
export function labeledLeftSquareCheckbox(obs: Observable<boolean>, label: DomArg, ...domArgs: CheckboxArg[]) {
|
|
return checkbox(obs, cssCheckboxSquare, label, true, ...domArgs);
|
|
}
|
|
|
|
export function labeledCircleCheckbox(obs: Observable<boolean>, label: DomArg, ...domArgs: CheckboxArg[]) {
|
|
return checkbox(obs, cssCheckboxCircle, label, false, ...domArgs);
|
|
}
|
|
|
|
export const Indeterminate = 'indeterminate';
|
|
export type TriState = boolean|'indeterminate';
|
|
|
|
function triStateCheckbox(
|
|
obs: Observable<TriState>, cssCheckbox: typeof cssCheckboxSquare, label: string = '', ...domArgs: CheckboxArg[]
|
|
) {
|
|
const checkboxObs = Computed.create(null, obs, (_use, state) => state === true)
|
|
.onWrite((checked) => obs.set(checked));
|
|
return checkbox(
|
|
checkboxObs, cssCheckbox, label, false,
|
|
dom.prop('indeterminate', (use) => use(obs) === 'indeterminate'),
|
|
dom.autoDispose(checkboxObs),
|
|
...domArgs
|
|
);
|
|
}
|
|
|
|
export function triStateSquareCheckbox(obs: Observable<TriState>, ...domArgs: CheckboxArg[]) {
|
|
return triStateCheckbox(obs, cssCheckboxSquare, '', ...domArgs);
|
|
}
|
|
|
|
export function labeledTriStateSquareCheckbox(obs: Observable<TriState>, label: string, ...domArgs: CheckboxArg[]) {
|
|
return triStateCheckbox(obs, cssCheckboxSquare, label, ...domArgs);
|
|
}
|
|
|
|
export function radioCheckboxOption<T>(selectedObservable: Observable<T>, optionId: T, content: DomContents) {
|
|
const selected = Computed.create(null, use => use(selectedObservable) === optionId)
|
|
.onWrite(val => val ? selectedObservable.set(optionId) : void 0);
|
|
return dom.update(
|
|
labeledCircleCheckbox(selected, content, dom.autoDispose(selected)),
|
|
testId(`option-${optionId}`),
|
|
cssBlockCheckbox.cls(''),
|
|
cssBlockCheckbox.cls('-block', selected),
|
|
);
|
|
}
|
|
|
|
export const cssRadioCheckboxOptions = styled('div', `
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
`);
|
|
|
|
export function toggle(value: Observable<boolean|null>, ...domArgs: DomElementArg[]): DomContents {
|
|
return dom('div.widget_switch',
|
|
(elem) => elem.style.setProperty('--grist-actual-cell-color', theme.controlFg.toString()),
|
|
dom.hide((use) => use(value) === null),
|
|
dom.cls('switch_on', (use) => use(value) || false),
|
|
dom.cls('switch_transition', true),
|
|
dom.on('click', () => value.set(!value.get())),
|
|
dom('div.switch_slider'),
|
|
dom('div.switch_circle'),
|
|
...domArgs
|
|
);
|
|
}
|
|
|
|
// We need to reset top and left of ::before element, as it is wrongly set
|
|
// on the inline checkbox.
|
|
// To simulate radio button behavior, we will block user input after option is selected, because
|
|
// checkbox doesn't support two-way binding.
|
|
const cssBlockCheckbox = styled('div', `
|
|
display: flex;
|
|
padding: 10px 8px;
|
|
border: 1px solid ${theme.controlSecondaryDisabledFg};
|
|
border-radius: 3px;
|
|
cursor: pointer;
|
|
& input::before, & input::after {
|
|
top: unset;
|
|
left: unset;
|
|
}
|
|
&:hover {
|
|
border-color: ${theme.controlFg};
|
|
}
|
|
&-block {
|
|
pointer-events: none;
|
|
}
|
|
&-block a {
|
|
pointer-events: all;
|
|
}
|
|
`);
|
|
|
|
const cssInlineRelative = styled('div', `
|
|
display: inline-block;
|
|
position: relative;
|
|
height: 16px;
|
|
`);
|
|
|
|
const cssReversedLabel = styled(cssLabel, `
|
|
justify-content: space-between;
|
|
gap: 8px;
|
|
& .${cssLabelText.className} {
|
|
margin: 0px;
|
|
}
|
|
`);
|