From 9d3bb437954aae4cddcdde76f34fddab4a90a0ec Mon Sep 17 00:00:00 2001 From: bennyboer Date: Tue, 19 Jul 2022 17:25:32 +0200 Subject: [PATCH 1/5] Started implementing combobox cell renderer --- example/src/app/app.component.ts | 32 +++++ .../combobox-cell-renderer-options.ts | 45 +++++++ .../combobox/combobox-cell-renderer-value.ts | 15 +++ .../cell/combobox/combobox-cell-renderer.ts | 115 ++++++++++++++++++ 4 files changed, 207 insertions(+) create mode 100644 src/renderer/canvas/cell/combobox/combobox-cell-renderer-options.ts create mode 100644 src/renderer/canvas/cell/combobox/combobox-cell-renderer-value.ts create mode 100644 src/renderer/canvas/cell/combobox/combobox-cell-renderer.ts diff --git a/example/src/app/app.component.ts b/example/src/app/app.component.ts index 4022996..89a8f5b 100644 --- a/example/src/app/app.component.ts +++ b/example/src/app/app.component.ts @@ -36,6 +36,8 @@ import {CheckboxCellRenderer} from "../../../src/renderer/canvas/cell/checkbox/c import {ICheckboxCellRendererValue} from "../../../src/renderer/canvas/cell/checkbox/checkbox-cell-renderer-value"; import {DOMCellRenderer} from "../../../src/renderer/canvas/cell/dom/dom-cell-renderer"; import {RatingCellRenderer} from "../../../src/renderer/canvas/cell/rating/rating-cell-renderer"; +import {ComboBoxCellRenderer} from "../../../src/renderer/canvas/cell/combobox/combobox-cell-renderer"; +import {IComboBoxCellRendererValue} from "../../../src/renderer/canvas/cell/combobox/combobox-cell-renderer-value"; @Component({ selector: "app-root", @@ -481,6 +483,7 @@ export class AppComponent implements AfterViewInit, OnDestroy { this.engine.registerCellRenderer(new RatingCellRenderer({ editable: true })); + this.engine.registerCellRenderer(new ComboBoxCellRenderer()); // Set an example border this.engine.getBorderModel().setBorder({ @@ -650,6 +653,35 @@ export class AppComponent implements AfterViewInit, OnDestroy { range: CellRange.fromSingleRowColumn(16, 3), rendererName: RatingCellRenderer.NAME, value: 3.5 + }, + { + range: CellRange.fromSingleRowColumn(18, 3), + rendererName: ComboBoxCellRenderer.NAME, + value: { + select_options: { + apple: {label: "Apple"}, + banana: {label: "Banana"}, + orange: {label: "Orange"} + } + } as IComboBoxCellRendererValue + }, + { + range: CellRange.fromSingleRowColumn(19, 3), + rendererName: ComboBoxCellRenderer.NAME, + value: { + selected_option_id: "cow", + select_options: { + cat: {label: "Meow"}, + dog: {label: "Woof"}, + wolf: {label: "Howl"}, + chicken: {label: "Bah-gawk"}, + cow: {label: "Moo"}, + bear: {label: "Roar"}, + bee: {label: "Buzz"}, + cricket: {label: "Chirp"}, + duck: {label: "Quack"} + } + } as IComboBoxCellRendererValue } ], (row, column) => row * column, diff --git a/src/renderer/canvas/cell/combobox/combobox-cell-renderer-options.ts b/src/renderer/canvas/cell/combobox/combobox-cell-renderer-options.ts new file mode 100644 index 0000000..c2ef2fd --- /dev/null +++ b/src/renderer/canvas/cell/combobox/combobox-cell-renderer-options.ts @@ -0,0 +1,45 @@ +import {ICell} from "../../../../cell/cell"; +import {IColor} from "../../../../util/color"; +import {Colors} from "../../../../util/colors"; + +export const DEFAULT_PLACEHOLDER: string = "Select..."; +export const DEFAULT_LABEL_COLOR: IColor = Colors.BLACK; +export const DEFAULT_PLACEHOLDER_COLOR: IColor = Colors.GRAY; +export const DEFAULT_PADDING: number = 4; + +export interface IComboBoxCellRendererOptions { + onChanged?: (cell: ICell) => void; + editable?: boolean; + placeholder?: string; + labelColor?: IColor; + placeholderColor?: IColor; + padding?: number; +} + +export const fillOptions = (options?: IComboBoxCellRendererOptions) => { + if (!options) { + options = {}; + } + + if (options.editable === undefined || options.editable === null) { + options.editable = true; + } + + if (!options.placeholder) { + options.placeholder = DEFAULT_PLACEHOLDER + } + + if (!options.labelColor) { + options.labelColor = DEFAULT_LABEL_COLOR; + } + + if (!options.placeholderColor) { + options.placeholderColor = DEFAULT_PLACEHOLDER_COLOR; + } + + if (options.padding === undefined || options.padding === null) { + options.padding = DEFAULT_PADDING; + } + + return options; +} diff --git a/src/renderer/canvas/cell/combobox/combobox-cell-renderer-value.ts b/src/renderer/canvas/cell/combobox/combobox-cell-renderer-value.ts new file mode 100644 index 0000000..89bdc2e --- /dev/null +++ b/src/renderer/canvas/cell/combobox/combobox-cell-renderer-value.ts @@ -0,0 +1,15 @@ +import {IComboBoxCellRendererOptions} from "./combobox-cell-renderer-options"; + +export interface IComboBoxCellRendererValue { + selected_option_id?: string; + select_options: IComboBoxSelectOptions; + options?: IComboBoxCellRendererOptions; +} + +export interface IComboBoxSelectOptions { + [id: string]: IComboBoxOption; +} + +export interface IComboBoxOption { + label: string; +} diff --git a/src/renderer/canvas/cell/combobox/combobox-cell-renderer.ts b/src/renderer/canvas/cell/combobox/combobox-cell-renderer.ts new file mode 100644 index 0000000..48709c6 --- /dev/null +++ b/src/renderer/canvas/cell/combobox/combobox-cell-renderer.ts @@ -0,0 +1,115 @@ +import {ICanvasCellRenderer} from "../canvas-cell-renderer"; +import {fillOptions, IComboBoxCellRendererOptions} from "./combobox-cell-renderer-options"; +import {TableEngine} from "../../../../table-engine"; +import {IRenderContext} from "../../canvas-renderer"; +import {ICell} from "../../../../cell/cell"; +import {ICellRendererEventListener} from "../../../cell/event/cell-renderer-event-listener"; +import {IRectangle} from "../../../../util/rect"; +import {ICellRendererMouseEvent} from "../../../cell/event/cell-renderer-mouse-event"; +import {IComboBoxCellRendererValue, IComboBoxOption} from "./combobox-cell-renderer-value"; +import {IColor} from "../../../../util/color"; +import {Colors} from "../../../../util/colors"; + +export class ComboBoxCellRenderer implements ICanvasCellRenderer { + + static readonly NAME: string = "combobox"; + + private readonly _options: IComboBoxCellRendererOptions; + + private _engine: TableEngine; + + private _eventListener: ICellRendererEventListener = { + onMouseUp: (event) => this._onClick(event) + }; + + constructor(options?: IComboBoxCellRendererOptions) { + this._options = fillOptions(options); + } + + private static _value(cell: ICell): IComboBoxCellRendererValue { + if (cell.value === undefined || cell.value === null) { + return { + select_options: {} + } as IComboBoxCellRendererValue; + } + + return cell.value as IComboBoxCellRendererValue; + } + + after(ctx: CanvasRenderingContext2D): void { + // Nothing to do after rendering all cells with this renderer + } + + before(ctx: CanvasRenderingContext2D, context: IRenderContext): void { + // Nothing to do before rendering all cells with this renderer + } + + cleanup(): void { + // Nothing to cleanup + } + + getCopyValue(cell: ICell): string { + return `${ComboBoxCellRenderer._value(cell)}`; + } + + getEventListener(): ICellRendererEventListener | null { + return this._eventListener; + } + + getName(): string { + return ComboBoxCellRenderer.NAME; + } + + initialize(engine: TableEngine): void { + this._engine = engine; + } + + onDisappearing(cell: ICell): void { + // Nothing to do when a cell disappears from the viewport + } + + render(ctx: CanvasRenderingContext2D, cell: ICell, bounds: IRectangle): void { + const value: IComboBoxCellRendererValue = ComboBoxCellRenderer._value(cell); + + let placeholder: string = this._options.placeholder; + let labelColor: IColor = this._options.labelColor; + let placeholderColor: IColor = this._options.placeholderColor; + let padding: number = this._options.padding; + if (!!value.options) { + if (!!value.options.placeholder) { + placeholder = value.options.placeholder; + } + if (!!value.options.labelColor) { + labelColor = value.options.labelColor; + } + if (!!value.options.placeholderColor) { + placeholderColor = value.options.placeholderColor; + } + if (value.options.padding !== undefined && value.options.padding !== null) { + padding = value.options.padding; + } + } + + const labelXOffset = bounds.left + padding; + const labelYOffset = bounds.top + Math.round(bounds.height / 2); + + const showPlaceholder = !value.selected_option_id; + if (showPlaceholder) { + ctx.fillStyle = Colors.toStyleStr(placeholderColor); + ctx.fillText(placeholder, labelXOffset, labelYOffset); + } else { + const selectedOption: IComboBoxOption = value.select_options[value.selected_option_id]; + + ctx.fillStyle = Colors.toStyleStr(labelColor); + ctx.fillText(selectedOption.label, labelXOffset, labelYOffset); + } + + // TODO Render select arrow (different color when non-editable) + } + + private _onClick(event: ICellRendererMouseEvent): void { + // TODO + console.log(event); + } + +} From 652c49119c964f185fb1114b20cbc6f01e6cfdf0 Mon Sep 17 00:00:00 2001 From: bennyboer Date: Wed, 20 Jul 2022 13:55:09 +0200 Subject: [PATCH 2/5] Added select arrow to combobox renderer --- .../combobox-cell-renderer-options.ts | 95 ++++++++-- .../cell/combobox/combobox-cell-renderer.ts | 163 ++++++++++++++++-- .../rating/rating-cell-renderer-options.ts | 4 +- src/util/colors.ts | 2 +- 4 files changed, 236 insertions(+), 28 deletions(-) diff --git a/src/renderer/canvas/cell/combobox/combobox-cell-renderer-options.ts b/src/renderer/canvas/cell/combobox/combobox-cell-renderer-options.ts index c2ef2fd..6e00dd5 100644 --- a/src/renderer/canvas/cell/combobox/combobox-cell-renderer-options.ts +++ b/src/renderer/canvas/cell/combobox/combobox-cell-renderer-options.ts @@ -2,18 +2,42 @@ import {ICell} from "../../../../cell/cell"; import {IColor} from "../../../../util/color"; import {Colors} from "../../../../util/colors"; -export const DEFAULT_PLACEHOLDER: string = "Select..."; +export const DEFAULT_PLACEHOLDER_TEXT: string = "Select..."; export const DEFAULT_LABEL_COLOR: IColor = Colors.BLACK; +export const DEFAULT_LABEL_COLOR_DISABLED: IColor = Colors.GRAY; export const DEFAULT_PLACEHOLDER_COLOR: IColor = Colors.GRAY; -export const DEFAULT_PADDING: number = 4; +export const DEFAULT_PADDING: number = 8; +export const DEFAULT_SELECT_ARROW_COLOR: IColor = Colors.GRAY; +export const DEFAULT_SELECT_ARROW_COLOR_DISABLED: IColor = Colors.LIGHTGRAY; +export const DEFAULT_SELECT_ARROW_HOVER_COLOR: IColor = Colors.CORAL; +export const DEFAULT_SELECT_ARROW_SIZE: number = 10; +export const DEFAULT_SELECT_ARROW_THICKNESS: number = 2; +export const DEFAULT_SELECT_ARROW_LINE_JOIN: "bevel" | "miter" | "round" = "round"; +export const DEFAULT_SELECT_ARROW_LINE_CAP: "butt" | "round" | "square" = "round"; export interface IComboBoxCellRendererOptions { onChanged?: (cell: ICell) => void; editable?: boolean; - placeholder?: string; labelColor?: IColor; - placeholderColor?: IColor; + disabledLabelColor?: IColor; padding?: number; + placeholder?: IPlaceholderOptions, + selectArrow?: ISelectArrowOptions +} + +export interface IPlaceholderOptions { + text?: string; + color?: IColor; +} + +export interface ISelectArrowOptions { + color?: IColor; + hoverColor?: IColor; + disabledColor?: IColor; + size?: number; + thickness?: number; + lineJoin?: "bevel" | "miter" | "round"; + lineCap?: "butt" | "round" | "square"; } export const fillOptions = (options?: IComboBoxCellRendererOptions) => { @@ -25,21 +49,72 @@ export const fillOptions = (options?: IComboBoxCellRendererOptions) => { options.editable = true; } - if (!options.placeholder) { - options.placeholder = DEFAULT_PLACEHOLDER - } - if (!options.labelColor) { options.labelColor = DEFAULT_LABEL_COLOR; } - if (!options.placeholderColor) { - options.placeholderColor = DEFAULT_PLACEHOLDER_COLOR; + if (!options.disabledLabelColor) { + options.disabledLabelColor = DEFAULT_LABEL_COLOR_DISABLED; } if (options.padding === undefined || options.padding === null) { options.padding = DEFAULT_PADDING; } + options.placeholder = fillPlaceholderOptions(options.placeholder); + options.selectArrow = fillSelectArrowOptions(options.selectArrow); + + return options; +} + +const fillPlaceholderOptions = (options?: IPlaceholderOptions) => { + if (!options) { + options = {}; + } + + if (!options.color) { + options.color = DEFAULT_PLACEHOLDER_COLOR; + } + + if (!options.text) { + options.text = DEFAULT_PLACEHOLDER_TEXT; + } + + return options; +} + +const fillSelectArrowOptions = (options?: ISelectArrowOptions) => { + if (!options) { + options = {}; + } + + if (!options.color) { + options.color = DEFAULT_SELECT_ARROW_COLOR; + } + + if (!options.hoverColor) { + options.hoverColor = DEFAULT_SELECT_ARROW_HOVER_COLOR; + } + + if (!options.disabledColor) { + options.disabledColor = DEFAULT_SELECT_ARROW_COLOR_DISABLED; + } + + if (options.size === undefined || options.size === null) { + options.size = DEFAULT_SELECT_ARROW_SIZE; + } + + if (options.thickness === undefined || options.thickness === null) { + options.thickness = DEFAULT_SELECT_ARROW_THICKNESS; + } + + if (!options.lineCap) { + options.lineCap = DEFAULT_SELECT_ARROW_LINE_CAP; + } + + if (!options.lineJoin) { + options.lineJoin = DEFAULT_SELECT_ARROW_LINE_JOIN; + } + return options; } diff --git a/src/renderer/canvas/cell/combobox/combobox-cell-renderer.ts b/src/renderer/canvas/cell/combobox/combobox-cell-renderer.ts index 48709c6..a19a83c 100644 --- a/src/renderer/canvas/cell/combobox/combobox-cell-renderer.ts +++ b/src/renderer/canvas/cell/combobox/combobox-cell-renderer.ts @@ -1,5 +1,10 @@ import {ICanvasCellRenderer} from "../canvas-cell-renderer"; -import {fillOptions, IComboBoxCellRendererOptions} from "./combobox-cell-renderer-options"; +import { + fillOptions, + IComboBoxCellRendererOptions, + IPlaceholderOptions, + ISelectArrowOptions +} from "./combobox-cell-renderer-options"; import {TableEngine} from "../../../../table-engine"; import {IRenderContext} from "../../canvas-renderer"; import {ICell} from "../../../../cell/cell"; @@ -19,7 +24,9 @@ export class ComboBoxCellRenderer implements ICanvasCellRenderer { private _engine: TableEngine; private _eventListener: ICellRendererEventListener = { - onMouseUp: (event) => this._onClick(event) + onMouseUp: (event) => this._onClick(event), + onMouseMove: (event) => this._onMouseMove(event), + onMouseOut: (event) => this._onMouseOut(event) }; constructor(options?: IComboBoxCellRendererOptions) { @@ -36,12 +43,24 @@ export class ComboBoxCellRenderer implements ICanvasCellRenderer { return cell.value as IComboBoxCellRendererValue; } + private static _cache(cell: ICell): IViewportCache { + if (!!cell.viewportCache) { + return cell.viewportCache as IViewportCache; + } else { + const cache: IViewportCache = { + hovered: false + }; + cell.viewportCache = cache; + return cache; + } + } + after(ctx: CanvasRenderingContext2D): void { // Nothing to do after rendering all cells with this renderer } before(ctx: CanvasRenderingContext2D, context: IRenderContext): void { - // Nothing to do before rendering all cells with this renderer + ctx.textAlign = "left"; } cleanup(): void { @@ -70,41 +89,113 @@ export class ComboBoxCellRenderer implements ICanvasCellRenderer { render(ctx: CanvasRenderingContext2D, cell: ICell, bounds: IRectangle): void { const value: IComboBoxCellRendererValue = ComboBoxCellRenderer._value(cell); + const cache: IViewportCache = ComboBoxCellRenderer._cache(cell); + + const style: IRenderingStyle = this._determineRenderingStyle(value, cache); - let placeholder: string = this._options.placeholder; + ComboBoxCellRenderer._renderLabel(ctx, bounds, value, style); + ComboBoxCellRenderer._renderSelectArrow(ctx, bounds, style); + } + + private _determineRenderingStyle(value: IComboBoxCellRendererValue, cache: IViewportCache): IRenderingStyle { + let editable: boolean = this._options.editable; + let hovered = cache.hovered; let labelColor: IColor = this._options.labelColor; - let placeholderColor: IColor = this._options.placeholderColor; let padding: number = this._options.padding; + let placeholderOptions: IPlaceholderOptions = this._options.placeholder; + let selectArrowOptions: ISelectArrowOptions = this._options.selectArrow; + if (!!value.options) { - if (!!value.options.placeholder) { - placeholder = value.options.placeholder; + if (value.options.editable !== undefined && value.options.editable !== null) { + editable = value.options.editable; } if (!!value.options.labelColor) { labelColor = value.options.labelColor; } - if (!!value.options.placeholderColor) { - placeholderColor = value.options.placeholderColor; - } if (value.options.padding !== undefined && value.options.padding !== null) { padding = value.options.padding; } + if (!!value.options.placeholder) { + placeholderOptions = value.options.placeholder; + } + if (!!value.options.selectArrow) { + selectArrowOptions = value.options.selectArrow; + } } - const labelXOffset = bounds.left + padding; + return { + editable, + hovered, + labelColor, + padding, + placeholderOptions, + selectArrowOptions + } + } + + private static _renderSelectArrow( + ctx: CanvasRenderingContext2D, + bounds: IRectangle, + style: IRenderingStyle + ) { + const selectArrowPath: ISelectArrowPath = ComboBoxCellRenderer._makeSelectArrowPath(); + + const selectArrowWidth: number = selectArrowPath.width * style.selectArrowOptions.size; + const selectArrowHeight: number = selectArrowPath.height * style.selectArrowOptions.size; + + const transformedSelectArrowPath: Path2D = new Path2D(); + transformedSelectArrowPath.addPath( + selectArrowPath.path, + new DOMMatrix() + .translateSelf( + bounds.left + bounds.width - selectArrowWidth - style.padding, + bounds.top + Math.round((bounds.height - selectArrowHeight) / 2) + ) + .scaleSelf(style.selectArrowOptions.size, style.selectArrowOptions.size) + ); + + console.log(style.hovered); + ctx.strokeStyle = Colors.toStyleStr(style.hovered ? style.selectArrowOptions.hoverColor : style.selectArrowOptions.color); + ctx.lineWidth = style.selectArrowOptions.thickness; + ctx.lineCap = style.selectArrowOptions.lineCap; + ctx.lineJoin = style.selectArrowOptions.lineJoin; + + ctx.stroke(transformedSelectArrowPath); + } + + private static _renderLabel( + ctx: CanvasRenderingContext2D, + bounds: IRectangle, + value: IComboBoxCellRendererValue, + style: IRenderingStyle + ) { + const labelXOffset = bounds.left + style.padding; const labelYOffset = bounds.top + Math.round(bounds.height / 2); const showPlaceholder = !value.selected_option_id; if (showPlaceholder) { - ctx.fillStyle = Colors.toStyleStr(placeholderColor); - ctx.fillText(placeholder, labelXOffset, labelYOffset); + ctx.fillStyle = Colors.toStyleStr(style.placeholderOptions.color); + ctx.fillText(style.placeholderOptions.text, labelXOffset, labelYOffset); } else { const selectedOption: IComboBoxOption = value.select_options[value.selected_option_id]; - ctx.fillStyle = Colors.toStyleStr(labelColor); + ctx.fillStyle = Colors.toStyleStr(style.labelColor); ctx.fillText(selectedOption.label, labelXOffset, labelYOffset); } + } + + private static _makeSelectArrowPath(): ISelectArrowPath { + const path: Path2D = new Path2D(); + + path.moveTo(0.0, 0.0); + path.lineTo(0.5, 0.45); + path.lineTo(1.0, 0.0); - // TODO Render select arrow (different color when non-editable) + return { + path, + width: 1.0, + height: 0.4 + }; } private _onClick(event: ICellRendererMouseEvent): void { @@ -112,4 +203,46 @@ export class ComboBoxCellRenderer implements ICanvasCellRenderer { console.log(event); } + private _onMouseMove(event: ICellRendererMouseEvent): void { + const cache: IViewportCache = ComboBoxCellRenderer._cache(event.cell); + const value: IComboBoxCellRendererValue = ComboBoxCellRenderer._value(event.cell); + + let editable: boolean = this._options.editable; + if (!!value.options && value.options.editable !== undefined && value.options.editable !== null) { + editable = value.options.editable; + } + + if (editable && !cache.hovered) { + cache.hovered = true; + this._engine.repaint(); + } + } + + private _onMouseOut(event: ICellRendererMouseEvent): void { + const cache: IViewportCache = ComboBoxCellRenderer._cache(event.cell); + if (cache.hovered) { + cache.hovered = false; + this._engine.repaint(); + } + } + +} + +interface ISelectArrowPath { + path: Path2D, + width: number, + height: number +} + +interface IRenderingStyle { + editable: boolean; + hovered: boolean; + labelColor: IColor; + padding: number; + placeholderOptions: IPlaceholderOptions; + selectArrowOptions: ISelectArrowOptions; +} + +interface IViewportCache { + hovered: boolean; } diff --git a/src/renderer/canvas/cell/rating/rating-cell-renderer-options.ts b/src/renderer/canvas/cell/rating/rating-cell-renderer-options.ts index 52d08d2..96cfe49 100644 --- a/src/renderer/canvas/cell/rating/rating-cell-renderer-options.ts +++ b/src/renderer/canvas/cell/rating/rating-cell-renderer-options.ts @@ -6,8 +6,8 @@ export const DEFAULT_STAR_COUNT: number = 5; export const DEFAULT_STAR_SPIKE_COUNT: number = 5; export const DEFAULT_COLOR: IColor = Colors.ORANGE; export const DEFAULT_INACTIVE_COLOR: IColor = Colors.LIGHTGRAY; -export const DEFAULT_HOVER_BORDER_COLOR: IColor = Colors.GRAY; -export const DEFAULT_HOVER_BORDER_THICKNESS: number = 2; +export const DEFAULT_HOVER_BORDER_COLOR: IColor = Colors.ORANGE; +export const DEFAULT_HOVER_BORDER_THICKNESS: number = 1; export const DEFAULT_SPACING: number = 2; export const DEFAULT_PADDING: number = 2; diff --git a/src/util/colors.ts b/src/util/colors.ts index b94f3ef..b06ba95 100644 --- a/src/util/colors.ts +++ b/src/util/colors.ts @@ -6,8 +6,8 @@ import {IColor} from "./color"; export class Colors { public static readonly WHITE: IColor = {red: 255, green: 255, blue: 255, alpha: 1.0}; - public static readonly BLACK: IColor = {red: 0, green: 0, blue: 0, alpha: 1.0}; + public static readonly TRANSPARENT: IColor = {red: 0, green: 0, blue: 0, alpha: 0.0}; public static readonly DARKGRAY: IColor = {red: 70, green: 70, blue: 70, alpha: 1.0}; public static readonly GRAY: IColor = {red: 127, green: 127, blue: 127, alpha: 1.0}; From 4006c9685315dc2a3dd3861e4613f26e13abcaaa Mon Sep 17 00:00:00 2001 From: bennyboer Date: Wed, 20 Jul 2022 14:59:22 +0200 Subject: [PATCH 3/5] Added dropdown overlay to combobox renderer --- example/src/styles.scss | 8 ++ .../cell/combobox/combobox-cell-renderer.ts | 110 +++++++++++++++++- 2 files changed, 115 insertions(+), 3 deletions(-) diff --git a/example/src/styles.scss b/example/src/styles.scss index 370b9ca..f674ade 100644 --- a/example/src/styles.scss +++ b/example/src/styles.scss @@ -12,3 +12,11 @@ body { .small-form-field .mat-form-field-infix { padding: 0.5em 0; } + +ul.table-engine-combobox-dropdown-list { + li { + &:hover { + background-color: #F9F9F9; + } + } +} diff --git a/src/renderer/canvas/cell/combobox/combobox-cell-renderer.ts b/src/renderer/canvas/cell/combobox/combobox-cell-renderer.ts index a19a83c..4d17d5a 100644 --- a/src/renderer/canvas/cell/combobox/combobox-cell-renderer.ts +++ b/src/renderer/canvas/cell/combobox/combobox-cell-renderer.ts @@ -14,6 +14,8 @@ import {ICellRendererMouseEvent} from "../../../cell/event/cell-renderer-mouse-e import {IComboBoxCellRendererValue, IComboBoxOption} from "./combobox-cell-renderer-value"; import {IColor} from "../../../../util/color"; import {Colors} from "../../../../util/colors"; +import {IOverlay} from "../../../../overlay/overlay"; +import {IPoint} from "../../../../util/point"; export class ComboBoxCellRenderer implements ICanvasCellRenderer { @@ -154,7 +156,6 @@ export class ComboBoxCellRenderer implements ICanvasCellRenderer { .scaleSelf(style.selectArrowOptions.size, style.selectArrowOptions.size) ); - console.log(style.hovered); ctx.strokeStyle = Colors.toStyleStr(style.hovered ? style.selectArrowOptions.hoverColor : style.selectArrowOptions.color); ctx.lineWidth = style.selectArrowOptions.thickness; ctx.lineCap = style.selectArrowOptions.lineCap; @@ -199,8 +200,111 @@ export class ComboBoxCellRenderer implements ICanvasCellRenderer { } private _onClick(event: ICellRendererMouseEvent): void { - // TODO - console.log(event); + const cache: IViewportCache = ComboBoxCellRenderer._cache(event.cell); + const value: IComboBoxCellRendererValue = ComboBoxCellRenderer._value(event.cell); + + let editable: boolean = this._options.editable; + if (!!value.options && value.options.editable !== undefined && value.options.editable !== null) { + editable = value.options.editable; + } + + if (editable) { + const cellBounds: IRectangle = this._engine.getCellModel().getBounds(event.cell.range); + this._openDropdownOverlay(value, cellBounds); + } + } + + private _openDropdownOverlay(value: IComboBoxCellRendererValue, cellBounds: IRectangle): void { + const listItemFontSize: number = 12; // TODO Configurable + const listItemHorizontalPadding: number = 8; // TODO Configurable + const listItemVerticalPadding: number = 4; // TODO Configurable + const listItemSeparatorSize: number = 1; // TODO Configurable + + const overlayElement: HTMLElement = document.createElement("div"); + overlayElement.style.background = "white"; + overlayElement.style.borderRadius = "0 0 2px 2px"; + overlayElement.style.boxShadow = "2px 2px 4px #999"; + overlayElement.tabIndex = -1; // Div element is focusable + + const listContainerElement: HTMLElement = document.createElement("div"); + listContainerElement.style.height = "100%"; + listContainerElement.style.overflow = "auto"; + + // TODO Calculate max available height to the top and bottom + // TODO Calculate height the dropdown would approx. need + // TODO When that height is too much for bottom use top direction + // TODO When top direction is also not enough decide the direction based on which offers more space + // TODO Restrict overlay height by that available space + + const list: HTMLElement = document.createElement("ul"); + list.className = "table-engine-combobox-dropdown-list"; + list.style.listStyleType = "none"; + list.style.margin = "0"; + list.style.padding = "0"; + + let approximateHeight = 0; + for (const optionId in value.select_options) { + const label = value.select_options[optionId].label; + + const listItem: HTMLElement = document.createElement("li"); + listItem.style.lineHeight = "1.0"; + listItem.style.fontSize = `${listItemFontSize}px`; + listItem.style.padding = `${listItemVerticalPadding}px ${listItemHorizontalPadding}px`; + listItem.style.borderBottom = `${listItemSeparatorSize}px solid #EAEAEA`; + listItem.textContent = label; + + list.appendChild(listItem); + + const mouseDownListener: (MouseEvent) => void = (event: MouseEvent) => { + event.stopPropagation(); // Prevent table selection + }; + const clickListener: (MouseEvent) => void = (event: MouseEvent) => { + event.stopPropagation(); // Stop table selection + console.log("Selected element " + optionId); + }; + listItem.addEventListener("mousedown", mouseDownListener); + listItem.addEventListener("click", clickListener); + + approximateHeight += listItemFontSize + listItemVerticalPadding * 2 + listItemSeparatorSize; + } + + listContainerElement.appendChild(list); + overlayElement.appendChild(listContainerElement); + + const maxDropdownHeight: number = 200; // TODO Configurable + let dropdownHeight = maxDropdownHeight; + if (approximateHeight < maxDropdownHeight) { + dropdownHeight = approximateHeight; + } + + const overlay: IOverlay = { + element: overlayElement, + bounds: { + left: cellBounds.left, + top: cellBounds.top + cellBounds.height, + width: cellBounds.width, + height: dropdownHeight + } + }; + this._engine.getOverlayManager().addOverlay(overlay); + + const scrollListener: (MouseEvent) => void = (event: MouseEvent) => { + event.stopPropagation(); // Stop table scrolling + }; + const blurListener: () => void = () => { + // Remove all event listeners again + overlayElement.removeEventListener("wheel", scrollListener); + overlayElement.removeEventListener("blur", blurListener); + + this._engine.getOverlayManager().removeOverlay(overlay); // Remove overlay + this._engine.requestFocus(); // Re-focus table + }; + overlayElement.addEventListener("wheel", scrollListener); + overlayElement.addEventListener("blur", blurListener); + + setTimeout(() => { + overlayElement.focus(); + }); } private _onMouseMove(event: ICellRendererMouseEvent): void { From 35fdff5c93b12655eac5f83315cfec44a7bce4e0 Mon Sep 17 00:00:00 2001 From: bennyboer Date: Wed, 20 Jul 2022 17:30:22 +0200 Subject: [PATCH 4/5] Selecting options of the combobox renderer --- example/src/styles.scss | 24 ++- src/renderer/canvas/canvas-renderer.ts | 27 +++ .../combobox-cell-renderer-options.ts | 78 ++++++-- .../cell/combobox/combobox-cell-renderer.ts | 170 +++++++++++------- src/renderer/renderer.ts | 22 +++ src/table-engine.ts | 18 ++ src/util/clipboard/clipboard-util.ts | 1 + 7 files changed, 264 insertions(+), 76 deletions(-) diff --git a/example/src/styles.scss b/example/src/styles.scss index f674ade..36d6556 100644 --- a/example/src/styles.scss +++ b/example/src/styles.scss @@ -13,10 +13,26 @@ body { padding: 0.5em 0; } -ul.table-engine-combobox-dropdown-list { - li { - &:hover { - background-color: #F9F9F9; +.table-engine-combobox-dropdown-list { + background: white; + border-radius: 0 0 2px 2px; + box-shadow: 2px 2px 4px #999; + + ul { + list-style-type: none; + margin: 0; + padding: 0; + + li { + line-height: 1.0; + font-size: 12px; + box-sizing: border-box; + padding: 4px 8px; + border-bottom: 1px solid #EAEAEA; + + &:hover { + background-color: #EAEAEA; + } } } } diff --git a/src/renderer/canvas/canvas-renderer.ts b/src/renderer/canvas/canvas-renderer.ts index 89a1984..4f458a7 100644 --- a/src/renderer/canvas/canvas-renderer.ts +++ b/src/renderer/canvas/canvas-renderer.ts @@ -1977,6 +1977,33 @@ export class CanvasRenderer implements ITableEngineRenderer { return this._zoom; } + public getScrollOffset(): IPoint { + return { + x: this._scrollOffset.x, + y: this._scrollOffset.y + }; + } + + public getViewport(): IRectangle { + if (!!this._lastRenderingContext) { + return this._lastRenderingContext.viewPort; + } + + return this._getViewPort(); + } + + public getFixedRowsHeight(): number { + return !!this._lastRenderingContext && !!this._lastRenderingContext.cells.fixedRowCells + ? this._lastRenderingContext.cells.fixedRowCells.viewPortBounds.height + : 0; + } + + public getFixedColumnsWidth(): number { + return !!this._lastRenderingContext && !!this._lastRenderingContext.cells.fixedColumnCells + ? this._lastRenderingContext.cells.fixedColumnCells.viewPortBounds.width + : 0; + } + /** * Scroll to the cell at the given row and column (if not already in the current view). * @param row to scroll to diff --git a/src/renderer/canvas/cell/combobox/combobox-cell-renderer-options.ts b/src/renderer/canvas/cell/combobox/combobox-cell-renderer-options.ts index 6e00dd5..9cfea6d 100644 --- a/src/renderer/canvas/cell/combobox/combobox-cell-renderer-options.ts +++ b/src/renderer/canvas/cell/combobox/combobox-cell-renderer-options.ts @@ -4,7 +4,10 @@ import {Colors} from "../../../../util/colors"; export const DEFAULT_PLACEHOLDER_TEXT: string = "Select..."; export const DEFAULT_LABEL_COLOR: IColor = Colors.BLACK; +export const DEFAULT_LABEL_COLOR_HOVERED: IColor = Colors.CORAL; export const DEFAULT_LABEL_COLOR_DISABLED: IColor = Colors.GRAY; +export const DEFAULT_LABEL_FONT_SIZE: number = 12; +export const DEFAULT_LABEL_FONT_FAMILY: string = "sans-serif"; export const DEFAULT_PLACEHOLDER_COLOR: IColor = Colors.GRAY; export const DEFAULT_PADDING: number = 8; export const DEFAULT_SELECT_ARROW_COLOR: IColor = Colors.GRAY; @@ -14,15 +17,25 @@ export const DEFAULT_SELECT_ARROW_SIZE: number = 10; export const DEFAULT_SELECT_ARROW_THICKNESS: number = 2; export const DEFAULT_SELECT_ARROW_LINE_JOIN: "bevel" | "miter" | "round" = "round"; export const DEFAULT_SELECT_ARROW_LINE_CAP: "butt" | "round" | "square" = "round"; +export const DEFAULT_DROPDOWN_OVERLAY_CLASS_NAME = "table-engine-combobox-dropdown-list"; +export const DEFAULT_DROPDOWN_MAX_HEIGHT = 200; export interface IComboBoxCellRendererOptions { onChanged?: (cell: ICell) => void; editable?: boolean; - labelColor?: IColor; - disabledLabelColor?: IColor; padding?: number; + label?: ILabelOptions; placeholder?: IPlaceholderOptions, - selectArrow?: ISelectArrowOptions + selectArrow?: ISelectArrowOptions, + dropdown?: IDropdownOptions +} + +export interface ILabelOptions { + color?: IColor; + hoveredColor?: IColor; + disabledColor?: IColor; + fontSize?: number; + fontFamily?: string; } export interface IPlaceholderOptions { @@ -40,6 +53,11 @@ export interface ISelectArrowOptions { lineCap?: "butt" | "round" | "square"; } +export interface IDropdownOptions { + overlayClassName?: string; + maxHeight?: number; +} + export const fillOptions = (options?: IComboBoxCellRendererOptions) => { if (!options) { options = {}; @@ -49,20 +67,42 @@ export const fillOptions = (options?: IComboBoxCellRendererOptions) => { options.editable = true; } - if (!options.labelColor) { - options.labelColor = DEFAULT_LABEL_COLOR; - } - - if (!options.disabledLabelColor) { - options.disabledLabelColor = DEFAULT_LABEL_COLOR_DISABLED; - } - if (options.padding === undefined || options.padding === null) { options.padding = DEFAULT_PADDING; } + options.label = fillLabelOptions(options.label); options.placeholder = fillPlaceholderOptions(options.placeholder); options.selectArrow = fillSelectArrowOptions(options.selectArrow); + options.dropdown = fillDropdownOptions(options.dropdown); + + return options; +} + +const fillLabelOptions = (options?: ILabelOptions) => { + if (!options) { + options = {}; + } + + if (!options.color) { + options.color = DEFAULT_LABEL_COLOR; + } + + if (!options.hoveredColor) { + options.hoveredColor = DEFAULT_LABEL_COLOR_HOVERED; + } + + if (!options.disabledColor) { + options.disabledColor = DEFAULT_LABEL_COLOR_DISABLED; + } + + if (options.fontSize === undefined || options.fontSize === null) { + options.fontSize = DEFAULT_LABEL_FONT_SIZE; + } + + if (!options.fontFamily) { + options.fontFamily = DEFAULT_LABEL_FONT_FAMILY; + } return options; } @@ -118,3 +158,19 @@ const fillSelectArrowOptions = (options?: ISelectArrowOptions) => { return options; } + +const fillDropdownOptions = (options?: IDropdownOptions) => { + if (!options) { + options = {}; + } + + if (!options.overlayClassName) { + options.overlayClassName = DEFAULT_DROPDOWN_OVERLAY_CLASS_NAME; + } + + if (options.maxHeight === undefined || options.maxHeight === null) { + options.maxHeight = DEFAULT_DROPDOWN_MAX_HEIGHT; + } + + return options; +} diff --git a/src/renderer/canvas/cell/combobox/combobox-cell-renderer.ts b/src/renderer/canvas/cell/combobox/combobox-cell-renderer.ts index 4d17d5a..a80f50d 100644 --- a/src/renderer/canvas/cell/combobox/combobox-cell-renderer.ts +++ b/src/renderer/canvas/cell/combobox/combobox-cell-renderer.ts @@ -1,7 +1,7 @@ import {ICanvasCellRenderer} from "../canvas-cell-renderer"; import { fillOptions, - IComboBoxCellRendererOptions, + IComboBoxCellRendererOptions, ILabelOptions, IPlaceholderOptions, ISelectArrowOptions } from "./combobox-cell-renderer-options"; @@ -12,10 +12,9 @@ import {ICellRendererEventListener} from "../../../cell/event/cell-renderer-even import {IRectangle} from "../../../../util/rect"; import {ICellRendererMouseEvent} from "../../../cell/event/cell-renderer-mouse-event"; import {IComboBoxCellRendererValue, IComboBoxOption} from "./combobox-cell-renderer-value"; -import {IColor} from "../../../../util/color"; import {Colors} from "../../../../util/colors"; import {IOverlay} from "../../../../overlay/overlay"; -import {IPoint} from "../../../../util/point"; +import {ICellRange} from "../../../../cell/range/cell-range"; export class ComboBoxCellRenderer implements ICanvasCellRenderer { @@ -70,7 +69,12 @@ export class ComboBoxCellRenderer implements ICanvasCellRenderer { } getCopyValue(cell: ICell): string { - return `${ComboBoxCellRenderer._value(cell)}`; + const value = ComboBoxCellRenderer._value(cell); + if (!!value.selected_option_id) { + return value.select_options[value.selected_option_id].label; + } + + return ""; } getEventListener(): ICellRendererEventListener | null { @@ -95,15 +99,15 @@ export class ComboBoxCellRenderer implements ICanvasCellRenderer { const style: IRenderingStyle = this._determineRenderingStyle(value, cache); - ComboBoxCellRenderer._renderLabel(ctx, bounds, value, style); - ComboBoxCellRenderer._renderSelectArrow(ctx, bounds, style); + const selectArrowWidth: number = ComboBoxCellRenderer._renderSelectArrow(ctx, bounds, style); + ComboBoxCellRenderer._renderLabel(ctx, bounds, value, style, selectArrowWidth); } private _determineRenderingStyle(value: IComboBoxCellRendererValue, cache: IViewportCache): IRenderingStyle { let editable: boolean = this._options.editable; let hovered = cache.hovered; - let labelColor: IColor = this._options.labelColor; let padding: number = this._options.padding; + let labelOptions: ILabelOptions = this._options.label; let placeholderOptions: IPlaceholderOptions = this._options.placeholder; let selectArrowOptions: ISelectArrowOptions = this._options.selectArrow; @@ -111,12 +115,12 @@ export class ComboBoxCellRenderer implements ICanvasCellRenderer { if (value.options.editable !== undefined && value.options.editable !== null) { editable = value.options.editable; } - if (!!value.options.labelColor) { - labelColor = value.options.labelColor; - } if (value.options.padding !== undefined && value.options.padding !== null) { padding = value.options.padding; } + if (!!value.options.label) { + labelOptions = value.options.label; + } if (!!value.options.placeholder) { placeholderOptions = value.options.placeholder; } @@ -128,8 +132,8 @@ export class ComboBoxCellRenderer implements ICanvasCellRenderer { return { editable, hovered, - labelColor, padding, + labelOptions, placeholderOptions, selectArrowOptions } @@ -139,7 +143,7 @@ export class ComboBoxCellRenderer implements ICanvasCellRenderer { ctx: CanvasRenderingContext2D, bounds: IRectangle, style: IRenderingStyle - ) { + ): number { const selectArrowPath: ISelectArrowPath = ComboBoxCellRenderer._makeSelectArrowPath(); const selectArrowWidth: number = selectArrowPath.width * style.selectArrowOptions.size; @@ -162,26 +166,54 @@ export class ComboBoxCellRenderer implements ICanvasCellRenderer { ctx.lineJoin = style.selectArrowOptions.lineJoin; ctx.stroke(transformedSelectArrowPath); + + return selectArrowWidth + 2 * style.padding; } private static _renderLabel( ctx: CanvasRenderingContext2D, bounds: IRectangle, value: IComboBoxCellRendererValue, - style: IRenderingStyle + style: IRenderingStyle, + selectArrowWidth: number ) { const labelXOffset = bounds.left + style.padding; const labelYOffset = bounds.top + Math.round(bounds.height / 2); - const showPlaceholder = !value.selected_option_id; - if (showPlaceholder) { - ctx.fillStyle = Colors.toStyleStr(style.placeholderOptions.color); - ctx.fillText(style.placeholderOptions.text, labelXOffset, labelYOffset); - } else { + let labelText = style.placeholderOptions.text; + let color = style.placeholderOptions.color; + + const hasLabelSelected = !!value.selected_option_id; + if (hasLabelSelected) { const selectedOption: IComboBoxOption = value.select_options[value.selected_option_id]; - ctx.fillStyle = Colors.toStyleStr(style.labelColor); - ctx.fillText(selectedOption.label, labelXOffset, labelYOffset); + labelText = selectedOption.label; + color = style.labelOptions.color; + } + + if (style.hovered) { + color = style.labelOptions.hoveredColor; + } + + ctx.font = `${style.labelOptions.fontSize}px ${style.labelOptions.fontFamily}`; + ctx.fillStyle = Colors.toStyleStr(color); + + // Check if label fits into box + const maxLabelWidth: number = bounds.width - style.padding - selectArrowWidth; + const measuredWidth: number = ctx.measureText(labelText).width; + const overflow: boolean = measuredWidth > maxLabelWidth; + if (overflow) { + const clippingRegion = new Path2D(); + clippingRegion.rect(bounds.left, bounds.top, bounds.width - selectArrowWidth, bounds.height); + + ctx.save(); + ctx.clip(clippingRegion); + } + + ctx.fillText(labelText, labelXOffset, labelYOffset); + + if (overflow) { + ctx.restore(); } } @@ -200,7 +232,6 @@ export class ComboBoxCellRenderer implements ICanvasCellRenderer { } private _onClick(event: ICellRendererMouseEvent): void { - const cache: IViewportCache = ComboBoxCellRenderer._cache(event.cell); const value: IComboBoxCellRendererValue = ComboBoxCellRenderer._value(event.cell); let editable: boolean = this._options.editable; @@ -209,82 +240,99 @@ export class ComboBoxCellRenderer implements ICanvasCellRenderer { } if (editable) { - const cellBounds: IRectangle = this._engine.getCellModel().getBounds(event.cell.range); - this._openDropdownOverlay(value, cellBounds); + this._openDropdownOverlay(value, event.cell.range); } } - private _openDropdownOverlay(value: IComboBoxCellRendererValue, cellBounds: IRectangle): void { - const listItemFontSize: number = 12; // TODO Configurable - const listItemHorizontalPadding: number = 8; // TODO Configurable - const listItemVerticalPadding: number = 4; // TODO Configurable - const listItemSeparatorSize: number = 1; // TODO Configurable + private _openDropdownOverlay(value: IComboBoxCellRendererValue, cellRange: ICellRange): void { + const cellBounds: IRectangle = this._engine.getCellModel().getBounds(cellRange); + + // Determine height available to the top and bottom of the given cell range + const viewport = this._engine.getViewport(); + const fixedRowsHeight: number = this._engine.getFixedRowsHeight(); + const availableHeightToTheTop = cellBounds.top - (viewport.top + fixedRowsHeight); + const availableHeightToTheBottom = (viewport.top + viewport.height - cellBounds.height) - cellBounds.top; const overlayElement: HTMLElement = document.createElement("div"); - overlayElement.style.background = "white"; - overlayElement.style.borderRadius = "0 0 2px 2px"; - overlayElement.style.boxShadow = "2px 2px 4px #999"; - overlayElement.tabIndex = -1; // Div element is focusable + overlayElement.className = this._options.dropdown.overlayClassName; + overlayElement.tabIndex = -1; // Div element must be focusable to enable the blur event const listContainerElement: HTMLElement = document.createElement("div"); listContainerElement.style.height = "100%"; listContainerElement.style.overflow = "auto"; - // TODO Calculate max available height to the top and bottom - // TODO Calculate height the dropdown would approx. need - // TODO When that height is too much for bottom use top direction - // TODO When top direction is also not enough decide the direction based on which offers more space - // TODO Restrict overlay height by that available space - const list: HTMLElement = document.createElement("ul"); - list.className = "table-engine-combobox-dropdown-list"; - list.style.listStyleType = "none"; - list.style.margin = "0"; - list.style.padding = "0"; - let approximateHeight = 0; for (const optionId in value.select_options) { const label = value.select_options[optionId].label; const listItem: HTMLElement = document.createElement("li"); - listItem.style.lineHeight = "1.0"; - listItem.style.fontSize = `${listItemFontSize}px`; - listItem.style.padding = `${listItemVerticalPadding}px ${listItemHorizontalPadding}px`; - listItem.style.borderBottom = `${listItemSeparatorSize}px solid #EAEAEA`; listItem.textContent = label; - list.appendChild(listItem); - const mouseDownListener: (MouseEvent) => void = (event: MouseEvent) => { event.stopPropagation(); // Prevent table selection }; const clickListener: (MouseEvent) => void = (event: MouseEvent) => { event.stopPropagation(); // Stop table selection - console.log("Selected element " + optionId); + value.selected_option_id = optionId; + blurListener(); }; listItem.addEventListener("mousedown", mouseDownListener); listItem.addEventListener("click", clickListener); - approximateHeight += listItemFontSize + listItemVerticalPadding * 2 + listItemSeparatorSize; + list.appendChild(listItem); } listContainerElement.appendChild(list); overlayElement.appendChild(listContainerElement); - const maxDropdownHeight: number = 200; // TODO Configurable + // Create render container to render overlay to measure the needed height + const renderContainer: HTMLElement = document.createElement("div"); + renderContainer.style.position = "absolute"; + renderContainer.style.left = "-9999px"; + renderContainer.style.top = "-9999px"; + renderContainer.style.visibility = "hidden"; + renderContainer.appendChild(overlayElement); + document.body.appendChild(renderContainer); + const neededOverlayHeight: number = renderContainer.getBoundingClientRect().height; + document.body.removeChild(renderContainer); + + const maxDropdownHeight: number = this._options.dropdown.maxHeight; let dropdownHeight = maxDropdownHeight; - if (approximateHeight < maxDropdownHeight) { - dropdownHeight = approximateHeight; + if (neededOverlayHeight < maxDropdownHeight) { + dropdownHeight = neededOverlayHeight; + } + + // Check in which direction (top or bottom) to open the combobox dropdown + let openToBottom: boolean = true; + if (availableHeightToTheBottom < dropdownHeight) { + if (availableHeightToTheTop < dropdownHeight) { + openToBottom = availableHeightToTheBottom > availableHeightToTheTop; + + // Cut dropdown height by the available space + if (openToBottom) { + dropdownHeight = availableHeightToTheBottom; + } else { + dropdownHeight = availableHeightToTheTop; + } + } else { + openToBottom = false; + } + } + + let dropdownBounds = { + left: cellBounds.left, + top: cellBounds.top + cellBounds.height, + width: cellBounds.width, + height: dropdownHeight + }; + if (!openToBottom) { + dropdownBounds.top = cellBounds.top - dropdownHeight; } const overlay: IOverlay = { element: overlayElement, - bounds: { - left: cellBounds.left, - top: cellBounds.top + cellBounds.height, - width: cellBounds.width, - height: dropdownHeight - } + bounds: dropdownBounds }; this._engine.getOverlayManager().addOverlay(overlay); @@ -341,8 +389,8 @@ interface ISelectArrowPath { interface IRenderingStyle { editable: boolean; hovered: boolean; - labelColor: IColor; padding: number; + labelOptions: ILabelOptions; placeholderOptions: IPlaceholderOptions; selectArrowOptions: ISelectArrowOptions; } diff --git a/src/renderer/renderer.ts b/src/renderer/renderer.ts index 75572c6..d0080f8 100644 --- a/src/renderer/renderer.ts +++ b/src/renderer/renderer.ts @@ -2,6 +2,8 @@ import {ICellRenderer} from "./cell/cell-renderer"; import {TableEngine} from "../table-engine"; import {IOverlayManager} from "../overlay/overlay-manager"; import {ITableEngineOptions} from "../options"; +import {IPoint} from "../util/point"; +import {IRectangle} from "../util/rect"; /** * Representation of a renderer of the table engine. @@ -50,6 +52,26 @@ export interface ITableEngineRenderer extends IOverlayManager { */ getZoom(): number; + /** + * Get the current scroll offset. + */ + getScrollOffset(): IPoint; + + /** + * Get the current viewport rectangle. + */ + getViewport(): IRectangle; + + /** + * Get the height of the fixed rows or 0. + */ + getFixedRowsHeight(): number; + + /** + * Get the width of the fixed columns or 0. + */ + getFixedColumnsWidth(): number; + /** * Cleanup the renderer when no more needed. */ diff --git a/src/table-engine.ts b/src/table-engine.ts index 85fe518..481f9aa 100644 --- a/src/table-engine.ts +++ b/src/table-engine.ts @@ -11,6 +11,8 @@ import {SelectionModel} from "./selection/model/selection-model"; import {IBorderModel} from "./border/model/border-model.interface"; import {BorderModel} from "./border/model/border-model"; import {IOverlayManager} from "./overlay/overlay-manager"; +import {IPoint} from "./util/point"; +import {IRectangle} from "./util/rect"; /** * Entry point of the table engine library. @@ -143,6 +145,22 @@ export class TableEngine { return this._renderer.getZoom(); } + public getScrollOffset(): IPoint { + return this._renderer.getScrollOffset(); + } + + public getViewport(): IRectangle { + return this._renderer.getViewport(); + } + + public getFixedRowsHeight(): number { + return this._renderer.getFixedRowsHeight(); + } + + public getFixedColumnsWidth(): number { + return this._renderer.getFixedColumnsWidth(); + } + /** * Get the table engines options. */ diff --git a/src/util/clipboard/clipboard-util.ts b/src/util/clipboard/clipboard-util.ts index 54c38cb..51474f3 100644 --- a/src/util/clipboard/clipboard-util.ts +++ b/src/util/clipboard/clipboard-util.ts @@ -18,6 +18,7 @@ export class ClipboardUtil { // Hide the dummy element from the user clipboardDummyElement.style.position = 'absolute'; clipboardDummyElement.style.left = '-9999px'; + clipboardDummyElement.style.userSelect = "text"; // Add to DOM document.body.appendChild(clipboardDummyElement); From 3d3d7d327d80da579f42b329c7586d5d50907278 Mon Sep 17 00:00:00 2001 From: bennyboer Date: Wed, 20 Jul 2022 17:39:13 +0200 Subject: [PATCH 5/5] Calling onChanged callback when combobox value has been selected --- .../cell/combobox/combobox-cell-renderer.ts | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/renderer/canvas/cell/combobox/combobox-cell-renderer.ts b/src/renderer/canvas/cell/combobox/combobox-cell-renderer.ts index a80f50d..d2929b1 100644 --- a/src/renderer/canvas/cell/combobox/combobox-cell-renderer.ts +++ b/src/renderer/canvas/cell/combobox/combobox-cell-renderer.ts @@ -235,17 +235,23 @@ export class ComboBoxCellRenderer implements ICanvasCellRenderer { const value: IComboBoxCellRendererValue = ComboBoxCellRenderer._value(event.cell); let editable: boolean = this._options.editable; - if (!!value.options && value.options.editable !== undefined && value.options.editable !== null) { - editable = value.options.editable; + let onChanged: (cell: ICell) => void = this._options.onChanged; + if (!!value.options) { + if (value.options.editable !== undefined && value.options.editable !== null) { + editable = value.options.editable; + } + if (!!value.options.onChanged) { + onChanged = value.options.onChanged; + } } if (editable) { - this._openDropdownOverlay(value, event.cell.range); + this._openDropdownOverlay(value, event.cell, onChanged); } } - private _openDropdownOverlay(value: IComboBoxCellRendererValue, cellRange: ICellRange): void { - const cellBounds: IRectangle = this._engine.getCellModel().getBounds(cellRange); + private _openDropdownOverlay(value: IComboBoxCellRendererValue, cell: ICell, onChanged: (cell: ICell) => void): void { + const cellBounds: IRectangle = this._engine.getCellModel().getBounds(cell.range); // Determine height available to the top and bottom of the given cell range const viewport = this._engine.getViewport(); @@ -275,6 +281,11 @@ export class ComboBoxCellRenderer implements ICanvasCellRenderer { const clickListener: (MouseEvent) => void = (event: MouseEvent) => { event.stopPropagation(); // Stop table selection value.selected_option_id = optionId; + + if (!!onChanged) { + onChanged(cell); + } + blurListener(); }; listItem.addEventListener("mousedown", mouseDownListener);