@ulu/frontend-vue 0.6.23 → 0.6.24
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/dist/components/collapsible/UluAccordion.vue.d.ts +2 -2
- package/dist/components/collapsible/UluModal.vue.d.ts +4 -4
- package/dist/components/collapsible/UluTabGroup.vue.d.ts +2 -2
- package/dist/components/elements/UluAlert.vue.d.ts +2 -2
- package/dist/components/elements/UluBadge.vue.d.ts +4 -4
- package/dist/components/elements/UluButton.vue.d.ts +10 -10
- package/dist/components/elements/UluButtonVerbose.vue.d.ts +4 -4
- package/dist/components/elements/UluCaptionedFigure.vue.d.ts +2 -2
- package/dist/components/elements/UluCard.vue.d.ts +4 -4
- package/dist/components/elements/UluList.vue.d.ts +2 -2
- package/dist/components/elements/UluSlider.vue.d.ts +2 -2
- package/dist/components/elements/UluTag.vue.d.ts +2 -2
- package/dist/components/forms/UluSearchForm.vue.d.ts +2 -2
- package/dist/components/layout/UluTitleRail.vue.d.ts +2 -2
- package/dist/components/systems/facets/UluFacetsList.vue.d.ts +2 -2
- package/dist/components/systems/facets/UluFacetsSearch.vue.d.ts +2 -2
- package/dist/components/utils/UluAction.vue.d.ts +2 -2
- package/dist/components/visualizations/UluProgressBar.vue.d.ts +2 -2
- package/dist/components/visualizations/UluProgressCircle.vue.d.ts +2 -2
- package/dist/composables/useUluFloating.d.ts.map +1 -1
- package/dist/composables/useUluFloating.js +26 -25
- package/dist/index.js +258 -240
- package/dist/mcp-data.json +1047 -381
- package/dist/plugins/index.d.ts +2 -1
- package/dist/plugins/popovers/UluPopover.vue.d.ts +188 -7
- package/dist/plugins/popovers/UluPopover.vue.d.ts.map +1 -1
- package/dist/plugins/popovers/UluPopover.vue.js +82 -133
- package/dist/plugins/popovers/UluPopoverBase.vue.d.ts +58 -0
- package/dist/plugins/popovers/UluPopoverBase.vue.d.ts.map +1 -0
- package/dist/plugins/popovers/UluPopoverBase.vue.js +146 -0
- package/dist/plugins/popovers/UluTooltipPopover.vue.d.ts +1 -4
- package/dist/plugins/popovers/UluTooltipPopover.vue.d.ts.map +1 -1
- package/dist/plugins/popovers/UluTooltipPopover.vue.js +24 -45
- package/dist/plugins/popovers/index.d.ts +4 -0
- package/dist/plugins/popovers/index.d.ts.map +1 -1
- package/dist/plugins/popovers/index.js +54 -49
- package/dist/plugins/tour/UluTourContent.vue.d.ts +10 -0
- package/dist/plugins/tour/UluTourContent.vue.d.ts.map +1 -0
- package/dist/plugins/tour/UluTourContent.vue.js +39 -0
- package/dist/plugins/tour/UluTourDisplay.vue.d.ts +100 -0
- package/dist/plugins/tour/UluTourDisplay.vue.d.ts.map +1 -0
- package/dist/plugins/tour/UluTourDisplay.vue.js +133 -0
- package/dist/plugins/tour/UluTourPager.vue.d.ts +3 -0
- package/dist/plugins/tour/UluTourPager.vue.d.ts.map +1 -0
- package/dist/plugins/tour/UluTourPager.vue.js +31 -0
- package/dist/plugins/tour/api.d.ts +52 -0
- package/dist/plugins/tour/api.d.ts.map +1 -0
- package/dist/plugins/tour/api.js +52 -0
- package/dist/plugins/tour/index.d.ts +7 -0
- package/dist/plugins/tour/index.d.ts.map +1 -0
- package/dist/plugins/tour/index.js +14 -0
- package/dist/plugins/tour/useTour.d.ts +8 -0
- package/dist/plugins/tour/useTour.d.ts.map +1 -0
- package/dist/plugins/tour/useTour.js +8 -0
- package/lib/composables/useUluFloating.js +2 -0
- package/lib/plugins/index.js +3 -3
- package/lib/plugins/popovers/UluPopover.vue +42 -125
- package/lib/plugins/popovers/UluPopoverBase.vue +197 -0
- package/lib/plugins/popovers/UluTooltipPopover.vue +11 -33
- package/lib/plugins/popovers/index.js +15 -5
- package/lib/plugins/tour/UluTourContent.vue +37 -0
- package/lib/plugins/tour/UluTourDisplay.vue +182 -0
- package/lib/plugins/tour/UluTourPager.vue +36 -0
- package/lib/plugins/tour/api.js +83 -0
- package/lib/plugins/tour/index.js +29 -0
- package/lib/plugins/tour/useTour.js +10 -0
- package/package.json +1 -1
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<span
|
|
3
|
+
class="popover"
|
|
4
|
+
ref="contentEl"
|
|
5
|
+
:style="floatingStyles"
|
|
6
|
+
:data-placement="placement"
|
|
7
|
+
:class="[
|
|
8
|
+
{
|
|
9
|
+
'popover--fixed': isFixedStrategy,
|
|
10
|
+
'is-active': isOpen
|
|
11
|
+
},
|
|
12
|
+
resolvedModifiers
|
|
13
|
+
]"
|
|
14
|
+
@keydown.esc="handleEsc"
|
|
15
|
+
tabindex="-1"
|
|
16
|
+
>
|
|
17
|
+
<span class="popover__inner">
|
|
18
|
+
<slot :isOpen="isOpen" :close="close" />
|
|
19
|
+
</span>
|
|
20
|
+
<span v-if="$slots.footer" class="popover__footer">
|
|
21
|
+
<slot name="footer" :isOpen="isOpen" :close="close" />
|
|
22
|
+
</span>
|
|
23
|
+
<span
|
|
24
|
+
v-if="resolvedConfig.arrow"
|
|
25
|
+
class="popover__arrow"
|
|
26
|
+
ref="contentArrow"
|
|
27
|
+
:style="arrowStyles"
|
|
28
|
+
data-ulu-popover-arrow
|
|
29
|
+
></span>
|
|
30
|
+
</span>
|
|
31
|
+
</template>
|
|
32
|
+
|
|
33
|
+
<script setup>
|
|
34
|
+
import { ref, computed, watch, onUnmounted, nextTick } from 'vue';
|
|
35
|
+
import { useUluFloating } from '../../composables/useUluFloating.js';
|
|
36
|
+
import { useModifiers } from '../../composables/useModifiers.js';
|
|
37
|
+
import { wasClickOutside } from '@ulu/utils/browser/dom.js';
|
|
38
|
+
|
|
39
|
+
const emit = defineEmits(['close']);
|
|
40
|
+
|
|
41
|
+
const props = defineProps({
|
|
42
|
+
/**
|
|
43
|
+
* Controls the open/active state of the popover
|
|
44
|
+
*/
|
|
45
|
+
isOpen: {
|
|
46
|
+
type: Boolean,
|
|
47
|
+
default: false
|
|
48
|
+
},
|
|
49
|
+
/**
|
|
50
|
+
* Close popover when click is outside
|
|
51
|
+
*/
|
|
52
|
+
clickOutsideCloses: {
|
|
53
|
+
type: Boolean,
|
|
54
|
+
default: true
|
|
55
|
+
},
|
|
56
|
+
/**
|
|
57
|
+
* Close popover when escape key is pressed
|
|
58
|
+
*/
|
|
59
|
+
escapeCloses: {
|
|
60
|
+
type: Boolean,
|
|
61
|
+
default: true
|
|
62
|
+
},
|
|
63
|
+
/**
|
|
64
|
+
* Direct focus when open/closing popover
|
|
65
|
+
*/
|
|
66
|
+
directFocus: {
|
|
67
|
+
type: Function,
|
|
68
|
+
default: ({ isOpen, trigger, content }) => {
|
|
69
|
+
if (isOpen && content) {
|
|
70
|
+
content.focus({ preventScroll: true });
|
|
71
|
+
} else if (!isOpen && trigger && trigger instanceof HTMLElement) {
|
|
72
|
+
// Note: using nextTick prevents scroll jumping in some cases
|
|
73
|
+
trigger.focus({ preventScroll: true });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
/**
|
|
78
|
+
* The target element for the popover to float alongside
|
|
79
|
+
*/
|
|
80
|
+
trigger: {
|
|
81
|
+
type: Object,
|
|
82
|
+
default: null
|
|
83
|
+
},
|
|
84
|
+
/**
|
|
85
|
+
* Floating UI configuration
|
|
86
|
+
*/
|
|
87
|
+
config: {
|
|
88
|
+
type: Object,
|
|
89
|
+
default: () => ({})
|
|
90
|
+
},
|
|
91
|
+
/**
|
|
92
|
+
* Modifiers (to add any modifier classes based on base class [ie. 'large'])
|
|
93
|
+
*/
|
|
94
|
+
modifiers: [String, Array, Object]
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const contentEl = ref(null);
|
|
98
|
+
|
|
99
|
+
const { resolvedModifiers } = useModifiers({ props, baseClass: "popover" });
|
|
100
|
+
|
|
101
|
+
const resolvedConfig = computed(() => props.config || {});
|
|
102
|
+
|
|
103
|
+
const { floatingStyles, placement, arrowStyles, update, isFixedStrategy, contentArrow } = useUluFloating(
|
|
104
|
+
computed(() => props.trigger),
|
|
105
|
+
contentEl,
|
|
106
|
+
resolvedConfig
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const close = () => {
|
|
110
|
+
emit('close');
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const handleEsc = (event) => {
|
|
114
|
+
if (props.isOpen && props.escapeCloses) {
|
|
115
|
+
event.preventDefault();
|
|
116
|
+
close();
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
let outsideHandler = null;
|
|
121
|
+
|
|
122
|
+
const destroyOutsideClick = () => {
|
|
123
|
+
if (outsideHandler) {
|
|
124
|
+
document.removeEventListener("click", outsideHandler);
|
|
125
|
+
outsideHandler = null;
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const addOutsideClick = () => {
|
|
130
|
+
destroyOutsideClick();
|
|
131
|
+
if (props.clickOutsideCloses) {
|
|
132
|
+
outsideHandler = (event) => {
|
|
133
|
+
if (!props.isOpen || !contentEl.value) return;
|
|
134
|
+
|
|
135
|
+
// Ignore clicks on elements that have been removed from the DOM
|
|
136
|
+
if (!document.body.contains(event.target)) return;
|
|
137
|
+
|
|
138
|
+
// If the click happened on a DOM node physically inside the popover,
|
|
139
|
+
// it is NOT an outside click, even if floating-ui instantly moved
|
|
140
|
+
// the popover across the screen causing `wasClickOutside` (bounding rect check) to fail.
|
|
141
|
+
if (contentEl.value.contains(event.target)) return;
|
|
142
|
+
|
|
143
|
+
if (wasClickOutside(contentEl.value, event)) {
|
|
144
|
+
// If the click is on the trigger itself, we let the trigger handle toggling
|
|
145
|
+
if (props.trigger instanceof HTMLElement && props.trigger.contains(event.target)) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
close();
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
// Defer so the click that opened the popover doesn't instantly close it
|
|
152
|
+
setTimeout(() => {
|
|
153
|
+
if (outsideHandler) {
|
|
154
|
+
document.addEventListener("click", outsideHandler);
|
|
155
|
+
}
|
|
156
|
+
}, 0);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
watch(() => props.isOpen, (newVal) => {
|
|
161
|
+
if (newVal) {
|
|
162
|
+
update();
|
|
163
|
+
addOutsideClick();
|
|
164
|
+
if (props.directFocus) {
|
|
165
|
+
nextTick(() => {
|
|
166
|
+
props.directFocus({ isOpen: true, trigger: props.trigger, content: contentEl.value });
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
} else {
|
|
170
|
+
destroyOutsideClick();
|
|
171
|
+
if (props.directFocus) {
|
|
172
|
+
nextTick(() => {
|
|
173
|
+
props.directFocus({ isOpen: false, trigger: props.trigger, content: contentEl.value });
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}, { immediate: true });
|
|
178
|
+
|
|
179
|
+
onUnmounted(() => {
|
|
180
|
+
destroyOutsideClick();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
defineExpose({
|
|
184
|
+
/**
|
|
185
|
+
* Emits the close event
|
|
186
|
+
*/
|
|
187
|
+
close,
|
|
188
|
+
/**
|
|
189
|
+
* Manually trigger a floating UI position update
|
|
190
|
+
*/
|
|
191
|
+
update,
|
|
192
|
+
/**
|
|
193
|
+
* The internal root popover element reference
|
|
194
|
+
*/
|
|
195
|
+
content: contentEl
|
|
196
|
+
});
|
|
197
|
+
</script>
|
|
@@ -1,39 +1,25 @@
|
|
|
1
|
-
<!-- NOTE: Need to rename classes when moving this into the library -->
|
|
2
1
|
<template>
|
|
3
|
-
<
|
|
4
|
-
class="popover
|
|
5
|
-
ref="content"
|
|
2
|
+
<UluPopoverBase
|
|
3
|
+
class="popover--tooltip is-active"
|
|
6
4
|
:id="TOOLTIP_ID"
|
|
7
|
-
:
|
|
8
|
-
:
|
|
9
|
-
|
|
10
|
-
'popover--fixed' : isFixedStrategy
|
|
11
|
-
},
|
|
12
|
-
resolvedConfig.class
|
|
13
|
-
]"
|
|
14
|
-
:style="floatingStyles"
|
|
5
|
+
:trigger="trigger"
|
|
6
|
+
:config="resolvedConfig"
|
|
7
|
+
:class="resolvedConfig.class"
|
|
15
8
|
>
|
|
16
|
-
<span v-if="resolvedConfig.isHtml"
|
|
17
|
-
|
|
18
|
-
<span v-else class="popover__inner">
|
|
9
|
+
<span v-if="resolvedConfig.isHtml" v-html="resolvedConfig.content"></span>
|
|
10
|
+
<template v-else>
|
|
19
11
|
<component v-if="resolvedConfig.component" :is="resolvedConfig.component" v-bind="resolvedConfig.componentProps"/>
|
|
20
12
|
<template v-else>
|
|
21
13
|
{{ resolvedConfig.content }}
|
|
22
14
|
</template>
|
|
23
|
-
</
|
|
24
|
-
|
|
25
|
-
v-if="resolvedConfig.arrow"
|
|
26
|
-
class="popover__arrow"
|
|
27
|
-
ref="contentArrow"
|
|
28
|
-
:style="arrowStyles"
|
|
29
|
-
></span>
|
|
30
|
-
</span>
|
|
15
|
+
</template>
|
|
16
|
+
</UluPopoverBase>
|
|
31
17
|
</template>
|
|
32
18
|
|
|
33
19
|
<script setup>
|
|
34
|
-
import {
|
|
35
|
-
import { useUluFloating } from "../../composables/useUluFloating.js";
|
|
20
|
+
import { computed } from "vue";
|
|
36
21
|
import { TOOLTIP_ID } from "./index.js";
|
|
22
|
+
import UluPopoverBase from "./UluPopoverBase.vue";
|
|
37
23
|
|
|
38
24
|
const props = defineProps({
|
|
39
25
|
config: Object,
|
|
@@ -43,13 +29,5 @@
|
|
|
43
29
|
}
|
|
44
30
|
});
|
|
45
31
|
|
|
46
|
-
const content = ref(null);
|
|
47
32
|
const resolvedConfig = computed(() => props.config);
|
|
48
|
-
const {
|
|
49
|
-
floatingStyles,
|
|
50
|
-
placement,
|
|
51
|
-
arrowStyles,
|
|
52
|
-
contentArrow,
|
|
53
|
-
isFixedStrategy
|
|
54
|
-
} = useUluFloating(toRef(props, 'trigger'), content, resolvedConfig);
|
|
55
33
|
</script>
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { reactive, markRaw } from 'vue';
|
|
5
5
|
import UluTooltipDisplay from './UluTooltipDisplay.vue';
|
|
6
6
|
import UluPopover from "./UluPopover.vue";
|
|
7
|
+
import UluPopoverBase from "./UluPopoverBase.vue";
|
|
7
8
|
import defaults from './defaults.js';
|
|
8
9
|
import { useTooltip } from './useTooltip.js';
|
|
9
10
|
|
|
@@ -73,9 +74,12 @@ export default function install(app, userOptions = {}) {
|
|
|
73
74
|
config: {},
|
|
74
75
|
});
|
|
75
76
|
const showTooltip = (triggerEl, configObj) => {
|
|
77
|
+
const isDOMElement = triggerEl instanceof Element;
|
|
78
|
+
|
|
76
79
|
// If a teleport target isn't already specified in the config,
|
|
77
80
|
// try to find a parent dialog to teleport to automatically.
|
|
78
|
-
|
|
81
|
+
// (Note: triggerEl might be a virtual element object from useTooltipFollow, not a real DOM element)
|
|
82
|
+
if (isDOMElement && !configObj.teleportTo) {
|
|
79
83
|
const dialog = triggerEl.closest("dialog");
|
|
80
84
|
if (dialog) {
|
|
81
85
|
configObj.teleportTo = dialog;
|
|
@@ -84,12 +88,12 @@ export default function install(app, userOptions = {}) {
|
|
|
84
88
|
|
|
85
89
|
// If a tooltip is already active for a different element, remove its attribute.
|
|
86
90
|
if (tooltipState.trigger && tooltipState.trigger !== triggerEl) {
|
|
87
|
-
if (tooltipState.trigger
|
|
91
|
+
if (tooltipState.trigger instanceof Element) {
|
|
88
92
|
tooltipState.trigger.removeAttribute('aria-describedby');
|
|
89
93
|
}
|
|
90
94
|
}
|
|
91
95
|
|
|
92
|
-
if (
|
|
96
|
+
if (isDOMElement) {
|
|
93
97
|
triggerEl.setAttribute('aria-describedby', TOOLTIP_ID);
|
|
94
98
|
}
|
|
95
99
|
|
|
@@ -97,8 +101,9 @@ export default function install(app, userOptions = {}) {
|
|
|
97
101
|
tooltipState.config = configObj;
|
|
98
102
|
tooltipState.visible = true;
|
|
99
103
|
};
|
|
104
|
+
|
|
100
105
|
const hideTooltip = () => {
|
|
101
|
-
if (tooltipState.trigger
|
|
106
|
+
if (tooltipState.trigger instanceof Element) {
|
|
102
107
|
tooltipState.trigger.removeAttribute('aria-describedby');
|
|
103
108
|
}
|
|
104
109
|
tooltipState.visible = false;
|
|
@@ -114,6 +119,7 @@ export default function install(app, userOptions = {}) {
|
|
|
114
119
|
// Register components and directives
|
|
115
120
|
app.component('UluTooltipDisplay', UluTooltipDisplay);
|
|
116
121
|
app.component('UluPopover', UluPopover);
|
|
122
|
+
app.component('UluPopoverBase', UluPopoverBase);
|
|
117
123
|
|
|
118
124
|
const elementHandlers = new WeakMap();
|
|
119
125
|
const popoverDefaults = mergedOptions.popover;
|
|
@@ -197,4 +203,8 @@ export default function install(app, userOptions = {}) {
|
|
|
197
203
|
}
|
|
198
204
|
}
|
|
199
205
|
});
|
|
200
|
-
}
|
|
206
|
+
}
|
|
207
|
+
export { default as UluPopover } from './UluPopover.vue';
|
|
208
|
+
export { default as UluPopoverBase } from './UluPopoverBase.vue';
|
|
209
|
+
export { default as UluTooltipDisplay } from './UluTooltipDisplay.vue';
|
|
210
|
+
export { default as UluTooltipPopover } from './UluTooltipPopover.vue';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="tour-content crop-margins">
|
|
3
|
+
<component
|
|
4
|
+
v-if="step?.title"
|
|
5
|
+
:is="isModal ? 'h2' : 'strong'"
|
|
6
|
+
:class="isModal ? 'h3' : 'h4'"
|
|
7
|
+
class="display-block no-margin"
|
|
8
|
+
>
|
|
9
|
+
{{ step.title }}
|
|
10
|
+
</component>
|
|
11
|
+
<UluRule />
|
|
12
|
+
<p v-if="step?.content">
|
|
13
|
+
{{ step.content }}
|
|
14
|
+
</p>
|
|
15
|
+
</div>
|
|
16
|
+
</template>
|
|
17
|
+
|
|
18
|
+
<script setup>
|
|
19
|
+
import UluRule from "../../components/elements/UluRule.vue";
|
|
20
|
+
|
|
21
|
+
defineProps({
|
|
22
|
+
/**
|
|
23
|
+
* The tour step configuration object
|
|
24
|
+
*/
|
|
25
|
+
step: {
|
|
26
|
+
type: Object,
|
|
27
|
+
required: true
|
|
28
|
+
},
|
|
29
|
+
/**
|
|
30
|
+
* Adjusts the typography to match a modal context rather than a popover context
|
|
31
|
+
*/
|
|
32
|
+
isModal: {
|
|
33
|
+
type: Boolean,
|
|
34
|
+
default: false
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
</script>
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Teleport to="body">
|
|
3
|
+
<template v-if="tourState.active">
|
|
4
|
+
|
|
5
|
+
<!-- 1. FULL COMPONENT TAKEOVER -->
|
|
6
|
+
<component
|
|
7
|
+
v-if="currentStep?.component"
|
|
8
|
+
:is="currentStep.component"
|
|
9
|
+
v-bind="currentStep.componentProps"
|
|
10
|
+
/>
|
|
11
|
+
|
|
12
|
+
<!-- 2. RENDER MODAL STEP -->
|
|
13
|
+
<UluModal
|
|
14
|
+
v-else-if="!currentStep?.target"
|
|
15
|
+
:modelValue="true"
|
|
16
|
+
v-bind="resolvedModalProps"
|
|
17
|
+
@close="api.stop()"
|
|
18
|
+
>
|
|
19
|
+
<component
|
|
20
|
+
v-if="currentStep?.contentComponent"
|
|
21
|
+
:is="currentStep.contentComponent"
|
|
22
|
+
v-bind="currentStep.componentProps"
|
|
23
|
+
/>
|
|
24
|
+
<UluTourContent v-else :step="currentStep" :is-modal="true" />
|
|
25
|
+
|
|
26
|
+
<template #footer v-if="!currentStep?.hideFooter">
|
|
27
|
+
<UluTourPager />
|
|
28
|
+
</template>
|
|
29
|
+
</UluModal>
|
|
30
|
+
|
|
31
|
+
<!-- 3. RENDER POPOVER STEP -->
|
|
32
|
+
<UluPopoverBase
|
|
33
|
+
v-else
|
|
34
|
+
ref="popoverBase"
|
|
35
|
+
:trigger="targetEl"
|
|
36
|
+
:config="resolvedConfig"
|
|
37
|
+
:style="{ zIndex: 9999 }"
|
|
38
|
+
:isOpen="true"
|
|
39
|
+
@close="api.stop()"
|
|
40
|
+
v-bind="resolvedPopoverProps"
|
|
41
|
+
>
|
|
42
|
+
<component
|
|
43
|
+
v-if="currentStep?.contentComponent"
|
|
44
|
+
:is="currentStep.contentComponent"
|
|
45
|
+
v-bind="currentStep.componentProps"
|
|
46
|
+
/>
|
|
47
|
+
<UluTourContent v-else :step="currentStep" :is-modal="false" />
|
|
48
|
+
|
|
49
|
+
<template #footer v-if="!currentStep?.hideFooter">
|
|
50
|
+
<UluTourPager />
|
|
51
|
+
</template>
|
|
52
|
+
</UluPopoverBase>
|
|
53
|
+
|
|
54
|
+
<!-- OPTIONAL HIGHLIGHT BACKDROP -->
|
|
55
|
+
<div
|
|
56
|
+
v-if="currentStep?.target && currentStep?.highlight && targetRect"
|
|
57
|
+
class="tour-highlight-backdrop"
|
|
58
|
+
:style="highlightStyles"
|
|
59
|
+
@click="api.stop()"
|
|
60
|
+
></div>
|
|
61
|
+
</template>
|
|
62
|
+
</Teleport>
|
|
63
|
+
</template>
|
|
64
|
+
|
|
65
|
+
<script setup>
|
|
66
|
+
import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue';
|
|
67
|
+
import UluModal from '../../components/collapsible/UluModal.vue';
|
|
68
|
+
import UluPopoverBase from '../popovers/UluPopoverBase.vue';
|
|
69
|
+
import UluTourPager from './UluTourPager.vue';
|
|
70
|
+
import UluTourContent from './UluTourContent.vue';
|
|
71
|
+
import { useTour } from './useTour.js';
|
|
72
|
+
|
|
73
|
+
const { api, state: tourState } = useTour();
|
|
74
|
+
|
|
75
|
+
const currentStep = computed(() => tourState.active?.steps[tourState.stepIndex]);
|
|
76
|
+
|
|
77
|
+
// Floating UI Setup for Popovers
|
|
78
|
+
const targetEl = ref(null);
|
|
79
|
+
const popoverBase = ref(null);
|
|
80
|
+
|
|
81
|
+
const resolvedConfig = computed(() => ({
|
|
82
|
+
placement: currentStep.value?.placement || 'bottom',
|
|
83
|
+
arrow: true,
|
|
84
|
+
offset: 8
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
// Prop Resolution
|
|
88
|
+
const resolvedModalProps = computed(() => {
|
|
89
|
+
return {
|
|
90
|
+
...(tourState.active?.modalProps || {}),
|
|
91
|
+
...(currentStep.value?.modalProps || {})
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const resolvedPopoverProps = computed(() => {
|
|
96
|
+
return {
|
|
97
|
+
modifiers: 'large',
|
|
98
|
+
...(tourState.active?.popoverProps || {}),
|
|
99
|
+
...(currentStep.value?.popoverProps || {})
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Highlight Backdrop Setup
|
|
104
|
+
const targetRect = ref(null);
|
|
105
|
+
|
|
106
|
+
const updateTargetRect = () => {
|
|
107
|
+
if (targetEl.value) {
|
|
108
|
+
const rect = targetEl.value.getBoundingClientRect();
|
|
109
|
+
targetRect.value = {
|
|
110
|
+
top: rect.top,
|
|
111
|
+
left: rect.left,
|
|
112
|
+
width: rect.width,
|
|
113
|
+
height: rect.height
|
|
114
|
+
};
|
|
115
|
+
} else {
|
|
116
|
+
targetRect.value = null;
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const highlightStyles = computed(() => {
|
|
121
|
+
if (!targetRect.value) return {};
|
|
122
|
+
const t = targetRect.value;
|
|
123
|
+
return {
|
|
124
|
+
position: 'fixed',
|
|
125
|
+
top: 0,
|
|
126
|
+
left: 0,
|
|
127
|
+
width: '100vw',
|
|
128
|
+
height: '100vh',
|
|
129
|
+
pointerEvents: 'auto',
|
|
130
|
+
zIndex: 9998,
|
|
131
|
+
background: 'rgba(0,0,0,0.5)',
|
|
132
|
+
clipPath: `polygon(
|
|
133
|
+
0% 0%,
|
|
134
|
+
100% 0%,
|
|
135
|
+
100% 100%,
|
|
136
|
+
0% 100%,
|
|
137
|
+
0% ${ t.top }px,
|
|
138
|
+
${ t.left }px ${ t.top }px,
|
|
139
|
+
${ t.left }px ${ t.top + t.height }px,
|
|
140
|
+
${ t.left + t.width }px ${ t.top + t.height }px,
|
|
141
|
+
${ t.left + t.width }px ${ t.top }px,
|
|
142
|
+
${ t.left }px ${ t.top }px,
|
|
143
|
+
0% ${ t.top }px
|
|
144
|
+
)`
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Watch for step changes to update floating UI target
|
|
149
|
+
watch(currentStep, async (step) => {
|
|
150
|
+
if (step?.target) {
|
|
151
|
+
await nextTick();
|
|
152
|
+
const el = document.querySelector(step.target);
|
|
153
|
+
if (el) {
|
|
154
|
+
targetEl.value = el;
|
|
155
|
+
// UluPopoverBase's isOpen watcher automatically calls update(),
|
|
156
|
+
// but since isOpen stays strictly true here when switching between popover steps,
|
|
157
|
+
// we might need to manually trigger an update if the target changed.
|
|
158
|
+
if (popoverBase.value) {
|
|
159
|
+
popoverBase.value.update();
|
|
160
|
+
}
|
|
161
|
+
if (step.highlight) {
|
|
162
|
+
updateTargetRect();
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
console.warn(`Tour target not found: ${step.target}`);
|
|
166
|
+
}
|
|
167
|
+
} else {
|
|
168
|
+
targetEl.value = null;
|
|
169
|
+
targetRect.value = null;
|
|
170
|
+
}
|
|
171
|
+
}, { immediate: true });
|
|
172
|
+
|
|
173
|
+
onMounted(() => {
|
|
174
|
+
window.addEventListener('resize', updateTargetRect);
|
|
175
|
+
window.addEventListener('scroll', updateTargetRect);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
onUnmounted(() => {
|
|
179
|
+
window.removeEventListener('resize', updateTargetRect);
|
|
180
|
+
window.removeEventListener('scroll', updateTargetRect);
|
|
181
|
+
});
|
|
182
|
+
</script>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="tour-pager layout-flex-justified">
|
|
3
|
+
<UluButton
|
|
4
|
+
@click="api.prev()"
|
|
5
|
+
text="Previous"
|
|
6
|
+
:disabled="state.stepIndex === 0"
|
|
7
|
+
icon="type:previous"
|
|
8
|
+
iconBefore
|
|
9
|
+
small
|
|
10
|
+
secondary
|
|
11
|
+
transparent
|
|
12
|
+
/>
|
|
13
|
+
<UluButton
|
|
14
|
+
primary
|
|
15
|
+
:icon="isLastStep ? null : 'type:next'"
|
|
16
|
+
@click="api.next()"
|
|
17
|
+
:text="isLastStep ? 'Finish' : 'Next'"
|
|
18
|
+
small
|
|
19
|
+
/>
|
|
20
|
+
</div>
|
|
21
|
+
</template>
|
|
22
|
+
|
|
23
|
+
<script setup>
|
|
24
|
+
import { computed } from 'vue';
|
|
25
|
+
import { useTour } from './useTour.js';
|
|
26
|
+
import UluButton from '../../components/elements/UluButton.vue';
|
|
27
|
+
|
|
28
|
+
const { api, state } = useTour();
|
|
29
|
+
|
|
30
|
+
const isLastStep = computed(() => {
|
|
31
|
+
return state.active && state.stepIndex === state.active.steps.length - 1;
|
|
32
|
+
});
|
|
33
|
+
const isFirstStep = computed(() => {
|
|
34
|
+
return state.active && state.stepIndex === 0;
|
|
35
|
+
});
|
|
36
|
+
</script>
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { markRaw } from "vue";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {Object} TourStep
|
|
5
|
+
* @property {String} [target] - The CSS selector for the target element. If omitted, step renders as a modal.
|
|
6
|
+
* @property {String} [title] - The title text for the step.
|
|
7
|
+
* @property {String} [content] - The content text for the step.
|
|
8
|
+
* @property {Object} [component] - A Vue component to completely replace the modal/popover wrapper and layout.
|
|
9
|
+
* @property {Object} [contentComponent] - A Vue component to replace the step text content, keeping the modal/popover wrappers and default footer.
|
|
10
|
+
* @property {Object} [componentProps] - Props to bind to the custom component.
|
|
11
|
+
* @property {Boolean} [hideFooter] - If true, hides the default tour footer (Next/Back buttons).
|
|
12
|
+
* @property {String} [placement] - Floating UI placement string (e.g. 'bottom', 'right').
|
|
13
|
+
* @property {Boolean} [highlight] - If true, darkens the backdrop around the target element.
|
|
14
|
+
* @property {Object} [modalProps] - Props to pass to the modal (if target is omitted).
|
|
15
|
+
* @property {Object} [popoverProps] - Props to pass to the popover (if target is present).
|
|
16
|
+
* @property {Function} [onEnter] - Async hook called before the step is rendered.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Creates the tour API methods used to control the tour lifecycle
|
|
21
|
+
* @param {Object} state - The reactive state object
|
|
22
|
+
* @returns {Object} Tour API object
|
|
23
|
+
*/
|
|
24
|
+
export const createApi = (state) => ({
|
|
25
|
+
/**
|
|
26
|
+
* Starts a new tour
|
|
27
|
+
* @param {Object} tour - The tour configuration object
|
|
28
|
+
* @param {TourStep[]} tour.steps - Array of steps for the tour
|
|
29
|
+
* @param {String} [tour.name] - Optional name for the tour
|
|
30
|
+
* @param {Object} [tour.modalProps] - Global props applied to all modal steps in this tour (can be overridden by step)
|
|
31
|
+
* @param {Object} [tour.popoverProps] - Global props applied to all popover steps in this tour (can be overridden by step)
|
|
32
|
+
*/
|
|
33
|
+
async start(tour) {
|
|
34
|
+
if (!tour?.steps?.length) return;
|
|
35
|
+
await this._runHooks(tour.steps[0]);
|
|
36
|
+
state.active = markRaw(tour);
|
|
37
|
+
state.stepIndex = 0;
|
|
38
|
+
},
|
|
39
|
+
/**
|
|
40
|
+
* Advances the tour to the next step, or stops if it is the last step
|
|
41
|
+
*/
|
|
42
|
+
async next() {
|
|
43
|
+
const active = state.active;
|
|
44
|
+
if (!active) return;
|
|
45
|
+
|
|
46
|
+
if (state.stepIndex < active.steps.length - 1) {
|
|
47
|
+
const nextIndex = state.stepIndex + 1;
|
|
48
|
+
await this._runHooks(active.steps[nextIndex]);
|
|
49
|
+
state.stepIndex = nextIndex;
|
|
50
|
+
} else {
|
|
51
|
+
this.stop();
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
/**
|
|
55
|
+
* Returns the tour to the previous step
|
|
56
|
+
*/
|
|
57
|
+
async prev() {
|
|
58
|
+
const active = state.active;
|
|
59
|
+
if (!active) return;
|
|
60
|
+
|
|
61
|
+
if (state.stepIndex > 0) {
|
|
62
|
+
const prevIndex = state.stepIndex - 1;
|
|
63
|
+
await this._runHooks(active.steps[prevIndex]);
|
|
64
|
+
state.stepIndex = prevIndex;
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
/**
|
|
68
|
+
* Stops and closes the current tour
|
|
69
|
+
*/
|
|
70
|
+
stop() {
|
|
71
|
+
state.active = null;
|
|
72
|
+
state.stepIndex = 0;
|
|
73
|
+
},
|
|
74
|
+
/**
|
|
75
|
+
* Internal method to run asynchronous lifecycle hooks on a step
|
|
76
|
+
* @param {Object} step - The tour step configuration
|
|
77
|
+
*/
|
|
78
|
+
async _runHooks(step) {
|
|
79
|
+
if (step && typeof step.onEnter === 'function') {
|
|
80
|
+
await step.onEnter();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module tourPlugin
|
|
3
|
+
*/
|
|
4
|
+
import { reactive } from 'vue';
|
|
5
|
+
import UluTourDisplay from './UluTourDisplay.vue';
|
|
6
|
+
import { createApi } from './api.js';
|
|
7
|
+
import { useTour } from './useTour.js';
|
|
8
|
+
|
|
9
|
+
export default function install(app) {
|
|
10
|
+
const state = reactive({
|
|
11
|
+
active: null,
|
|
12
|
+
stepIndex: 0
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const api = createApi(state);
|
|
16
|
+
|
|
17
|
+
app.component("UluTourDisplay", UluTourDisplay);
|
|
18
|
+
|
|
19
|
+
app.config.globalProperties.$uluTour = api;
|
|
20
|
+
app.config.globalProperties.$uluTourState = state;
|
|
21
|
+
|
|
22
|
+
app.provide('uluTour', api);
|
|
23
|
+
app.provide('uluTourState', state);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export { useTour };
|
|
27
|
+
export { default as UluTourDisplay } from './UluTourDisplay.vue';
|
|
28
|
+
export { default as UluTourPager } from './UluTourPager.vue';
|
|
29
|
+
export { default as UluTourContent } from './UluTourContent.vue';
|