1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-06-27 21:05:53 +00:00
FrankerFaceZ/src/utilities/tooltip.js
SirStendec 9dc8252df0 4.0.0-rc1.5
Add an option to hide the mouse over the player. This doesn't work well in Chrome. Oh well, we tried.
Add an option to not automatically join raids for that comfy falling asleep experience.
Fix in-line actions not rendering when they should.
Fix detokenizeMessage for changes to Twitch's message format.
Don't try to preview the `create` clip URL.
Start using functional components where possible for performance.
Stop logging GraphQL errors to save our Sentry quota.

Begin implementing chat rendering on top of Vue. For now, we've got functional components for every type of chat token. We've got a lot of work ahead of us. This will eventually be used for mod card history, chat panes, and maybe even pinned rooms.

Add an event to clean orphan tooltips. Useful for when we click something we know will change DOM elements with active tooltips, like a Close button for example.
2018-05-10 19:56:39 -04:00

355 lines
No EOL
8.2 KiB
JavaScript

'use strict';
// ============================================================================
// Dynamic Tooltip Handling
//
// Better because you can assign arbitrary content.
// Better because they are asynchronous with loading indication.
// Better because they aren't hidden by parents with overflow: hidden;
// ============================================================================
import {createElement, setChildren} from 'utilities/dom';
import {maybe_call} from 'utilities/object';
import Popper from 'popper.js';
let last_id = 0;
export const DefaultOptions = {
html: false,
delayShow: 0,
delayHide: 0,
live: true,
tooltipClass: 'ffz__tooltip',
innerClass: 'ffz__tooltip--inner',
arrowClass: 'ffz__tooltip--arrow'
}
// ============================================================================
// Tooltip Class
// ============================================================================
export class Tooltip {
constructor(parent, cls, options) {
if ( typeof parent === 'string' )
parent = document.querySelector(parent);
if (!( parent instanceof Node ))
throw new TypeError('invalid parent');
this.options = Object.assign({}, DefaultOptions, options);
this.live = this.options.live;
this.parent = parent;
this.cls = cls;
if ( ! this.live ) {
if ( typeof cls === 'string' )
this.elements = parent.querySelectorAll(cls);
else if ( Array.isArray(cls) )
this.elements = cls;
else if ( cls instanceof Node )
this.elements = [cls];
else
throw new TypeError('invalid elements');
this.elements = new Set(this.elements);
} else {
this.cls = cls;
this.elements = new Set;
}
this._accessor = `_ffz_tooltip$${last_id++}`;
this._onMouseOut = e => this._exit(e.target);
if ( this.options.manual ) {
// Do nothing~!
} else if ( this.live ) {
this._onMouseOver = e => {
const target = e.target;
if ( target && target.classList && target.classList.contains(this.cls) )
this._enter(target);
};
parent.addEventListener('mouseover', this._onMouseOver);
parent.addEventListener('mouseout', this._onMouseOut);
} else {
this._onMouseOver = e => {
const target = e.target;
if ( this.elements.has(target) )
this._enter(e.target);
}
if ( this.elements.size <= 5 )
for(const el of this.elements) {
el.addEventListener('mouseenter', this._onMouseOver);
el.addEventListener('mouseleave', this._onMouseOut);
}
else {
parent.addEventListener('mouseover', this._onMouseOver);
parent.addEventListener('mouseout', this._onMouseOut);
}
}
}
destroy() {
if ( this.options.manual ) {
// Do nothing~!
} else if ( this.live || this.elements.size > 5 ) {
parent.removeEventListener('mouseover', this._onMouseOver);
parent.removeEventListener('mouseout', this._onMouseOut);
} else
for(const el of this.elements) {
el.removeEventListener('mouseenter', this._onMouseOver);
el.removeEventListener('mouseleave', this._onMouseOut);
}
for(const el of this.elements) {
const tip = el[this._accessor];
if ( tip && tip.visible )
this.hide(tip);
el[this._accessor] = null;
}
this.elements = null;
this._onMouseOut = this._onMouseOver = null;
this.parent = null;
}
cleanup() {
for(const el of this.elements) {
const tip = el[this._accessor];
if ( document.body.contains(el) )
continue;
if ( tip && tip.visible )
this.hide(tip);
}
}
_enter(target) {
let tip = target[this._accessor];
if ( ! tip )
tip = target[this._accessor] = {target};
tip.state = true;
if ( tip._show_timer ) {
clearTimeout(tip._show_timer);
tip._show_timer = null;
}
if ( tip.visible )
return;
const delay = maybe_call(this.options.delayShow, null, target, tip);
if ( delay === 0 )
this.show(tip);
else
tip._show_timer = setTimeout(() => {
tip._show_timer = null;
if ( tip.state )
this.show(tip);
}, delay);
}
_exit(target) {
const tip = target[this._accessor];
if ( ! tip )
return;
tip.state = false;
if ( tip._show_timer ) {
clearTimeout(tip._show_timer);
tip._show_timer = null;
}
if ( ! tip.visible )
return;
const delay = maybe_call(this.options.delayHide, null, target, tip);
if ( delay === 0 )
this.hide(tip);
else
tip._show_timer = setTimeout(() => {
tip._show_timer = null;
if ( ! tip.state )
this.hide(tip);
}, delay);
}
show(tip) {
const opts = this.options,
target = tip.target;
this.elements.add(target);
// Set this early in case content uses it early.
tip.update = () => tip._update(); // tip.popper && tip.popper.scheduleUpdate();
tip.show = () => this.show(tip);
tip.hide = () => this.hide(tip);
tip.rerender = () => {
if ( tip.visible ) {
this.hide(tip);
this.show(tip);
}
}
let content = maybe_call(opts.content, null, target, tip);
if ( content === undefined )
content = tip.target.title;
if ( tip.visible || (! content && ! opts.onShow) )
return;
// Build the DOM.
const arrow = createElement('div', opts.arrowClass),
inner = tip.element = createElement('div', opts.innerClass),
el = tip.outer = createElement('div', {
className: opts.tooltipClass
}, [inner, arrow]);
arrow.setAttribute('x-arrow', true);
if ( opts.arrowInner )
arrow.appendChild(createElement('div', opts.arrowInner));
if ( tip.add_class ) {
inner.classList.add(tip.add_class);
tip.add_class = undefined;
}
const interactive = maybe_call(opts.interactive, null, target, tip);
el.classList.toggle('interactive', interactive || false);
if ( ! opts.manual ) {
el.addEventListener('mouseover', el._ffz_over_handler = () => {
if ( ! document.contains(target) )
this.hide(tip);
else if ( maybe_call(opts.interactive, null, target, tip) )
this._enter(target);
else
this._exit(target);
});
el.addEventListener('mouseout', el._ffz_out_handler = () => this._exit(target));
}
// Assign our content. If there's a Promise, we'll need
// to do this weirdly.
const use_html = maybe_call(opts.html, null, target, tip),
setter = use_html ? 'innerHTML' : 'textContent';
const pop_opts = Object.assign({
modifiers: {
flip: {
behavior: ['top', 'bottom', 'left', 'right']
}
},
arrowElement: arrow
}, opts.popper);
tip._update = () => {
if ( tip.popper ) {
tip.popper.destroy();
tip.popper = new Popper(target, el, pop_opts);
}
}
if ( content instanceof Promise || (content.then && content.toString() === '[object Promise]') ) {
inner.innerHTML = '<div class="ffz-i-zreknarf loader"></div>';
content.then(content => {
if ( ! content )
return this.hide(tip);
if ( use_html && (content instanceof Node || Array.isArray(content)) ) {
inner.innerHTML = '';
setChildren(inner, content, opts.sanitizeChildren);
} else
inner[setter] = content;
tip._update();
}).catch(err => {
if ( this.options.logger )
this.options.logger.error('Error rendering tooltip content.', err);
inner.textContent = `There was an error showing this tooltip.\n${err}`;
tip._update();
});
} else if ( content ) {
if ( use_html && (content instanceof Node || Array.isArray(content)) )
setChildren(inner, content, opts.sanitizeChildren);
else
inner[setter] = content;
}
// Add everything to the DOM and create the Popper instance.
tip.popper = new Popper(target, el, pop_opts);
this.parent.appendChild(el);
tip.visible = true;
if ( opts.onShow )
opts.onShow(target, tip);
}
hide(tip) { // eslint-disable-line class-methods-use-this
const opts = this.options;
if ( opts.onHide )
opts.onHide(tip.target, tip);
if ( tip.popper ) {
tip.popper.destroy();
tip.popper = null;
}
if ( tip.outer ) {
const el = tip.outer;
if ( el._ffz_over_handler )
el.removeEventListener('mouseover', el._ffz_over_handler);
if ( el._ffz_out_handler )
el.removeEventListener('mouseout', el._ffz_out_handler);
el.remove();
tip.outer = el._ffz_out_handler = el._ffz_over_handler = null;
}
if ( this.live )
this.elements.delete(tip.target);
tip._update = tip.rerender = tip.update = noop;
tip.element = null;
tip.visible = false;
}
}
export default Tooltip;
// Function Intentionally Left Blank
function noop() { }