2017-11-13 01:23:39 -05:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
// Tooltip Handling
|
|
|
|
// ============================================================================
|
|
|
|
|
2018-05-10 19:56:39 -04:00
|
|
|
import {createElement, sanitize} from 'utilities/dom';
|
2023-11-13 20:47:45 -05:00
|
|
|
import {has, maybe_call} from 'utilities/object';
|
2017-11-13 01:23:39 -05:00
|
|
|
|
2023-11-13 20:47:45 -05:00
|
|
|
import Tooltip, { TooltipInstance } from 'utilities/tooltip';
|
|
|
|
import Module, { GenericModule, buildAddonProxy } from 'utilities/module';
|
2021-05-13 15:54:21 -04:00
|
|
|
import awaitMD, {getMD} from 'utilities/markdown';
|
2023-11-04 19:00:27 -04:00
|
|
|
import { DEBUG } from 'src/utilities/constants';
|
2023-11-13 20:47:45 -05:00
|
|
|
import type { AddonInfo, DomFragment, OptionallyCallable } from '../utilities/types';
|
|
|
|
import type TranslationManager from '../i18n';
|
|
|
|
|
|
|
|
declare global {
|
|
|
|
interface HTMLElement {
|
|
|
|
_ffz_child: Element | null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export type TooltipEvents = {
|
|
|
|
/**
|
|
|
|
* When this event is emitted, the tooltip provider will attempt to remove
|
|
|
|
* old, invalid tool-tips.
|
|
|
|
*/
|
|
|
|
':cleanup': [],
|
|
|
|
|
|
|
|
':hover': [target: HTMLElement, tip: TooltipInstance, event: MouseEvent];
|
|
|
|
':leave': [target: HTMLElement, tip: TooltipInstance, event: MouseEvent];
|
|
|
|
};
|
|
|
|
|
|
|
|
type TooltipOptional<TReturn> = OptionallyCallable<[target: HTMLElement, tip: TooltipInstance], TReturn>;
|
|
|
|
|
|
|
|
type TooltipExtra = {
|
|
|
|
__source?: string;
|
|
|
|
|
|
|
|
popperConfig(target: HTMLElement, tip: TooltipInstance, options: any): any;
|
|
|
|
|
|
|
|
delayShow: TooltipOptional<number>;
|
|
|
|
delayHide: TooltipOptional<number>;
|
|
|
|
|
|
|
|
interactive: TooltipOptional<boolean>;
|
|
|
|
hover_events: TooltipOptional<boolean>;
|
|
|
|
|
|
|
|
onShow(target: HTMLElement, tip: TooltipInstance): void;
|
|
|
|
onHide(target: HTMLElement, tip: TooltipInstance): void;
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
export type TooltipDefinition = Partial<TooltipExtra> &
|
|
|
|
((target: HTMLElement, tip: TooltipInstance) => DomFragment);
|
|
|
|
|
|
|
|
|
|
|
|
export default class TooltipProvider extends Module<'tooltips', TooltipEvents> {
|
|
|
|
|
|
|
|
// Storage
|
|
|
|
types: Record<string, TooltipDefinition | undefined>;
|
|
|
|
|
|
|
|
// Dependencies
|
|
|
|
i18n: TranslationManager = null as any;
|
|
|
|
|
|
|
|
// State
|
|
|
|
container?: HTMLElement | null;
|
|
|
|
tip_element?: HTMLElement | null;
|
|
|
|
tips?: Tooltip | null;
|
|
|
|
|
|
|
|
|
|
|
|
constructor(name?: string, parent?: GenericModule) {
|
|
|
|
super(name, parent);
|
2017-11-13 01:23:39 -05:00
|
|
|
|
|
|
|
this.types = {};
|
|
|
|
|
2019-10-23 19:30:27 -04:00
|
|
|
this.inject('i18n');
|
|
|
|
|
2017-11-13 01:23:39 -05:00
|
|
|
this.should_enable = true;
|
|
|
|
|
|
|
|
this.types.json = target => {
|
|
|
|
const title = target.dataset.title;
|
|
|
|
return [
|
2018-04-01 18:24:08 -04:00
|
|
|
title && createElement('strong', null, title),
|
|
|
|
createElement('code', {
|
2017-11-13 01:23:39 -05:00
|
|
|
className: `block${title ? ' pd-t-05 border-t mg-t-05' : ''}`,
|
|
|
|
style: {
|
|
|
|
fontFamily: 'monospace',
|
|
|
|
textAlign: 'left'
|
|
|
|
}
|
|
|
|
}, target.dataset.data)
|
|
|
|
]
|
2021-02-19 18:42:16 -05:00
|
|
|
};
|
2017-12-21 20:37:58 -05:00
|
|
|
|
2019-06-17 15:32:38 -04:00
|
|
|
this.types.child = target => {
|
|
|
|
const child = target.querySelector(':scope > .ffz-tooltip-child');
|
|
|
|
if ( ! child )
|
|
|
|
return null;
|
|
|
|
|
|
|
|
target._ffz_child = child;
|
|
|
|
child.remove();
|
|
|
|
child.classList.remove('ffz-tooltip-child');
|
|
|
|
return child;
|
|
|
|
};
|
|
|
|
|
|
|
|
this.types.child.onHide = target => {
|
|
|
|
const child = target._ffz_child;
|
|
|
|
if ( child ) {
|
|
|
|
target._ffz_child = null;
|
|
|
|
child.remove();
|
|
|
|
|
|
|
|
if ( ! target.querySelector(':scope > .ffz-tooltip-child') ) {
|
|
|
|
child.classList.add('ffz-tooltip-child');
|
|
|
|
target.appendChild(child);
|
|
|
|
}
|
|
|
|
}
|
2021-02-19 18:42:16 -05:00
|
|
|
};
|
|
|
|
|
|
|
|
this.types.markdown = (target, tip) => {
|
|
|
|
tip.add_class = 'ffz-tooltip--markdown';
|
|
|
|
|
2021-05-13 15:54:21 -04:00
|
|
|
const md = getMD();
|
2021-02-19 18:42:16 -05:00
|
|
|
if ( ! md )
|
2021-05-13 15:54:21 -04:00
|
|
|
return awaitMD().then(md => md.render(target.dataset.title));
|
2021-02-19 18:42:16 -05:00
|
|
|
|
|
|
|
return md.render(target.dataset.title);
|
|
|
|
};
|
2019-06-17 15:32:38 -04:00
|
|
|
|
2023-11-13 20:47:45 -05:00
|
|
|
this.types.text = target => sanitize(target.dataset.title ?? '');
|
2018-04-02 03:30:22 -04:00
|
|
|
this.types.html = target => target.dataset.title;
|
2020-07-18 15:44:02 -04:00
|
|
|
|
|
|
|
this.onFSChange = this.onFSChange.bind(this);
|
2017-11-13 01:23:39 -05:00
|
|
|
}
|
|
|
|
|
2023-11-04 19:00:27 -04:00
|
|
|
|
2023-11-13 20:47:45 -05:00
|
|
|
getAddonProxy(addon_id: string, addon: AddonInfo, module: GenericModule) {
|
2023-11-04 19:00:27 -04:00
|
|
|
if ( ! addon_id )
|
|
|
|
return this;
|
|
|
|
|
2023-11-13 20:47:45 -05:00
|
|
|
const overrides: Record<string, any> = {},
|
2023-11-04 19:00:27 -04:00
|
|
|
is_dev = DEBUG || addon?.dev;
|
2023-11-13 20:47:45 -05:00
|
|
|
let warnings: Record<string, boolean | string> | undefined;
|
2023-11-04 19:00:27 -04:00
|
|
|
|
2023-11-13 20:47:45 -05:00
|
|
|
overrides.define = (key: string, handler: TooltipDefinition) => {
|
2023-11-04 19:00:27 -04:00
|
|
|
if ( handler )
|
|
|
|
handler.__source = addon_id;
|
|
|
|
|
|
|
|
return this.define(key, handler);
|
|
|
|
};
|
|
|
|
|
2023-11-13 20:47:45 -05:00
|
|
|
if ( is_dev ) {
|
2023-11-04 19:00:27 -04:00
|
|
|
overrides.cleanup = () => {
|
|
|
|
module.log.warn('[DEV-CHECK] Instead of calling tooltips.cleanup(), you can emit the event "tooltips:cleanup"');
|
|
|
|
return this.cleanup();
|
|
|
|
};
|
|
|
|
|
2023-11-13 20:47:45 -05:00
|
|
|
warnings = {
|
|
|
|
types: 'Please use tooltips.define()'
|
|
|
|
};
|
|
|
|
}
|
2023-11-04 19:00:27 -04:00
|
|
|
|
2023-11-13 20:47:45 -05:00
|
|
|
return buildAddonProxy(
|
|
|
|
module,
|
|
|
|
this,
|
|
|
|
'tooltips',
|
|
|
|
overrides,
|
|
|
|
warnings
|
|
|
|
);
|
2023-11-04 19:00:27 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2017-11-13 01:23:39 -05:00
|
|
|
onEnable() {
|
2021-02-24 14:38:25 -05:00
|
|
|
const container = this.getRoot();
|
2019-10-09 16:02:25 -04:00
|
|
|
|
2020-07-18 15:44:02 -04:00
|
|
|
window.addEventListener('fullscreenchange', this.onFSChange);
|
|
|
|
|
2019-06-20 15:15:54 -04:00
|
|
|
// is_minimal = false; //container && container.classList.contains('twilight-minimal-root');
|
2017-12-21 20:37:58 -05:00
|
|
|
|
2020-07-18 15:44:02 -04:00
|
|
|
this.container = container;
|
|
|
|
this.tip_element = container;
|
|
|
|
this.tips = this._createInstance(container);
|
|
|
|
|
|
|
|
this.on(':cleanup', this.cleanup);
|
2023-11-04 19:00:27 -04:00
|
|
|
|
|
|
|
this.on('addon:fully-unload', addon_id => {
|
|
|
|
let removed = 0;
|
|
|
|
for(const [key, handler] of Object.entries(this.types)) {
|
|
|
|
if ( handler?.__source === addon_id ) {
|
|
|
|
removed++;
|
|
|
|
this.types[key] = undefined;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( removed ) {
|
|
|
|
this.log.debug(`Cleaned up ${removed} entries when unloading addon:`, addon_id);
|
|
|
|
this.cleanup();
|
|
|
|
}
|
|
|
|
});
|
2020-07-18 15:44:02 -04:00
|
|
|
}
|
|
|
|
|
2023-11-04 19:00:27 -04:00
|
|
|
|
2023-11-13 20:47:45 -05:00
|
|
|
define(key: string, handler: TooltipDefinition) {
|
|
|
|
// TODO: Determine if any tooltips are already open.
|
|
|
|
// If so, we need to close them / maybe re-open them?
|
2023-11-04 19:00:27 -04:00
|
|
|
this.types[key] = handler;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2021-02-24 14:38:25 -05:00
|
|
|
getRoot() { // eslint-disable-line class-methods-use-this
|
2023-11-13 20:47:45 -05:00
|
|
|
return document.querySelector<HTMLElement>('.sunlight-root') ||
|
2023-09-26 17:40:25 -04:00
|
|
|
//document.querySelector('#root>div') ||
|
|
|
|
document.querySelector('#root') ||
|
|
|
|
document.querySelector('.clips-root') ||
|
|
|
|
document.body;
|
2021-02-24 14:38:25 -05:00
|
|
|
}
|
|
|
|
|
2023-11-13 20:47:45 -05:00
|
|
|
_createInstance(container: HTMLElement, klass = 'ffz-tooltip', default_type = 'text', tip_container?: HTMLElement) {
|
2020-07-29 02:22:45 -04:00
|
|
|
return new Tooltip(container, klass, {
|
2017-11-13 01:23:39 -05:00
|
|
|
html: true,
|
2019-10-23 19:30:27 -04:00
|
|
|
i18n: this.i18n,
|
2020-07-18 15:44:02 -04:00
|
|
|
live: true,
|
2020-08-04 18:26:11 -04:00
|
|
|
check_modifiers: true,
|
2021-02-24 14:38:25 -05:00
|
|
|
container: tip_container || container,
|
2019-10-23 19:30:27 -04:00
|
|
|
|
2020-07-29 02:22:45 -04:00
|
|
|
delayHide: this.checkDelayHide.bind(this, default_type),
|
|
|
|
delayShow: this.checkDelayShow.bind(this, default_type),
|
|
|
|
content: this.process.bind(this, default_type),
|
|
|
|
interactive: this.checkInteractive.bind(this, default_type),
|
|
|
|
hover_events: this.checkHoverEvents.bind(this, default_type),
|
2019-06-17 15:32:38 -04:00
|
|
|
|
2020-07-29 02:22:45 -04:00
|
|
|
onShow: this.delegateOnShow.bind(this, default_type),
|
|
|
|
onHide: this.delegateOnHide.bind(this, default_type),
|
2019-06-17 15:32:38 -04:00
|
|
|
|
2020-07-29 02:22:45 -04:00
|
|
|
popperConfig: this.delegatePopperConfig.bind(this, default_type),
|
2017-11-13 01:23:39 -05:00
|
|
|
popper: {
|
2017-12-21 20:37:58 -05:00
|
|
|
placement: 'top',
|
|
|
|
modifiers: {
|
2021-11-10 18:27:52 -05:00
|
|
|
flip: {}
|
2017-12-21 20:37:58 -05:00
|
|
|
}
|
2019-06-12 21:13:53 -04:00
|
|
|
},
|
|
|
|
|
|
|
|
onHover: (target, tip, event) => {
|
|
|
|
this.emit(':hover', target, tip, event)
|
|
|
|
},
|
|
|
|
|
|
|
|
onLeave: (target, tip, event) => {
|
|
|
|
this.emit(':leave', target, tip, event);
|
2017-11-13 01:23:39 -05:00
|
|
|
}
|
|
|
|
});
|
2020-07-18 15:44:02 -04:00
|
|
|
}
|
2018-05-10 19:56:39 -04:00
|
|
|
|
2020-07-18 15:44:02 -04:00
|
|
|
|
|
|
|
onFSChange() {
|
2023-11-13 20:47:45 -05:00
|
|
|
if ( ! this.container )
|
|
|
|
this.container = this.getRoot();
|
|
|
|
|
|
|
|
let tip_element = this.container;
|
|
|
|
if ( document.fullscreenElement instanceof HTMLElement )
|
|
|
|
tip_element = document.fullscreenElement;
|
|
|
|
|
2020-07-18 15:44:02 -04:00
|
|
|
if ( tip_element !== this.tip_element ) {
|
|
|
|
this.tip_element = tip_element;
|
2023-11-13 20:47:45 -05:00
|
|
|
if ( this.tips ) {
|
|
|
|
this.tips.destroy();
|
|
|
|
this.tips = this._createInstance(tip_element);
|
|
|
|
}
|
2020-07-18 15:44:02 -04:00
|
|
|
}
|
2018-05-10 19:56:39 -04:00
|
|
|
}
|
|
|
|
|
2020-07-18 15:44:02 -04:00
|
|
|
|
2018-05-10 19:56:39 -04:00
|
|
|
cleanup() {
|
2023-11-13 20:47:45 -05:00
|
|
|
if ( this.tips )
|
|
|
|
this.tips.cleanup();
|
2017-11-13 01:23:39 -05:00
|
|
|
}
|
|
|
|
|
2020-08-04 18:26:11 -04:00
|
|
|
|
2023-11-13 20:47:45 -05:00
|
|
|
delegatePopperConfig(
|
|
|
|
default_type: string,
|
|
|
|
target: HTMLElement,
|
|
|
|
tip: TooltipInstance,
|
|
|
|
options: any
|
|
|
|
) {
|
2020-07-29 02:22:45 -04:00
|
|
|
const type = target.dataset.tooltipType || default_type,
|
2020-07-23 02:33:20 -04:00
|
|
|
handler = this.types[type];
|
|
|
|
|
2021-02-19 18:42:16 -05:00
|
|
|
if ( target.dataset.tooltipSide )
|
2023-11-13 20:47:45 -05:00
|
|
|
options.placement = target.dataset.tooltipSide;
|
2021-02-19 18:42:16 -05:00
|
|
|
|
2020-07-23 02:33:20 -04:00
|
|
|
if ( handler && handler.popperConfig )
|
2023-11-13 20:47:45 -05:00
|
|
|
return handler.popperConfig(target, tip, options);
|
2020-07-23 02:33:20 -04:00
|
|
|
|
2023-11-13 20:47:45 -05:00
|
|
|
return options;
|
2020-07-23 02:33:20 -04:00
|
|
|
}
|
|
|
|
|
2023-11-13 20:47:45 -05:00
|
|
|
delegateOnShow(
|
|
|
|
default_type: string,
|
|
|
|
target: HTMLElement,
|
|
|
|
tip: TooltipInstance
|
|
|
|
) {
|
2020-07-29 02:22:45 -04:00
|
|
|
const type = target.dataset.tooltipType || default_type,
|
2019-06-17 15:32:38 -04:00
|
|
|
handler = this.types[type];
|
|
|
|
|
|
|
|
if ( handler && handler.onShow )
|
|
|
|
handler.onShow(target, tip);
|
|
|
|
}
|
|
|
|
|
2023-11-13 20:47:45 -05:00
|
|
|
delegateOnHide(
|
|
|
|
default_type: string,
|
|
|
|
target: HTMLElement,
|
|
|
|
tip: TooltipInstance
|
|
|
|
) {
|
2020-07-29 02:22:45 -04:00
|
|
|
const type = target.dataset.tooltipType || default_type,
|
2019-06-17 15:32:38 -04:00
|
|
|
handler = this.types[type];
|
|
|
|
|
|
|
|
if ( handler && handler.onHide )
|
|
|
|
handler.onHide(target, tip);
|
|
|
|
}
|
|
|
|
|
2023-11-13 20:47:45 -05:00
|
|
|
checkDelayShow(
|
|
|
|
default_type: string,
|
|
|
|
target: HTMLElement,
|
|
|
|
tip: TooltipInstance
|
|
|
|
) {
|
2020-07-29 02:22:45 -04:00
|
|
|
const type = target.dataset.tooltipType || default_type,
|
2017-11-14 04:11:43 -05:00
|
|
|
handler = this.types[type];
|
|
|
|
|
2023-11-13 20:47:45 -05:00
|
|
|
if ( handler?.delayShow != null )
|
2017-11-14 04:11:43 -05:00
|
|
|
return maybe_call(handler.delayShow, null, target, tip);
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2023-11-13 20:47:45 -05:00
|
|
|
checkDelayHide(
|
|
|
|
default_type: string,
|
|
|
|
target: HTMLElement,
|
|
|
|
tip: TooltipInstance
|
|
|
|
) {
|
2020-07-29 02:22:45 -04:00
|
|
|
const type = target.dataset.tooltipType || default_type,
|
2017-11-14 04:11:43 -05:00
|
|
|
handler = this.types[type];
|
|
|
|
|
2023-11-13 20:47:45 -05:00
|
|
|
if ( handler?.delayHide != null )
|
2017-11-14 04:11:43 -05:00
|
|
|
return maybe_call(handler.delayHide, null, target, tip);
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2023-11-13 20:47:45 -05:00
|
|
|
checkInteractive(
|
|
|
|
default_type: string,
|
|
|
|
target: HTMLElement,
|
|
|
|
tip: TooltipInstance
|
|
|
|
) {
|
2020-07-29 02:22:45 -04:00
|
|
|
const type = target.dataset.tooltipType || default_type,
|
2017-11-14 04:11:43 -05:00
|
|
|
handler = this.types[type];
|
|
|
|
|
2023-11-13 20:47:45 -05:00
|
|
|
if ( handler?.interactive != null )
|
2017-11-14 04:11:43 -05:00
|
|
|
return maybe_call(handler.interactive, null, target, tip);
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2023-11-13 20:47:45 -05:00
|
|
|
checkHoverEvents(
|
|
|
|
default_type: string,
|
|
|
|
target: HTMLElement,
|
|
|
|
tip: TooltipInstance
|
|
|
|
) {
|
2020-07-29 02:22:45 -04:00
|
|
|
const type = target.dataset.tooltipType || default_type,
|
2019-06-12 21:13:53 -04:00
|
|
|
handler = this.types[type];
|
|
|
|
|
2023-11-13 20:47:45 -05:00
|
|
|
if ( handler?.hover_events != null )
|
2019-06-12 21:13:53 -04:00
|
|
|
return maybe_call(handler.hover_events, null, target, tip);
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2023-11-13 20:47:45 -05:00
|
|
|
process(
|
|
|
|
default_type: string,
|
|
|
|
target: HTMLElement,
|
|
|
|
tip: TooltipInstance
|
|
|
|
) {
|
2020-07-29 02:22:45 -04:00
|
|
|
const type = target.dataset.tooltipType || default_type || 'text',
|
2021-02-19 18:42:16 -05:00
|
|
|
align = target.dataset.tooltipAlign,
|
2017-11-13 01:23:39 -05:00
|
|
|
handler = this.types[type];
|
|
|
|
|
2021-02-19 18:42:16 -05:00
|
|
|
if ( align )
|
|
|
|
tip.align = align;
|
|
|
|
|
2017-11-13 01:23:39 -05:00
|
|
|
if ( ! handler )
|
|
|
|
return [
|
2018-04-01 18:24:08 -04:00
|
|
|
createElement('strong', null, 'Unhandled Tooltip Type'),
|
|
|
|
createElement('code', {
|
2019-06-12 21:13:53 -04:00
|
|
|
className: 'tw-block pd-t-05 border-t mg-t-05',
|
2017-11-13 01:23:39 -05:00
|
|
|
style: {
|
|
|
|
fontFamily: 'monospace',
|
|
|
|
textAlign: 'left'
|
|
|
|
}
|
|
|
|
}, JSON.stringify(target.dataset, null, 4))
|
|
|
|
];
|
|
|
|
|
|
|
|
return handler(target, tip);
|
|
|
|
}
|
2023-11-13 20:47:45 -05:00
|
|
|
}
|