@slickgrid-universal/custom-tooltip-plugin 5.0.0-beta.0 → 5.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/slickCustomTooltip.ts +543 -543
|
@@ -1,543 +1,543 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
CancellablePromiseWrapper,
|
|
3
|
-
Column,
|
|
4
|
-
ContainerService,
|
|
5
|
-
CustomDataView,
|
|
6
|
-
CustomTooltipOption,
|
|
7
|
-
Formatter,
|
|
8
|
-
FormatterResultWithHtml,
|
|
9
|
-
FormatterResultWithText,
|
|
10
|
-
GridOption,
|
|
11
|
-
Observable,
|
|
12
|
-
RxJsFacade,
|
|
13
|
-
SharedService,
|
|
14
|
-
SlickEventData,
|
|
15
|
-
SlickGrid,
|
|
16
|
-
Subscription,
|
|
17
|
-
} from '@slickgrid-universal/common';
|
|
18
|
-
import {
|
|
19
|
-
calculateAvailableSpace,
|
|
20
|
-
CancelledException,
|
|
21
|
-
cancellablePromise,
|
|
22
|
-
createDomElement,
|
|
23
|
-
findFirstAttribute,
|
|
24
|
-
getOffset,
|
|
25
|
-
SlickEventHandler,
|
|
26
|
-
} from '@slickgrid-universal/common';
|
|
27
|
-
import { classNameToList, isPrimitiveOrHTML } from '@slickgrid-universal/utils';
|
|
28
|
-
|
|
29
|
-
type CellType = 'slick-cell' | 'slick-header-column' | 'slick-headerrow-column';
|
|
30
|
-
|
|
31
|
-
const CLOSEST_TOOLTIP_FILLED_ATTR = ['title', 'data-slick-tooltip'];
|
|
32
|
-
const SELECTOR_CLOSEST_TOOLTIP_ATTR = '[title], [data-slick-tooltip]';
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* A plugin to add Custom Tooltip when hovering a cell, it subscribes to the cell "onMouseEnter" and "onMouseLeave" events.
|
|
36
|
-
* The "customTooltip" is defined in the Column Definition OR Grid Options (the first found will have priority over the second)
|
|
37
|
-
* To specify a tooltip when hovering a cell, extend the column definition like so:
|
|
38
|
-
*
|
|
39
|
-
* Available plugin options (same options are available in both column definition and/or grid options)
|
|
40
|
-
* Example 1 - via Column Definition
|
|
41
|
-
* this.columnDefinitions = [
|
|
42
|
-
* {
|
|
43
|
-
* id: "action", name: "Action", field: "action", formatter: fakeButtonFormatter,
|
|
44
|
-
* customTooltip: {
|
|
45
|
-
* formatter: tooltipTaskFormatter,
|
|
46
|
-
* usabilityOverride: (args) => !!(args.dataContext.id % 2) // show it only every second row
|
|
47
|
-
* }
|
|
48
|
-
* }
|
|
49
|
-
* ];
|
|
50
|
-
*
|
|
51
|
-
* OR Example 2 - via Grid Options (for all columns), NOTE: the column definition tooltip options will win over the options defined in the grid options
|
|
52
|
-
* this.gridOptions = {
|
|
53
|
-
* enableCellNavigation: true,
|
|
54
|
-
* customTooltip: {
|
|
55
|
-
* },
|
|
56
|
-
* };
|
|
57
|
-
*/
|
|
58
|
-
|
|
59
|
-
// add a default CSS class name that is used and required by SlickGrid Theme
|
|
60
|
-
const DEFAULT_CLASS_NAME = 'slick-custom-tooltip';
|
|
61
|
-
|
|
62
|
-
export class SlickCustomTooltip {
|
|
63
|
-
name: 'CustomTooltip' = 'CustomTooltip' as const;
|
|
64
|
-
|
|
65
|
-
protected _addonOptions?: CustomTooltipOption;
|
|
66
|
-
protected _cellAddonOptions?: CustomTooltipOption;
|
|
67
|
-
protected _cellNodeElm?: HTMLElement;
|
|
68
|
-
protected _cellType: CellType = 'slick-cell';
|
|
69
|
-
protected _cancellablePromise?: CancellablePromiseWrapper;
|
|
70
|
-
protected _observable$?: Subscription;
|
|
71
|
-
protected _rxjs?: RxJsFacade | null = null;
|
|
72
|
-
protected _sharedService?: SharedService | null = null;
|
|
73
|
-
protected _tooltipBodyElm?: HTMLDivElement;
|
|
74
|
-
protected _tooltipElm?: HTMLDivElement;
|
|
75
|
-
protected _mousePosition: { x: number; y: number; } = { x: 0, y: 0 };
|
|
76
|
-
protected _mouseTarget?: HTMLElement | null;
|
|
77
|
-
protected _hasMultipleTooltips = false;
|
|
78
|
-
protected _defaultOptions = {
|
|
79
|
-
bodyClassName: 'tooltip-body',
|
|
80
|
-
className: '',
|
|
81
|
-
offsetArrow: 3, // same as `$slick-tooltip-arrow-side-margin` CSS/SASS variable
|
|
82
|
-
offsetLeft: 0,
|
|
83
|
-
offsetRight: 0,
|
|
84
|
-
offsetTopBottom: 4,
|
|
85
|
-
hideArrow: false,
|
|
86
|
-
regularTooltipWhiteSpace: 'pre-line',
|
|
87
|
-
whiteSpace: 'normal',
|
|
88
|
-
} as CustomTooltipOption;
|
|
89
|
-
protected _grid!: SlickGrid;
|
|
90
|
-
protected _eventHandler: SlickEventHandler;
|
|
91
|
-
|
|
92
|
-
constructor() {
|
|
93
|
-
this._eventHandler = new SlickEventHandler();
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
get addonOptions(): CustomTooltipOption | undefined {
|
|
97
|
-
return this._addonOptions;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
get cancellablePromise() {
|
|
101
|
-
return this._cancellablePromise;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
get cellAddonOptions(): CustomTooltipOption | undefined {
|
|
105
|
-
return this._cellAddonOptions;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
get bodyClassName(): string {
|
|
109
|
-
return this._cellAddonOptions?.bodyClassName ?? 'tooltip-body';
|
|
110
|
-
}
|
|
111
|
-
get className(): string {
|
|
112
|
-
// we'll always add our default class name for the CSS style to display as intended
|
|
113
|
-
// and then append any custom CSS class to default when provided
|
|
114
|
-
let className = DEFAULT_CLASS_NAME;
|
|
115
|
-
if (this._addonOptions?.className) {
|
|
116
|
-
className += ` ${this._addonOptions.className}`;
|
|
117
|
-
}
|
|
118
|
-
return className;
|
|
119
|
-
}
|
|
120
|
-
get dataView(): CustomDataView {
|
|
121
|
-
return this._grid.getData<CustomDataView>() || {};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/** Getter for the Grid Options pulled through the Grid Object */
|
|
125
|
-
get gridOptions(): GridOption {
|
|
126
|
-
return this._grid?.getOptions() || {} as GridOption;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/** Getter for the grid uid */
|
|
130
|
-
get gridUid(): string {
|
|
131
|
-
return this._grid?.getUID() || '';
|
|
132
|
-
}
|
|
133
|
-
get gridUidSelector(): string {
|
|
134
|
-
return this.gridUid ? `.${this.gridUid}` : '';
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
get tooltipElm(): HTMLDivElement | undefined {
|
|
138
|
-
return this._tooltipElm;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
addRxJsResource(rxjs: RxJsFacade) {
|
|
142
|
-
this._rxjs = rxjs;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
init(grid: SlickGrid, containerService: ContainerService) {
|
|
146
|
-
this._grid = grid;
|
|
147
|
-
this._rxjs = containerService.get<RxJsFacade>('RxJsFacade');
|
|
148
|
-
this._sharedService = containerService.get<SharedService>('SharedService');
|
|
149
|
-
this._addonOptions = { ...this._defaultOptions, ...(this._sharedService?.gridOptions?.customTooltip) } as CustomTooltipOption;
|
|
150
|
-
this._eventHandler
|
|
151
|
-
.subscribe(grid.onMouseEnter, this.handleOnMouseOver.bind(this))
|
|
152
|
-
.subscribe(grid.onHeaderMouseOver, (e, args) => this.handleOnHeaderMouseOverByType(e, args, 'slick-header-column'))
|
|
153
|
-
.subscribe(grid.onHeaderRowMouseOver, (e, args) => this.handleOnHeaderMouseOverByType(e, args, 'slick-headerrow-column'))
|
|
154
|
-
.subscribe(grid.onMouseLeave, this.hideTooltip.bind(this))
|
|
155
|
-
.subscribe(grid.onHeaderMouseOut, this.hideTooltip.bind(this))
|
|
156
|
-
.subscribe(grid.onHeaderRowMouseOut, this.hideTooltip.bind(this));
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
dispose() {
|
|
160
|
-
// hide (remove) any tooltip and unsubscribe from all events
|
|
161
|
-
this.hideTooltip();
|
|
162
|
-
this._cancellablePromise = undefined;
|
|
163
|
-
this._eventHandler.unsubscribeAll();
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* hide (remove) tooltip from the DOM, it will also remove it from the DOM and also cancel any pending requests (as mentioned below).
|
|
168
|
-
* When using async process, it will also cancel any opened Promise/Observable that might still be pending.
|
|
169
|
-
*/
|
|
170
|
-
hideTooltip() {
|
|
171
|
-
this._cancellablePromise?.cancel();
|
|
172
|
-
this._observable$?.unsubscribe();
|
|
173
|
-
const cssClasses = classNameToList(this.className).join('.');
|
|
174
|
-
const prevTooltip = document.body.querySelector(`.${cssClasses}${this.gridUidSelector}`);
|
|
175
|
-
prevTooltip?.remove();
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
getOptions(): CustomTooltipOption | undefined {
|
|
179
|
-
return this._addonOptions;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
setOptions(newOptions: CustomTooltipOption) {
|
|
183
|
-
this._addonOptions = { ...this._addonOptions, ...newOptions } as CustomTooltipOption;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// --
|
|
187
|
-
// protected functions
|
|
188
|
-
// ---------------------
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Async process callback will hide any prior tooltip & then merge the new result with the item `dataContext` under a `__params` property
|
|
192
|
-
* (unless a new prop name is provided) to provice as dataContext object to the asyncPostFormatter.
|
|
193
|
-
*/
|
|
194
|
-
protected asyncProcessCallback(asyncResult: any, cell: { row: number, cell: number; }, value: any, columnDef: Column, dataContext: any) {
|
|
195
|
-
this.hideTooltip();
|
|
196
|
-
const itemWithAsyncData = { ...dataContext, [this.addonOptions?.asyncParamsPropName ?? '__params']: asyncResult };
|
|
197
|
-
if (this._cellAddonOptions?.useRegularTooltip) {
|
|
198
|
-
this.renderRegularTooltip(this._cellAddonOptions!.asyncPostFormatter, cell, value, columnDef, itemWithAsyncData);
|
|
199
|
-
} else {
|
|
200
|
-
this.renderTooltipFormatter(this._cellAddonOptions!.asyncPostFormatter, cell, value, columnDef, itemWithAsyncData);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/** depending on the selector type, execute the necessary handler code */
|
|
205
|
-
protected handleOnHeaderMouseOverByType(event: SlickEventData, args: any, selector: CellType) {
|
|
206
|
-
this._cellType = selector;
|
|
207
|
-
this._mousePosition = { x: event.clientX || 0, y: event.clientY || 0 };
|
|
208
|
-
this._mouseTarget = document.elementFromPoint(event.clientX || 0, event.clientY || 0)?.closest(SELECTOR_CLOSEST_TOOLTIP_ATTR);
|
|
209
|
-
|
|
210
|
-
// before doing anything, let's remove any previous tooltip before
|
|
211
|
-
// and cancel any opened Promise/Observable when using async
|
|
212
|
-
this.hideTooltip();
|
|
213
|
-
|
|
214
|
-
const cell = {
|
|
215
|
-
row: -1, // negative row to avoid pulling any dataContext while rendering
|
|
216
|
-
cell: this._grid.getColumns().findIndex((col) => (args?.column?.id ?? '') === col.id)
|
|
217
|
-
};
|
|
218
|
-
const columnDef = args.column;
|
|
219
|
-
const item = {};
|
|
220
|
-
const isHeaderRowType = selector === 'slick-headerrow-column';
|
|
221
|
-
|
|
222
|
-
// run the override function (when defined), if the result is false it won't go further
|
|
223
|
-
args = args || {};
|
|
224
|
-
args.cell = cell.cell;
|
|
225
|
-
args.row = cell.row;
|
|
226
|
-
args.columnDef = columnDef;
|
|
227
|
-
args.dataContext = item;
|
|
228
|
-
args.grid = this._grid;
|
|
229
|
-
args.type = isHeaderRowType ? 'header-row' : 'header';
|
|
230
|
-
this._cellAddonOptions = { ...this._addonOptions, ...(columnDef?.customTooltip) } as CustomTooltipOption;
|
|
231
|
-
if (columnDef?.disableTooltip || (typeof this._cellAddonOptions?.usabilityOverride === 'function' && !this._cellAddonOptions.usabilityOverride(args))) {
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
if (columnDef && event.target) {
|
|
236
|
-
this._cellNodeElm = (event.target as HTMLDivElement).closest(`.${selector}`) as HTMLDivElement;
|
|
237
|
-
const formatter = isHeaderRowType ? this._cellAddonOptions.headerRowFormatter : this._cellAddonOptions.headerFormatter;
|
|
238
|
-
|
|
239
|
-
if (this._cellAddonOptions?.useRegularTooltip || !formatter) {
|
|
240
|
-
const formatterOrText = !isHeaderRowType ? columnDef.name : this._cellAddonOptions?.useRegularTooltip ? null : formatter;
|
|
241
|
-
this.renderRegularTooltip(formatterOrText, cell, null, columnDef, item);
|
|
242
|
-
} else if (this._cellNodeElm && typeof formatter === 'function') {
|
|
243
|
-
this.renderTooltipFormatter(formatter, cell, null, columnDef, item);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
protected async handleOnMouseOver(event: SlickEventData) {
|
|
249
|
-
this._cellType = 'slick-cell';
|
|
250
|
-
this._mousePosition = { x: event.clientX || 0, y: event.clientY || 0 };
|
|
251
|
-
this._mouseTarget = document.elementFromPoint(event.clientX || 0, event.clientY || 0)?.closest(SELECTOR_CLOSEST_TOOLTIP_ATTR);
|
|
252
|
-
|
|
253
|
-
// before doing anything, let's remove any previous tooltip before
|
|
254
|
-
// and cancel any opened Promise/Observable when using async
|
|
255
|
-
this.hideTooltip();
|
|
256
|
-
|
|
257
|
-
if (event && this._grid) {
|
|
258
|
-
// get cell only when it's possible (ie, Composite Editor will not be able to get cell and so it will never show any tooltip)
|
|
259
|
-
const targetClassName = event?.target?.closest('.slick-cell')?.className;
|
|
260
|
-
const cell = (targetClassName && /l\d+/.exec(targetClassName || '')) ? this._grid.getCellFromEvent(event) : null;
|
|
261
|
-
|
|
262
|
-
if (cell) {
|
|
263
|
-
const item = this.dataView ? this.dataView.getItem(cell.row) : this._grid.getDataItem(cell.row);
|
|
264
|
-
const columnDef = this._grid.getColumns()[cell.cell];
|
|
265
|
-
this._cellNodeElm = this._grid.getCellNode(cell.row, cell.cell) as HTMLDivElement;
|
|
266
|
-
|
|
267
|
-
if (item && columnDef) {
|
|
268
|
-
this._cellAddonOptions = { ...this._addonOptions, ...(columnDef?.customTooltip) } as CustomTooltipOption;
|
|
269
|
-
|
|
270
|
-
if (columnDef?.disableTooltip || (typeof this._cellAddonOptions?.usabilityOverride === 'function' && !this._cellAddonOptions.usabilityOverride({ cell: cell.cell, row: cell.row, dataContext: item, column: columnDef, grid: this._grid, type: 'cell' }))) {
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
const value = item.hasOwnProperty(columnDef.field) ? item[columnDef.field] : null;
|
|
275
|
-
|
|
276
|
-
// when cell is currently lock for editing, we'll force a tooltip title search
|
|
277
|
-
const cellValue = this._grid.getEditorLock().isActive() ? null : value;
|
|
278
|
-
|
|
279
|
-
// when there aren't any formatter OR when user specifically want to use a regular tooltip (via "title" attribute)
|
|
280
|
-
if ((this._cellAddonOptions.useRegularTooltip && !this._cellAddonOptions?.asyncProcess) || !this._cellAddonOptions?.formatter) {
|
|
281
|
-
this.renderRegularTooltip(columnDef.formatter, cell, cellValue, columnDef, item);
|
|
282
|
-
} else {
|
|
283
|
-
// when we aren't using regular tooltip and we do have a tooltip formatter, let's render it
|
|
284
|
-
if (typeof this._cellAddonOptions?.formatter === 'function') {
|
|
285
|
-
this.renderTooltipFormatter(this._cellAddonOptions.formatter, cell, cellValue, columnDef, item);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// when tooltip is an Async (delayed, e.g. with a backend API call)
|
|
289
|
-
if (typeof this._cellAddonOptions?.asyncProcess === 'function') {
|
|
290
|
-
const asyncProcess = this._cellAddonOptions.asyncProcess(cell.row, cell.cell, value, columnDef, item, this._grid);
|
|
291
|
-
if (!this._cellAddonOptions.asyncPostFormatter) {
|
|
292
|
-
console.error(`[Slickgrid-Universal] when using "asyncProcess" with Custom Tooltip, you must also provide an "asyncPostFormatter" formatter.`);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
if (asyncProcess instanceof Promise) {
|
|
296
|
-
// create a new cancellable promise which will resolve, unless it's cancelled, with the udpated `dataContext` object that includes the `__params`
|
|
297
|
-
this._cancellablePromise = cancellablePromise(asyncProcess);
|
|
298
|
-
this._cancellablePromise.promise
|
|
299
|
-
.then((asyncResult: any) => this.asyncProcessCallback(asyncResult, cell, value, columnDef, item))
|
|
300
|
-
.catch((error: Error) => {
|
|
301
|
-
// we will throw back any errors, unless it's a cancelled promise which in that case will be disregarded (thrown by the promise wrapper cancel() call)
|
|
302
|
-
if (!(error instanceof CancelledException)) {
|
|
303
|
-
console.error(error);
|
|
304
|
-
}
|
|
305
|
-
});
|
|
306
|
-
} else if (this._rxjs?.isObservable(asyncProcess)) {
|
|
307
|
-
const rxjs = this._rxjs as RxJsFacade;
|
|
308
|
-
this._observable$ = (asyncProcess as unknown as Observable<any>)
|
|
309
|
-
.pipe(
|
|
310
|
-
// use `switchMap` so that it cancels any previous subscription, it must return an observable so we can use `of` for that, and then finally we can subscribe to the new observable
|
|
311
|
-
rxjs.switchMap((asyncResult) => rxjs.of(asyncResult))
|
|
312
|
-
).subscribe(
|
|
313
|
-
(asyncResult: any) => this.asyncProcessCallback(asyncResult, cell, value, columnDef, item),
|
|
314
|
-
(error: any) => console.error(error)
|
|
315
|
-
);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
/**
|
|
325
|
-
* Parse the Custom Formatter (when provided) or return directly the text when it is already a string.
|
|
326
|
-
* We will also sanitize the text in both cases before returning it so that it can be used safely.
|
|
327
|
-
*/
|
|
328
|
-
protected parseFormatterAndSanitize(formatterOrText: Formatter | string | undefined, cell: { row: number; cell: number; }, value: any, columnDef: Column, item: unknown): string {
|
|
329
|
-
if (typeof formatterOrText === 'function') {
|
|
330
|
-
const tooltipResult = formatterOrText(cell.row, cell.cell, value, columnDef, item, this._grid);
|
|
331
|
-
const formatterText = isPrimitiveOrHTML(tooltipResult) ? tooltipResult : (tooltipResult as FormatterResultWithHtml).html || (tooltipResult as FormatterResultWithText).text;
|
|
332
|
-
return this._grid.sanitizeHtmlString((formatterText instanceof HTMLElement ? formatterText.textContent : formatterText as string) || '');
|
|
333
|
-
} else if (typeof formatterOrText === 'string') {
|
|
334
|
-
return this._grid.sanitizeHtmlString(formatterOrText);
|
|
335
|
-
}
|
|
336
|
-
return '';
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
/**
|
|
340
|
-
* Parse the cell formatter and assume it might be html
|
|
341
|
-
* then create a temporary html element to easily retrieve the first [title=""] attribute text content
|
|
342
|
-
* also clear the "title" attribute from the grid div text content so that it won't show also as a 2nd browser tooltip
|
|
343
|
-
*/
|
|
344
|
-
protected renderRegularTooltip(formatterOrText: Formatter | string | undefined, cell: { row: number; cell: number; }, value: any, columnDef: Column, item: any) {
|
|
345
|
-
const tmpDiv = document.createElement('div');
|
|
346
|
-
this._grid.applyHtmlCode(tmpDiv, this.parseFormatterAndSanitize(formatterOrText, cell, value, columnDef, item));
|
|
347
|
-
this._hasMultipleTooltips = (this._cellNodeElm?.querySelectorAll(SELECTOR_CLOSEST_TOOLTIP_ATTR).length || 0) > 1;
|
|
348
|
-
|
|
349
|
-
let tmpTitleElm: HTMLElement | null | undefined;
|
|
350
|
-
const cellElm = (this._cellAddonOptions?.useRegularTooltipFromCellTextOnly || !this._mouseTarget)
|
|
351
|
-
? this._cellNodeElm as HTMLElement
|
|
352
|
-
: this._mouseTarget;
|
|
353
|
-
|
|
354
|
-
let tooltipText = columnDef?.toolTip ?? '';
|
|
355
|
-
if (!tooltipText) {
|
|
356
|
-
if (this._cellType === 'slick-cell' && cellElm && (cellElm.clientWidth < cellElm.scrollWidth) && !this._cellAddonOptions?.useRegularTooltipFromFormatterOnly) {
|
|
357
|
-
tooltipText = cellElm.textContent?.trim() ?? '';
|
|
358
|
-
if (this._cellAddonOptions?.tooltipTextMaxLength && tooltipText.length > this._cellAddonOptions?.tooltipTextMaxLength) {
|
|
359
|
-
tooltipText = tooltipText.substring(0, this._cellAddonOptions.tooltipTextMaxLength - 3) + '...';
|
|
360
|
-
}
|
|
361
|
-
tmpTitleElm = cellElm;
|
|
362
|
-
} else {
|
|
363
|
-
if (this._cellAddonOptions?.useRegularTooltipFromFormatterOnly) {
|
|
364
|
-
tmpTitleElm = tmpDiv.querySelector<HTMLDivElement>(SELECTOR_CLOSEST_TOOLTIP_ATTR);
|
|
365
|
-
} else {
|
|
366
|
-
tmpTitleElm = findFirstAttribute(cellElm, CLOSEST_TOOLTIP_FILLED_ATTR)
|
|
367
|
-
? cellElm
|
|
368
|
-
: tmpDiv.querySelector<HTMLDivElement>(SELECTOR_CLOSEST_TOOLTIP_ATTR);
|
|
369
|
-
|
|
370
|
-
if ((!tmpTitleElm || !findFirstAttribute(tmpTitleElm, CLOSEST_TOOLTIP_FILLED_ATTR)) && cellElm) {
|
|
371
|
-
tmpTitleElm = cellElm.querySelector<HTMLDivElement>(SELECTOR_CLOSEST_TOOLTIP_ATTR);
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
if (tmpTitleElm?.style.display === 'none' || (this._hasMultipleTooltips && (!cellElm || cellElm === this._cellNodeElm))) {
|
|
376
|
-
tooltipText = '';
|
|
377
|
-
} else if (!tooltipText || (typeof formatterOrText === 'function' && this._cellAddonOptions?.useRegularTooltipFromFormatterOnly)) {
|
|
378
|
-
tooltipText = findFirstAttribute(tmpTitleElm, CLOSEST_TOOLTIP_FILLED_ATTR) || '';
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
if (tooltipText !== '') {
|
|
384
|
-
this.renderTooltipFormatter(formatterOrText, cell, value, columnDef, item, tooltipText, tmpTitleElm);
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
// also clear any "title" attribute to avoid showing a 2nd browser tooltip
|
|
388
|
-
this.swapAndClearTitleAttribute(tmpTitleElm, tooltipText);
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
protected renderTooltipFormatter(formatter: Formatter | string | undefined, cell: { row: number; cell: number; }, value: any, columnDef: Column, item: unknown, tooltipText?: string, inputTitleElm?: Element | null) {
|
|
392
|
-
// create the tooltip DOM element with the text returned by the Formatter
|
|
393
|
-
this._tooltipElm = createDomElement('div', { className: this.className });
|
|
394
|
-
this._tooltipBodyElm = createDomElement('div', { className: this.bodyClassName });
|
|
395
|
-
this._tooltipElm.classList.add(this.gridUid);
|
|
396
|
-
this._tooltipElm.classList.add('l' + cell.cell);
|
|
397
|
-
this._tooltipElm.classList.add('r' + cell.cell);
|
|
398
|
-
this.tooltipElm?.appendChild(this._tooltipBodyElm);
|
|
399
|
-
|
|
400
|
-
// when cell is currently lock for editing, we'll force a tooltip title search
|
|
401
|
-
// that can happen when user has a formatter but is currently editing and in that case we want the new value
|
|
402
|
-
// e.g.: when user is currently editing and uses the Slider, when dragging its value is changing, so we wish to use the editing value instead of the previous cell value.
|
|
403
|
-
if (value === null || value === undefined) {
|
|
404
|
-
const tmpTitleElm = this._cellNodeElm?.querySelector<HTMLDivElement>(SELECTOR_CLOSEST_TOOLTIP_ATTR);
|
|
405
|
-
value = findFirstAttribute(tmpTitleElm, CLOSEST_TOOLTIP_FILLED_ATTR) || value;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
let outputText = tooltipText || this.parseFormatterAndSanitize(formatter, cell, value, columnDef, item) || '';
|
|
409
|
-
outputText = (this._cellAddonOptions?.tooltipTextMaxLength && outputText.length > this._cellAddonOptions.tooltipTextMaxLength) ? outputText.substring(0, this._cellAddonOptions.tooltipTextMaxLength - 3) + '...' : outputText;
|
|
410
|
-
|
|
411
|
-
let finalOutputText = '';
|
|
412
|
-
if (!tooltipText || this._cellAddonOptions?.renderRegularTooltipAsHtml) {
|
|
413
|
-
finalOutputText = this._grid.sanitizeHtmlString(outputText);
|
|
414
|
-
this._grid.applyHtmlCode(this._tooltipBodyElm, finalOutputText);
|
|
415
|
-
this._tooltipBodyElm.style.whiteSpace = this._cellAddonOptions?.whiteSpace ?? this._defaultOptions.whiteSpace as string;
|
|
416
|
-
} else {
|
|
417
|
-
finalOutputText = outputText || '';
|
|
418
|
-
this._tooltipBodyElm.textContent = finalOutputText;
|
|
419
|
-
this._tooltipBodyElm.style.whiteSpace = this._cellAddonOptions?.regularTooltipWhiteSpace ?? this._defaultOptions.regularTooltipWhiteSpace as string; // use `pre` so that sequences of white space are collapsed. Lines are broken at newline characters
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
// optional max height/width of the tooltip container
|
|
423
|
-
if (this._cellAddonOptions?.maxHeight) {
|
|
424
|
-
this._tooltipElm.style.maxHeight = `${this._cellAddonOptions.maxHeight}px`;
|
|
425
|
-
}
|
|
426
|
-
if (this._cellAddonOptions?.maxWidth) {
|
|
427
|
-
this._tooltipElm.style.maxWidth = `${this._cellAddonOptions.maxWidth}px`;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
// when do have text to show, then append the new tooltip to the html body & reposition the tooltip
|
|
431
|
-
if (finalOutputText) {
|
|
432
|
-
document.body.appendChild(this._tooltipElm);
|
|
433
|
-
|
|
434
|
-
// reposition the tooltip on top of the cell that triggered the mouse over event
|
|
435
|
-
this.reposition(cell);
|
|
436
|
-
|
|
437
|
-
// 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)
|
|
438
|
-
if (!this._cellAddonOptions?.hideArrow) {
|
|
439
|
-
this._tooltipElm.classList.add('tooltip-arrow');
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
// also clear any "title" attribute to avoid showing a 2nd browser tooltip
|
|
443
|
-
this.swapAndClearTitleAttribute(inputTitleElm, outputText);
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
/**
|
|
448
|
-
* Reposition the Tooltip to be top-left position over the cell.
|
|
449
|
-
* By default we use an "auto" mode which will allow to position the Tooltip to the best logical position in the window, also when we mention position, we are talking about the relative position against the grid cell.
|
|
450
|
-
* We can assume that in 80% of the time the default position is top-right, the default is "auto" but we can also override it and use a specific position.
|
|
451
|
-
* Most of the time positioning of the tooltip will be to the "top-right" of the cell is ok but if our column is completely on the right side then we'll want to change the position to "left" align.
|
|
452
|
-
* Same goes for the top/bottom position, Most of the time positioning the tooltip to the "top" but if we are hovering a cell at the top of the grid and there's no room to display it then we might need to reposition to "bottom" instead.
|
|
453
|
-
*/
|
|
454
|
-
protected reposition(cell: { row: number; cell: number; }) {
|
|
455
|
-
if (this._tooltipElm) {
|
|
456
|
-
this._cellNodeElm = this._cellNodeElm || this._grid.getCellNode(cell.row, cell.cell) as HTMLDivElement;
|
|
457
|
-
const cellPosition = getOffset(this._cellNodeElm) || { top: 0, left: 0 };
|
|
458
|
-
const cellContainerWidth = this._cellNodeElm.offsetWidth;
|
|
459
|
-
const calculatedTooltipHeight = this._tooltipElm.getBoundingClientRect().height;
|
|
460
|
-
const calculatedTooltipWidth = this._tooltipElm.getBoundingClientRect().width;
|
|
461
|
-
const calculatedBodyWidth = document.body.offsetWidth || window.innerWidth;
|
|
462
|
-
|
|
463
|
-
// first calculate the default (top/left) position
|
|
464
|
-
let newPositionTop = (cellPosition.top || 0) - this._tooltipElm.offsetHeight - (this._cellAddonOptions?.offsetTopBottom ?? 0);
|
|
465
|
-
let newPositionLeft = (cellPosition.left || 0) - (this._cellAddonOptions?.offsetRight ?? 0);
|
|
466
|
-
|
|
467
|
-
// user could explicitely use a "left-align" arrow position, (when user knows his column is completely on the right in the grid)
|
|
468
|
-
// or when using "auto" and we detect not enough available space then we'll position to the "left" of the cell
|
|
469
|
-
// NOTE the class name is for the arrow and is inverse compare to the tooltip itself, so if user ask for "left-align", then the arrow will in fact be "arrow-right-align"
|
|
470
|
-
const position = this._cellAddonOptions?.position ?? 'auto';
|
|
471
|
-
let finalTooltipPosition = '';
|
|
472
|
-
if (position === 'center') {
|
|
473
|
-
newPositionLeft += (cellContainerWidth / 2) - (calculatedTooltipWidth / 2) + (this._cellAddonOptions?.offsetRight ?? 0);
|
|
474
|
-
finalTooltipPosition = 'top-center';
|
|
475
|
-
this._tooltipElm.classList.remove('arrow-left-align', 'arrow-right-align');
|
|
476
|
-
this._tooltipElm.classList.add('arrow-center-align');
|
|
477
|
-
} else if (position === 'right-align' || ((position === 'auto' || position !== 'left-align') && (newPositionLeft + calculatedTooltipWidth) > calculatedBodyWidth)) {
|
|
478
|
-
finalTooltipPosition = 'right';
|
|
479
|
-
newPositionLeft -= (calculatedTooltipWidth - cellContainerWidth - (this._cellAddonOptions?.offsetLeft ?? 0));
|
|
480
|
-
this._tooltipElm.classList.remove('arrow-center-align', 'arrow-left-align');
|
|
481
|
-
this._tooltipElm.classList.add('arrow-right-align');
|
|
482
|
-
} else {
|
|
483
|
-
finalTooltipPosition = 'left';
|
|
484
|
-
this._tooltipElm.classList.remove('arrow-center-align', 'arrow-right-align');
|
|
485
|
-
this._tooltipElm.classList.add('arrow-left-align');
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
// do the same calculation/reposition with top/bottom (default is top of the cell or in other word starting from the cell going down)
|
|
489
|
-
// NOTE the class name is for the arrow and is inverse compare to the tooltip itself, so if user ask for "bottom", then the arrow will in fact be "arrow-top"
|
|
490
|
-
if (position === 'bottom' || ((position === 'auto' || position !== 'top') && calculatedTooltipHeight > calculateAvailableSpace(this._cellNodeElm).top)) {
|
|
491
|
-
newPositionTop = (cellPosition.top || 0) + (this.gridOptions.rowHeight ?? 0) + (this._cellAddonOptions?.offsetTopBottom ?? 0);
|
|
492
|
-
finalTooltipPosition = `bottom-${finalTooltipPosition}`;
|
|
493
|
-
this._tooltipElm.classList.remove('arrow-down');
|
|
494
|
-
this._tooltipElm.classList.add('arrow-up');
|
|
495
|
-
} else {
|
|
496
|
-
finalTooltipPosition = `top-${finalTooltipPosition}`;
|
|
497
|
-
this._tooltipElm.classList.remove('arrow-up');
|
|
498
|
-
this._tooltipElm.classList.add('arrow-down');
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
// when having multiple tooltips, we'll try to reposition tooltip to mouse position
|
|
502
|
-
if (this._tooltipElm && (this._hasMultipleTooltips || this.cellAddonOptions?.repositionByMouseOverTarget)) {
|
|
503
|
-
const mouseElmOffset = getOffset(this._mouseTarget)!;
|
|
504
|
-
if (finalTooltipPosition.includes('left') || finalTooltipPosition === 'top-center') {
|
|
505
|
-
newPositionLeft = mouseElmOffset.left - (this._addonOptions?.offsetArrow ?? 3);
|
|
506
|
-
} else if (finalTooltipPosition.includes('right')) {
|
|
507
|
-
newPositionLeft = mouseElmOffset.left - calculatedTooltipWidth + (this._mouseTarget?.offsetWidth ?? 0) + (this._addonOptions?.offsetArrow ?? 3);
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
// reposition the tooltip over the cell (90% of the time this will end up using a position on the "right" of the cell)
|
|
512
|
-
this._tooltipElm.style.top = `${newPositionTop}px`;
|
|
513
|
-
this._tooltipElm.style.left = `${newPositionLeft}px`;
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
/**
|
|
518
|
-
* swap and copy the "title" attribute into a new custom attribute then clear the "title" attribute
|
|
519
|
-
* from the grid div text content so that it won't show also as a 2nd browser tooltip
|
|
520
|
-
*/
|
|
521
|
-
protected swapAndClearTitleAttribute(inputTitleElm?: Element | null, tooltipText?: string) {
|
|
522
|
-
// the title attribute might be directly on the slick-cell container element (when formatter returns a result object)
|
|
523
|
-
// OR in a child element (most commonly as a custom formatter)
|
|
524
|
-
let cellWithTitleElm: Element | null | undefined;
|
|
525
|
-
if (inputTitleElm) {
|
|
526
|
-
cellWithTitleElm = (this._cellNodeElm && ((this._cellNodeElm.hasAttribute('title') && this._cellNodeElm.getAttribute('title')) ? this._cellNodeElm : this._cellNodeElm?.querySelector('[title]')));
|
|
527
|
-
}
|
|
528
|
-
const titleElm = inputTitleElm || (this._cellNodeElm && ((this._cellNodeElm.hasAttribute('title') && this._cellNodeElm.getAttribute('title')) ? this._cellNodeElm : this._cellNodeElm?.querySelector('[title]')));
|
|
529
|
-
|
|
530
|
-
// flip tooltip text from `title` to `data-slick-tooltip`
|
|
531
|
-
if (titleElm) {
|
|
532
|
-
titleElm.setAttribute('data-slick-tooltip', tooltipText || '');
|
|
533
|
-
if (titleElm.hasAttribute('title')) {
|
|
534
|
-
titleElm.setAttribute('title', '');
|
|
535
|
-
}
|
|
536
|
-
// targeted element might actually not be the cell element,
|
|
537
|
-
// so let's also clear the cell element title attribute to avoid showing native + custom tooltips
|
|
538
|
-
if (cellWithTitleElm?.hasAttribute('title')) {
|
|
539
|
-
cellWithTitleElm.setAttribute('title', '');
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
}
|
|
1
|
+
import type {
|
|
2
|
+
CancellablePromiseWrapper,
|
|
3
|
+
Column,
|
|
4
|
+
ContainerService,
|
|
5
|
+
CustomDataView,
|
|
6
|
+
CustomTooltipOption,
|
|
7
|
+
Formatter,
|
|
8
|
+
FormatterResultWithHtml,
|
|
9
|
+
FormatterResultWithText,
|
|
10
|
+
GridOption,
|
|
11
|
+
Observable,
|
|
12
|
+
RxJsFacade,
|
|
13
|
+
SharedService,
|
|
14
|
+
SlickEventData,
|
|
15
|
+
SlickGrid,
|
|
16
|
+
Subscription,
|
|
17
|
+
} from '@slickgrid-universal/common';
|
|
18
|
+
import {
|
|
19
|
+
calculateAvailableSpace,
|
|
20
|
+
CancelledException,
|
|
21
|
+
cancellablePromise,
|
|
22
|
+
createDomElement,
|
|
23
|
+
findFirstAttribute,
|
|
24
|
+
getOffset,
|
|
25
|
+
SlickEventHandler,
|
|
26
|
+
} from '@slickgrid-universal/common';
|
|
27
|
+
import { classNameToList, isPrimitiveOrHTML } from '@slickgrid-universal/utils';
|
|
28
|
+
|
|
29
|
+
type CellType = 'slick-cell' | 'slick-header-column' | 'slick-headerrow-column';
|
|
30
|
+
|
|
31
|
+
const CLOSEST_TOOLTIP_FILLED_ATTR = ['title', 'data-slick-tooltip'];
|
|
32
|
+
const SELECTOR_CLOSEST_TOOLTIP_ATTR = '[title], [data-slick-tooltip]';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A plugin to add Custom Tooltip when hovering a cell, it subscribes to the cell "onMouseEnter" and "onMouseLeave" events.
|
|
36
|
+
* The "customTooltip" is defined in the Column Definition OR Grid Options (the first found will have priority over the second)
|
|
37
|
+
* To specify a tooltip when hovering a cell, extend the column definition like so:
|
|
38
|
+
*
|
|
39
|
+
* Available plugin options (same options are available in both column definition and/or grid options)
|
|
40
|
+
* Example 1 - via Column Definition
|
|
41
|
+
* this.columnDefinitions = [
|
|
42
|
+
* {
|
|
43
|
+
* id: "action", name: "Action", field: "action", formatter: fakeButtonFormatter,
|
|
44
|
+
* customTooltip: {
|
|
45
|
+
* formatter: tooltipTaskFormatter,
|
|
46
|
+
* usabilityOverride: (args) => !!(args.dataContext.id % 2) // show it only every second row
|
|
47
|
+
* }
|
|
48
|
+
* }
|
|
49
|
+
* ];
|
|
50
|
+
*
|
|
51
|
+
* OR Example 2 - via Grid Options (for all columns), NOTE: the column definition tooltip options will win over the options defined in the grid options
|
|
52
|
+
* this.gridOptions = {
|
|
53
|
+
* enableCellNavigation: true,
|
|
54
|
+
* customTooltip: {
|
|
55
|
+
* },
|
|
56
|
+
* };
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
// add a default CSS class name that is used and required by SlickGrid Theme
|
|
60
|
+
const DEFAULT_CLASS_NAME = 'slick-custom-tooltip';
|
|
61
|
+
|
|
62
|
+
export class SlickCustomTooltip {
|
|
63
|
+
name: 'CustomTooltip' = 'CustomTooltip' as const;
|
|
64
|
+
|
|
65
|
+
protected _addonOptions?: CustomTooltipOption;
|
|
66
|
+
protected _cellAddonOptions?: CustomTooltipOption;
|
|
67
|
+
protected _cellNodeElm?: HTMLElement;
|
|
68
|
+
protected _cellType: CellType = 'slick-cell';
|
|
69
|
+
protected _cancellablePromise?: CancellablePromiseWrapper;
|
|
70
|
+
protected _observable$?: Subscription;
|
|
71
|
+
protected _rxjs?: RxJsFacade | null = null;
|
|
72
|
+
protected _sharedService?: SharedService | null = null;
|
|
73
|
+
protected _tooltipBodyElm?: HTMLDivElement;
|
|
74
|
+
protected _tooltipElm?: HTMLDivElement;
|
|
75
|
+
protected _mousePosition: { x: number; y: number; } = { x: 0, y: 0 };
|
|
76
|
+
protected _mouseTarget?: HTMLElement | null;
|
|
77
|
+
protected _hasMultipleTooltips = false;
|
|
78
|
+
protected _defaultOptions = {
|
|
79
|
+
bodyClassName: 'tooltip-body',
|
|
80
|
+
className: '',
|
|
81
|
+
offsetArrow: 3, // same as `$slick-tooltip-arrow-side-margin` CSS/SASS variable
|
|
82
|
+
offsetLeft: 0,
|
|
83
|
+
offsetRight: 0,
|
|
84
|
+
offsetTopBottom: 4,
|
|
85
|
+
hideArrow: false,
|
|
86
|
+
regularTooltipWhiteSpace: 'pre-line',
|
|
87
|
+
whiteSpace: 'normal',
|
|
88
|
+
} as CustomTooltipOption;
|
|
89
|
+
protected _grid!: SlickGrid;
|
|
90
|
+
protected _eventHandler: SlickEventHandler;
|
|
91
|
+
|
|
92
|
+
constructor() {
|
|
93
|
+
this._eventHandler = new SlickEventHandler();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
get addonOptions(): CustomTooltipOption | undefined {
|
|
97
|
+
return this._addonOptions;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
get cancellablePromise() {
|
|
101
|
+
return this._cancellablePromise;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
get cellAddonOptions(): CustomTooltipOption | undefined {
|
|
105
|
+
return this._cellAddonOptions;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
get bodyClassName(): string {
|
|
109
|
+
return this._cellAddonOptions?.bodyClassName ?? 'tooltip-body';
|
|
110
|
+
}
|
|
111
|
+
get className(): string {
|
|
112
|
+
// we'll always add our default class name for the CSS style to display as intended
|
|
113
|
+
// and then append any custom CSS class to default when provided
|
|
114
|
+
let className = DEFAULT_CLASS_NAME;
|
|
115
|
+
if (this._addonOptions?.className) {
|
|
116
|
+
className += ` ${this._addonOptions.className}`;
|
|
117
|
+
}
|
|
118
|
+
return className;
|
|
119
|
+
}
|
|
120
|
+
get dataView(): CustomDataView {
|
|
121
|
+
return this._grid.getData<CustomDataView>() || {};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Getter for the Grid Options pulled through the Grid Object */
|
|
125
|
+
get gridOptions(): GridOption {
|
|
126
|
+
return this._grid?.getOptions() || {} as GridOption;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Getter for the grid uid */
|
|
130
|
+
get gridUid(): string {
|
|
131
|
+
return this._grid?.getUID() || '';
|
|
132
|
+
}
|
|
133
|
+
get gridUidSelector(): string {
|
|
134
|
+
return this.gridUid ? `.${this.gridUid}` : '';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
get tooltipElm(): HTMLDivElement | undefined {
|
|
138
|
+
return this._tooltipElm;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
addRxJsResource(rxjs: RxJsFacade) {
|
|
142
|
+
this._rxjs = rxjs;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
init(grid: SlickGrid, containerService: ContainerService) {
|
|
146
|
+
this._grid = grid;
|
|
147
|
+
this._rxjs = containerService.get<RxJsFacade>('RxJsFacade');
|
|
148
|
+
this._sharedService = containerService.get<SharedService>('SharedService');
|
|
149
|
+
this._addonOptions = { ...this._defaultOptions, ...(this._sharedService?.gridOptions?.customTooltip) } as CustomTooltipOption;
|
|
150
|
+
this._eventHandler
|
|
151
|
+
.subscribe(grid.onMouseEnter, this.handleOnMouseOver.bind(this))
|
|
152
|
+
.subscribe(grid.onHeaderMouseOver, (e, args) => this.handleOnHeaderMouseOverByType(e, args, 'slick-header-column'))
|
|
153
|
+
.subscribe(grid.onHeaderRowMouseOver, (e, args) => this.handleOnHeaderMouseOverByType(e, args, 'slick-headerrow-column'))
|
|
154
|
+
.subscribe(grid.onMouseLeave, this.hideTooltip.bind(this))
|
|
155
|
+
.subscribe(grid.onHeaderMouseOut, this.hideTooltip.bind(this))
|
|
156
|
+
.subscribe(grid.onHeaderRowMouseOut, this.hideTooltip.bind(this));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
dispose() {
|
|
160
|
+
// hide (remove) any tooltip and unsubscribe from all events
|
|
161
|
+
this.hideTooltip();
|
|
162
|
+
this._cancellablePromise = undefined;
|
|
163
|
+
this._eventHandler.unsubscribeAll();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* hide (remove) tooltip from the DOM, it will also remove it from the DOM and also cancel any pending requests (as mentioned below).
|
|
168
|
+
* When using async process, it will also cancel any opened Promise/Observable that might still be pending.
|
|
169
|
+
*/
|
|
170
|
+
hideTooltip() {
|
|
171
|
+
this._cancellablePromise?.cancel();
|
|
172
|
+
this._observable$?.unsubscribe();
|
|
173
|
+
const cssClasses = classNameToList(this.className).join('.');
|
|
174
|
+
const prevTooltip = document.body.querySelector(`.${cssClasses}${this.gridUidSelector}`);
|
|
175
|
+
prevTooltip?.remove();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
getOptions(): CustomTooltipOption | undefined {
|
|
179
|
+
return this._addonOptions;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
setOptions(newOptions: CustomTooltipOption) {
|
|
183
|
+
this._addonOptions = { ...this._addonOptions, ...newOptions } as CustomTooltipOption;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// --
|
|
187
|
+
// protected functions
|
|
188
|
+
// ---------------------
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Async process callback will hide any prior tooltip & then merge the new result with the item `dataContext` under a `__params` property
|
|
192
|
+
* (unless a new prop name is provided) to provice as dataContext object to the asyncPostFormatter.
|
|
193
|
+
*/
|
|
194
|
+
protected asyncProcessCallback(asyncResult: any, cell: { row: number, cell: number; }, value: any, columnDef: Column, dataContext: any) {
|
|
195
|
+
this.hideTooltip();
|
|
196
|
+
const itemWithAsyncData = { ...dataContext, [this.addonOptions?.asyncParamsPropName ?? '__params']: asyncResult };
|
|
197
|
+
if (this._cellAddonOptions?.useRegularTooltip) {
|
|
198
|
+
this.renderRegularTooltip(this._cellAddonOptions!.asyncPostFormatter, cell, value, columnDef, itemWithAsyncData);
|
|
199
|
+
} else {
|
|
200
|
+
this.renderTooltipFormatter(this._cellAddonOptions!.asyncPostFormatter, cell, value, columnDef, itemWithAsyncData);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** depending on the selector type, execute the necessary handler code */
|
|
205
|
+
protected handleOnHeaderMouseOverByType(event: SlickEventData, args: any, selector: CellType) {
|
|
206
|
+
this._cellType = selector;
|
|
207
|
+
this._mousePosition = { x: event.clientX || 0, y: event.clientY || 0 };
|
|
208
|
+
this._mouseTarget = document.elementFromPoint(event.clientX || 0, event.clientY || 0)?.closest(SELECTOR_CLOSEST_TOOLTIP_ATTR);
|
|
209
|
+
|
|
210
|
+
// before doing anything, let's remove any previous tooltip before
|
|
211
|
+
// and cancel any opened Promise/Observable when using async
|
|
212
|
+
this.hideTooltip();
|
|
213
|
+
|
|
214
|
+
const cell = {
|
|
215
|
+
row: -1, // negative row to avoid pulling any dataContext while rendering
|
|
216
|
+
cell: this._grid.getColumns().findIndex((col) => (args?.column?.id ?? '') === col.id)
|
|
217
|
+
};
|
|
218
|
+
const columnDef = args.column;
|
|
219
|
+
const item = {};
|
|
220
|
+
const isHeaderRowType = selector === 'slick-headerrow-column';
|
|
221
|
+
|
|
222
|
+
// run the override function (when defined), if the result is false it won't go further
|
|
223
|
+
args = args || {};
|
|
224
|
+
args.cell = cell.cell;
|
|
225
|
+
args.row = cell.row;
|
|
226
|
+
args.columnDef = columnDef;
|
|
227
|
+
args.dataContext = item;
|
|
228
|
+
args.grid = this._grid;
|
|
229
|
+
args.type = isHeaderRowType ? 'header-row' : 'header';
|
|
230
|
+
this._cellAddonOptions = { ...this._addonOptions, ...(columnDef?.customTooltip) } as CustomTooltipOption;
|
|
231
|
+
if (columnDef?.disableTooltip || (typeof this._cellAddonOptions?.usabilityOverride === 'function' && !this._cellAddonOptions.usabilityOverride(args))) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (columnDef && event.target) {
|
|
236
|
+
this._cellNodeElm = (event.target as HTMLDivElement).closest(`.${selector}`) as HTMLDivElement;
|
|
237
|
+
const formatter = isHeaderRowType ? this._cellAddonOptions.headerRowFormatter : this._cellAddonOptions.headerFormatter;
|
|
238
|
+
|
|
239
|
+
if (this._cellAddonOptions?.useRegularTooltip || !formatter) {
|
|
240
|
+
const formatterOrText = !isHeaderRowType ? columnDef.name : this._cellAddonOptions?.useRegularTooltip ? null : formatter;
|
|
241
|
+
this.renderRegularTooltip(formatterOrText, cell, null, columnDef, item);
|
|
242
|
+
} else if (this._cellNodeElm && typeof formatter === 'function') {
|
|
243
|
+
this.renderTooltipFormatter(formatter, cell, null, columnDef, item);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
protected async handleOnMouseOver(event: SlickEventData) {
|
|
249
|
+
this._cellType = 'slick-cell';
|
|
250
|
+
this._mousePosition = { x: event.clientX || 0, y: event.clientY || 0 };
|
|
251
|
+
this._mouseTarget = document.elementFromPoint(event.clientX || 0, event.clientY || 0)?.closest(SELECTOR_CLOSEST_TOOLTIP_ATTR);
|
|
252
|
+
|
|
253
|
+
// before doing anything, let's remove any previous tooltip before
|
|
254
|
+
// and cancel any opened Promise/Observable when using async
|
|
255
|
+
this.hideTooltip();
|
|
256
|
+
|
|
257
|
+
if (event && this._grid) {
|
|
258
|
+
// get cell only when it's possible (ie, Composite Editor will not be able to get cell and so it will never show any tooltip)
|
|
259
|
+
const targetClassName = event?.target?.closest('.slick-cell')?.className;
|
|
260
|
+
const cell = (targetClassName && /l\d+/.exec(targetClassName || '')) ? this._grid.getCellFromEvent(event) : null;
|
|
261
|
+
|
|
262
|
+
if (cell) {
|
|
263
|
+
const item = this.dataView ? this.dataView.getItem(cell.row) : this._grid.getDataItem(cell.row);
|
|
264
|
+
const columnDef = this._grid.getColumns()[cell.cell];
|
|
265
|
+
this._cellNodeElm = this._grid.getCellNode(cell.row, cell.cell) as HTMLDivElement;
|
|
266
|
+
|
|
267
|
+
if (item && columnDef) {
|
|
268
|
+
this._cellAddonOptions = { ...this._addonOptions, ...(columnDef?.customTooltip) } as CustomTooltipOption;
|
|
269
|
+
|
|
270
|
+
if (columnDef?.disableTooltip || (typeof this._cellAddonOptions?.usabilityOverride === 'function' && !this._cellAddonOptions.usabilityOverride({ cell: cell.cell, row: cell.row, dataContext: item, column: columnDef, grid: this._grid, type: 'cell' }))) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const value = item.hasOwnProperty(columnDef.field) ? item[columnDef.field] : null;
|
|
275
|
+
|
|
276
|
+
// when cell is currently lock for editing, we'll force a tooltip title search
|
|
277
|
+
const cellValue = this._grid.getEditorLock().isActive() ? null : value;
|
|
278
|
+
|
|
279
|
+
// when there aren't any formatter OR when user specifically want to use a regular tooltip (via "title" attribute)
|
|
280
|
+
if ((this._cellAddonOptions.useRegularTooltip && !this._cellAddonOptions?.asyncProcess) || !this._cellAddonOptions?.formatter) {
|
|
281
|
+
this.renderRegularTooltip(columnDef.formatter, cell, cellValue, columnDef, item);
|
|
282
|
+
} else {
|
|
283
|
+
// when we aren't using regular tooltip and we do have a tooltip formatter, let's render it
|
|
284
|
+
if (typeof this._cellAddonOptions?.formatter === 'function') {
|
|
285
|
+
this.renderTooltipFormatter(this._cellAddonOptions.formatter, cell, cellValue, columnDef, item);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// when tooltip is an Async (delayed, e.g. with a backend API call)
|
|
289
|
+
if (typeof this._cellAddonOptions?.asyncProcess === 'function') {
|
|
290
|
+
const asyncProcess = this._cellAddonOptions.asyncProcess(cell.row, cell.cell, value, columnDef, item, this._grid);
|
|
291
|
+
if (!this._cellAddonOptions.asyncPostFormatter) {
|
|
292
|
+
console.error(`[Slickgrid-Universal] when using "asyncProcess" with Custom Tooltip, you must also provide an "asyncPostFormatter" formatter.`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (asyncProcess instanceof Promise) {
|
|
296
|
+
// create a new cancellable promise which will resolve, unless it's cancelled, with the udpated `dataContext` object that includes the `__params`
|
|
297
|
+
this._cancellablePromise = cancellablePromise(asyncProcess);
|
|
298
|
+
this._cancellablePromise.promise
|
|
299
|
+
.then((asyncResult: any) => this.asyncProcessCallback(asyncResult, cell, value, columnDef, item))
|
|
300
|
+
.catch((error: Error) => {
|
|
301
|
+
// we will throw back any errors, unless it's a cancelled promise which in that case will be disregarded (thrown by the promise wrapper cancel() call)
|
|
302
|
+
if (!(error instanceof CancelledException)) {
|
|
303
|
+
console.error(error);
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
} else if (this._rxjs?.isObservable(asyncProcess)) {
|
|
307
|
+
const rxjs = this._rxjs as RxJsFacade;
|
|
308
|
+
this._observable$ = (asyncProcess as unknown as Observable<any>)
|
|
309
|
+
.pipe(
|
|
310
|
+
// use `switchMap` so that it cancels any previous subscription, it must return an observable so we can use `of` for that, and then finally we can subscribe to the new observable
|
|
311
|
+
rxjs.switchMap((asyncResult) => rxjs.of(asyncResult))
|
|
312
|
+
).subscribe(
|
|
313
|
+
(asyncResult: any) => this.asyncProcessCallback(asyncResult, cell, value, columnDef, item),
|
|
314
|
+
(error: any) => console.error(error)
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Parse the Custom Formatter (when provided) or return directly the text when it is already a string.
|
|
326
|
+
* We will also sanitize the text in both cases before returning it so that it can be used safely.
|
|
327
|
+
*/
|
|
328
|
+
protected parseFormatterAndSanitize(formatterOrText: Formatter | string | undefined, cell: { row: number; cell: number; }, value: any, columnDef: Column, item: unknown): string {
|
|
329
|
+
if (typeof formatterOrText === 'function') {
|
|
330
|
+
const tooltipResult = formatterOrText(cell.row, cell.cell, value, columnDef, item, this._grid);
|
|
331
|
+
const formatterText = isPrimitiveOrHTML(tooltipResult) ? tooltipResult : (tooltipResult as FormatterResultWithHtml).html || (tooltipResult as FormatterResultWithText).text;
|
|
332
|
+
return this._grid.sanitizeHtmlString((formatterText instanceof HTMLElement ? formatterText.textContent : formatterText as string) || '');
|
|
333
|
+
} else if (typeof formatterOrText === 'string') {
|
|
334
|
+
return this._grid.sanitizeHtmlString(formatterOrText);
|
|
335
|
+
}
|
|
336
|
+
return '';
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Parse the cell formatter and assume it might be html
|
|
341
|
+
* then create a temporary html element to easily retrieve the first [title=""] attribute text content
|
|
342
|
+
* also clear the "title" attribute from the grid div text content so that it won't show also as a 2nd browser tooltip
|
|
343
|
+
*/
|
|
344
|
+
protected renderRegularTooltip(formatterOrText: Formatter | string | undefined, cell: { row: number; cell: number; }, value: any, columnDef: Column, item: any) {
|
|
345
|
+
const tmpDiv = document.createElement('div');
|
|
346
|
+
this._grid.applyHtmlCode(tmpDiv, this.parseFormatterAndSanitize(formatterOrText, cell, value, columnDef, item));
|
|
347
|
+
this._hasMultipleTooltips = (this._cellNodeElm?.querySelectorAll(SELECTOR_CLOSEST_TOOLTIP_ATTR).length || 0) > 1;
|
|
348
|
+
|
|
349
|
+
let tmpTitleElm: HTMLElement | null | undefined;
|
|
350
|
+
const cellElm = (this._cellAddonOptions?.useRegularTooltipFromCellTextOnly || !this._mouseTarget)
|
|
351
|
+
? this._cellNodeElm as HTMLElement
|
|
352
|
+
: this._mouseTarget;
|
|
353
|
+
|
|
354
|
+
let tooltipText = columnDef?.toolTip ?? '';
|
|
355
|
+
if (!tooltipText) {
|
|
356
|
+
if (this._cellType === 'slick-cell' && cellElm && (cellElm.clientWidth < cellElm.scrollWidth) && !this._cellAddonOptions?.useRegularTooltipFromFormatterOnly) {
|
|
357
|
+
tooltipText = cellElm.textContent?.trim() ?? '';
|
|
358
|
+
if (this._cellAddonOptions?.tooltipTextMaxLength && tooltipText.length > this._cellAddonOptions?.tooltipTextMaxLength) {
|
|
359
|
+
tooltipText = tooltipText.substring(0, this._cellAddonOptions.tooltipTextMaxLength - 3) + '...';
|
|
360
|
+
}
|
|
361
|
+
tmpTitleElm = cellElm;
|
|
362
|
+
} else {
|
|
363
|
+
if (this._cellAddonOptions?.useRegularTooltipFromFormatterOnly) {
|
|
364
|
+
tmpTitleElm = tmpDiv.querySelector<HTMLDivElement>(SELECTOR_CLOSEST_TOOLTIP_ATTR);
|
|
365
|
+
} else {
|
|
366
|
+
tmpTitleElm = findFirstAttribute(cellElm, CLOSEST_TOOLTIP_FILLED_ATTR)
|
|
367
|
+
? cellElm
|
|
368
|
+
: tmpDiv.querySelector<HTMLDivElement>(SELECTOR_CLOSEST_TOOLTIP_ATTR);
|
|
369
|
+
|
|
370
|
+
if ((!tmpTitleElm || !findFirstAttribute(tmpTitleElm, CLOSEST_TOOLTIP_FILLED_ATTR)) && cellElm) {
|
|
371
|
+
tmpTitleElm = cellElm.querySelector<HTMLDivElement>(SELECTOR_CLOSEST_TOOLTIP_ATTR);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (tmpTitleElm?.style.display === 'none' || (this._hasMultipleTooltips && (!cellElm || cellElm === this._cellNodeElm))) {
|
|
376
|
+
tooltipText = '';
|
|
377
|
+
} else if (!tooltipText || (typeof formatterOrText === 'function' && this._cellAddonOptions?.useRegularTooltipFromFormatterOnly)) {
|
|
378
|
+
tooltipText = findFirstAttribute(tmpTitleElm, CLOSEST_TOOLTIP_FILLED_ATTR) || '';
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (tooltipText !== '') {
|
|
384
|
+
this.renderTooltipFormatter(formatterOrText, cell, value, columnDef, item, tooltipText, tmpTitleElm);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// also clear any "title" attribute to avoid showing a 2nd browser tooltip
|
|
388
|
+
this.swapAndClearTitleAttribute(tmpTitleElm, tooltipText);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
protected renderTooltipFormatter(formatter: Formatter | string | undefined, cell: { row: number; cell: number; }, value: any, columnDef: Column, item: unknown, tooltipText?: string, inputTitleElm?: Element | null) {
|
|
392
|
+
// create the tooltip DOM element with the text returned by the Formatter
|
|
393
|
+
this._tooltipElm = createDomElement('div', { className: this.className });
|
|
394
|
+
this._tooltipBodyElm = createDomElement('div', { className: this.bodyClassName });
|
|
395
|
+
this._tooltipElm.classList.add(this.gridUid);
|
|
396
|
+
this._tooltipElm.classList.add('l' + cell.cell);
|
|
397
|
+
this._tooltipElm.classList.add('r' + cell.cell);
|
|
398
|
+
this.tooltipElm?.appendChild(this._tooltipBodyElm);
|
|
399
|
+
|
|
400
|
+
// when cell is currently lock for editing, we'll force a tooltip title search
|
|
401
|
+
// that can happen when user has a formatter but is currently editing and in that case we want the new value
|
|
402
|
+
// e.g.: when user is currently editing and uses the Slider, when dragging its value is changing, so we wish to use the editing value instead of the previous cell value.
|
|
403
|
+
if (value === null || value === undefined) {
|
|
404
|
+
const tmpTitleElm = this._cellNodeElm?.querySelector<HTMLDivElement>(SELECTOR_CLOSEST_TOOLTIP_ATTR);
|
|
405
|
+
value = findFirstAttribute(tmpTitleElm, CLOSEST_TOOLTIP_FILLED_ATTR) || value;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
let outputText = tooltipText || this.parseFormatterAndSanitize(formatter, cell, value, columnDef, item) || '';
|
|
409
|
+
outputText = (this._cellAddonOptions?.tooltipTextMaxLength && outputText.length > this._cellAddonOptions.tooltipTextMaxLength) ? outputText.substring(0, this._cellAddonOptions.tooltipTextMaxLength - 3) + '...' : outputText;
|
|
410
|
+
|
|
411
|
+
let finalOutputText = '';
|
|
412
|
+
if (!tooltipText || this._cellAddonOptions?.renderRegularTooltipAsHtml) {
|
|
413
|
+
finalOutputText = this._grid.sanitizeHtmlString(outputText);
|
|
414
|
+
this._grid.applyHtmlCode(this._tooltipBodyElm, finalOutputText);
|
|
415
|
+
this._tooltipBodyElm.style.whiteSpace = this._cellAddonOptions?.whiteSpace ?? this._defaultOptions.whiteSpace as string;
|
|
416
|
+
} else {
|
|
417
|
+
finalOutputText = outputText || '';
|
|
418
|
+
this._tooltipBodyElm.textContent = finalOutputText;
|
|
419
|
+
this._tooltipBodyElm.style.whiteSpace = this._cellAddonOptions?.regularTooltipWhiteSpace ?? this._defaultOptions.regularTooltipWhiteSpace as string; // use `pre` so that sequences of white space are collapsed. Lines are broken at newline characters
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// optional max height/width of the tooltip container
|
|
423
|
+
if (this._cellAddonOptions?.maxHeight) {
|
|
424
|
+
this._tooltipElm.style.maxHeight = `${this._cellAddonOptions.maxHeight}px`;
|
|
425
|
+
}
|
|
426
|
+
if (this._cellAddonOptions?.maxWidth) {
|
|
427
|
+
this._tooltipElm.style.maxWidth = `${this._cellAddonOptions.maxWidth}px`;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// when do have text to show, then append the new tooltip to the html body & reposition the tooltip
|
|
431
|
+
if (finalOutputText) {
|
|
432
|
+
document.body.appendChild(this._tooltipElm);
|
|
433
|
+
|
|
434
|
+
// reposition the tooltip on top of the cell that triggered the mouse over event
|
|
435
|
+
this.reposition(cell);
|
|
436
|
+
|
|
437
|
+
// 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)
|
|
438
|
+
if (!this._cellAddonOptions?.hideArrow) {
|
|
439
|
+
this._tooltipElm.classList.add('tooltip-arrow');
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// also clear any "title" attribute to avoid showing a 2nd browser tooltip
|
|
443
|
+
this.swapAndClearTitleAttribute(inputTitleElm, outputText);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Reposition the Tooltip to be top-left position over the cell.
|
|
449
|
+
* By default we use an "auto" mode which will allow to position the Tooltip to the best logical position in the window, also when we mention position, we are talking about the relative position against the grid cell.
|
|
450
|
+
* We can assume that in 80% of the time the default position is top-right, the default is "auto" but we can also override it and use a specific position.
|
|
451
|
+
* Most of the time positioning of the tooltip will be to the "top-right" of the cell is ok but if our column is completely on the right side then we'll want to change the position to "left" align.
|
|
452
|
+
* Same goes for the top/bottom position, Most of the time positioning the tooltip to the "top" but if we are hovering a cell at the top of the grid and there's no room to display it then we might need to reposition to "bottom" instead.
|
|
453
|
+
*/
|
|
454
|
+
protected reposition(cell: { row: number; cell: number; }) {
|
|
455
|
+
if (this._tooltipElm) {
|
|
456
|
+
this._cellNodeElm = this._cellNodeElm || this._grid.getCellNode(cell.row, cell.cell) as HTMLDivElement;
|
|
457
|
+
const cellPosition = getOffset(this._cellNodeElm) || { top: 0, left: 0 };
|
|
458
|
+
const cellContainerWidth = this._cellNodeElm.offsetWidth;
|
|
459
|
+
const calculatedTooltipHeight = this._tooltipElm.getBoundingClientRect().height;
|
|
460
|
+
const calculatedTooltipWidth = this._tooltipElm.getBoundingClientRect().width;
|
|
461
|
+
const calculatedBodyWidth = document.body.offsetWidth || window.innerWidth;
|
|
462
|
+
|
|
463
|
+
// first calculate the default (top/left) position
|
|
464
|
+
let newPositionTop = (cellPosition.top || 0) - this._tooltipElm.offsetHeight - (this._cellAddonOptions?.offsetTopBottom ?? 0);
|
|
465
|
+
let newPositionLeft = (cellPosition.left || 0) - (this._cellAddonOptions?.offsetRight ?? 0);
|
|
466
|
+
|
|
467
|
+
// user could explicitely use a "left-align" arrow position, (when user knows his column is completely on the right in the grid)
|
|
468
|
+
// or when using "auto" and we detect not enough available space then we'll position to the "left" of the cell
|
|
469
|
+
// NOTE the class name is for the arrow and is inverse compare to the tooltip itself, so if user ask for "left-align", then the arrow will in fact be "arrow-right-align"
|
|
470
|
+
const position = this._cellAddonOptions?.position ?? 'auto';
|
|
471
|
+
let finalTooltipPosition = '';
|
|
472
|
+
if (position === 'center') {
|
|
473
|
+
newPositionLeft += (cellContainerWidth / 2) - (calculatedTooltipWidth / 2) + (this._cellAddonOptions?.offsetRight ?? 0);
|
|
474
|
+
finalTooltipPosition = 'top-center';
|
|
475
|
+
this._tooltipElm.classList.remove('arrow-left-align', 'arrow-right-align');
|
|
476
|
+
this._tooltipElm.classList.add('arrow-center-align');
|
|
477
|
+
} else if (position === 'right-align' || ((position === 'auto' || position !== 'left-align') && (newPositionLeft + calculatedTooltipWidth) > calculatedBodyWidth)) {
|
|
478
|
+
finalTooltipPosition = 'right';
|
|
479
|
+
newPositionLeft -= (calculatedTooltipWidth - cellContainerWidth - (this._cellAddonOptions?.offsetLeft ?? 0));
|
|
480
|
+
this._tooltipElm.classList.remove('arrow-center-align', 'arrow-left-align');
|
|
481
|
+
this._tooltipElm.classList.add('arrow-right-align');
|
|
482
|
+
} else {
|
|
483
|
+
finalTooltipPosition = 'left';
|
|
484
|
+
this._tooltipElm.classList.remove('arrow-center-align', 'arrow-right-align');
|
|
485
|
+
this._tooltipElm.classList.add('arrow-left-align');
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// do the same calculation/reposition with top/bottom (default is top of the cell or in other word starting from the cell going down)
|
|
489
|
+
// NOTE the class name is for the arrow and is inverse compare to the tooltip itself, so if user ask for "bottom", then the arrow will in fact be "arrow-top"
|
|
490
|
+
if (position === 'bottom' || ((position === 'auto' || position !== 'top') && calculatedTooltipHeight > calculateAvailableSpace(this._cellNodeElm).top)) {
|
|
491
|
+
newPositionTop = (cellPosition.top || 0) + (this.gridOptions.rowHeight ?? 0) + (this._cellAddonOptions?.offsetTopBottom ?? 0);
|
|
492
|
+
finalTooltipPosition = `bottom-${finalTooltipPosition}`;
|
|
493
|
+
this._tooltipElm.classList.remove('arrow-down');
|
|
494
|
+
this._tooltipElm.classList.add('arrow-up');
|
|
495
|
+
} else {
|
|
496
|
+
finalTooltipPosition = `top-${finalTooltipPosition}`;
|
|
497
|
+
this._tooltipElm.classList.remove('arrow-up');
|
|
498
|
+
this._tooltipElm.classList.add('arrow-down');
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// when having multiple tooltips, we'll try to reposition tooltip to mouse position
|
|
502
|
+
if (this._tooltipElm && (this._hasMultipleTooltips || this.cellAddonOptions?.repositionByMouseOverTarget)) {
|
|
503
|
+
const mouseElmOffset = getOffset(this._mouseTarget)!;
|
|
504
|
+
if (finalTooltipPosition.includes('left') || finalTooltipPosition === 'top-center') {
|
|
505
|
+
newPositionLeft = mouseElmOffset.left - (this._addonOptions?.offsetArrow ?? 3);
|
|
506
|
+
} else if (finalTooltipPosition.includes('right')) {
|
|
507
|
+
newPositionLeft = mouseElmOffset.left - calculatedTooltipWidth + (this._mouseTarget?.offsetWidth ?? 0) + (this._addonOptions?.offsetArrow ?? 3);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// reposition the tooltip over the cell (90% of the time this will end up using a position on the "right" of the cell)
|
|
512
|
+
this._tooltipElm.style.top = `${newPositionTop}px`;
|
|
513
|
+
this._tooltipElm.style.left = `${newPositionLeft}px`;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* swap and copy the "title" attribute into a new custom attribute then clear the "title" attribute
|
|
519
|
+
* from the grid div text content so that it won't show also as a 2nd browser tooltip
|
|
520
|
+
*/
|
|
521
|
+
protected swapAndClearTitleAttribute(inputTitleElm?: Element | null, tooltipText?: string) {
|
|
522
|
+
// the title attribute might be directly on the slick-cell container element (when formatter returns a result object)
|
|
523
|
+
// OR in a child element (most commonly as a custom formatter)
|
|
524
|
+
let cellWithTitleElm: Element | null | undefined;
|
|
525
|
+
if (inputTitleElm) {
|
|
526
|
+
cellWithTitleElm = (this._cellNodeElm && ((this._cellNodeElm.hasAttribute('title') && this._cellNodeElm.getAttribute('title')) ? this._cellNodeElm : this._cellNodeElm?.querySelector('[title]')));
|
|
527
|
+
}
|
|
528
|
+
const titleElm = inputTitleElm || (this._cellNodeElm && ((this._cellNodeElm.hasAttribute('title') && this._cellNodeElm.getAttribute('title')) ? this._cellNodeElm : this._cellNodeElm?.querySelector('[title]')));
|
|
529
|
+
|
|
530
|
+
// flip tooltip text from `title` to `data-slick-tooltip`
|
|
531
|
+
if (titleElm) {
|
|
532
|
+
titleElm.setAttribute('data-slick-tooltip', tooltipText || '');
|
|
533
|
+
if (titleElm.hasAttribute('title')) {
|
|
534
|
+
titleElm.setAttribute('title', '');
|
|
535
|
+
}
|
|
536
|
+
// targeted element might actually not be the cell element,
|
|
537
|
+
// so let's also clear the cell element title attribute to avoid showing native + custom tooltips
|
|
538
|
+
if (cellWithTitleElm?.hasAttribute('title')) {
|
|
539
|
+
cellWithTitleElm.setAttribute('title', '');
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|