nly-adminlte-vue
Version:
nly adminlte3 components
945 lines (927 loc) • 32.5 kB
JavaScript
// Tooltip "Class" (Built as a renderless Vue instance)
//
// Handles trigger events, etc.
// Instantiates template on demand
import Vue from "../../../utils/vue";
import getScopId from "../../../utils/get-scope-id";
import looseEqual from "../../../utils/loose-equal";
import { arrayIncludes, concat, from as arrayFrom } from "../../../utils/array";
import {
closest,
contains,
getAttr,
getById,
hasAttr,
hasClass,
isDisabled,
isElement,
isVisible,
removeAttr,
select,
setAttr
} from "../../../utils/dom";
import {
EVENT_OPTIONS_NO_CAPTURE,
eventOn,
eventOff,
eventOnOff
} from "../../../utils/events";
import {
isFunction,
isNumber,
isPlainObject,
isString,
isUndefined,
isUndefinedOrNull
} from "../../../utils/inspect";
import { keys } from "../../../utils/object";
import { warn } from "../../../utils/warn";
import { NlyEvent } from "../../../utils/nly-event.class";
import { NlyaTooltipTemplate } from "./nlya-tooltip-template";
const noop = () => {};
const NAME = "NlyaTooltip";
// Modal container selector for appending tooltip/popover
const MODAL_SELECTOR = ".modal-content";
// Modal `$root` hidden event
const MODAL_CLOSE_EVENT = "nlya::modal::hidden";
// For dropdown sniffing
const DROPDOWN_CLASS = "dropdown";
const DROPDOWN_OPEN_SELECTOR = ".dropdown-menu.show";
// Data specific to popper and template
// We don't use props, as we need reactivity (we can't pass reactive props)
const templateData = {
// Text string or Scoped slot function
title: "",
// Text string or Scoped slot function
content: "",
// String
variant: null,
// String, Array, Object
customClass: null,
triggers: "",
placement: "auto",
// String or array of strings
fallbackPlacement: "flip",
// Element or Component reference (or function that returns element) of
// the element that will have the trigger events bound, and is also
// default element for positioning
target: null,
// HTML ID, Element or Component reference
container: null, // 'body'
// Boolean
noFade: false,
// 'scrollParent', 'viewport', 'window', Element, or Component reference
boundary: "scrollParent",
// Tooltip/popover will try and stay away from
// boundary edge by this many pixels (Number)
boundaryPadding: 5,
// Arrow offset (Number)
offset: 0,
// Hover/focus delay (Number or Object)
delay: 0,
// Arrow of Tooltip/popover will try and stay away from
// the edge of tooltip/popover edge by this many pixels
arrowPadding: 6,
// Interactive state (Boolean)
interactive: true,
// Disabled state (Boolean)
disabled: false,
// ID to use for tooltip/popover
id: null,
// Flag used by directives only, for HTML content
html: false
};
// @vue/component
export const NlyaTooltip = Vue.extend({
name: NAME,
props: {
// None
},
data() {
return {
// BTooltip/BPopover/VBTooltip/VBPopover will update this data
// Via the exposed updateData() method on this instance
...templateData,
// State management data
activeTrigger: {
// manual: false,
hover: false,
click: false,
focus: false
},
localShow: false
};
},
computed: {
templateType() {
return "tooltip";
},
computedId() {
return this.id || `__nly_${this.templateType}_${this._uid}__`;
},
computedDelay() {
// Normalizes delay into object form
const delay = { show: 0, hide: 0 };
if (isPlainObject(this.delay)) {
delay.show = Math.max(parseInt(this.delay.show, 10) || 0, 0);
delay.hide = Math.max(parseInt(this.delay.hide, 10) || 0, 0);
} else if (isNumber(this.delay) || isString(this.delay)) {
delay.show = delay.hide = Math.max(parseInt(this.delay, 10) || 0, 0);
}
return delay;
},
computedTriggers() {
// Returns the triggers in sorted array form
// TODO: Switch this to object form for easier lookup
return concat(this.triggers)
.filter(Boolean)
.join(" ")
.trim()
.toLowerCase()
.split(/\s+/)
.sort();
},
isWithActiveTrigger() {
for (const trigger in this.activeTrigger) {
if (this.activeTrigger[trigger]) {
return true;
}
}
return false;
},
computedTemplateData() {
return {
title: this.title,
content: this.content,
variant: this.variant,
customClass: this.customClass,
noFade: this.noFade,
interactive: this.interactive
};
}
},
watch: {
computedTriggers(newTriggers, oldTriggers) {
// Triggers have changed, so re-register them
/* istanbul ignore next */
if (!looseEqual(newTriggers, oldTriggers)) {
this.$nextTick(() => {
// Disable trigger listeners
this.unListen();
// Clear any active triggers that are no longer in the list of triggers
oldTriggers.forEach(trigger => {
if (!arrayIncludes(newTriggers, trigger)) {
if (this.activeTrigger[trigger]) {
this.activeTrigger[trigger] = false;
}
}
});
// Re-enable the trigger listeners
this.listen();
});
}
},
computedTemplateData() {
// If any of the while open reactive "props" change,
// ensure that the template updates accordingly
this.handleTemplateUpdate();
},
disabled(newVal) {
newVal ? this.disable() : this.enable();
}
},
created() {
// Create non-reactive properties
this.$_tip = null;
this.$_hoverTimeout = null;
this.$_hoverState = "";
this.$_visibleInterval = null;
this.$_enabled = !this.disabled;
this.$_noop = noop.bind(this);
// Destroy ourselves when the parent is destroyed
if (this.$parent) {
this.$parent.$once("hook:beforeDestroy", this.$destroy);
}
this.$nextTick(() => {
const target = this.getTarget();
if (target && contains(document.body, target)) {
// Copy the parent's scoped style attribute
this.scopeId = getScopId(this.$parent);
// Set up all trigger handlers and listeners
this.listen();
} else {
/* istanbul ignore next */
warn("Unable to find target element in document.", this.templateType);
}
});
},
updated() /* istanbul ignore next */ {
// Usually called when the slots/data changes
this.$nextTick(this.handleTemplateUpdate);
},
deactivated() /* istanbul ignore next */ {
// In a keepalive that has been deactivated, so hide
// the tooltip/popover if it is showing
this.forceHide();
},
beforeDestroy() /* istanbul ignore next */ {
// Remove all handler/listeners
this.unListen();
this.setWhileOpenListeners(false);
// Clear any timeouts/intervals
this.clearHoverTimeout();
this.clearVisibilityInterval();
// Destroy the template
this.destroyTemplate();
},
methods: {
// --- Methods for creating and destroying the template ---
getTemplate() {
return NlyaTooltipTemplate;
},
updateData(data = {}) {
// Method for updating popper/template data
// We only update data if it exists, and has not changed
let titleUpdated = false;
keys(templateData).forEach(prop => {
if (!isUndefined(data[prop]) && this[prop] !== data[prop]) {
this[prop] = data[prop];
if (prop === "title") {
titleUpdated = true;
}
}
});
if (titleUpdated && this.localShow) {
// If the title has updated, we may need to handle the title
// attribute on the trigger target. We only do this while the
// template is open
this.fixTitle();
}
},
createTemplateAndShow() {
// Creates the template instance and show it
const container = this.getContainer();
const Template = this.getTemplate();
const $tip = (this.$_tip = new Template({
parent: this,
// The following is not reactive to changes in the props data
propsData: {
// These values cannot be changed while template is showing
id: this.computedId,
html: this.html,
placement: this.placement,
fallbackPlacement: this.fallbackPlacement,
target: this.getPlacementTarget(),
boundary: this.getBoundary(),
// Ensure the following are integers
offset: parseInt(this.offset, 10) || 0,
arrowPadding: parseInt(this.arrowPadding, 10) || 0,
boundaryPadding: parseInt(this.boundaryPadding, 10) || 0
}
}));
// We set the initial reactive data (values that can be changed while open)
this.handleTemplateUpdate();
// Template transition phase events (handled once only)
// When the template has mounted, but not visibly shown yet
$tip.$once("show", this.onTemplateShow);
// When the template has completed showing
$tip.$once("shown", this.onTemplateShown);
// When the template has started to hide
$tip.$once("hide", this.onTemplateHide);
// When the template has completed hiding
$tip.$once("hidden", this.onTemplateHidden);
// When the template gets destroyed for any reason
$tip.$once("hook:destroyed", this.destroyTemplate);
// Convenience events from template
// To save us from manually adding/removing DOM
// listeners to tip element when it is open
$tip.$on("focusin", this.handleEvent);
$tip.$on("focusout", this.handleEvent);
$tip.$on("mouseenter", this.handleEvent);
$tip.$on("mouseleave", this.handleEvent);
// Mount (which triggers the `show`)
$tip.$mount(container.appendChild(document.createElement("div")));
// Template will automatically remove its markup from DOM when hidden
},
hideTemplate() {
// Trigger the template to start hiding
// The template will emit the `hide` event after this and
// then emit the `hidden` event once it is fully hidden
// The `hook:destroyed` will also be called (safety measure)
this.$_tip && this.$_tip.hide();
// Clear out any stragging active triggers
this.clearActiveTriggers();
// Reset the hover state
this.$_hoverState = "";
},
// Destroy the template instance and reset state
destroyTemplate() {
this.setWhileOpenListeners(false);
this.clearHoverTimeout();
this.$_hoverState = "";
this.clearActiveTriggers();
this.localPlacementTarget = null;
try {
this.$_tip && this.$_tip.$destroy();
// eslint-disable-next-line no-empty
} catch {}
this.$_tip = null;
this.removeAriaDescribedby();
this.restoreTitle();
this.localShow = false;
},
getTemplateElement() {
return this.$_tip ? this.$_tip.$el : null;
},
handleTemplateUpdate() {
// Update our template title/content "props"
// So that the template updates accordingly
const $tip = this.$_tip;
if ($tip) {
const props = [
"title",
"content",
"variant",
"customClass",
"noFade",
"interactive"
];
// Only update the values if they have changed
props.forEach(prop => {
if ($tip[prop] !== this[prop]) {
$tip[prop] = this[prop];
}
});
}
},
// --- Show/Hide handlers ---
// Show the tooltip
show() {
const target = this.getTarget();
if (
!target ||
!contains(document.body, target) ||
!isVisible(target) ||
this.dropdownOpen() ||
((isUndefinedOrNull(this.title) || this.title === "") &&
(isUndefinedOrNull(this.content) || this.content === ""))
) {
// If trigger element isn't in the DOM or is not visible, or
// is on an open dropdown toggle, or has no content, then
// we exit without showing
return;
}
// If tip already exists, exit early
if (this.$_tip || this.localShow) {
/* istanbul ignore next */
return;
}
// In the process of showing
this.localShow = true;
const showEvt = this.buildEvent("show", { cancelable: true });
this.emitEvent(showEvt);
// Don't show if event cancelled
/* istanbul ignore next: ignore for now */
if (showEvt.defaultPrevented) {
// Destroy the template (if for some reason it was created)
/* istanbul ignore next */
this.destroyTemplate();
/* istanbul ignore next */
return;
}
// Fix the title attribute on target
this.fixTitle();
// Set aria-describedby on target
this.addAriaDescribedby();
// Create and show the tooltip
this.createTemplateAndShow();
},
hide(force = false) {
// Hide the tooltip
const tip = this.getTemplateElement();
if (!tip || !this.localShow) {
/* istanbul ignore next */
this.restoreTitle();
/* istanbul ignore next */
return;
}
// We disable cancelling if `force` is true
const hideEvt = this.buildEvent("hide", { cancelable: !force });
this.emitEvent(hideEvt);
/* istanbul ignore next: ignore for now */
if (hideEvt.defaultPrevented) {
// Don't hide if event cancelled
/* istanbul ignore next */
return;
}
// Tell the template to hide
this.hideTemplate();
},
forceHide() {
// Forcefully hides/destroys the template, regardless of any active triggers
const tip = this.getTemplateElement();
if (!tip || !this.localShow) {
/* istanbul ignore next */
return;
}
// Disable while open listeners/watchers
// This is also done in the template `hide` evt handler
this.setWhileOpenListeners(false);
// Clear any hover enter/leave event
this.clearHoverTimeout();
this.$_hoverState = "";
this.clearActiveTriggers();
// Disable the fade animation on the template
if (this.$_tip) {
this.$_tip.noFade = true;
}
// Hide the tip (with force = true)
this.hide(true);
},
enable() {
this.$_enabled = true;
this.emitEvent(this.buildEvent("enabled"));
},
disable() {
this.$_enabled = false;
this.emitEvent(this.buildEvent("disabled"));
},
// --- Handlers for template events ---
// When template is inserted into DOM, but not yet shown
onTemplateShow() {
// Enable while open listeners/watchers
this.setWhileOpenListeners(true);
},
// When template show transition completes
onTemplateShown() {
const prevHoverState = this.$_hoverState;
this.$_hoverState = "";
if (prevHoverState === "out") {
this.leave(null);
}
this.emitEvent(this.buildEvent("shown"));
},
// When template is starting to hide
onTemplateHide() {
// Disable while open listeners/watchers
this.setWhileOpenListeners(false);
},
// When template has completed closing (just before it self destructs)
onTemplateHidden() {
// Destroy the template
this.destroyTemplate();
this.emitEvent(this.buildEvent("hidden"));
},
// --- Utility methods ---
getTarget() {
// Handle case where target may be a component ref
let target = this.target ? this.target.$el || this.target : null;
// If an ID
target = isString(target) ? getById(target.replace(/^#/, "")) : target;
// If a function
target = isFunction(target) ? target() : target;
// If an element ref
return isElement(target) ? target : null;
},
getPlacementTarget() {
// This is the target that the tooltip will be placed on, which may not
// necessarily be the same element that has the trigger event listeners
// For now, this is the same as target
// TODO:
// Add in child selector support
// Add in visibility checks for this element
// Fallback to target if not found
return this.getTarget();
},
getTargetId() {
// Returns the ID of the trigger element
const target = this.getTarget();
return target && target.id ? target.id : null;
},
getContainer() {
// Handle case where container may be a component ref
const container = this.container
? this.container.$el || this.container
: false;
const body = document.body;
const target = this.getTarget();
// If we are in a modal, we append to the modal instead
// of body, unless a container is specified
// TODO:
// Template should periodically check to see if it is in dom
// And if not, self destruct (if container got v-if'ed out of DOM)
// Or this could possibly be part of the visibility check
return container === false
? closest(MODAL_SELECTOR, target) || body
: isString(container)
? getById(container.replace(/^#/, "")) || body
: body;
},
getBoundary() {
return this.boundary
? this.boundary.$el || this.boundary
: "scrollParent";
},
isInModal() {
const target = this.getTarget();
return target && closest(MODAL_SELECTOR, target);
},
isDropdown() {
// Returns true if trigger is a dropdown
const target = this.getTarget();
return target && hasClass(target, DROPDOWN_CLASS);
},
dropdownOpen() {
// Returns true if trigger is a dropdown and the dropdown menu is open
const target = this.getTarget();
return (
this.isDropdown() && target && select(DROPDOWN_OPEN_SELECTOR, target)
);
},
clearHoverTimeout() {
if (this.$_hoverTimeout) {
clearTimeout(this.$_hoverTimeout);
this.$_hoverTimeout = null;
}
},
clearVisibilityInterval() {
if (this.$_visibleInterval) {
clearInterval(this.$_visibleInterval);
this.$_visibleInterval = null;
}
},
clearActiveTriggers() {
for (const trigger in this.activeTrigger) {
this.activeTrigger[trigger] = false;
}
},
addAriaDescribedby() {
// Add aria-describedby on trigger element, without removing any other IDs
const target = this.getTarget();
let desc = getAttr(target, "aria-describedby") || "";
desc = desc
.split(/\s+/)
.concat(this.computedId)
.join(" ")
.trim();
// Update/add aria-described by
setAttr(target, "aria-describedby", desc);
},
removeAriaDescribedby() {
// Remove aria-describedby on trigger element, without removing any other IDs
const target = this.getTarget();
let desc = getAttr(target, "aria-describedby") || "";
desc = desc
.split(/\s+/)
.filter(d => d !== this.computedId)
.join(" ")
.trim();
// Update or remove aria-describedby
if (desc) {
/* istanbul ignore next */
setAttr(target, "aria-describedby", desc);
} else {
removeAttr(target, "aria-describedby");
}
},
fixTitle() {
// If the target has a title attribute, null it out and
// store on data-title
const target = this.getTarget();
if (target && getAttr(target, "title")) {
// We only update title attribute if it has a value
setAttr(target, "data-original-title", getAttr(target, "title") || "");
setAttr(target, "title", "");
}
},
restoreTitle() {
// If target had a title, restore the title attribute
// and remove the data-title attribute
const target = this.getTarget();
if (target && hasAttr(target, "data-original-title")) {
setAttr(target, "title", getAttr(target, "data-original-title") || "");
removeAttr(target, "data-original-title");
}
},
buildEvent(type, options = {}) {
// Defaults to a non-cancellable event
return new NlyEvent(type, {
cancelable: false,
target: this.getTarget(),
relatedTarget: this.getTemplateElement() || null,
componentId: this.computedId,
vueTarget: this,
// Add in option overrides
...options
});
},
emitEvent(nlyaEvt) {
const evtName = nlyaEvt.type;
const $root = this.$root;
if ($root && $root.$emit) {
// Emit an event on $root
$root.$emit(`nlya::${this.templateType}::${evtName}`, nlyaEvt);
}
this.$emit(evtName, nlyaEvt);
},
// --- Event handler setup methods ---
listen() {
// Enable trigger event handlers
const el = this.getTarget();
if (!el) {
/* istanbul ignore next */
return;
}
// Listen for global show/hide events
this.setRootListener(true);
// Set up our listeners on the target trigger element
this.computedTriggers.forEach(trigger => {
if (trigger === "click") {
eventOn(el, "click", this.handleEvent, EVENT_OPTIONS_NO_CAPTURE);
} else if (trigger === "focus") {
eventOn(el, "focusin", this.handleEvent, EVENT_OPTIONS_NO_CAPTURE);
eventOn(el, "focusout", this.handleEvent, EVENT_OPTIONS_NO_CAPTURE);
} else if (trigger === "blur") {
// Used to close $tip when element looses focus
/* istanbul ignore next */
eventOn(el, "focusout", this.handleEvent, EVENT_OPTIONS_NO_CAPTURE);
} else if (trigger === "hover") {
eventOn(el, "mouseenter", this.handleEvent, EVENT_OPTIONS_NO_CAPTURE);
eventOn(el, "mouseleave", this.handleEvent, EVENT_OPTIONS_NO_CAPTURE);
}
}, this);
},
unListen() /* istanbul ignore next */ {
// Remove trigger event handlers
const events = [
"click",
"focusin",
"focusout",
"mouseenter",
"mouseleave"
];
const target = this.getTarget();
// Stop listening for global show/hide/enable/disable events
this.setRootListener(false);
// Clear out any active target listeners
events.forEach(evt => {
target &&
eventOff(target, evt, this.handleEvent, EVENT_OPTIONS_NO_CAPTURE);
}, this);
},
setRootListener(on) {
const $root = this.$root;
if ($root) {
const method = on ? "$on" : "$off";
const type = this.templateType;
$root[method](`nlya::hide::${type}`, this.doHide);
$root[method](`nlya::show::${type}`, this.doShow);
$root[method](`nlya::disable::${type}`, this.doDisable);
$root[method](`nlya::enable::${type}`, this.doEnable);
}
},
setWhileOpenListeners(on) {
// Events that are only registered when the template is showing
// Modal close events
this.setModalListener(on);
// Dropdown open events (if we are attached to a dropdown)
this.setDropdownListener(on);
// Periodic $element visibility check
// For handling when tip target is in <keepalive>, tabs, carousel, etc
this.visibleCheck(on);
// On-touch start listeners
this.setOnTouchStartListener(on);
},
// Handler for periodic visibility check
visibleCheck(on) {
this.clearVisibilityInterval();
const target = this.getTarget();
const tip = this.getTemplateElement();
if (on) {
this.$_visibleInterval = setInterval(() => {
if (
tip &&
this.localShow &&
(!target.parentNode || !isVisible(target))
) {
// Target element is no longer visible or not in DOM, so force-hide the tooltip
this.forceHide();
}
}, 100);
}
},
setModalListener(on) {
// Handle case where tooltip/target is in a modal
if (this.isInModal()) {
// We can listen for modal hidden events on `$root`
this.$root[on ? "$on" : "$off"](MODAL_CLOSE_EVENT, this.forceHide);
}
},
setOnTouchStartListener(
on
) /* istanbul ignore next: JSDOM doesn't support `ontouchstart` */ {
// If this is a touch-enabled device we add extra empty
// `mouseover` listeners to the body's immediate children
// Only needed because of broken event delegation on iOS
// https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html
if ("ontouchstart" in document.documentElement) {
arrayFrom(document.body.children).forEach(el => {
eventOnOff(on, el, "mouseover", this.$_noop);
});
}
},
setDropdownListener(on) {
const target = this.getTarget();
if (!target || !this.$root || !this.isDropdown) {
return;
}
// We can listen for dropdown shown events on its instance
// TODO:
// We could grab the ID from the dropdown, and listen for
// $root events for that particular dropdown id
// Dropdown shown and hidden events will need to emit
// Note: Dropdown auto-ID happens in a `$nextTick()` after mount
// So the ID lookup would need to be done in a `$nextTick()`
if (target.__vue__) {
target.__vue__[on ? "$on" : "$off"]("shown", this.forceHide);
}
},
// --- Event handlers ---
handleEvent(evt) {
// General trigger event handler
// target is the trigger element
const target = this.getTarget();
if (
!target ||
isDisabled(target) ||
!this.$_enabled ||
this.dropdownOpen()
) {
// If disabled or not enabled, or if a dropdown that is open, don't do anything
// If tip is shown before element gets disabled, then tip will not
// close until no longer disabled or forcefully closed
return;
}
const type = evt.type;
const triggers = this.computedTriggers;
if (type === "click" && arrayIncludes(triggers, "click")) {
this.click(evt);
} else if (type === "mouseenter" && arrayIncludes(triggers, "hover")) {
// `mouseenter` is a non-bubbling event
this.enter(evt);
} else if (type === "focusin" && arrayIncludes(triggers, "focus")) {
// `focusin` is a bubbling event
// `evt` includes `relatedTarget` (element loosing focus)
this.enter(evt);
} else if (
(type === "focusout" &&
(arrayIncludes(triggers, "focus") ||
arrayIncludes(triggers, "blur"))) ||
(type === "mouseleave" && arrayIncludes(triggers, "hover"))
) {
// `focusout` is a bubbling event
// `mouseleave` is a non-bubbling event
// `tip` is the template (will be null if not open)
const tip = this.getTemplateElement();
// `evtTarget` is the element which is loosing focus/hover and
const evtTarget = evt.target;
// `relatedTarget` is the element gaining focus/hover
const relatedTarget = evt.relatedTarget;
/* istanbul ignore next */
if (
// From tip to target
(tip &&
contains(tip, evtTarget) &&
contains(target, relatedTarget)) ||
// From target to tip
(tip &&
contains(target, evtTarget) &&
contains(tip, relatedTarget)) ||
// Within tip
(tip && contains(tip, evtTarget) && contains(tip, relatedTarget)) ||
// Within target
(contains(target, evtTarget) && contains(target, relatedTarget))
) {
// If focus/hover moves within `tip` and `target`, don't trigger a leave
return;
}
// Otherwise trigger a leave
this.leave(evt);
}
},
doHide(id) {
// Programmatically hide tooltip or popover
if (!id || this.getTargetId() === id || this.computedId === id) {
// Close all tooltips or popovers, or this specific tip (with ID)
this.forceHide();
}
},
doShow(id) {
// Programmatically show tooltip or popover
if (!id || this.getTargetId() === id || this.computedId === id) {
// Open all tooltips or popovers, or this specific tip (with ID)
this.show();
}
},
doDisable(id) /*istanbul ignore next: ignore for now */ {
// Programmatically disable tooltip or popover
if (!id || this.getTargetId() === id || this.computedId === id) {
// Disable all tooltips or popovers (no ID), or this specific tip (with ID)
this.disable();
}
},
doEnable(id) /*istanbul ignore next: ignore for now */ {
// Programmatically enable tooltip or popover
if (!id || this.getTargetId() === id || this.computedId === id) {
// Enable all tooltips or popovers (no ID), or this specific tip (with ID)
this.enable();
}
},
click() {
if (!this.$_enabled || this.dropdownOpen()) {
/* istanbul ignore next */
return;
}
this.activeTrigger.click = !this.activeTrigger.click;
if (this.isWithActiveTrigger) {
this.enter(null);
} else {
/* istanbul ignore next */
this.leave(null);
}
},
toggle() /* istanbul ignore next */ {
// Manual toggle handler
if (!this.$_enabled || this.dropdownOpen()) {
/* istanbul ignore next */
return;
}
// Should we register as an active trigger?
// this.activeTrigger.manual = !this.activeTrigger.manual
if (this.localShow) {
this.leave(null);
} else {
this.enter(null);
}
},
enter(evt = null) {
// Opening trigger handler
// Note: Click events are sent with evt === null
if (evt) {
this.activeTrigger[evt.type === "focusin" ? "focus" : "hover"] = true;
}
/* istanbul ignore next */
if (this.localShow || this.$_hoverState === "in") {
this.$_hoverState = "in";
return;
}
this.clearHoverTimeout();
this.$_hoverState = "in";
if (!this.computedDelay.show) {
this.show();
} else {
// Hide any title attribute while enter delay is active
this.fixTitle();
this.$_hoverTimeout = setTimeout(() => {
/* istanbul ignore else */
if (this.$_hoverState === "in") {
this.show();
} else if (!this.localShow) {
this.restoreTitle();
}
}, this.computedDelay.show);
}
},
leave(evt = null) {
// Closing trigger handler
// Note: Click events are sent with evt === null
if (evt) {
this.activeTrigger[evt.type === "focusout" ? "focus" : "hover"] = false;
/* istanbul ignore next */
if (
evt.type === "focusout" &&
arrayIncludes(this.computedTriggers, "blur")
) {
// Special case for `blur`: we clear out the other triggers
this.activeTrigger.click = false;
this.activeTrigger.hover = false;
}
}
/* istanbul ignore next: ignore for now */
if (this.isWithActiveTrigger) {
return;
}
this.clearHoverTimeout();
this.$_hoverState = "out";
if (!this.computedDelay.hide) {
this.hide();
} else {
this.$_hoverTimeout = setTimeout(() => {
if (this.$_hoverState === "out") {
this.hide();
}
}, this.computedDelay.hide);
}
}
}
});