mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) Brings the new color select.
Summary: Component is implemented as a grainjs ui component and can be tested using `yarn serve-projects`. This diff does not bring color select to Grist just yet. Follow up: - Make it possible to set a custom color by typing hex value directly in. - Disable the button while save call is pending. Test Plan: - Adds a project test Reviewers: paulfitz Reviewed By: paulfitz Subscribers: dsagal Differential Revision: https://phab.getgrist.com/D2733
This commit is contained in:
parent
6af811f7ab
commit
e7c4686536
68
app/client/ui2018/ColorPalette.ts
Normal file
68
app/client/ui2018/ColorPalette.ts
Normal file
@ -0,0 +1,68 @@
|
||||
/*
|
||||
* The palettes were inspired by comparisons of a handful of popular services.
|
||||
*/
|
||||
export const lighter = [
|
||||
"#FFFFFF",
|
||||
"#DCDCDC",
|
||||
"#B4B4B4",
|
||||
"#FECBCC",
|
||||
"#FD8182",
|
||||
"#FC363B",
|
||||
"#F3E1D2",
|
||||
"#D6A77F",
|
||||
"#C37739",
|
||||
"#FEE7C3",
|
||||
"#FECC81",
|
||||
"#FDA630",
|
||||
"#FFFACD",
|
||||
"#FEF47A",
|
||||
"#FEEB36",
|
||||
"#E1FEDE",
|
||||
"#98FD90",
|
||||
"#35FD31",
|
||||
"#CCFEFE",
|
||||
"#8AFCFE",
|
||||
"#2EF8FE",
|
||||
"#D3E7FE",
|
||||
"#75B5FC",
|
||||
"#2486FB",
|
||||
"#E8D0FE",
|
||||
"#BC77FC",
|
||||
"#9633FB",
|
||||
"#FED6FB",
|
||||
"#FD79F4",
|
||||
"#FC2AED"
|
||||
];
|
||||
|
||||
export const darker = [
|
||||
"#888888",
|
||||
"#414141",
|
||||
"#000000",
|
||||
"#E00A17",
|
||||
"#B60610",
|
||||
"#740206",
|
||||
"#AA632B",
|
||||
"#824617",
|
||||
"#653008",
|
||||
"#FD9D28",
|
||||
"#E38D22",
|
||||
"#B36F19",
|
||||
"#E8D62F",
|
||||
"#C0B225",
|
||||
"#928619",
|
||||
"#2AE028",
|
||||
"#1FAA1C",
|
||||
"#126E0E",
|
||||
"#24D6DB",
|
||||
"#189DA1",
|
||||
"#0C686A",
|
||||
"#157AFB",
|
||||
"#0F64CF",
|
||||
"#084794",
|
||||
"#8725FB",
|
||||
"#6318B8",
|
||||
"#460D81",
|
||||
"#E621D7",
|
||||
"#B818AC",
|
||||
"#760C6E"
|
||||
];
|
321
app/client/ui2018/ColorSelect.ts
Normal file
321
app/client/ui2018/ColorSelect.ts
Normal file
@ -0,0 +1,321 @@
|
||||
import { darker, lighter } from "app/client/ui2018/ColorPalette";
|
||||
import { colors, testId, vars } from 'app/client/ui2018/cssVars';
|
||||
import { icon } from "app/client/ui2018/icons";
|
||||
import { Computed, Disposable, dom, DomArg, Observable, onKeyDown, styled } from "grainjs";
|
||||
import { IOpenController, setPopupToCreateDom } from "popweasel";
|
||||
|
||||
/**
|
||||
* colorSelect allows to select color for both fill and text cell color. It allows for fast
|
||||
* selection thanks to two color palette, `lighter` and `darker`, and also to pick custom color with
|
||||
* native color picker. Pressing Escape reverts to the saved value. Caller is expected to handle
|
||||
* logging of onSave() callback rejection. In case of rejection, values are reverted to their saved one.
|
||||
*/
|
||||
export function colorSelect(textColor: Observable<string>, fillColor: Observable<string>,
|
||||
onSave: () => Promise<void>): Element {
|
||||
const selectBtn = cssSelectBtn(
|
||||
cssContent(
|
||||
cssButtonIcon(
|
||||
'T',
|
||||
dom.style('color', textColor),
|
||||
dom.style('background-color', fillColor),
|
||||
cssLightBorder.cls(''),
|
||||
testId('btn-icon'),
|
||||
),
|
||||
'Cell Color',
|
||||
),
|
||||
icon('Dropdown'),
|
||||
testId('color-select'),
|
||||
);
|
||||
|
||||
const domCreator = (ctl: IOpenController) => buildColorPicker(ctl, textColor, fillColor, onSave);
|
||||
setPopupToCreateDom(selectBtn, domCreator, {
|
||||
trigger: ['click'],
|
||||
placement: 'bottom-end',
|
||||
});
|
||||
|
||||
return selectBtn;
|
||||
}
|
||||
|
||||
function buildColorPicker(ctl: IOpenController, textColor: Observable<string>, fillColor: Observable<string>,
|
||||
onSave: () => Promise<void>): Element {
|
||||
const textColorModel = PickerModel.create(null, textColor);
|
||||
const fillColorModel = PickerModel.create(null, fillColor);
|
||||
|
||||
function revert() {
|
||||
textColorModel.revert();
|
||||
fillColorModel.revert();
|
||||
ctl.close();
|
||||
}
|
||||
|
||||
ctl.onDispose(async () => {
|
||||
if (textColorModel.needsSaving() || fillColorModel.needsSaving()) {
|
||||
try {
|
||||
// TODO: disable the trigger btn while saving
|
||||
await onSave();
|
||||
} catch (e) {
|
||||
/* Does no logging: onSave() callback is expected to handle their reporting */
|
||||
textColorModel.revert();
|
||||
fillColorModel.revert();
|
||||
}
|
||||
}
|
||||
textColorModel.dispose();
|
||||
fillColorModel.dispose();
|
||||
});
|
||||
|
||||
const colorSquare = (...args: DomArg[]) => cssColorSquare(
|
||||
...args,
|
||||
dom.style('color', textColor),
|
||||
dom.style('background-color', fillColor),
|
||||
cssLightBorder.cls(''),
|
||||
);
|
||||
|
||||
return cssContainer(
|
||||
dom.create(PickerComponent, fillColorModel, {
|
||||
colorSquare: colorSquare(),
|
||||
title: 'Fill',
|
||||
defaultMode: 'lighter'
|
||||
}),
|
||||
cssVSpacer(),
|
||||
dom.create(PickerComponent, textColorModel, {
|
||||
colorSquare: colorSquare('T'),
|
||||
title: 'Text',
|
||||
defaultMode: 'darker'
|
||||
}),
|
||||
|
||||
// gives focus and binds keydown events
|
||||
(elem: any) => {setTimeout(() => elem.focus(), 0); },
|
||||
onKeyDown({
|
||||
Escape: () => { revert(); },
|
||||
Enter: () => { ctl.close(); },
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
interface PickerComponentOptions {
|
||||
colorSquare: Element;
|
||||
title: string;
|
||||
defaultMode: 'darker'|'lighter';
|
||||
}
|
||||
|
||||
// PickerModel is a helper model that helps keep track of the server value for an observable that
|
||||
// needs to be changed locally without saving. To use, you must call `model.setValue(...)` instead
|
||||
// of `obs.set(...)`. Then it offers `model.needsSaving()` that tells you whether current value
|
||||
// needs saving, and `model.revert()` that reverts obs to the its server value.
|
||||
class PickerModel extends Disposable {
|
||||
private _serverValue = this.obs.get();
|
||||
private _localChange: boolean = false;
|
||||
constructor(public obs: Observable<string>) {
|
||||
super();
|
||||
this.autoDispose(this.obs.addListener((val) => {
|
||||
if (this._localChange) { return; }
|
||||
this._serverValue = val;
|
||||
}));
|
||||
}
|
||||
|
||||
// Set the value picked by the user
|
||||
public setValue(val: string) {
|
||||
this._localChange = true;
|
||||
this.obs.set(val);
|
||||
this._localChange = false;
|
||||
}
|
||||
|
||||
// Revert obs to its server value
|
||||
public revert() {
|
||||
this.obs.set(this._serverValue);
|
||||
}
|
||||
|
||||
// Is current value different from the server value?
|
||||
public needsSaving() {
|
||||
return this.obs.get() !== this._serverValue;
|
||||
}
|
||||
}
|
||||
|
||||
class PickerComponent extends Disposable {
|
||||
|
||||
private _color = Computed.create(this, this._model.obs, (use, val) => val.toUpperCase());
|
||||
private _mode = Observable.create<'darker'|'lighter'>(this, this._guessMode());
|
||||
|
||||
constructor(private _model: PickerModel, private _options: PickerComponentOptions) {
|
||||
super();
|
||||
}
|
||||
|
||||
public buildDom() {
|
||||
const title = this._options.title;
|
||||
return [
|
||||
cssHeaderRow(
|
||||
cssColorPreview(
|
||||
dom.update(
|
||||
this._options.colorSquare,
|
||||
cssColorInput(
|
||||
{type: 'color'},
|
||||
dom.attr('value', this._color),
|
||||
dom.on('input', (ev, elem) => this._setValue(elem.value)),
|
||||
testId(`${title}-input`),
|
||||
),
|
||||
),
|
||||
// TODO: make it possible to type in hex value.
|
||||
cssHexBox(
|
||||
dom.attr('value', (use) => use(this._color || '#000000')),
|
||||
{readonly: true},
|
||||
dom.on('click', (ev, e) => e.select()),
|
||||
testId(`${title}-hex`),
|
||||
)
|
||||
),
|
||||
title,
|
||||
cssBrickToggle(
|
||||
cssBrick(
|
||||
cssBrick.cls('-selected', (use) => use(this._mode) === 'darker'),
|
||||
dom.on('click', () => this._mode.set('darker')),
|
||||
testId(`${title}-darker-brick`),
|
||||
),
|
||||
cssBrick(
|
||||
cssBrick.cls('-lighter'),
|
||||
cssBrick.cls('-selected', (use) => use(this._mode) === 'lighter'),
|
||||
dom.on('click', () => this._mode.set('lighter')),
|
||||
testId(`${title}-lighter-brick`),
|
||||
),
|
||||
),
|
||||
),
|
||||
cssPalette(
|
||||
dom.domComputed(this._mode, (mode) => (mode === 'lighter' ? lighter : darker).map(color => (
|
||||
cssColorSquare(
|
||||
dom.style('background-color', color),
|
||||
cssLightBorder.cls('', (use) => use(this._mode) === 'lighter'),
|
||||
cssColorSquare.cls('-selected', (use) => use(this._color) === color),
|
||||
dom.style('outline-color', (use) => use(this._mode) === 'lighter' ? '' : color),
|
||||
dom.on('click', () => this._setValue(color)),
|
||||
testId(`color-${color}`),
|
||||
)
|
||||
))),
|
||||
testId(`${title}-palette`),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
private _setValue(val: string) {
|
||||
this._model.setValue(val);
|
||||
}
|
||||
|
||||
private _guessMode(): 'darker'|'lighter' {
|
||||
if (lighter.indexOf(this._color.get()) > -1) {
|
||||
return 'lighter';
|
||||
}
|
||||
if (darker.indexOf(this._color.get()) > -1) {
|
||||
return 'darker';
|
||||
}
|
||||
return this._options.defaultMode;
|
||||
}
|
||||
}
|
||||
|
||||
const cssColorInput = styled('input', `
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
border: none;
|
||||
`);
|
||||
|
||||
const cssBrickToggle = styled('div', `
|
||||
display: flex;
|
||||
`);
|
||||
|
||||
const cssBrick = styled('div', `
|
||||
height: 20px;
|
||||
width: 34px;
|
||||
background-color: #414141;
|
||||
box-shadow: inset 0 0 0 2px #FFFFFF;
|
||||
&-selected {
|
||||
border: 1px solid #414141;
|
||||
box-shadow: inset 0 0 0 1px #FFFFFF;
|
||||
}
|
||||
&-lighter {
|
||||
background-color: #DCDCDC;
|
||||
}
|
||||
`);
|
||||
|
||||
const cssColorPreview = styled('div', `
|
||||
display: flex;
|
||||
`);
|
||||
|
||||
const cssHeaderRow = styled('div', `
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
`);
|
||||
|
||||
|
||||
const cssPalette = styled('div', `
|
||||
width: 236px;
|
||||
height: 68px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-content: space-between;
|
||||
`);
|
||||
|
||||
const cssVSpacer = styled('div', `
|
||||
height: 12px;
|
||||
`);
|
||||
|
||||
const cssContainer = styled('div', `
|
||||
padding: 18px 16px;
|
||||
background-color: white;
|
||||
box-shadow: 0 2px 16px 0 rgba(38,38,51,0.6);
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
`);
|
||||
|
||||
const cssContent = styled('div', `
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`);
|
||||
|
||||
const cssHexBox = styled('input', `
|
||||
border: 1px solid #D9D9D9;
|
||||
border-left: none;
|
||||
font-size: ${vars.smallFontSize};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: ${colors.slate};
|
||||
width: 56px;
|
||||
outline: none;
|
||||
padding: 0 3px;
|
||||
`);
|
||||
|
||||
const cssLightBorder = styled('div', `
|
||||
border: 1px solid #D9D9D9;
|
||||
`);
|
||||
|
||||
const cssColorSquare = styled('div', `
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
&-selected {
|
||||
outline: 1px solid #D9D9D9;
|
||||
outline-offset: 1px;
|
||||
}
|
||||
`);
|
||||
|
||||
const cssButtonIcon = styled(cssColorSquare, `
|
||||
margin-right: 6px;
|
||||
`);
|
||||
|
||||
const cssSelectBtn = styled('div', `
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
justify-content: space-between;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #D9D9D9;
|
||||
padding: 5px 9px;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
`);
|
Loading…
Reference in New Issue
Block a user