diff --git a/package.json b/package.json index d41bf6bd..7a1a3b5e 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "frankerfacez", "author": "Dan Salvato LLC", - "version": "4.20.10", + "version": "4.20.11", "description": "FrankerFaceZ is a Twitch enhancement suite.", "license": "Apache-2.0", "scripts": { diff --git a/src/modules/chat/actions/index.jsx b/src/modules/chat/actions/index.jsx index 2c05a847..b42dd576 100644 --- a/src/modules/chat/actions/index.jsx +++ b/src/modules/chat/actions/index.jsx @@ -334,7 +334,7 @@ export default class Actions extends Module { target._ffz_destroy = target._ffz_outside = target._ffz_on_destroy = null; } - const parent = document.body.querySelector('#root>div') || document.body, + const parent = document.fullscreenElement || document.body.querySelector('#root>div') || document.body, tt = target._ffz_popup = new Tooltip(parent, target, { logger: this.log, manual: true, @@ -807,8 +807,8 @@ export default class Actions extends Module { return this.log.warn(`No click handler for action provider "${data.action}"`); } - if ( target._ffz_tooltip$0 ) - target._ffz_tooltip$0.hide(); + if ( target._ffz_tooltip ) + target._ffz_tooltip.hide(); return data.definition.click.call(this, event, data); } @@ -827,8 +827,8 @@ export default class Actions extends Module { if ( target.classList.contains('disabled') ) return; - if ( target._ffz_tooltip$0 ) - target._ffz_tooltip$0.hide(); + if ( target._ffz_tooltip ) + target._ffz_tooltip.hide(); if ( ! data.definition.context && ! data.definition.uses_reason ) return; @@ -847,8 +847,8 @@ export default class Actions extends Module { event.preventDefault(); const target = event.target; - if ( target._ffz_tooltip$0 ) - target._ffz_tooltip$0.hide(); + if ( target._ffz_tooltip ) + target._ffz_tooltip.hide(); this.renderUserContext(target, actions); } diff --git a/src/modules/chat/emotes.js b/src/modules/chat/emotes.js index 7e18cbaa..baa13ec6 100644 --- a/src/modules/chat/emotes.js +++ b/src/modules/chat/emotes.js @@ -386,7 +386,7 @@ export default class Emotes extends Module { return; this.toggleFavorite(source, id); - const tt = target._ffz_tooltip$0; + const tt = target._ffz_tooltip; if ( tt && tt.visible ) { tt.hide(); setTimeout(() => document.contains(target) && tt.show(), 0); diff --git a/src/modules/chat/overrides.js b/src/modules/chat/overrides.js index a3b77a54..a7354323 100644 --- a/src/modules/chat/overrides.js +++ b/src/modules/chat/overrides.js @@ -56,7 +56,7 @@ export default class Overrides extends Module { v.$destroy(); } - const parent = document.body.querySelector('#root>div') || document.body; + const parent = document.fullscreenElement || document.body.querySelector('#root>div') || document.body; popup = new Tooltip(parent, [], { logger: this.log, diff --git a/src/modules/main_menu/components/profile-editor.vue b/src/modules/main_menu/components/profile-editor.vue index 09bdf89d..af6b0ebb 100644 --- a/src/modules/main_menu/components/profile-editor.vue +++ b/src/modules/main_menu/components/profile-editor.vue @@ -144,8 +144,10 @@ export default { props: ['item', 'context'], data() { + const settings = this.context.getFFZ().resolve('settings'); + return { - filters: deep_copy(require('src/settings/filters.js')), + filters: deep_copy(settings.filters), old_name: null, old_desc: null, diff --git a/src/modules/tooltips.js b/src/modules/tooltips.js index cb43f2bc..4b681d94 100644 --- a/src/modules/tooltips.js +++ b/src/modules/tooltips.js @@ -59,16 +59,30 @@ export default class TooltipProvider extends Module { this.types.text = target => sanitize(target.dataset.title); this.types.html = target => target.dataset.title; + + this.onFSChange = this.onFSChange.bind(this); } onEnable() { const container = document.querySelector('.sunlight-root') || document.querySelector('#root>div') || document.querySelector('#root') || document.querySelector('.clips-root') || document.body; + window.addEventListener('fullscreenchange', this.onFSChange); + // is_minimal = false; //container && container.classList.contains('twilight-minimal-root'); - this.tips = new Tooltip(container, 'ffz-tooltip', { + this.container = container; + this.tip_element = container; + this.tips = this._createInstance(container); + + this.on(':cleanup', this.cleanup); + } + + + _createInstance(container) { + return new Tooltip(container, 'ffz-tooltip', { html: true, i18n: this.i18n, + live: true, delayHide: this.checkDelayHide.bind(this), delayShow: this.checkDelayShow.bind(this), @@ -99,10 +113,20 @@ export default class TooltipProvider extends Module { this.emit(':leave', target, tip, event); } }); - - this.on(':cleanup', this.cleanup); } + + + onFSChange() { + const tip_element = document.fullscreenElement || this.container; + if ( tip_element !== this.tip_element ) { + this.tips.destroy(); + this.tip_element = tip_element; + this.tips = this._createInstance(tip_element); + } + } + + cleanup() { this.tips.cleanup(); } diff --git a/src/settings/index.js b/src/settings/index.js index 39407620..32da7753 100644 --- a/src/settings/index.js +++ b/src/settings/index.js @@ -12,6 +12,8 @@ import SettingsProfile from './profile'; import SettingsContext from './context'; import MigrationManager from './migration'; +import * as FILTERS from './filters'; + // ============================================================================ // SettingsManager @@ -38,12 +40,19 @@ export default class SettingsManager extends Module { this.ui_structures = new Map; this.definitions = new Map; + // Filters + this.filters = {}; + + for(const key in FILTERS) + if ( has(FILTERS, key) ) + this.filters[key] = FILTERS[key]; + + // Create our provider as early as possible. const provider = this.provider = this._createProvider(); this.log.info(`Using Provider: ${provider.constructor.name}`); provider.on('changed', this._onProviderChange, this); - this.migrations = new MigrationManager(this); // Also create the main context as early as possible. @@ -63,6 +72,20 @@ export default class SettingsManager extends Module { this.enable(); } + + addFilter(key, data) { + if ( this.filters[key] ) + return this.log.warn('Tried to add already existing filter', key); + + this.filters[key] = data; + this.updateRoutes(); + } + + getFilterBasicEditor() { // eslint-disable-line class-methods-use-this + return () => import(/* webpackChunkName: 'main-menu' */ './components/basic-toggle.vue') + } + + generateLog() { const out = []; for(const [key, value] of this.main_context.__cache.entries()) diff --git a/src/settings/profile.js b/src/settings/profile.js index c75a25d5..6706ea1f 100644 --- a/src/settings/profile.js +++ b/src/settings/profile.js @@ -58,7 +58,7 @@ export default class SettingsProfile extends EventEmitter { matches(context) { if ( ! this.matcher ) - this.matcher = createTester(this.context, require('./filters')); + this.matcher = createTester(this.context, this.manager.filters); return this.matcher(context); } diff --git a/src/sites/twitch-twilight/index.js b/src/sites/twitch-twilight/index.js index 807485e1..b53391df 100644 --- a/src/sites/twitch-twilight/index.js +++ b/src/sites/twitch-twilight/index.js @@ -12,6 +12,7 @@ import Fine from 'utilities/compat/fine'; import FineRouter from 'utilities/compat/fine-router'; import Apollo from 'utilities/compat/apollo'; import TwitchData from 'utilities/twitch-data'; +import Subpump from 'utilities/compat/subpump'; import Switchboard from './switchboard'; @@ -36,6 +37,7 @@ export default class Twilight extends BaseSite { this.inject(Apollo, false); this.inject(TwitchData); this.inject(Switchboard); + this.inject(Subpump); this._dom_updates = []; } diff --git a/src/sites/twitch-twilight/modules/channel.js b/src/sites/twitch-twilight/modules/channel.js index eb65b73d..87c2d341 100644 --- a/src/sites/twitch-twilight/modules/channel.js +++ b/src/sites/twitch-twilight/modules/channel.js @@ -7,6 +7,7 @@ import Module from 'utilities/module'; import { Color } from 'utilities/color'; import {debounce} from 'utilities/object'; +import { valueToNode } from 'C:/Users/Stendec/AppData/Local/Microsoft/TypeScript/3.8/node_modules/@babel/types/lib/index'; const USER_PAGES = ['user', 'video', 'user-video', 'user-clip', 'user-videos', 'user-clips', 'user-collections', 'user-events', 'user-followers', 'user-following']; @@ -22,6 +23,7 @@ export default class Channel extends Module { this.inject('settings'); this.inject('site.css_tweaks'); this.inject('site.elemental'); + this.inject('site.subpump'); this.inject('site.fine'); this.inject('site.router'); this.inject('site.twitch_data'); @@ -37,11 +39,16 @@ export default class Channel extends Module { } }); - /*this.SideNav = this.elemental.define( - 'side-nav', '.side-bar-contents .side-nav-section:first-child', - null, - {childNodes: true, subtree: true}, 1 - );*/ + this.settings.add('channel.hosting.enable', { + default: true, + ui: { + path: 'Channel > Behavior >> Hosting', + title: 'Enable Channel Hosting', + component: 'setting-check-box' + }, + changed: val => ! val && this.InfoBar.each(el => this.updateBar(el)) + }); + this.ChannelRoot = this.elemental.define( 'channel-root', '.channel-root', @@ -59,10 +66,6 @@ export default class Channel extends Module { onEnable() { this.updateChannelColor(); - //this.SideNav.on('mount', this.updateHidden, this); - //this.SideNav.on('mutate', this.updateHidden, this); - //this.SideNav.each(el => this.updateHidden(el)); - this.ChannelRoot.on('mount', this.updateRoot, this); this.ChannelRoot.on('mutate', this.updateRoot, this); this.ChannelRoot.on('unmount', this.removeRoot, this); @@ -73,10 +76,13 @@ export default class Channel extends Module { this.InfoBar.on('unmount', this.removeBar, this); this.InfoBar.each(el => this.updateBar(el)); + this.subpump.on(':pubsub-message', this.onPubSub, this); + this.router.on(':route', route => { if ( route?.name === 'user' ) setTimeout(this.maybeClickChat.bind(this), 1000); }, this); + this.maybeClickChat(); } @@ -88,21 +94,50 @@ export default class Channel extends Module { } } - /*updateHidden(el) { // eslint-disable-line class-methods-use-this - if ( ! el._ffz_raf ) - el._ffz_raf = requestAnimationFrame(() => { - el._ffz_raf = null; - const nodes = el.querySelectorAll('.side-nav-card'); - for(const node of nodes) { - const react = this.fine.getReactInstance(node), - props = react?.return?.return?.return?.memoizedProps; + setHost(channel_id, channel_login, target_id, target_login) { + const topic = `stream-chat-room-v1.${channel_id}`; - const offline = props?.offline ?? node.querySelector('.side-nav-card__avatar--offline') != null; - node.classList.toggle('ffz--offline-side-nav', offline); + this.subpump.inject(topic, { + type: 'host_target_change', + data: { + channel_id, + channel_login, + target_channel_id: target_id || null, + target_channel_login: target_login || null, + previous_target_channel_id: null, + num_viewers: 0 + } + }); + + this.subpump.inject(topic, { + type: 'host_target_change_v2', + data: { + channel_id, + channel_login, + target_channel_id: target_id || null, + target_channel_login: target_login || null, + previous_target_channel_id: null, + num_viewers: 0 + } + }); + } + + + onPubSub(event) { + if ( event.prefix !== 'stream-chat-room-v1' || this.settings.get('channel.hosting.enable') ) + return; + + const type = event.message.type; + if ( type === 'host_target_change' || type === 'host_target_change_v2' ) { + this.log.info('Nulling Host Target Change', type); + event.message.data.target_channel_id = null; + event.message.data.target_channel_login = null; + event.message.data.previous_target_channel_id = null; + event.message.data.num_viewers = 0; + event.markChanged(); + } + } - } - }); - }*/ updateSubscription(login) { if ( this._subbed_login === login ) @@ -154,6 +189,9 @@ export default class Channel extends Module { return; } + if ( ! this.settings.get('channel.hosting.enable') && props.hostLogin ) + this.setHost(props.channelID, props.channelLogin, null, null); + this.updateSubscription(props.channelLogin); this.updateMetadata(el); } diff --git a/src/sites/twitch-twilight/modules/chat/emote_menu.jsx b/src/sites/twitch-twilight/modules/chat/emote_menu.jsx index f9f4384f..05cff99c 100644 --- a/src/sites/twitch-twilight/modules/chat/emote_menu.jsx +++ b/src/sites/twitch-twilight/modules/chat/emote_menu.jsx @@ -1155,7 +1155,7 @@ export default class EmoteMenu extends Module { /*clickRefresh(event) { const target = event.currentTarget, - tt = target && target._ffz_tooltip$0; + tt = target && target._ffz_tooltip; if ( tt && tt.hide ) tt.hide(); diff --git a/src/sites/twitch-twilight/modules/directory/index.jsx b/src/sites/twitch-twilight/modules/directory/index.jsx index 28eadfac..c5f8e7f9 100644 --- a/src/sites/twitch-twilight/modules/directory/index.jsx +++ b/src/sites/twitch-twilight/modules/directory/index.jsx @@ -19,9 +19,9 @@ export const CARD_CONTEXTS = ((e ={}) => { })(); -const CREATIVE_ID = 488191; +//const CREATIVE_ID = 488191; -const DIR_ROUTES = ['dir', 'dir-community', 'dir-community-index', 'dir-creative', 'dir-following', 'dir-game-index', 'dir-game-clips', 'dir-game-videos', 'dir-all', 'dir-category', 'user-videos', 'user-clips']; +const DIR_ROUTES = ['front-page', 'dir', 'dir-community', 'dir-community-index', 'dir-creative', 'dir-following', 'dir-game-index', 'dir-game-clips', 'dir-game-videos', 'dir-all', 'dir-category', 'user-videos', 'user-clips']; export default class Directory extends SiteModule { @@ -53,7 +53,6 @@ export default class Directory extends SiteModule { DIR_ROUTES ); - this.settings.add('directory.hidden.style', { default: 2, @@ -133,6 +132,18 @@ export default class Directory extends SiteModule { changed: value => this.css_tweaks.toggleHide('dir-live-ind', value) }); + this.settings.add('directory.hide-promoted', { + default: false, + + ui: { + path: 'Directory > Channels >> Appearance', + title: 'Do not show Promoted streams in the directory.', + component: 'setting-check-box' + }, + + changed: () => this.updateCards() + }); + this.settings.add('directory.hide-vodcasts', { default: false, @@ -324,7 +335,8 @@ export default class Directory extends SiteModule { el.dataset.ffzType = props.streamType; const should_hide = (props.streamType === 'rerun' && this.settings.get('directory.hide-vodcasts')) || - (props.context != null && props.context !== CARD_CONTEXTS.SingleGameList && this.settings.provider.get('directory.game.blocked-games', []).includes(game)); + (props.context != null && props.context !== CARD_CONTEXTS.SingleGameList && this.settings.provider.get('directory.game.blocked-games', []).includes(game)) || + (props.sourceType === 'PROMOTION' && this.settings.get('directory.hide-promoted')); let hide_container = el.closest('.tw-tower > div'); if ( ! hide_container ) diff --git a/src/sites/twitch-twilight/modules/featured-follow.vue b/src/sites/twitch-twilight/modules/featured-follow.vue index 6f82a896..880b862b 100644 --- a/src/sites/twitch-twilight/modules/featured-follow.vue +++ b/src/sites/twitch-twilight/modules/featured-follow.vue @@ -92,7 +92,7 @@ export default { methods: { clickWithTip(event, fn, ...args) { const el = event.target, - tip = el && el._ffz_tooltip$0, + tip = el && el._ffz_tooltip, visible = tip && tip.visible; visible && tip.hide(); diff --git a/src/sites/twitch-twilight/modules/menu_button.jsx b/src/sites/twitch-twilight/modules/menu_button.jsx index 4fcebbc0..b0d3ba9a 100644 --- a/src/sites/twitch-twilight/modules/menu_button.jsx +++ b/src/sites/twitch-twilight/modules/menu_button.jsx @@ -434,8 +434,8 @@ export default class MenuButton extends SiteModule { setChildren(toggle, this.renderButtonIcon(profile)); toggle.dataset.title = this.renderButtonTip(profile); - if ( toggle['_ffz_tooltip$0']?.rerender ) - toggle['_ffz_tooltip$0'].rerender(); + if ( toggle['_ffz_tooltip']?.rerender ) + toggle['_ffz_tooltip'].rerender(); this.emit('tooltips:cleanup'); diff --git a/src/sites/twitch-twilight/modules/player.jsx b/src/sites/twitch-twilight/modules/player.jsx index 144751e2..1b4b14af 100644 --- a/src/sites/twitch-twilight/modules/player.jsx +++ b/src/sites/twitch-twilight/modules/player.jsx @@ -502,16 +502,6 @@ export default class Player extends Module { } }); - /*this.settings.add('player.hide-squad-banner', { - default: false, - ui: { - path: 'Channel > Appearance >> General', - title: 'Hide the Squad Streaming Bar', - component: 'setting-check-box' - }, - changed: () => this.SquadStreamBar.forceUpdate() - });*/ - this.settings.add('player.hide-mouse', { default: true, ui: { @@ -541,25 +531,6 @@ export default class Player extends Module { const t = this; - /*this.SquadStreamBar.ready(cls => { - const old_should_render = cls.prototype.shouldRenderSquadBanner; - - cls.prototype.shouldRenderSquadBanner = function(...args) { - if ( t.settings.get('player.hide-squad-banner') ) - return false; - - return old_should_render.call(this, ...args); - } - - this.SquadStreamBar.forceUpdate(); - this.updateSquadContext(); - }); - - this.SquadStreamBar.on('mount', this.updateSquadContext, this); - this.SquadStreamBar.on('update', this.updateSquadContext, this); - this.SquadStreamBar.on('unmount', this.updateSquadContext, this);*/ - - this.Player.ready((cls, instances) => { const old_attach = cls.prototype.maybeAttachDomEventListeners; @@ -677,24 +648,8 @@ export default class Player extends Module { } cls.prototype.ffzStopAutoplay = function() { - if ( t.settings.get('player.no-autoplay') || (! t.settings.get('player.home.autoplay') && t.router.current.name === 'front-page') ) { - const player = this.props.mediaPlayerInstance, - events = this.props.playerEvents; - - if ( player && player.pause && player.getPlayerState?.() === 'Playing' ) - player.pause(); - else if ( events ) { - const immediatePause = () => { - if ( this.props.mediaPlayerInstance?.pause ) { - this.props.mediaPlayerInstance.pause(); - off(events, 'Playing', immediatePause); - } - } - - t.log.info('Unable to immediately pause. Listening for playing event.'); - on(events, 'Playing', immediatePause); - } - } + if ( t.settings.get('player.no-autoplay') || (! t.settings.get('player.home.autoplay') && t.router.current.name === 'front-page') ) + this.stopPlayer(this.props.mediaPlayerInstance, this.props.playerEvents, this); } cls.prototype.ffzScheduleState = function() { @@ -858,6 +813,8 @@ export default class Player extends Module { this.tryTheatreMode(inst); }); + this.PlayerSource.on('mount', this.checkCarousel, this); + this.PlayerSource.on('update', this.checkCarousel, this); this.on('i18n:update', () => { for(const inst of this.Player.instances) { @@ -867,6 +824,46 @@ export default class Player extends Module { } + stopPlayer(player, events, inst) { + if ( player && player.pause && (player.getPlayerState?.() || player.core?.getPlayerState?.()) === 'Playing' ) + player.pause(); + else if ( events && ! events._ffz_stopping ) { + events._ffz_stopping = true; + + const immediatePause = () => { + if ( inst.props.mediaPlayerInstance?.pause ) { + inst.props.mediaPlayerInstance.pause(); + off(events, 'Playing', immediatePause); + events._ffz_stopping = false; + } + } + + this.log.info('Unable to immediately pause. Listening for playing event.'); + on(events, 'Playing', immediatePause); + } + } + + + checkCarousel(inst) { + if ( this.settings.get('channel.hosting.enable') ) + return; + + if ( inst.props?.playerType === 'channel_home_carousel' ) { + if ( inst.props.content?.hostChannel === inst._ffz_cached_login ) + return; + + inst._ffz_cached_login = inst.props.content?.hostChannel; + if ( ! inst._ffz_cached_login ) + return; + + const player = inst.props.mediaPlayerInstance, + events = inst.props.playerEvents; + + this.stopPlayer(player, events, inst); + } + } + + updateAutoPlaybackRate(inst, val) { const player = inst.props?.mediaPlayerInstance; if ( ! player ) diff --git a/src/utilities/compat/subpump.js b/src/utilities/compat/subpump.js new file mode 100644 index 00000000..638c8b0d --- /dev/null +++ b/src/utilities/compat/subpump.js @@ -0,0 +1,151 @@ +'use strict'; + +// ============================================================================ +// Subpump +// It controls Twitch PubSub. +// ============================================================================ + +import Module from 'utilities/module'; +import { FFZEvent } from 'utilities/events'; + +export class PubSubEvent extends FFZEvent { + constructor(data) { + super(data); + + this._obj = undefined; + this._changed = false; + } + + markChanged() { + this._changed = true; + } + + get topic() { + return this.event.topic; + } + + get message() { + if ( this._obj === undefined ) + this._obj = JSON.parse(this.event.message); + + return this._obj; + } + + set message(val) { + this._obj = val; + this._changed = true; + } +} + +export default class Subpump extends Module { + + constructor(...args) { + super(...args); + this.instance = null; + } + + onEnable(tries = 0) { + const instances = window.__Twitch__pubsubInstances; + if ( ! instances ) { + if ( tries > 10 ) + this.log.warn('Unable to find PubSub.'); + else + new Promise(r => setTimeout(r, 50)).then(() => this.onEnable(tries + 1)); + + return; + } + + for(const [key, val] of Object.entries(instances)) + if ( val?._client ) { + if ( this.instance ) { + this.log.warn('Multiple PubSub instances detected. Things might act weird.'); + continue; + } + + this.instance = val; + this.hookClient(val._client); + } + + if ( ! this.instance ) + this.log.warn('Unable to find a PubSub instance.'); + } + + hookClient(client) { + const t = this, + orig_message = client._onMessage; + + client._unbindPrimary(client._primarySocket); + + client._onMessage = function(e) { + try { + if ( e.type === 'MESSAGE' && e.data?.topic ) { + const raw_topic = e.data.topic, + idx = raw_topic.indexOf('.'), + prefix = idx === -1 ? raw_topic : raw_topic.slice(0, idx), + trail = idx === -1 ? '' : raw_topic.slice(idx + 1); + + const event = new PubSubEvent({ + prefix, + trail, + event: e.data + }); + + t.emit(':pubsub-message', event); + if ( event.defaultPrevented ) + return; + + if ( event._changed ) + e.data.message = JSON.stringify(event._obj); + } + + } catch(err) { + this.log.error('Error processing PubSub event.', err); + } + + return orig_message.call(this, e); + }; + + client._bindPrimary(client._primarySocket); + + const listener = client._listens, + orig_on = listener.on, + orig_off = listener.off; + + listener.on = function(topic, fn, ctx) { + const has_topic = !! listener._events?.[topic], + out = orig_on.call(this, topic, fn, ctx); + + if ( ! has_topic ) + t.emit(':add-topic', topic) + + return out; + } + + listener.off = function(topic, fn) { + const has_topic = !! listener._events?.[topic], + out = orig_off.call(this, topic, fn); + + if ( has_topic && ! listener._events?.[topic] ) + t.emit(':remove-topic', topic); + + return out; + } + } + + inject(topic, message) { + const listens = this.instance?._client?._listens; + if ( ! listens ) + throw new Error('No PubSub instance available'); + + listens._trigger(topic, JSON.stringify(message)); + } + + get topics() { + const events = this.instance?._client?._listens._events; + if ( ! events ) + return []; + + return Object.keys(events); + } + +} \ No newline at end of file diff --git a/src/utilities/tooltip.js b/src/utilities/tooltip.js index 0b35f903..002471ec 100644 --- a/src/utilities/tooltip.js +++ b/src/utilities/tooltip.js @@ -105,8 +105,8 @@ export class Tooltip { if ( this.options.manual ) { // Do nothing~! } else if ( this.live || this.elements.size > 5 ) { - parent.removeEventListener('mouseover', this._onMouseOver); - parent.removeEventListener('mouseout', this._onMouseOut); + this.parent.removeEventListener('mouseover', this._onMouseOver); + this.parent.removeEventListener('mouseout', this._onMouseOut); } else for(const el of this.elements) { el.removeEventListener('mouseenter', this._onMouseOver); @@ -119,6 +119,7 @@ export class Tooltip { this.hide(tip); el[this._accessor] = null; + el._ffz_tooltip = null; } this.elements = null; @@ -205,12 +206,18 @@ export class Tooltip { target = tip.target; this.elements.add(target); + target._ffz_tooltip = tip; // Set this early in case content uses it early. tip._promises = []; tip.waitForDom = () => tip.element ? Promise.resolve() : new Promise(s => {tip._promises.push(s)}); tip.update = () => tip._update(); // tip.popper && tip.popper.scheduleUpdate(); - tip.show = () => this.show(tip); + tip.show = () => { + let tip = target[this._accessor]; + if ( ! tip ) + tip = target[this._accessor] = {target}; + this.show(tip); + }; tip.hide = () => this.hide(tip); tip.rerender = () => { if ( tip.visible ) { @@ -385,6 +392,10 @@ export class Tooltip { if ( this.live && this.elements ) this.elements.delete(tip.target); + if ( tip.target._ffz_tooltip === tip ) + tip.target._ffz_tooltip = null; + + tip.target[this._accessor] = null; tip._update = tip.rerender = tip.update = noop; tip.element = null; tip.visible = false;