tippy.vue
Version:
Nesting-free Vue components for Tippy.js - a drop-in addition with no structural or css changes required
644 lines (631 loc) • 22.4 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('tippy.js'), require('vue')) :
typeof define === 'function' && define.amd ? define(['exports', 'tippy.js', 'vue'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.TippyVue = {}, global.tippy, global.Vue));
})(this, (function (exports, tippy, vue) { 'use strict';
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var tippy__default = /*#__PURE__*/_interopDefaultLegacy(tippy);
const _mode = Symbol("v-tippy mode");
const _tippy = Symbol("v-tippy instance");
function createOptions(value) {
if (typeof value === 'string') {
return { content: value };
}
else if (typeof value === 'undefined') {
return {};
}
else {
return value;
}
}
const TippyDirective = {
mounted(el, binding) {
if (binding.value === undefined) {
el[_mode] = 'target';
el.dataset.tippyTarget = binding.arg || "";
}
else {
el[_mode] = 'inline';
el[_tippy] = tippy__default["default"](el, createOptions(binding.value));
}
},
beforeUnmount(el) {
if (el[_mode] === 'inline') {
let tip = el[_tippy];
tip && tip.destroy();
}
else {
delete el.dataset.tippyTarget;
}
},
updated(el, binding) {
if (el[_mode] === 'inline') {
let tip = el[_tippy];
tip && tip.setProps(createOptions(binding.value));
}
}
};
/* eslint-disable @typescript-eslint/no-unused-vars */
const commonEmits = {
mount: (instance) => true,
show: (instance) => true,
shown: (instance) => true,
hidden: (instance) => true,
hide: (instance) => true,
trigger: (instance, event) => true,
untrigger: (instance, event) => true,
};
/**
* Infers the plugin type to provide type hinting for the parameter
*/
function inferPlugin(plugin) {
return plugin;
}
/**
* Creates a plugin that exposes a Tippy.js option as a Vue prop
* @param name The name of the Tippy.js option
* @param type The type of the Vue property (e.g. `String`, `Boolean`, etc.)
* @param def The default value, if any
*/
function optionPlugin(name, type, def) {
return {
props: {
[name]: {
type: type ? type : null,
required: false,
default: def,
},
},
build(props, options) {
if (props[name].value !== undefined) {
options[name] = props[name].value;
}
}
};
}
function commonSetup(props, plugins, baseContext, tip, hooks) {
const context = baseContext;
let refs = vue.toRefs(props);
const tippyOptions = vue.computed(() => {
const options = {};
for (const plugin of plugins) {
let buildFn = plugin.build;
if (buildFn)
buildFn(refs, options);
}
options.onShow = joinCallbacks(instance => context.emit("show", instance), hooks && hooks.onShow, options.onShow);
options.onShown = joinCallbacks(instance => context.emit("shown", instance), hooks && hooks.onShown, options.onShown);
options.onHidden = joinCallbacks(instance => context.emit("hidden", instance), hooks && hooks.onHidden, options.onHidden);
options.onHide = joinCallbacks(instance => context.emit("hide", instance), hooks && hooks.onHide, options.onHide);
options.onMount = joinCallbacks(instance => context.emit("mount", instance), hooks && hooks.onMount, options.onMount);
options.onTrigger = joinCallbacks((instance, event) => context.emit("trigger", instance, event), hooks && hooks.onTrigger, options.onTrigger);
options.onUntrigger = joinCallbacks((instance, event) => context.emit("untrigger", instance, event), hooks && hooks.onUntrigger, options.onUntrigger);
return options;
});
for (const plugin of plugins) {
let setupFn = plugin.setup;
if (setupFn)
setupFn(refs, tip);
}
vue.watch(tippyOptions, value => {
if (tip.value)
tip.value.setProps(value);
}, {
deep: true
});
return {
tippyOptions
};
}
function joinCallbacks(...callbacks) {
return (...args) => {
let result;
for (let callback of callbacks) {
if (callback)
result = callback(...args);
}
return result;
};
}
/**
* Extra options for tippy.js
*/
const extra = inferPlugin({
props: {
extra: {
type: Object,
required: false,
},
},
build(props, options) {
Object.assign(options, props.extra.value || {});
}
});
/**
* Whether the tooltip should be enabled
*/
const enabled = inferPlugin({
props: {
enabled: {
type: Boolean,
required: false,
default: true,
}
},
setup(props, tip) {
vue.watch(props.enabled, value => {
if (!tip.value)
return;
if (value) {
tip.value.enable();
}
else {
tip.value.hide();
tip.value.disable();
}
});
}
});
/**
* Where to place the tooltip relative to the target element
*/
const placement = optionPlugin("placement", String, 'top');
/**
* Whether the tippy should be interactive. You don't need to specify a value for this property, its presence is
* sufficient (e.g. `<tippy interactive>`).
*
* This is a shorthand for `interactive: true` in the `extra` property.
*/
const interactive = optionPlugin("interactive", Boolean);
/**
* Whether to hide the tooltip when the target element is clicked. Defaults to false when using the `'manual'`
* trigger, otherwise defaults to true.
*/
const hideOnClick = optionPlugin("hideOnClick", Boolean);
/**
* Whether the tippy should *always* be appended to the `<body>`. You don't need to specify a value for this property,
* its presence is sufficient (e.g. `<tippy on-body>`).
*
* Normally, tooltips will be appended to the document body element, *however*, interactive elements are appended
* adjacent to their trigger, in the interest of maintaining keyboard focus order.
* [more info](https://atomiks.github.io/tippyjs/v6/accessibility/#clipping-issues)
*
* This can cause zIndex issues, so sometimes it's necessary to put an interactive tooltip on the body element.
*
* This is a shorthand for `appendTo: () => document.body` in the `extra` property. (Note that you can't access
* `document` directly in a vue template, so you would have to use a computed property if you wanted to set this in
* `extra` yourself.
*/
const onBody = inferPlugin({
props: {
onBody: {
type: Boolean,
required: false,
},
},
build(props, options) {
if (props.onBody.value === true) {
options.appendTo = () => document.body;
}
}
});
/**
* The events that trigger the tooltip. Setting the trigger key in `extra` will override this property.
*/
const trigger = inferPlugin({
props: {
trigger: {
type: String,
required: false,
},
},
build(props, options) {
if (props.trigger.value) {
options.trigger = props.trigger.value;
if (props.trigger.value === 'manual' && options.hideOnClick === undefined) {
options.hideOnClick = false;
}
}
}
});
const delayPattern = /^([0-9]+)$|^([0-9]+|-)\s*,\s*([0-9]+|-)$/;
function parseDelay(input) {
if (typeof input === "string") {
let match = input.match(delayPattern);
if (match) {
if (match[1]) {
return parseFloat(match[1]);
}
else {
return [
match[2] === '-' ? null : parseFloat(match[2]),
match[3] === '-' ? null : parseFloat(match[3])
];
}
}
else {
return undefined;
}
}
else {
return input;
}
}
/**
* The delay when showing or hiding the tooltip. One of four formats:
* - A number (or number string) representing the delay in milliseconds
* - A string consisting of two comma-separated elements representing the show and hide delays, each of which is
* either a number or a '-'
* - An array of two `number | null` elements
*/
const delay = inferPlugin({
props: {
delay: {
type: [String, Number, Array],
required: false,
validator(value) {
return parseDelay(value) !== undefined;
}
}
},
build(props, options) {
if (props.delay.value !== undefined) {
options.delay = parseDelay(props.delay.value);
}
}
});
/**
* Only used when using the manual trigger. To show/hide when using another trigger, use `tippy().show()` and
* `tippy().hide()`
*/
const visible = inferPlugin({
props: {
visible: {
type: Boolean,
required: false,
},
},
setup(props, tip) {
vue.watch(props.visible, value => {
if (!tip.value || (props.trigger && props.trigger.value !== 'manual'))
return;
if (value) {
tip.value.show();
}
else {
tip.value.hide();
}
});
}
});
/**
* Tippy.js options that should be overridden by the individual instances.
*/
const overrides = inferPlugin({
props: {
overrides: {
type: Array,
required: false,
},
},
build(props, options) {
const sOptions = options;
sOptions.overrides = (sOptions.overrides || []).concat(props.overrides.value || []);
}
});
/**
* The CSS transition to use when moving between instances within the singleton
*/
const moveTransition = optionPlugin("moveTransition", String);
var builtin = /*#__PURE__*/Object.freeze({
__proto__: null,
extra: extra,
enabled: enabled,
placement: placement,
interactive: interactive,
hideOnClick: hideOnClick,
onBody: onBody,
trigger: trigger,
delay: delay,
visible: visible,
overrides: overrides,
moveTransition: moveTransition
});
const defaultTippyProps = [
visible,
enabled,
placement,
onBody,
interactive,
trigger,
hideOnClick,
delay,
extra,
];
const baseProps$1 = {
/**
* The v-tippy target name. Defaults to `""` (the default name used by `v-tippy`)
*/
target: {
type: String,
required: false,
default: ""
},
/**
* Whether to perform a deep search for targets (using querySelector) or to only search for direct siblings.
*/
deepSearch: {
type: Boolean,
required: false,
default: false
},
singleton: {
type: String,
required: false,
default: null,
},
/**
* Whether to eagerly render the content or only render when the tooltip is visible
*/
eager: {
type: Boolean,
required: false,
default: false
},
};
function createTippyComponent(...plugins) {
let pluginProps = {};
for (const plugin of plugins) {
Object.assign(pluginProps, plugin.props);
}
return vue.defineComponent({
props: {
...baseProps$1,
...pluginProps
},
/* eslint-disable @typescript-eslint/no-unused-vars */
emits: {
attach: (instance) => true,
...commonEmits
},
/* eslint-enable @typescript-eslint/no-unused-vars */
render() {
return vue.h('div', {
'tippy-missing-target': this.tippyTargetMissing ? '' : undefined,
}, (this.$props.eager || this.singletonInstance || this.shouldRenderContent) && this.$slots.default ? this.$slots.default() : []);
},
setup(props, context) {
const tip = vue.ref();
const singletonInstance = vue.ref();
const tippyTargetMissing = vue.ref(false);
const shouldRenderContent = vue.ref(false);
const { tippyOptions } = commonSetup(props, plugins, context, tip, {
onShow() {
shouldRenderContent.value = true;
},
onHidden() {
shouldRenderContent.value = false;
}
});
return {
tip,
tippyOptions,
singletonInstance,
tippyTargetMissing,
shouldRenderContent,
};
},
methods: {
attach() {
// destroy old tip
if (this.tip) {
const tip = this.tip;
this.tip = undefined;
if (this.singletonInstance) {
this.singletonInstance.remove(tip);
this.singletonInstance = undefined;
}
tip.destroy();
}
// find the target
let target;
if (this.target === '_parent') {
target = this.$el.parentElement;
}
else if (this.deepSearch) {
target = this.$el.parentElement.querySelector(`[data-tippy-target="${this.target}"]`);
}
else {
const targetValue = this.target;
target = findElement(this.$el, {
test(el) {
let a = el;
return a && a.dataset && a.dataset.tippyTarget === targetValue;
}
});
}
this.tippyTargetMissing = !target;
if (!target) {
throw new Error(`Unable to find tippy target named '${this.target}'`);
}
// find the singleton
if (this.singleton != null) {
const targetValue = this.singleton;
const singletonElement = findElement(this.$el, {
test(el) {
let a = el;
return a && a.dataset && a.dataset.tippySingleton === targetValue;
},
recurse: true
});
this.singletonInstance = singletonElement ? singletonElement._tippySingleton : undefined;
}
else {
this.singletonInstance = undefined;
}
// create and bind tip
this.tip = tippy__default["default"](target, this.tippyOptions);
if (!this.tip) {
throw new Error(`Unable to create tippy instance`);
}
this.tip.setContent(this.$el);
this.singletonInstance && this.singletonInstance.add(this.tip);
if (this.enabled === false) {
this.tip.disable();
}
if (this.trigger === 'manual' && this.visible === true) {
this.tip.show();
}
this.$emit("attach", this.tip);
}
},
async mounted() {
await vue.nextTick();
this.attach();
},
beforeUnmount() {
this.tip && this.tip.destroy();
},
});
}
/**
* @param start the element to start at. will not test the starting element or any of its parents
* @param search the search parameters to use
*/
function findElement(start, search) {
let found = null;
let current = start;
do {
found = findSibling(current, search.test, search.selftest === undefined ? false : search.selftest);
current = current.parentElement;
} while (search.recurse && current && !found);
return found;
}
function findSibling(element, test, testSelf) {
if (testSelf && test(element)) {
return element;
}
for (let sibling = element.previousElementSibling; sibling; sibling = sibling.previousElementSibling) {
if (test(sibling))
return sibling;
}
for (let sibling = element.nextElementSibling; sibling; sibling = sibling.nextElementSibling) {
if (test(sibling))
return sibling;
}
return null;
}
const defaultTippySingletonProps = [
overrides,
moveTransition,
enabled,
placement,
onBody,
interactive,
trigger,
hideOnClick,
delay,
extra,
];
const baseProps = {
/**
* The singleton name. Defaults to `""` (the default name used by `<tippy singleton>`)
*/
name: {
type: String,
required: false,
default: ""
},
};
function createTippySingletonComponent(...plugins) {
let pluginProps = {};
for (const plugin of plugins) {
Object.assign(pluginProps, plugin.props);
}
return vue.defineComponent({
props: {
...baseProps,
...pluginProps
},
/* eslint-disable @typescript-eslint/no-unused-vars */
emits: {
add: (instance) => true,
remove: (instance) => true,
...commonEmits
},
/* eslint-enable @typescript-eslint/no-unused-vars */
render() {
return vue.h('div', {
'style': 'display: none;',
'data-tippy-singleton': this.name
}, []);
},
setup(props, context) {
const singleton = vue.ref();
const { tippyOptions } = commonSetup(props, plugins, context, singleton);
const instances = vue.ref([]);
return {
tippyOptions,
instances,
singleton,
};
},
mounted() {
this.$el._tippySingleton = this;
this.singleton = tippy.createSingleton(this.instances, this.tippyOptions);
if (this.enabled === false) {
this.singleton.disable();
}
},
beforeUnmount() {
this.singleton && this.singleton.destroy();
},
methods: {
remove(instance) {
const index = this.instances.indexOf(instance);
if (index === -1) {
return;
}
this.instances.splice(index, 1);
this.$emit('remove', instance);
this.singleton && this.singleton.setInstances(this.instances);
},
add(instance) {
if (this.instances.indexOf(instance) !== -1) {
return;
}
this.instances.push(instance);
this.$emit('add', instance);
this.singleton && this.singleton.setInstances(this.instances);
}
}
});
}
function install(app, options) {
if (options && options.tippyDefaults) {
tippy__default["default"].setDefaultProps(options.tippyDefaults);
}
app.directive('tippy', TippyDirective);
app.component('tippy', createTippyComponent(...(options && options.tippyProps || defaultTippyProps)));
app.component('tippy-singleton', createTippySingletonComponent(...(options && options.tippySingletonProps || defaultTippySingletonProps)));
}
const TippyPlugin = {
install
};
const Tippy = createTippyComponent(...defaultTippyProps);
const TippySingleton = createTippySingletonComponent(...defaultTippySingletonProps);
exports.Tippy = Tippy;
exports.TippyDirective = TippyDirective;
exports.TippyPlugin = TippyPlugin;
exports.TippySingleton = TippySingleton;
exports.createTippyComponent = createTippyComponent;
exports.createTippySingletonComponent = createTippySingletonComponent;
exports.defaultTippyProps = defaultTippyProps;
exports.defaultTippySingletonProps = defaultTippySingletonProps;
exports.inferPlugin = inferPlugin;
exports.install = install;
exports.optionPlugin = optionPlugin;
exports.props = builtin;
Object.defineProperty(exports, '__esModule', { value: true });
}));
//# sourceMappingURL=index.umd.js.map