Skip to content

Commit

Permalink
feat(plugin): create new Custom Tooltip plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
ghiscoding committed Oct 7, 2021
1 parent ed6bc7c commit 4c8c4f6
Show file tree
Hide file tree
Showing 16 changed files with 294 additions and 14 deletions.
57 changes: 55 additions & 2 deletions examples/webpack-demo-vanilla-bundle/src/examples/example03.ts
Expand Up @@ -91,7 +91,11 @@ export class Example3 {
aggregators: [new Aggregators.Sum('cost')],
aggregateCollapsed: false,
collapsed: false
}
},
customTooltip: {
formatter: this.tooltipTaskFormatter.bind(this),
// usabilityOverride: (args) => !!(args.dataContext?.id % 2) // show it only every second row
},
},
{
id: 'duration', name: 'Duration', field: 'duration', sortable: true, filterable: true,
Expand Down Expand Up @@ -291,6 +295,14 @@ export class Example3 {
excelExportOptions: {
exportWithFormatter: true
},
// Custom Tooltip options can be defined in a Column or Grid Options or a mixed of both (first options found wins)
enableCustomTooltip: true,
customTooltip: {
arrowMarginLeft: '30%',
formatter: this.tooltipFormatter.bind(this),
usabilityOverride: (args) => (args.cell !== 0 && args.cell !== args.grid.getColumns().length - 1), // don't show on first/last columns
// hideArrow: true, // defaults to False
},
registerExternalResources: [this.excelExportService],
enableFiltering: true,
rowSelectionOptions: {
Expand Down Expand Up @@ -435,7 +447,7 @@ export class Example3 {
}
}

groupByFieldName(_fieldName, _index) {
groupByFieldName(/* _fieldName, _index */) {
this.clearGrouping();
if (this.draggableGroupingPlugin && this.draggableGroupingPlugin.setDroppedGroups) {
this.showPreHeader();
Expand Down Expand Up @@ -524,4 +536,45 @@ export class Example3 {
this.sgb?.slickGrid.gotoCell(command.row, command.cell, false);
}
}

tooltipFormatter(row, cell, value, column, dataContext) {
const tooltipTitle = 'Custom Tooltip';
return `<div class="color-sf-primary-dark" style="font-weight: bold">${tooltipTitle}</div>
<div class="tooltip-2cols-row"><div>Id:</div> <div>${dataContext.id}</div></div>
<div class="tooltip-2cols-row"><div>Title:</div> <div>${dataContext.title}</div></div>
<div class="tooltip-2cols-row"><div>Completion:</div> <div>${this.loadCompletionIcons(dataContext.percentComplete)}</div></div>
`;
}

tooltipTaskFormatter(row, cell, value, column, dataContext, grid) {
const tooltipTitle = `Task ${dataContext.id} - Tooltip`;

// use a 2nd Formatter to get the percent completion
const completionBar = Formatters.percentCompleteBarWithText(row, cell, dataContext.percentComplete, column, dataContext, grid);
const out = `<div class="color-se-danger" style="font-weight: bold">${tooltipTitle}</div>
<div class="tooltip-2cols-row"><div>Completion:</div> <div>${completionBar}</div></div>
`;
return out;
}

loadCompletionIcons(percentComplete: number) {
let output = '';
let iconCount = 0;
if (percentComplete > 5 && percentComplete < 25) {
iconCount = 1;
} else if (percentComplete >= 25 && percentComplete < 50) {
iconCount = 2;
} else if (percentComplete >= 50 && percentComplete < 75) {
iconCount = 3;
} else if (percentComplete >= 75 && percentComplete < 100) {
iconCount = 4;
} else if (percentComplete === 100) {
iconCount = 5;
}
for (let i = 0; i < iconCount; i++) {
const icon = iconCount === 5 ? 'color-success' : iconCount >= 3 ? 'color-alt-warning' : 'color-se-secondary-light';
output += `<span class="mdi mdi-check-circle-outline ${icon}"></span>`;
}
return output;
}
}
Expand Up @@ -185,8 +185,8 @@ export class Example14 {
id: 'complexity', name: 'Complexity', field: 'complexity',
resizeCalcWidthRatio: 0.82, // default calc ratio is 1 or 0.95 for field type of string
sortable: true, filterable: true, columnGroup: 'Analysis',
formatter: (_row, _cell, value) => this.complexityLevelList[value].label,
exportCustomFormatter: (_row, _cell, value) => this.complexityLevelList[value].label,
formatter: (_row, _cell, value) => this.complexityLevelList[value]?.label,
exportCustomFormatter: (_row, _cell, value) => this.complexityLevelList[value]?.label,
filter: {
model: Filters.multipleSelect,
collection: this.complexityLevelList
Expand Down
4 changes: 0 additions & 4 deletions examples/webpack-demo-vanilla-bundle/src/main.ts
Expand Up @@ -5,10 +5,6 @@ import './styles.scss';

// import all other 3rd party libs required by Slickgrid-Universal
// also only import jQueryUI necessary widget (note autocomplete & slider are imported in their respective editors/filters)
import 'jquery';
import 'jquery-ui/ui/widgets/draggable';
import 'jquery-ui/ui/widgets/droppable';
import 'jquery-ui/ui/widgets/sortable';
import { Renderer } from './renderer';
import * as SlickerModule from '@slickgrid-universal/vanilla-bundle';
import { App } from './app';
Expand Down
1 change: 1 addition & 0 deletions packages/common/src/enums/extensionName.enum.ts
Expand Up @@ -6,6 +6,7 @@ export enum ExtensionName {
checkboxSelector = 'checkboxSelector',
columnPicker = 'columnPicker',
contextMenu = 'contextMenu',
customTooltip = 'customTooltip',
draggableGrouping = 'draggableGrouping',
groupItemMetaProvider = 'groupItemMetaProvider',
gridMenu = 'gridMenu',
Expand Down
1 change: 1 addition & 0 deletions packages/common/src/extensions/index.ts
Expand Up @@ -13,3 +13,4 @@ export * from './headerMenuExtension';
export * from './rowDetailViewExtension';
export * from './rowMoveManagerExtension';
export * from './rowSelectionExtension';
export * from './slickCustomTooltipExtension';
108 changes: 108 additions & 0 deletions packages/common/src/extensions/slickCustomTooltipExtension.ts
@@ -0,0 +1,108 @@
import { CustomTooltipOption, GridOption, SlickDataView, SlickEventData, SlickEventHandler, SlickGrid, SlickNamespace } from '../interfaces/index';
import { getHtmlElementOffset, sanitizeTextByAvailableSanitizer } from '../services/utilities';
import { SharedService } from '../services/shared.service';

// using external SlickGrid JS libraries
declare const Slick: SlickNamespace;

export class SlickCustomTooltip {
protected _addonOptions?: CustomTooltipOption;
protected _defaultOptions = {
className: 'slick-custom-tooltip',
offsetLeft: 0,
offsetTop: 5,
hideArrow: false,
} as CustomTooltipOption;
protected _grid!: SlickGrid;
protected _eventHandler: SlickEventHandler;

constructor(protected readonly sharedService: SharedService) {
this._eventHandler = new Slick.EventHandler();
}

get addonOptions(): CustomTooltipOption | undefined {
return this._addonOptions;
}

get className(): string {
return this._addonOptions?.className ?? 'slick-custom-tooltip';
}
get dataView(): SlickDataView {
return this._grid.getData<SlickDataView>() || {};
}

/** Getter for the Grid Options pulled through the Grid Object */
get gridOptions(): GridOption {
return this._grid.getOptions() || {};
}

/** Getter for the grid uid */
get gridUid(): string {
return this._grid.getUID() || '';
}
get gridUidSelector(): string {
return this.gridUid ? `.${this.gridUid}` : '';
}

init(grid: SlickGrid) {
this._grid = grid;
this._eventHandler
.subscribe(grid.onMouseEnter, this.handleOnMouseEnter.bind(this) as EventListener)
.subscribe(grid.onMouseLeave, this.handleOnMouseLeave.bind(this) as EventListener);
}

dispose() {
this._eventHandler.unsubscribeAll();
}

handleOnMouseEnter(e: SlickEventData) {
if (this._grid && e) {
const cell = this._grid.getCellFromEvent(e);
if (cell) {
const item = this.dataView.getItem(cell.row);
const columnDef = this._grid.getColumns()[cell.cell];
if (item && columnDef) {
this._addonOptions = { ...this._defaultOptions, ...(this.sharedService?.gridOptions?.customTooltip), ...(columnDef?.customTooltip) };

let showTooltip = true;
if (typeof this._addonOptions?.usabilityOverride === 'function') {
showTooltip = this._addonOptions.usabilityOverride({ cell: cell.cell, row: cell.row, dataContext: item, column: columnDef, grid: this._grid });
}

if (showTooltip && typeof this._addonOptions?.formatter === 'function') {
const itemValue = item.hasOwnProperty(columnDef.field) ? item[columnDef.field] : null;
const value = sanitizeTextByAvailableSanitizer(this.gridOptions, itemValue);
const tooltipText = this._addonOptions.formatter(cell.row, cell.cell, value, columnDef, item, this._grid);

// create the tooltip DOM element with the text returned by the Formatter
const tooltipElm = document.createElement('div');
tooltipElm.className = `${this.className} ${this.gridUid}`;
tooltipElm.innerHTML = typeof tooltipText === 'object' ? tooltipText.text : tooltipText;
document.body.appendChild(tooltipElm);

// reposition the tooltip on top of the cell that triggered the mouse over event
const cellPosition = getHtmlElementOffset(this._grid.getCellNode(cell.row, cell.cell));
tooltipElm.style.left = `${cellPosition.left}px`;
tooltipElm.style.top = `${cellPosition.top - tooltipElm.clientHeight - (this._addonOptions?.offsetTop ?? 0)}px`;

// user could optionally hide the tooltip arrow (we can simply update the CSS variables, that's the only way we have to update CSS pseudo)
const root = document.documentElement;
if (this._addonOptions?.hideArrow) {
root.style.setProperty('--slick-tooltip-arrow-border-left', '0');
root.style.setProperty('--slick-tooltip-arrow-border-right', '0');
}
if (this._addonOptions?.arrowMarginLeft) {
const marginLeft = typeof this._addonOptions.arrowMarginLeft === 'string' ? this._addonOptions.arrowMarginLeft : `${this._addonOptions.arrowMarginLeft}px`;
root.style.setProperty('--slick-tooltip-arrow-margin-left', marginLeft);
}
}
}
}
}
}

handleOnMouseLeave() {
const prevTooltip = document.body.querySelector(`.${this.className}${this.gridUidSelector}`);
prevTooltip?.remove();
}
}
8 changes: 8 additions & 0 deletions packages/common/src/interfaces/column.interface.ts
Expand Up @@ -3,6 +3,7 @@ import {
CellMenu,
ColumnEditor,
ColumnFilter,
CustomTooltipOption,
EditorValidator,
Formatter,
Grouping,
Expand Down Expand Up @@ -53,6 +54,13 @@ export interface Column<T = any> {
/** CSS class to add to the column cell */
cssClass?: string;

/**
* Custom Tooltip Options, you must first enable `enableCustomTooltip: true`.
* The tooltip could defined in any of the Column Definition or in the Grid Options,
* it will first try to find it in the Column that the user is hovering over or else (when not found) go and try to find it in the Grid Options
*/
customTooltip?: CustomTooltipOption;

/** Data key, for example this could be used as a property key for complex object comparison (e.g. dataKey: 'id') */
dataKey?: string;

Expand Down
35 changes: 35 additions & 0 deletions packages/common/src/interfaces/customTooltipOption.interface.ts
@@ -0,0 +1,35 @@
import { Column, Formatter, SlickGrid } from './index';

export interface CustomTooltipOption {
/**
* defaults to 25(px), left margin to display the arrow.
* when a number is provided it will assume the value is in pixel,
* or else we could also a string for example "50%" would show the arrow in the center.
*/
arrowMarginLeft?: number | string;

/** defaults to False, should we hide the tooltip pointer arrow? */
hideArrow?: boolean;

/** defaults to "slick-custom-tooltip" */
className?: string;

/** Formatter to execute for display the data that will show */
formatter: Formatter;

/** defaults to 0, optional left offset, it must be a positive/negative number (in pixel) that will be added to the offset position calculation of the tooltip container. */
offsetLeft?: number;

/** defaults to 0, optional top offset, it must be a positive/negative number (in pixel) that will be added to the offset position calculation of the tooltip container. */
offsetTop?: number;

// --
// callback functions
// -------------------

// --
// Methods

/** Callback method that user can override the default behavior of showing the tooltip. If it returns False, then the tooltip won't show */
usabilityOverride?: (args: { cell: number; row: number; column: Column; dataContext: any; grid: SlickGrid; }) => boolean;
}
17 changes: 14 additions & 3 deletions packages/common/src/interfaces/gridOption.interface.ts
Expand Up @@ -9,6 +9,7 @@ import {
CompositeEditorOpenDetailOption,
ContextMenu,
CustomFooterOption,
CustomTooltipOption,
DataViewOption,
DraggableGrouping,
EditCommand,
Expand Down Expand Up @@ -124,6 +125,9 @@ export interface GridOption {
/** Checkbox Select Plugin options (columnId, cssClass, toolTip, width) */
checkboxSelector?: CheckboxSelectorOption;

/** A callback function that will be used to define row spanning accross multiple columns */
colspanCallback?: (item: any) => ItemMetadata;

/** Defaults to " - ", separator between the column group label and the column label. */
columnGroupSeparator?: string;

Expand All @@ -139,15 +143,19 @@ export interface GridOption {
/** Defaults to false, which leads to create the footer row of the grid */
createFooterRow?: boolean;

/** A callback function that will be used to define row spanning accross multiple columns */
colspanCallback?: (item: any) => ItemMetadata;

/** Default to false, which leads to create an extra pre-header panel (on top of column header) for column grouping purposes */
createPreHeaderPanel?: boolean;

/** Custom Footer Options */
customFooterOptions?: CustomFooterOption;

/**
* Custom Tooltip Options, you must first enable `enableCustomTooltip: true`.
* The tooltip could defined in any of the Column Definition or in the Grid Options,
* it will first try to find it in the Column that the user is hovering over or else (when not found) go and try to find it in the Grid Options
*/
customTooltip?: CustomTooltipOption;

/** Data item column value extractor (getter) that can be used by the Excel like copy buffer plugin */
dataItemColumnValueExtractor?: (item: any, columnDef: Column) => any;

Expand Down Expand Up @@ -275,6 +283,9 @@ export interface GridOption {
/** Do we want to enable Context Menu? (mouse right+click) */
enableContextMenu?: boolean;

/** Do we want to enable Custom Tooltip feature? */
enableCustomTooltip?: boolean;

/**
* Defaults to false, do we want to make a deep copy of the dataset before loading it into the grid?
* Useful with Salesforce to avoid proxy object error when trying to update a property of an item object by reference (which SlickGrid does a lot)
Expand Down
1 change: 1 addition & 0 deletions packages/common/src/interfaces/index.ts
Expand Up @@ -35,6 +35,7 @@ export * from './currentPinning.interface';
export * from './currentRowSelection.interface';
export * from './currentSorter.interface';
export * from './customFooterOption.interface';
export * from './customTooltipOption.interface';
export * from './dataViewOption.interface';
export * from './domEvent.interface';
export * from './draggableGrouping.interface';
Expand Down
8 changes: 8 additions & 0 deletions packages/common/src/services/extension.service.ts
Expand Up @@ -20,6 +20,7 @@ import {
RowDetailViewExtension,
RowMoveManagerExtension,
RowSelectionExtension,
SlickCustomTooltip,
} from '../extensions/index';
import { SharedService } from './shared.service';
import { TranslaterService } from './translater.service';
Expand Down Expand Up @@ -126,6 +127,13 @@ export class ExtensionService {
this.translateItems(this.sharedService.allColumns, 'nameKey', 'name');
}

// Custom Tooltip Plugin
if (this.sharedService.gridOptions.enableCustomTooltip) {
const tooltipPlugin = new SlickCustomTooltip(this.sharedService);
tooltipPlugin.init(this.sharedService.slickGrid);
this._extensionList[ExtensionName.customTooltip] = { name: ExtensionName.customTooltip, class: tooltipPlugin, instance: tooltipPlugin };
}

// Auto Tooltip Plugin
if (this.sharedService.gridOptions.enableAutoTooltip && this.autoTooltipExtension && this.autoTooltipExtension.register) {
const instance = this.autoTooltipExtension.register();
Expand Down

0 comments on commit 4c8c4f6

Please sign in to comment.