From 1995a961782a6ba0a07a09266cc3e5b41d34679e Mon Sep 17 00:00:00 2001 From: Cyprien P Date: Tue, 2 Mar 2021 13:27:08 +0100 Subject: [PATCH] (core) Add new color select to the app Summary: - Fix transparency support on color select - Fix z-index conflicts with color select and right panel - Makes widget's default text color visible to color select Test Plan: - Updates nbrowser/CellColor and browser/Widget.test to support new interface. Should not cause regression. Reviewers: paulfitz, dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D2735 --- app/client/models/entities/ViewFieldRec.ts | 5 +-- app/client/ui2018/ColorSelect.ts | 21 +++++----- app/client/widgets/AbstractWidget.js | 34 +++++++-------- app/client/widgets/CheckBox.js | 2 +- app/client/widgets/FieldBuilder.ts | 2 +- app/client/widgets/HyperLinkTextBox.ts | 2 +- app/client/widgets/NTextBox.ts | 6 +-- app/client/widgets/NewAbstractWidget.ts | 48 ++++++++-------------- app/client/widgets/Switch.js | 2 +- 9 files changed, 51 insertions(+), 71 deletions(-) diff --git a/app/client/models/entities/ViewFieldRec.ts b/app/client/models/entities/ViewFieldRec.ts index 42f88416..1fcb0f27 100644 --- a/app/client/models/entities/ViewFieldRec.ts +++ b/app/client/models/entities/ViewFieldRec.ts @@ -70,7 +70,7 @@ export interface ViewFieldRec extends IRowModel<"_grist_Views_section_field"> { disableModify: ko.Computed; disableEditData: ko.Computed; - textColor: modelUtil.KoSaveableObservable; + textColor: modelUtil.KoSaveableObservable; fillColor: modelUtil.KoSaveableObservable; // Helper which adds/removes/updates field's displayCol to match the formula. @@ -201,8 +201,7 @@ export function createViewFieldRec(this: ViewFieldRec, docModel: DocModel): void this.disableModify = ko.pureComputed(() => this.column().disableModify()); this.disableEditData = ko.pureComputed(() => this.column().disableEditData()); - this.textColor = modelUtil.fieldWithDefault( - this.widgetOptionsJson.prop('textColor') as modelUtil.KoSaveableObservable, ''); + this.textColor = this.widgetOptionsJson.prop('textColor') as modelUtil.KoSaveableObservable; const fillColorProp = modelUtil.fieldWithDefault( this.widgetOptionsJson.prop('fillColor') as modelUtil.KoSaveableObservable, "#FFFFFF00"); diff --git a/app/client/ui2018/ColorSelect.ts b/app/client/ui2018/ColorSelect.ts index 15d2614e..a2e94d49 100644 --- a/app/client/ui2018/ColorSelect.ts +++ b/app/client/ui2018/ColorSelect.ts @@ -2,7 +2,7 @@ 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"; +import { defaultMenuOptions, IOpenController, setPopupToCreateDom } from "popweasel"; /** * colorSelect allows to select color for both fill and text cell color. It allows for fast @@ -17,7 +17,7 @@ export function colorSelect(textColor: Observable, fillColor: Observable cssButtonIcon( 'T', dom.style('color', textColor), - dom.style('background-color', fillColor), + dom.style('background-color', (use) => use(fillColor).slice(0, 7)), cssLightBorder.cls(''), testId('btn-icon'), ), @@ -28,10 +28,7 @@ export function colorSelect(textColor: Observable, fillColor: Observable ); const domCreator = (ctl: IOpenController) => buildColorPicker(ctl, textColor, fillColor, onSave); - setPopupToCreateDom(selectBtn, domCreator, { - trigger: ['click'], - placement: 'bottom-end', - }); + setPopupToCreateDom(selectBtn, domCreator, {...defaultMenuOptions, placement: 'bottom-end'}); return selectBtn; } @@ -72,13 +69,13 @@ function buildColorPicker(ctl: IOpenController, textColor: Observable, f return cssContainer( dom.create(PickerComponent, fillColorModel, { colorSquare: colorSquare(), - title: 'Fill', + title: 'fill', defaultMode: 'lighter' }), cssVSpacer(), dom.create(PickerComponent, textColorModel, { colorSquare: colorSquare('T'), - title: 'Text', + title: 'text', defaultMode: 'darker' }), @@ -132,7 +129,7 @@ class PickerModel extends Disposable { class PickerComponent extends Disposable { - private _color = Computed.create(this, this._model.obs, (use, val) => val.toUpperCase()); + private _color = Computed.create(this, this._model.obs, (use, val) => val.toUpperCase().slice(0, 7)); private _mode = Observable.create<'darker'|'lighter'>(this, this._guessMode()); constructor(private _model: PickerModel, private _options: PickerComponentOptions) { @@ -155,7 +152,7 @@ class PickerComponent extends Disposable { ), // TODO: make it possible to type in hex value. cssHexBox( - dom.attr('value', (use) => use(this._color || '#000000')), + dom.attr('value', this._color), {readonly: true}, dom.on('click', (ev, e) => e.select()), testId(`${title}-hex`), @@ -244,6 +241,7 @@ const cssHeaderRow = styled('div', ` display: flex; justify-content: space-between; margin-bottom: 8px; + text-transform: capitalize; `); @@ -265,6 +263,8 @@ const cssContainer = styled('div', ` padding: 18px 16px; background-color: white; box-shadow: 0 2px 16px 0 rgba(38,38,51,0.6); + z-index: 20; + margin: 2px 0; &:focus { outline: none; } @@ -318,4 +318,5 @@ const cssSelectBtn = styled('div', ` padding: 5px 9px; user-select: none; cursor: pointer; + background-color: white; `); diff --git a/app/client/widgets/AbstractWidget.js b/app/client/widgets/AbstractWidget.js index dd65d949..d68e8556 100644 --- a/app/client/widgets/AbstractWidget.js +++ b/app/client/widgets/AbstractWidget.js @@ -1,24 +1,28 @@ var dispose = require('../lib/dispose'); const ko = require('knockout'); -const {fromKo} = require('grainjs'); +const {Computed, fromKo} = require('grainjs'); const ValueFormatter = require('app/common/ValueFormatter'); const {cssLabel, cssRow} = require('app/client/ui/RightPanel'); -const {colorSelect} = require('app/client/ui2018/buttonSelect'); -const {testId} = require('app/client/ui2018/cssVars'); -const {cssHalfWidth, cssInlineLabel} = require('app/client/widgets/NewAbstractWidget'); +const {colorSelect} = require('app/client/ui2018/ColorSelect'); /** * AbstractWidget - The base of the inheritance tree for widgets. * @param {Function} field - The RowModel for this view field. + * @param {string|undefined} options.defaultTextColor - A hex value to set the default text color + * for the widget. Omit defaults to '#000000'. */ -function AbstractWidget(field) { +function AbstractWidget(field, opts = {}) { this.field = field; this.options = field.widgetOptionsJson; + const {defaultTextColor = '#000000'} = opts; this.valueFormatter = this.autoDispose(ko.computed(() => { return ValueFormatter.createFormatter(field.displayColModel().type(), this.options()); })); + + this.textColor = Computed.create(this, (use) => use(this.field.textColor) || defaultTextColor) + .onWrite((val) => this.field.textColor(val === defaultTextColor ? undefined : val)); } dispose.makeDisposable(AbstractWidget); @@ -49,21 +53,11 @@ AbstractWidget.prototype.buildColorConfigDom = function() { return [ cssLabel('CELL COLOR'), cssRow( - cssHalfWidth( - colorSelect( - fromKo(this.field.textColor), - (val) => this.field.textColor.saveOnly(val), - testId('text-color'), - ), - cssInlineLabel('Text') - ), - cssHalfWidth( - colorSelect( - fromKo(this.field.fillColor), - (val) => this.field.fillColor.saveOnly(val), - testId('fill-color'), - ), - cssInlineLabel('Fill') + colorSelect( + this.textColor, + fromKo(this.field.fillColor), + // Calling `field.widgetOptionsJson.save()` saves both fill and text color settings. + () => this.field.widgetOptionsJson.save() ) ) ]; diff --git a/app/client/widgets/CheckBox.js b/app/client/widgets/CheckBox.js index f3745d5e..cfcc2240 100644 --- a/app/client/widgets/CheckBox.js +++ b/app/client/widgets/CheckBox.js @@ -8,7 +8,7 @@ var AbstractWidget = require('./AbstractWidget'); * CheckBox - A bi-state CheckBox widget */ function CheckBox(field) { - AbstractWidget.call(this, field); + AbstractWidget.call(this, field, {defaultTextColor: '#606060'}); } dispose.makeDisposable(CheckBox); _.extend(CheckBox.prototype, AbstractWidget.prototype); diff --git a/app/client/widgets/FieldBuilder.ts b/app/client/widgets/FieldBuilder.ts index 5e0bdae4..f15d8bef 100644 --- a/app/client/widgets/FieldBuilder.ts +++ b/app/client/widgets/FieldBuilder.ts @@ -421,7 +421,7 @@ export class FieldBuilder extends Disposable { if (this.isDisposed()) { return null; } // Work around JS errors during field removal. const cellDom = widget ? widget.buildDom(row) : buildErrorDom(row, this.field); return dom(cellDom, kd.toggleClass('has_cursor', isActive), - kd.style('--grist-cell-color', this.field.textColor), + kd.style('--grist-cell-color', () => this.field.textColor() || ''), kd.style('--grist-cell-background-color', this.field.fillColor)); }) ); diff --git a/app/client/widgets/HyperLinkTextBox.ts b/app/client/widgets/HyperLinkTextBox.ts index 30ec5cc6..fe1020ac 100644 --- a/app/client/widgets/HyperLinkTextBox.ts +++ b/app/client/widgets/HyperLinkTextBox.ts @@ -12,7 +12,7 @@ import {dom, styled} from 'grainjs'; */ export class HyperLinkTextBox extends NTextBox { constructor(field: ViewFieldRec) { - super(field); + super(field, {defaultTextColor: colors.lightGreen.value}); } public buildDom(row: DataRowModel) { diff --git a/app/client/widgets/NTextBox.ts b/app/client/widgets/NTextBox.ts index 9435b7e2..27dd1870 100644 --- a/app/client/widgets/NTextBox.ts +++ b/app/client/widgets/NTextBox.ts @@ -4,7 +4,7 @@ import {ViewFieldRec} from 'app/client/models/entities/ViewFieldRec'; import {cssRow} from 'app/client/ui/RightPanel'; import {alignmentSelect, makeButtonSelect} from 'app/client/ui2018/buttonSelect'; import {testId} from 'app/client/ui2018/cssVars'; -import {NewAbstractWidget} from 'app/client/widgets/NewAbstractWidget'; +import {NewAbstractWidget, Options} from 'app/client/widgets/NewAbstractWidget'; import {dom, DomContents, fromKo, Observable} from 'grainjs'; /** @@ -14,8 +14,8 @@ export class NTextBox extends NewAbstractWidget { protected alignment: Observable; protected wrapping: Observable; - constructor(field: ViewFieldRec) { - super(field); + constructor(field: ViewFieldRec, options: Options = {}) { + super(field, options); this.alignment = fromKoSave(this.options.prop('alignment')); this.wrapping = fromKo(this.field.wrapping); diff --git a/app/client/widgets/NewAbstractWidget.ts b/app/client/widgets/NewAbstractWidget.ts index 1f6b2a4f..135a83a5 100644 --- a/app/client/widgets/NewAbstractWidget.ts +++ b/app/client/widgets/NewAbstractWidget.ts @@ -7,13 +7,17 @@ import {DocData} from 'app/client/models/DocData'; import {ViewFieldRec} from 'app/client/models/entities/ViewFieldRec'; import {SaveableObjObservable} from 'app/client/models/modelUtil'; import {cssLabel, cssRow} from 'app/client/ui/RightPanel'; -import {colorSelect} from 'app/client/ui2018/buttonSelect'; -import {colors, testId} from 'app/client/ui2018/cssVars'; +import {colorSelect} from 'app/client/ui2018/ColorSelect'; import {BaseFormatter, createFormatter} from 'app/common/ValueFormatter'; -import {Disposable, DomContents, fromKo, Observable, styled} from 'grainjs'; +import {Computed, Disposable, DomContents, fromKo, Observable} from 'grainjs'; import * as ko from 'knockout'; +export interface Options { + // A hex value to set the default widget text color. Default to '#000000' if omitted. + defaultTextColor?: string; +} + /** * NewAbstractWidget - The base of the inheritance tree for widgets. * @param {Function} field - The RowModel for this view field. @@ -31,10 +35,13 @@ export abstract class NewAbstractWidget extends Disposable { protected textColor: Observable; protected fillColor: Observable; - constructor(protected field: ViewFieldRec) { + constructor(protected field: ViewFieldRec, opts: Options = {}) { super(); + const {defaultTextColor = '#000000'} = opts; this.options = field.widgetOptionsJson; - this.textColor = fromKo(this.field.textColor); + this.textColor = Computed.create(this, (use) => ( + use(this.field.textColor) || defaultTextColor + )).onWrite((val) => this.field.textColor(val === defaultTextColor ? undefined : val)); this.fillColor = fromKo(this.field.fillColor); // Note that its easier to create a knockout computed from the several knockout observables, @@ -59,21 +66,11 @@ export abstract class NewAbstractWidget extends Disposable { return [ cssLabel('CELL COLOR'), cssRow( - cssHalfWidth( - colorSelect( - this.textColor, - (val) => this.field.textColor.saveOnly(val), - testId('text-color') - ), - cssInlineLabel('Text'), - ), - cssHalfWidth( - colorSelect( - this.fillColor, - (val) => this.field.fillColor.saveOnly(val), - testId('fill-color') - ), - cssInlineLabel('Fill') + colorSelect( + this.textColor, + this.fillColor, + // Calling `field.widgetOptionsJson.save()` saves both fill and text color settings. + () => this.field.widgetOptionsJson.save() ) ) ]; @@ -98,14 +95,3 @@ export abstract class NewAbstractWidget extends Disposable { */ protected _getDocComm(): DocComm { return this._getDocData().docComm; } } - -export const cssHalfWidth = styled('div', ` - display: flex; - flex: 1 1 50%; - align-items: center; -`); - -export const cssInlineLabel = styled('span', ` - margin: 0 8px; - color: ${colors.dark}; -`); diff --git a/app/client/widgets/Switch.js b/app/client/widgets/Switch.js index d5ebfba8..79fa8260 100644 --- a/app/client/widgets/Switch.js +++ b/app/client/widgets/Switch.js @@ -8,7 +8,7 @@ var AbstractWidget = require('./AbstractWidget'); * Switch - A bi-state Switch widget */ function Switch(field) { - AbstractWidget.call(this, field); + AbstractWidget.call(this, field, {defaultTextColor: '#2CB0AF'}); } dispose.makeDisposable(Switch); _.extend(Switch.prototype, AbstractWidget.prototype);