@vue-interface/tooltip 2.0.4 → 2.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ import Tooltip from './src/Tooltip.vue';
2
+ import { TooltipDirective, TooltipPlugin } from './src/TooltipPlugin';
3
+
4
+ export {
5
+ Tooltip,
6
+ TooltipDirective,
7
+ TooltipPlugin
8
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vue-interface/tooltip",
3
- "version": "2.0.4",
3
+ "version": "2.0.6",
4
4
  "description": "A Vue tooltip component.",
5
5
  "readme": "README.md",
6
6
  "type": "module",
@@ -11,6 +11,7 @@
11
11
  ".": {
12
12
  "source": "./index.ts",
13
13
  "types": "./dist/index.d.ts",
14
+ "style": "./index.css",
14
15
  "import": "./dist/tooltip.js",
15
16
  "require": "./dist/tooltip.umd.js"
16
17
  },
@@ -45,7 +46,9 @@
45
46
  "@floating-ui/dom": "^1.7.2"
46
47
  },
47
48
  "files": [
49
+ "src",
48
50
  "dist",
51
+ "index.ts",
49
52
  "index.css",
50
53
  "README.md",
51
54
  "LICENSE"
@@ -0,0 +1,174 @@
1
+ <script lang="ts" setup>
2
+ import { arrow, autoUpdate, flip as flipFn, FlipOptions, MaybeElement, offset as offsetFn, OffsetOptions, ReferenceElement, useFloating, UseFloatingOptions } from '@floating-ui/vue';
3
+ import { computed, isRef, onUnmounted, Ref, ref, shallowReadonly, ShallowRef, useTemplateRef, watchEffect } from 'vue';
4
+
5
+ export type TooltipProps = {
6
+ title?: string;
7
+ show?: boolean;
8
+ target?: Ref<MaybeElement<ReferenceElement>> | ReferenceElement;
9
+ placement?: UseFloatingOptions['placement'];
10
+ strategy?: UseFloatingOptions['strategy'];
11
+ middleware?: (arrow: Readonly<ShallowRef<HTMLDivElement | null>>) => UseFloatingOptions['middleware'];
12
+ flip?: FlipOptions;
13
+ offset?: OffsetOptions;
14
+ };
15
+
16
+ const props = withDefaults(defineProps<TooltipProps>(), {
17
+ title: undefined,
18
+ target: undefined,
19
+ placement: 'top',
20
+ middleware: undefined,
21
+ strategy: undefined,
22
+ flip: undefined,
23
+ offset: undefined,
24
+ });
25
+
26
+ defineSlots<{
27
+ default: () => void
28
+ }>();
29
+
30
+ const tooltipEl = useTemplateRef<HTMLDivElement>('tooltipEl');
31
+ const arrowEl = useTemplateRef<HTMLDivElement>('arrowEl');
32
+ const isShowing = ref(false);
33
+ const hash = Math.random().toString(36).slice(2, 12);
34
+
35
+ const targetEl = isRef(props.target)
36
+ ? props.target
37
+ : shallowReadonly(ref(props.target));
38
+
39
+ const id = computed(() => {
40
+ if(!(targetEl.value instanceof Element)) {
41
+ return;
42
+ }
43
+
44
+ return targetEl.value.getAttribute('data-tooltip-id');
45
+ });
46
+
47
+
48
+ watchEffect(() => {
49
+ if(!targetEl.value || id.value) {
50
+ return;
51
+ }
52
+
53
+ if(targetEl.value instanceof Element) {
54
+ targetEl.value.setAttribute('data-tooltip-id', hash);
55
+ targetEl.value.addEventListener('mouseover', open);
56
+ targetEl.value.addEventListener('mouseout', close);
57
+ }
58
+ });
59
+
60
+ watchEffect(() => {
61
+ isShowing.value = props.show;
62
+ });
63
+
64
+ const dynamicOffset = computed<OffsetOptions>(() => {
65
+ if(props.offset) {
66
+ return props.offset;
67
+ }
68
+
69
+ return () => {
70
+ const { height } = arrowEl.value ? getComputedStyle(arrowEl.value) : { height: '0px' };
71
+
72
+ return {
73
+ mainAxis: parseInt(height.replace('px', '')),
74
+ };
75
+ };
76
+ });
77
+
78
+ const { floatingStyles, middlewareData, placement } = useFloating(targetEl, tooltipEl, {
79
+ placement: props.placement,
80
+ middleware: props.middleware?.(arrowEl) ?? [
81
+ flipFn(props.flip),
82
+ offsetFn(dynamicOffset.value),
83
+ arrow({
84
+ element: arrowEl
85
+ }),
86
+ ],
87
+ whileElementsMounted: autoUpdate
88
+ });
89
+
90
+ const tooltipClasses = computed(() => {
91
+ return {
92
+ show: isShowing.value
93
+ };
94
+ });
95
+
96
+ type Side = 'bottom' | 'left' | 'top' | 'right';
97
+
98
+ const side = computed(() => placement.value.split('-')[0] as Side);
99
+
100
+ const arrowPosition = computed(() => {
101
+ return {
102
+ top: 'bottom',
103
+ right: 'left',
104
+ bottom: 'top',
105
+ left: 'right'
106
+ }[side.value] as Side;
107
+ });
108
+
109
+ const arrowRotation = computed<Record<Side,string>>(() => ({
110
+ top: 'rotate(225deg)',
111
+ right: 'rotate(-45deg)',
112
+ bottom: 'rotate(45deg)',
113
+ left: 'rotate(135deg)',
114
+ }));
115
+
116
+ function open() {
117
+ isShowing.value = true;
118
+ }
119
+
120
+ function close() {
121
+ isShowing.value = false;
122
+ }
123
+
124
+ onUnmounted(() => {
125
+ if(targetEl.value instanceof Element) {
126
+ targetEl.value.removeAttribute('data-tooltip-id');
127
+ }
128
+ });
129
+
130
+ defineExpose({
131
+ open,
132
+ close,
133
+ tooltipEl,
134
+ arrowEl,
135
+ isShowing,
136
+ hash,
137
+ });
138
+ </script>
139
+
140
+ <template>
141
+ <Teleport to="body">
142
+ <div
143
+ ref="tooltipEl"
144
+ class="tooltip"
145
+ role="tooltip"
146
+ :data-tooltip-id="hash"
147
+ :class="tooltipClasses"
148
+ :style="floatingStyles">
149
+ <div
150
+ ref="arrowEl"
151
+ class="tooltip-arrow"
152
+ :style="{
153
+ transform: arrowRotation[arrowPosition],
154
+ ...Object.assign({
155
+ left:
156
+ middlewareData.arrow?.x != null
157
+ ? `${middlewareData.arrow.x}px`
158
+ : '',
159
+ top:
160
+ middlewareData.arrow?.y != null
161
+ ? `${middlewareData.arrow.y}px`
162
+ : ''
163
+ }, {
164
+ [arrowPosition]: `calc(${-(arrowEl?.offsetWidth ?? 0) / 2}px)`
165
+ })
166
+ }" />
167
+ <div
168
+ ref="inner"
169
+ class="tooltip-inner">
170
+ <slot>{{ title }}</slot>
171
+ </div>
172
+ </div>
173
+ </Teleport>
174
+ </template>
@@ -0,0 +1,137 @@
1
+ import { h, render, type App, type Directive } from 'vue';
2
+ import Tooltip, { type TooltipProps } from './Tooltip.vue';
3
+
4
+ const prefix = 'data-tooltip';
5
+ const prefixRegExp = new RegExp(`^${prefix}\-`);
6
+
7
+ function getAttributes(el: Element): Record<string,string> {
8
+ return Array.from(el.attributes)
9
+ .map(a => [a.name, a.value])
10
+ .filter(([key]) => key === 'title' || key.match(prefixRegExp))
11
+ .map(([key, value]) => [key.replace(new RegExp(prefixRegExp), ''), value])
12
+ .reduce((carry, attr) => Object.assign(carry, { [attr[0]]: attr[1] }), {});
13
+ }
14
+
15
+ function createTooltip(target: Element, props?: TooltipProps) {
16
+ const container = document.createElement('template');
17
+
18
+ const vnode = h(Tooltip, Object.assign({
19
+ target
20
+ }, getAttributes(target), props));
21
+
22
+ render(vnode, container);
23
+
24
+ const title = target.getAttribute('title');
25
+
26
+ if(title) {
27
+ target.setAttribute(`${prefix}-og-title`, title);
28
+ target.removeAttribute('title');
29
+ }
30
+
31
+ return () => {
32
+ if(vnode.component) {
33
+ vnode.component.exposed?.tooltipEl.value?.remove();
34
+ }
35
+ };
36
+ }
37
+
38
+ function destroyTooltip(target: Element) {
39
+ const tooltips = document.querySelectorAll(
40
+ `[${prefix}-id="${target.getAttribute(`${prefix}-id`)}"]`
41
+ );
42
+
43
+ for(const tooltip of tooltips) {
44
+ tooltip.remove();
45
+ }
46
+
47
+ target.removeAttribute(`${prefix}-id`);
48
+
49
+ const title = target.getAttribute(`${prefix}-og-title`);
50
+
51
+ if(title) {
52
+ target.setAttribute('title', title);
53
+ }
54
+ }
55
+
56
+ function shouldCreateTooltip(target: Node): target is Element {
57
+ if(!(target instanceof Element)) {
58
+ return false;
59
+ }
60
+
61
+ const attrs = getAttributes(target);
62
+
63
+ return !attrs.id && !!attrs.title;
64
+ }
65
+
66
+ function shouldRemoveTooltip(target: Node): target is Element {
67
+ if(!(target instanceof Element)) {
68
+ return false;
69
+ }
70
+
71
+ return target.hasAttribute(`${prefix}-id`);
72
+ }
73
+
74
+ export const TooltipDirective: Directive<Element, string|TooltipProps> = {
75
+ beforeMount(target, binding) {
76
+ createTooltip(target, typeof binding.value === 'string' ? {
77
+ title: binding.value
78
+ }: binding.value);
79
+ },
80
+ beforeUnmount(target) {
81
+ destroyTooltip(target);
82
+ }
83
+ };
84
+
85
+ export function TooltipPlugin(app: App<Element>) {
86
+ app.mixin({
87
+ beforeMount() {
88
+ const observer = new MutationObserver((mutations) => {
89
+ for(const { addedNodes, removedNodes } of mutations) {
90
+ addedNodes.forEach((node) => {
91
+ if(shouldCreateTooltip(node)) {
92
+ createTooltip(node);
93
+ }
94
+ });
95
+
96
+ removedNodes.forEach((node) => {
97
+ if(shouldRemoveTooltip(node)) {
98
+ destroyTooltip(node);
99
+ }
100
+ });
101
+ }
102
+ });
103
+
104
+ observer.observe(document.body, {
105
+ childList: true,
106
+ subtree: true
107
+ });
108
+ },
109
+ mounted() {
110
+ let el = this.$el;
111
+
112
+ if(this.$el instanceof Text) {
113
+ el = this.$el.parentNode;
114
+ }
115
+
116
+ const walker = document.createTreeWalker(
117
+ el,
118
+ NodeFilter.SHOW_ALL,
119
+ (node: Node) => {
120
+ if(!(node instanceof Element)) {
121
+ return NodeFilter.FILTER_REJECT;
122
+ }
123
+
124
+ return NodeFilter.FILTER_ACCEPT;
125
+ }
126
+ );
127
+
128
+ while(walker.nextNode()) {
129
+ if(shouldCreateTooltip(walker.currentNode)) {
130
+ createTooltip(walker.currentNode);
131
+ }
132
+ }
133
+ }
134
+ });
135
+
136
+ app.directive<Element, string|TooltipProps>('tooltip', TooltipDirective);
137
+ }