Skip to content

Commit

Permalink
Header colored (gristlabs#581)
Browse files Browse the repository at this point in the history
  • Loading branch information
CamilleLegeron authored and Ocarthon committed Aug 10, 2023
1 parent 526a5df commit 955b884
Show file tree
Hide file tree
Showing 16 changed files with 408 additions and 39 deletions.
5 changes: 5 additions & 0 deletions app/client/components/GridView.css
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,9 @@
visibility: hidden;
}

.column_name .menu_toggle {
z-index: 1;
}
/* Etc */

.g-column-main-menu {
Expand Down Expand Up @@ -408,11 +411,13 @@
width: 13px;
height: 13px;
margin-right: 4px;
z-index: 1;
}

.g-column-label .kf_editable_label {
padding-left: 1px;
padding-right: 1px;
z-index: 1;
}

.g-column-label-spacer {
Expand Down
20 changes: 20 additions & 0 deletions app/client/components/GridView.js
Original file line number Diff line number Diff line change
Expand Up @@ -1073,8 +1073,28 @@ GridView.prototype.buildDom = function() {
self.editingFormula() &&
ko.unwrap(self.hoverColumn) === field._index()
);

const headerTextColor = ko.computed(() => field.headerTextColor() || '');
const headerFillColor = ko.computed(() => field.headerFillColor() || '');
const headerFontBold = ko.computed(() => field.headerFontBold());
const headerFontItalic = ko.computed(() => field.headerFontItalic());
const headerFontUnderline = ko.computed(() => field.headerFontUnderline());
const headerFontStrikethrough = ko.computed(() => field.headerFontStrikethrough());

return dom(
'div.column_name.field',
dom.autoDispose(headerTextColor),
dom.autoDispose(headerFillColor),
dom.autoDispose(headerFontBold),
dom.autoDispose(headerFontItalic),
dom.autoDispose(headerFontUnderline),
dom.autoDispose(headerFontStrikethrough),
kd.style('--grist-header-color', headerTextColor),
kd.style('--grist-header-background-color', headerFillColor),
kd.toggleClass('font-bold', headerFontBold),
kd.toggleClass('font-italic', headerFontItalic),
kd.toggleClass('font-underline', headerFontUnderline),
kd.toggleClass('font-strikethrough', headerFontStrikethrough),
kd.style('--frozen-position', () => ko.unwrap(this.frozenPositions.at(field._index()))),
kd.toggleClass("frozen", () => ko.unwrap(this.frozenMap.at(field._index()))),
kd.toggleClass("hover-column", isTooltip),
Expand Down
15 changes: 10 additions & 5 deletions app/client/components/viewCommon.css
Original file line number Diff line number Diff line change
Expand Up @@ -195,8 +195,11 @@
}

.column_name {
color: var(--grist-theme-table-header-fg, unset);
background-color: var(--grist-theme-table-header-bg, var(--grist-color-light-grey));
color: var(--grist-header-color,
var(--grist-theme-table-header-fg), unset);
background-color: var(--grist-header-background-color,
var(--grist-theme-table-header-bg,
var(--grist-color-light-grey)));
text-align: center;
cursor: pointer;
/* Column headers always show vertical gridlines, to make it clear how to resize them */
Expand All @@ -207,9 +210,11 @@
border-left-color: var(--grist-theme-table-header-border, var(--grist-color-dark-grey));
}

.column_name.selected {
color: var(--grist-theme-table-header-selected-fg, unset);
background-color: var(--grist-theme-table-header-selected-bg, var(--grist-color-medium-grey-opaque));
.column_name.selected > .selection {
background-color: var(--grist-theme-selection-header);
position: absolute;
inset: 0;
pointer-events: none;
}

.gridview_data_row_num.selected {
Expand Down
9 changes: 9 additions & 0 deletions app/client/models/Styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ export interface Style {
fontStrikethrough?: boolean|undefined;
}

export interface HeaderStyle {
headerTextColor?: string | undefined; // this can be string, undefined or an absent key.
headerFillColor?: string | undefined;
headerFontBold?: boolean | undefined;
headerFontUnderline?: boolean | undefined;
headerFontItalic?: boolean | undefined;
headerFontStrikethrough?: boolean | undefined;
}

export class CombinedStyle implements Style {
public readonly textColor?: string;
public readonly fillColor?: string;
Expand Down
64 changes: 64 additions & 0 deletions app/client/models/ViewFieldConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export class ViewFieldConfig {
public options: CommonOptions;
/** Style options for a field or multiple fields */
public style: ko.Computed<StyleOptions>;
/** Header style options for a field or multiple fields */
public headerStyle: ko.Computed<StyleOptions>;

// Rest of the options mimic the same options from ViewFieldRec.
public wrap: modelUtil.KoSaveableObservable<boolean|undefined>;
Expand Down Expand Up @@ -255,6 +257,68 @@ export class ViewFieldConfig {
result.revert = () => { zip(fields, state).forEach(([f, s]) => f!.style(s!)); };
return result;
});

this.headerStyle = ko.pureComputed(() => {
const fields = this.fields();
const multiSelect = fields.length > 1;
const savableOptions = modelUtil.savingComputed({
read: () => {
// For one column, just proxy this to the field.
if (!multiSelect) {
return this._field.widgetOptionsJson();
}
// Assemble final json object.
const result: any = {};
// First get all widgetOption jsons from all columns/fields.
const optionList = fields.map(f => f.widgetOptionsJson());
// And fill only those that are common
for(const key of ['headerTextColor', 'headerFillColor', 'headerFontBold',
'headerFontItalic', 'headerFontUnderline', 'headerFontStrikethrough']) {
// Setting null means that this options is there, but has no value.
result[key] = null;
// If all columns have the same value, use it.
if (allSame(optionList.map(v => v[key]))) {
result[key] = optionList[0][key] ?? null;
}
}
return result;
},
write: (setter, value) => {
if (!multiSelect) {
return setter(this._field.widgetOptionsJson, value);
}
// When the creator panel is saving widgetOptions, it will pass
// our virtual widgetObject, which has nulls for mixed values.
// If this option wasn't changed (set), we don't want to save it.
value = {...value};
for(const key of Object.keys(value)) {
if (value[key] === null) {
delete value[key];
}
}
// Now update all options, for all fields, by amending the options
// object from the field/column.
for(const item of fields) {
const previous = item.widgetOptionsJson.peek();
setter(item.widgetOptionsJson, {
...previous,
...value,
});
}
}
});
// Style picker needs to be able revert to previous value, if user cancels.
const state = fields.map(f => f.headerStyle.peek());
// We need some additional information about each property.
const result: StyleOptions = extendObservable(modelUtil.objObservable(savableOptions), {
// Property has mixed value, if not all options are the same.
mixed: prop => ko.pureComputed(() => !allSame(fields.map(f => f.widgetOptionsJson.prop(prop)()))),
// Property has empty value, if all options are empty (are null, undefined, empty Array or empty Object).
empty: prop => ko.pureComputed(() => allEmpty(fields.map(f => f.widgetOptionsJson.prop(prop)()))),
});
result.revert = () => { zip(fields, state).forEach(([f, s]) => f!.headerStyle(s!)); };
return result;
});
}

// Helper for Choice/ChoiceList columns, that saves widget options and renames values in a document
Expand Down
30 changes: 28 additions & 2 deletions app/client/models/entities/ViewFieldRec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {ColumnRec, DocModel, IRowModel, refListRecords, refRecord, ViewSectionRe
import {formatterForRec} from 'app/client/models/entities/ColumnRec';
import * as modelUtil from 'app/client/models/modelUtil';
import {removeRule, RuleOwner} from 'app/client/models/RuleOwner';
import {Style} from 'app/client/models/Styles';
import { HeaderStyle, Style } from 'app/client/models/Styles';
import {ViewFieldConfig} from 'app/client/models/ViewFieldConfig';
import * as UserType from 'app/client/widgets/UserType';
import {DocumentSettings} from 'app/common/DocumentSettings';
Expand Down Expand Up @@ -76,8 +76,15 @@ export interface ViewFieldRec extends IRowModel<"_grist_Views_section_field">, R
fontUnderline: modelUtil.KoSaveableObservable<boolean|undefined>;
fontItalic: modelUtil.KoSaveableObservable<boolean|undefined>;
fontStrikethrough: modelUtil.KoSaveableObservable<boolean|undefined>;
// Helper computed to change style of a cell without saving it.
headerTextColor: modelUtil.KoSaveableObservable<string|undefined>;
headerFillColor: modelUtil.KoSaveableObservable<string|undefined>;
headerFontBold: modelUtil.KoSaveableObservable<boolean|undefined>;
headerFontUnderline: modelUtil.KoSaveableObservable<boolean|undefined>;
headerFontItalic: modelUtil.KoSaveableObservable<boolean|undefined>;
headerFontStrikethrough: modelUtil.KoSaveableObservable<boolean|undefined>;
// Helper computed to change style of a cell and headerStyle without saving it.
style: ko.PureComputed<Style>;
headerStyle: ko.PureComputed<HeaderStyle>;

config: ViewFieldConfig;

Expand Down Expand Up @@ -236,6 +243,12 @@ export function createViewFieldRec(this: ViewFieldRec, docModel: DocModel): void
this.fontUnderline = this.widgetOptionsJson.prop('fontUnderline');
this.fontItalic = this.widgetOptionsJson.prop('fontItalic');
this.fontStrikethrough = this.widgetOptionsJson.prop('fontStrikethrough');
this.headerTextColor = this.widgetOptionsJson.prop('headerTextColor');
this.headerFillColor = this.widgetOptionsJson.prop('headerFillColor');
this.headerFontBold = this.widgetOptionsJson.prop('headerFontBold');
this.headerFontUnderline = this.widgetOptionsJson.prop('headerFontUnderline');
this.headerFontItalic = this.widgetOptionsJson.prop('headerFontItalic');
this.headerFontStrikethrough = this.widgetOptionsJson.prop('headerFontStrikethrough');

this.documentSettings = ko.pureComputed(() => docModel.docInfoRow.documentSettingsJson());
this.style = ko.pureComputed({
Expand All @@ -251,6 +264,19 @@ export function createViewFieldRec(this: ViewFieldRec, docModel: DocModel): void
this.widgetOptionsJson.update(style);
},
});
this.headerStyle = ko.pureComputed({
read: () => ({
headerTextColor: this.headerTextColor(),
headerFillColor: this.headerFillColor(),
headerFontBold: this.headerFontBold(),
headerFontUnderline: this.headerFontUnderline(),
headerFontItalic: this.headerFontItalic(),
headerFontStrikethrough: this.headerFontStrikethrough(),
}) as HeaderStyle,
write: (headerStyle: HeaderStyle) => {
this.widgetOptionsJson.update(headerStyle);
},
});

this.tableId = ko.pureComputed(() => this.column().table().tableId());
this.rulesList = ko.pureComputed(() => this._fieldOrColumn().rules());
Expand Down
1 change: 1 addition & 0 deletions app/client/ui2018/cssVars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,7 @@ export const theme = {
colors.selectionOpaque),
selectionOpaqueDarkBg: new CustomProp('theme-selection-opaque-dark-bg', undefined,
colors.selectionDarkerOpaque),
selectionHeader: new CustomProp('theme-selection-header', undefined, colors.mediumGrey),

/* Widgets */
widgetBg: new CustomProp('theme-widget-bg', undefined, 'white'),
Expand Down
51 changes: 50 additions & 1 deletion app/client/widgets/CellStyle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {GristDoc} from 'app/client/components/GristDoc';
import {ViewFieldRec} from 'app/client/models/entities/ViewFieldRec';
import {textButton} from 'app/client/ui2018/buttons';
import {ColorOption, colorSelect} from 'app/client/ui2018/ColorSelect';
import {theme, vars} from 'app/client/ui2018/cssVars';
import {testId, theme, vars} from 'app/client/ui2018/cssVars';
import {ConditionalStyle} from 'app/client/widgets/ConditionalStyle';
import {Computed, Disposable, dom, DomContents, fromKo, styled} from 'grainjs';

Expand All @@ -21,12 +21,61 @@ export class CellStyle extends Disposable {
}

public buildDom(): DomContents {
const isTableWidget = this._field.viewSection().parentKey() === 'record';
return [
dom.maybe(use => isTableWidget, () => {
return [
cssLine(
cssLabel(t('HEADER STYLE')),
),
cssRow(
testId('header-color-select'),
dom.domComputedOwned(fromKo(this._field.config.headerStyle), (holder, options) => {
const headerTextColor = fromKo(options.prop("headerTextColor"));
const headerFillColor = fromKo(options.prop("headerFillColor"));
const headerFontBold = fromKo(options.prop("headerFontBold"));
const headerFontUnderline = fromKo(options.prop("headerFontUnderline"));
const headerFontItalic = fromKo(options.prop("headerFontItalic"));
const headerFontStrikethrough = fromKo(options.prop("headerFontStrikethrough"));
const hasMixedStyle = Computed.create(holder, use => {
if (!use(this._field.config.multiselect)) { return false; }
const commonStyle = [
use(options.mixed('headerTextColor')),
use(options.mixed('headerFillColor')),
use(options.mixed('headerFontBold')),
use(options.mixed('headerFontUnderline')),
use(options.mixed('headerFontItalic')),
use(options.mixed('headerFontStrikethrough'))
];
return commonStyle.some(Boolean);
});
return colorSelect(
{
textColor: new ColorOption(
{ color: headerTextColor, defaultColor: this._defaultTextColor, noneText: 'default' }
),
fillColor: new ColorOption(
{ color: headerFillColor, allowsNone: true, noneText: 'none' }
),
fontBold: headerFontBold,
fontItalic: headerFontItalic,
fontUnderline: headerFontUnderline,
fontStrikethrough: headerFontStrikethrough
}, {
onSave: () => options.save(),
onRevert: () => options.revert(),
placeholder: use => use(hasMixedStyle) ? t('Mixed style') : t('Default header style')
}
);
}),
)];
}),
cssLine(
cssLabel(t('CELL STYLE')),
cssButton(t('Open row styles'), dom.on('click', allCommands.viewTabOpen.run)),
),
cssRow(
testId('cell-color-select'),
dom.domComputedOwned(fromKo(this._field.config.style), (holder, options) => {
const textColor = fromKo(options.prop("textColor"));
const fillColor = fromKo(options.prop("fillColor"));
Expand Down
1 change: 1 addition & 0 deletions app/common/ThemePrefs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ export interface ThemeColors {
'selection-opaque-fg': string;
'selection-opaque-bg': string;
'selection-opaque-dark-bg': string;
'selection-header': string;

/* Widgets */
'widget-bg': string;
Expand Down
1 change: 1 addition & 0 deletions app/common/themes/GristDark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ export const GristDark: ThemeColors = {
'selection-opaque-fg': 'white',
'selection-opaque-bg': '#2F4748',
'selection-opaque-dark-bg': '#253E3E',
'selection-header': 'rgba(107,107,144,0.4)',

/* Widgets */
'widget-bg': '#32323F',
Expand Down
1 change: 1 addition & 0 deletions app/common/themes/GristLight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ export const GristLight: ThemeColors = {
'selection-opaque-fg': 'black',
'selection-opaque-bg': '#DCF4EB',
'selection-opaque-dark-bg': '#D6EEE5',
'selection-header': 'rgba(217,217,217,0.6)',

/* Widgets */
'widget-bg': 'white',
Expand Down
4 changes: 3 additions & 1 deletion static/locales/en.client.json
Original file line number Diff line number Diff line change
Expand Up @@ -851,7 +851,9 @@
"Cell Style": "Cell Style",
"Default cell style": "Default cell style",
"Mixed style": "Mixed style",
"Open row styles": "Open row styles"
"Open row styles": "Open row styles",
"Default header style": "Default header style",
"Header Style": "Header Style"
},
"ChoiceTextBox": {
"CHOICES": "CHOICES"
Expand Down
Loading

0 comments on commit 955b884

Please sign in to comment.