diff --git a/app/client/components/GridView.css b/app/client/components/GridView.css index 9580c545..d44a32e7 100644 --- a/app/client/components/GridView.css +++ b/app/client/components/GridView.css @@ -367,6 +367,9 @@ visibility: hidden; } +.column_name .menu_toggle { + z-index: 1; +} /* Etc */ .g-column-main-menu { @@ -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 { diff --git a/app/client/components/GridView.js b/app/client/components/GridView.js index bb7b6e54..1e567508 100644 --- a/app/client/components/GridView.js +++ b/app/client/components/GridView.js @@ -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), diff --git a/app/client/components/viewCommon.css b/app/client/components/viewCommon.css index edd431ba..dbdd4d96 100644 --- a/app/client/components/viewCommon.css +++ b/app/client/components/viewCommon.css @@ -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 */ @@ -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 { diff --git a/app/client/models/Styles.ts b/app/client/models/Styles.ts index 91382a69..054878a7 100644 --- a/app/client/models/Styles.ts +++ b/app/client/models/Styles.ts @@ -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; diff --git a/app/client/models/ViewFieldConfig.ts b/app/client/models/ViewFieldConfig.ts index 4c28ff8a..fce20d35 100644 --- a/app/client/models/ViewFieldConfig.ts +++ b/app/client/models/ViewFieldConfig.ts @@ -17,6 +17,8 @@ export class ViewFieldConfig { public options: CommonOptions; /** Style options for a field or multiple fields */ public style: ko.Computed; + /** Header style options for a field or multiple fields */ + public headerStyle: ko.Computed; // Rest of the options mimic the same options from ViewFieldRec. public wrap: modelUtil.KoSaveableObservable; @@ -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 diff --git a/app/client/models/entities/ViewFieldRec.ts b/app/client/models/entities/ViewFieldRec.ts index ca38abf2..3d5ed04e 100644 --- a/app/client/models/entities/ViewFieldRec.ts +++ b/app/client/models/entities/ViewFieldRec.ts @@ -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'; @@ -76,8 +76,15 @@ export interface ViewFieldRec extends IRowModel<"_grist_Views_section_field">, R fontUnderline: modelUtil.KoSaveableObservable; fontItalic: modelUtil.KoSaveableObservable; fontStrikethrough: modelUtil.KoSaveableObservable; - // Helper computed to change style of a cell without saving it. + headerTextColor: modelUtil.KoSaveableObservable; + headerFillColor: modelUtil.KoSaveableObservable; + headerFontBold: modelUtil.KoSaveableObservable; + headerFontUnderline: modelUtil.KoSaveableObservable; + headerFontItalic: modelUtil.KoSaveableObservable; + headerFontStrikethrough: modelUtil.KoSaveableObservable; + // Helper computed to change style of a cell and headerStyle without saving it. style: ko.PureComputed