@ulu/frontend-vue 0.1.0-beta.1
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/LICENSE +21 -0
- package/README.md +9 -0
- package/dist/breakpoints-ClT9bfZm.js +211 -0
- package/dist/frontend-vue.css +1 -0
- package/dist/frontend-vue.js +82 -0
- package/dist/frontend-vue.umd.cjs +561 -0
- package/dist/index-P5Rwl_Dl.js +7263 -0
- package/dist/index.es-HlG3u0J5.js +3134 -0
- package/lib/_index.scss +14 -0
- package/lib/components/_index.scss +6 -0
- package/lib/components/collapsible/UluAccordion.vue +82 -0
- package/lib/components/collapsible/UluCollapsibleRegion.vue +278 -0
- package/lib/components/collapsible/UluDropdown.vue +42 -0
- package/lib/components/collapsible/UluModal.vue +384 -0
- package/lib/components/collapsible/UluOverflowPopover.vue +52 -0
- package/lib/components/collapsible/UluTab.vue +9 -0
- package/lib/components/collapsible/UluTabGroup.vue +31 -0
- package/lib/components/collapsible/UluTabList.vue +9 -0
- package/lib/components/collapsible/UluTabPanel.vue +9 -0
- package/lib/components/collapsible/UluTabPanels.vue +9 -0
- package/lib/components/elements/UluAlert.vue +81 -0
- package/lib/components/elements/UluBadge.vue +58 -0
- package/lib/components/elements/UluBadgeStack.vue +27 -0
- package/lib/components/elements/UluButton.vue +161 -0
- package/lib/components/elements/UluCallout.vue +30 -0
- package/lib/components/elements/UluCard.vue +241 -0
- package/lib/components/elements/UluDefinitionList.vue +40 -0
- package/lib/components/elements/UluExternalLink.vue +47 -0
- package/lib/components/elements/UluIcon.vue +108 -0
- package/lib/components/elements/UluList.vue +87 -0
- package/lib/components/elements/UluMain.vue +5 -0
- package/lib/components/elements/UluSpokeSpinner.vue +25 -0
- package/lib/components/elements/UluTag.vue +53 -0
- package/lib/components/forms/UluCheckboxMenu.vue +36 -0
- package/lib/components/forms/UluFileDisplay.vue +39 -0
- package/lib/components/forms/UluFormDropzone.vue +62 -0
- package/lib/components/forms/UluFormFile.vue +47 -0
- package/lib/components/forms/UluFormMessage.vue +20 -0
- package/lib/components/forms/UluFormSelect.vue +37 -0
- package/lib/components/forms/UluFormText.vue +32 -0
- package/lib/components/forms/UluSearchForm.vue +31 -0
- package/lib/components/index.js +54 -0
- package/lib/components/layout/UluAdaptiveLayout.vue +11 -0
- package/lib/components/layout/UluDataGrid.vue +41 -0
- package/lib/components/layout/UluTitleRail.vue +56 -0
- package/lib/components/layout/UluWhenBreakpoint.vue +86 -0
- package/lib/components/navigation/UluBreadcrumb.vue +72 -0
- package/lib/components/navigation/UluMenu.vue +105 -0
- package/lib/components/navigation/UluMenuStack.vue +49 -0
- package/lib/components/navigation/UluNavStrip.vue +48 -0
- package/lib/components/navigation/UluSkipLink.vue +5 -0
- package/lib/components/systems/facets/UluFacets.vue +380 -0
- package/lib/components/systems/facets/UluFacetsList.vue +39 -0
- package/lib/components/systems/facets/UluFacetsSearch.vue +67 -0
- package/lib/components/systems/facets/_facets.scss +64 -0
- package/lib/components/systems/index.js +17 -0
- package/lib/components/systems/scroll-anchors/UluScrollAnchors.vue +152 -0
- package/lib/components/systems/scroll-anchors/UluScrollAnchorsNav.vue +37 -0
- package/lib/components/systems/scroll-anchors/UluScrollAnchorsNavAnimated.vue +124 -0
- package/lib/components/systems/scroll-anchors/UluScrollAnchorsSection.vue +63 -0
- package/lib/components/systems/scroll-anchors/symbols.js +6 -0
- package/lib/components/systems/skeleton/UluShowSkeleton.vue +13 -0
- package/lib/components/systems/skeleton/UluSkeletonContent.vue +60 -0
- package/lib/components/systems/skeleton/UluSkeletonMedia.vue +11 -0
- package/lib/components/systems/skeleton/UluSkeletonTextInline.vue +9 -0
- package/lib/components/systems/slider/UluImageSlideShow.vue +75 -0
- package/lib/components/systems/slider/UluSlideShow.vue +331 -0
- package/lib/components/systems/slider/UluSlideShowSlide.vue +25 -0
- package/lib/components/systems/table-sticky/UluTableSticky.vue +793 -0
- package/lib/components/systems/table-sticky/UluTableStickyRows.vue +73 -0
- package/lib/components/systems/table-sticky/UluTableStickyTable.vue +237 -0
- package/lib/components/systems/table-sticky/_table-sticky.scss +185 -0
- package/lib/components/utils/UluCondText.vue +28 -0
- package/lib/components/utils/UluEmpty.vue +3 -0
- package/lib/components/utils/UluEmptyView.vue +3 -0
- package/lib/components/utils/UluPlaceholderImage.vue +53 -0
- package/lib/components/utils/UluPlaceholderText.vue +25 -0
- package/lib/components/utils/UluRouteAnnouncer.vue +83 -0
- package/lib/components/visualizations/UluAnimateNumber.vue +32 -0
- package/lib/components/visualizations/UluProgressBar.vue +94 -0
- package/lib/components/visualizations/UluProgressDonut.vue +97 -0
- package/lib/composables/index.js +10 -0
- package/lib/composables/useBreakpointManager.js +68 -0
- package/lib/composables/useIcon.js +62 -0
- package/lib/composables/useModifiers.js +93 -0
- package/lib/composables/useWindowResize.js +64 -0
- package/lib/index.js +10 -0
- package/lib/plugins/_index.scss +7 -0
- package/lib/plugins/breakpoints/index.js +47 -0
- package/lib/plugins/index.js +11 -0
- package/lib/plugins/modals/UluModalsDisplay.vue +59 -0
- package/lib/plugins/modals/api.js +76 -0
- package/lib/plugins/modals/index.js +60 -0
- package/lib/plugins/modals/useModals.js +9 -0
- package/lib/plugins/popovers/UluPopover.vue +189 -0
- package/lib/plugins/popovers/UluTooltipDisplay.vue +15 -0
- package/lib/plugins/popovers/UluTooltipPopover.vue +83 -0
- package/lib/plugins/popovers/defaults.js +108 -0
- package/lib/plugins/popovers/directive.js +95 -0
- package/lib/plugins/popovers/index.js +18 -0
- package/lib/plugins/popovers/manager.js +54 -0
- package/lib/plugins/popovers/useFollow.js +80 -0
- package/lib/plugins/popovers/utils.js +5 -0
- package/lib/plugins/toast/UluToast.vue +87 -0
- package/lib/plugins/toast/UluToastDisplay.vue +35 -0
- package/lib/plugins/toast/_toast.scss +198 -0
- package/lib/plugins/toast/defaults.js +30 -0
- package/lib/plugins/toast/index.js +17 -0
- package/lib/plugins/toast/store.js +71 -0
- package/lib/plugins/toast/useToast.js +18 -0
- package/lib/settings.js +119 -0
- package/lib/utils/dom.js +14 -0
- package/lib/utils/placeholder.js +6 -0
- package/lib/utils/vue-router.js +219 -0
- package/package.json +75 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { ref, markRaw } from "vue";
|
|
2
|
+
|
|
3
|
+
// Create array to be used internally to manage individual modals
|
|
4
|
+
const modals = [];
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Reactive State Object (used inside global components)
|
|
8
|
+
*/
|
|
9
|
+
const state = ref({
|
|
10
|
+
/**
|
|
11
|
+
* Holds active component options (including component, and options)
|
|
12
|
+
*/
|
|
13
|
+
active: null,
|
|
14
|
+
/**
|
|
15
|
+
* Populated with any props passed to open method, bound to modal component
|
|
16
|
+
*/
|
|
17
|
+
activeProps: null,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Reactive data from state
|
|
22
|
+
*/
|
|
23
|
+
const data = state.value;
|
|
24
|
+
|
|
25
|
+
export const modalsState = {
|
|
26
|
+
data,
|
|
27
|
+
modals,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const createApi = (resolveModalOptions) => ({
|
|
31
|
+
open(name, props = null) {
|
|
32
|
+
const modal = this.get(name);
|
|
33
|
+
data.active = markRaw(modal);
|
|
34
|
+
data.activeProps = Object.assign({}, modal.props, props);
|
|
35
|
+
},
|
|
36
|
+
/**
|
|
37
|
+
* Close the active modal
|
|
38
|
+
* @param {String|Node} focusTo The element or selector for an element to programmatically focus after modal close
|
|
39
|
+
* @see https://www.deque.com/blog/accessible-routing-in-javascript-frameworks/
|
|
40
|
+
*/
|
|
41
|
+
close() {
|
|
42
|
+
data.active = null;
|
|
43
|
+
data.activeProps = null;
|
|
44
|
+
},
|
|
45
|
+
/**
|
|
46
|
+
* Get a modal's config object by name
|
|
47
|
+
* @return {Object} Modal config object
|
|
48
|
+
*/
|
|
49
|
+
get(name) {
|
|
50
|
+
const modal = modals.find(m => m.name === name);
|
|
51
|
+
if (modal) {
|
|
52
|
+
return modal;
|
|
53
|
+
} else {
|
|
54
|
+
throw new Error(`Unable to find modal named: ${ name }`);
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
/**
|
|
58
|
+
* Add a modal config
|
|
59
|
+
*/
|
|
60
|
+
add(config) {
|
|
61
|
+
const resolved = resolveModalOptions(config);
|
|
62
|
+
modals.push(resolved);
|
|
63
|
+
},
|
|
64
|
+
/**
|
|
65
|
+
* Removes a modal config by name
|
|
66
|
+
* @return {Object} Modal that was removed
|
|
67
|
+
*/
|
|
68
|
+
remove(name) {
|
|
69
|
+
const index = modals.findIndex(m => m.name === name);
|
|
70
|
+
if (index > -1) {
|
|
71
|
+
return modals.splice(index, 1);
|
|
72
|
+
} else {
|
|
73
|
+
warn("unable to find modal to remove");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module plugins/modals/index.js
|
|
3
|
+
* @version 3.0.0
|
|
4
|
+
* Modals plugin (adds components, global registry, etc)
|
|
5
|
+
* - Updated version handles both independent and global registered/triggered modals
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import UluModalsDisplay from "./UluModalsDisplay.vue";
|
|
9
|
+
import UluModal from "../../components/collapsible/UluModal.vue";
|
|
10
|
+
import { createApi, modalsState } from "./api.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Default plugin options
|
|
14
|
+
* @typedef {Object} UluModalsPluginOptions
|
|
15
|
+
* @property {String} componentNameDisplay Name for modals component that displays app-wide modals
|
|
16
|
+
* @property {String} componentNameModal Name for modal component
|
|
17
|
+
* @property {Array} modals Modals configs [{ name, component, props }]
|
|
18
|
+
* @property {UluModalOptions} modalOptions Options to merge into individual modal options (to serve as defaults for each modal, see UluModalOptions)
|
|
19
|
+
*/
|
|
20
|
+
const pluginDefaults = {
|
|
21
|
+
componentNameDisplay: "UluModalsDisplay",
|
|
22
|
+
componentNameModal: "UluModal",
|
|
23
|
+
modals: [],
|
|
24
|
+
modalOptions: {}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Modals Vue Plugin
|
|
29
|
+
* @param {Object} App Vue app instance passed to plugin
|
|
30
|
+
* @param {UluModalsPluginOptions} userOptions Options to change (see defaults)
|
|
31
|
+
*/
|
|
32
|
+
export default function install(app, userOptions) {
|
|
33
|
+
const options = Object.assign({}, pluginDefaults, userOptions);
|
|
34
|
+
|
|
35
|
+
// Merges in default modal config options on individual modal config
|
|
36
|
+
const resolveModalOptions = opts => Object.assign({}, options.modalOptions, opts);
|
|
37
|
+
|
|
38
|
+
const api = createApi(resolveModalOptions);
|
|
39
|
+
|
|
40
|
+
// Register the two global components
|
|
41
|
+
app.component(options.componentNameDisplay, UluModalsDisplay);
|
|
42
|
+
app.component(options.componentNameModal, UluModal);
|
|
43
|
+
|
|
44
|
+
// Create array to be used internally to manage individual modals
|
|
45
|
+
options.modals.forEach(config => {
|
|
46
|
+
api.add(config)
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
modalsState.options = options;
|
|
50
|
+
|
|
51
|
+
// Global property with API so user can interact with modals
|
|
52
|
+
app.config.globalProperties.$uluModals = api;
|
|
53
|
+
|
|
54
|
+
// Provide the api for composition api usage
|
|
55
|
+
app.provide('uluModals', api);
|
|
56
|
+
|
|
57
|
+
// Global property for modals component to access state
|
|
58
|
+
// and for debugging logging if needed
|
|
59
|
+
app.config.globalProperties.$uluModalsState = modalsState;
|
|
60
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
<!-- NOTE: Need to rename classes when moving this into the library -->
|
|
2
|
+
<template>
|
|
3
|
+
<button
|
|
4
|
+
type="button"
|
|
5
|
+
ref="trigger"
|
|
6
|
+
@click="toggle"
|
|
7
|
+
:disabled="disabled"
|
|
8
|
+
:class="[
|
|
9
|
+
{ [activeClass] : isOpen },
|
|
10
|
+
classes.trigger
|
|
11
|
+
]"
|
|
12
|
+
:aria-expanded="isOpen ? 'true' : 'false'"
|
|
13
|
+
:aria-controls="id"
|
|
14
|
+
:aria-label="triggerAlt"
|
|
15
|
+
v-ulu-tooltip="tooltip ? tooltip : null"
|
|
16
|
+
>
|
|
17
|
+
<slot name="trigger" :isOpen="isOpen"/>
|
|
18
|
+
</button>
|
|
19
|
+
<span
|
|
20
|
+
class="popover"
|
|
21
|
+
ref="content"
|
|
22
|
+
:class="[
|
|
23
|
+
size ? `popover--${ size }` : '',
|
|
24
|
+
{
|
|
25
|
+
'popover--no-padding' : noPadding,
|
|
26
|
+
'is-active' : isOpen
|
|
27
|
+
},
|
|
28
|
+
classes.content,
|
|
29
|
+
]"
|
|
30
|
+
:aria-hidden="isOpen ? 'false' : 'true'"
|
|
31
|
+
:id="id"
|
|
32
|
+
:style="floatingStyles"
|
|
33
|
+
:data-placement="placement"
|
|
34
|
+
@keydown.esc="changeTo(false)"
|
|
35
|
+
tabindex="-1"
|
|
36
|
+
>
|
|
37
|
+
<span class="popover__inner">
|
|
38
|
+
<slot name="content" :close="close"/>
|
|
39
|
+
</span>
|
|
40
|
+
<span v-if="$slots.footer" class="popover__footer">
|
|
41
|
+
<slot name="footer" :close="close"/>
|
|
42
|
+
</span>
|
|
43
|
+
<span
|
|
44
|
+
v-if="config.arrow"
|
|
45
|
+
class="popover__arrow"
|
|
46
|
+
ref="contentArrow"
|
|
47
|
+
:style="arrowStyles"
|
|
48
|
+
data-ulu-popover-arrow
|
|
49
|
+
></span>
|
|
50
|
+
</span>
|
|
51
|
+
</template>
|
|
52
|
+
<script setup>
|
|
53
|
+
import { computed, ref, unref, nextTick } from "vue";
|
|
54
|
+
import { options as defaults } from "./manager.js";
|
|
55
|
+
import { newUid } from "./utils.js";
|
|
56
|
+
import {
|
|
57
|
+
useFloating,
|
|
58
|
+
autoUpdate,
|
|
59
|
+
offset,
|
|
60
|
+
inline,
|
|
61
|
+
flip,
|
|
62
|
+
shift,
|
|
63
|
+
arrow,
|
|
64
|
+
} from "@floating-ui/vue";
|
|
65
|
+
|
|
66
|
+
const emit = defineEmits(["toggle"]);
|
|
67
|
+
const props = defineProps({
|
|
68
|
+
triggerAlt: String,
|
|
69
|
+
disabled: Boolean,
|
|
70
|
+
tooltip: String,
|
|
71
|
+
size: String,
|
|
72
|
+
noPadding: Boolean,
|
|
73
|
+
config: Object,
|
|
74
|
+
startOpen: Boolean,
|
|
75
|
+
activeClass: {
|
|
76
|
+
type: String,
|
|
77
|
+
default: "is-active"
|
|
78
|
+
},
|
|
79
|
+
classes: {
|
|
80
|
+
type: Object,
|
|
81
|
+
default: () => ({})
|
|
82
|
+
},
|
|
83
|
+
clickOutsideCloses: {
|
|
84
|
+
type: Boolean,
|
|
85
|
+
default: true
|
|
86
|
+
},
|
|
87
|
+
directFocus: {
|
|
88
|
+
type: Function,
|
|
89
|
+
default: ({ isOpen, content }) => {
|
|
90
|
+
if (isOpen) {
|
|
91
|
+
content.focus({ preventScroll: true });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const id = newUid();
|
|
98
|
+
const config = Object.assign({}, defaults.popover, props.config);
|
|
99
|
+
const isOpen = ref(props.startOpen || false);
|
|
100
|
+
const trigger = ref(null);
|
|
101
|
+
const content = ref(null);
|
|
102
|
+
const contentArrow = ref(null);
|
|
103
|
+
|
|
104
|
+
const middleware = [
|
|
105
|
+
...(config.inline ? [ inline() ] : []),
|
|
106
|
+
...(config.offset ? [ offset(config.offset) ] : []),
|
|
107
|
+
flip(),
|
|
108
|
+
shift(),
|
|
109
|
+
...(config.arrow ? [ arrow({ element: contentArrow }) ] : []),
|
|
110
|
+
];
|
|
111
|
+
const options = {
|
|
112
|
+
placement: config.placement,
|
|
113
|
+
whileElementsMounted: autoUpdate,
|
|
114
|
+
middleware
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const {
|
|
118
|
+
floatingStyles,
|
|
119
|
+
placement,
|
|
120
|
+
middlewareData,
|
|
121
|
+
update,
|
|
122
|
+
isPositioned,
|
|
123
|
+
} = useFloating(trigger, content, options);
|
|
124
|
+
|
|
125
|
+
const arrowStyles = computed(() => {
|
|
126
|
+
const pos = middlewareData.value?.arrow;
|
|
127
|
+
if (!pos) return null;
|
|
128
|
+
return {
|
|
129
|
+
position: "absolute",
|
|
130
|
+
left: pos?.x != null ? `${ pos.x }px` : "",
|
|
131
|
+
top: pos?.y != null ? `${ pos.y }px` : "",
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
if (config.onReady) {
|
|
136
|
+
config.onReady({ update, isPositioned });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const toggle = () => {
|
|
140
|
+
changeTo(!isOpen.value);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const changeTo = (toOpen) => {
|
|
144
|
+
isOpen.value = toOpen;
|
|
145
|
+
const focusArgs = {
|
|
146
|
+
trigger: unref(trigger),
|
|
147
|
+
content: unref(content),
|
|
148
|
+
isOpen: unref(isOpen)
|
|
149
|
+
};
|
|
150
|
+
const eventArgs = { isOpen: focusArgs.isOpen };
|
|
151
|
+
nextTick(() => {
|
|
152
|
+
if (isOpen.value) {
|
|
153
|
+
update();
|
|
154
|
+
// Push to next event, without this will get triggered by the original click event
|
|
155
|
+
window.setTimeout(() => {
|
|
156
|
+
addOutsideClick();
|
|
157
|
+
props.directFocus(focusArgs);
|
|
158
|
+
emit("toggle", eventArgs);
|
|
159
|
+
}, 0);
|
|
160
|
+
} else {
|
|
161
|
+
destroyOutsideClick();
|
|
162
|
+
props.directFocus(focusArgs);
|
|
163
|
+
emit("toggle", eventArgs);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
let outsideHandler;
|
|
169
|
+
const addOutsideClick = () => {
|
|
170
|
+
if (props.clickOutsideCloses) {
|
|
171
|
+
if (outsideHandler) {
|
|
172
|
+
destroyOutsideClick();
|
|
173
|
+
}
|
|
174
|
+
outsideHandler = event => {
|
|
175
|
+
if (!content.value.contains(event.target)) {
|
|
176
|
+
changeTo(false);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
document.addEventListener("click", outsideHandler);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
const destroyOutsideClick = () => {
|
|
183
|
+
if (outsideHandler) {
|
|
184
|
+
document.removeEventListener("click", outsideHandler);
|
|
185
|
+
outsideHandler = null;
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
const close = () => changeTo(false);
|
|
189
|
+
</script>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Note:
|
|
3
|
+
Using the name TooltipDisplay incase we want to create abstract
|
|
4
|
+
component (wrapper, renderless, etc) in the future
|
|
5
|
+
-->
|
|
6
|
+
<template>
|
|
7
|
+
<Teleport :to="options.plugin.tooltipTeleportTo">
|
|
8
|
+
<TooltipPopover v-if="active" :config="activeConfig"/>
|
|
9
|
+
</Teleport>
|
|
10
|
+
</template>
|
|
11
|
+
|
|
12
|
+
<script setup>
|
|
13
|
+
import { active, activeConfig, options } from "./manager.js";
|
|
14
|
+
import TooltipPopover from "./UluTooltipPopover.vue";
|
|
15
|
+
</script>
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
<!-- NOTE: Need to rename classes when moving this into the library -->
|
|
2
|
+
<template>
|
|
3
|
+
<span
|
|
4
|
+
class="popover popover--tooltip is-active"
|
|
5
|
+
ref="content"
|
|
6
|
+
aria-hidden="true"
|
|
7
|
+
:data-placement="placement"
|
|
8
|
+
:class="config.class"
|
|
9
|
+
:style="floatingStyles"
|
|
10
|
+
>
|
|
11
|
+
<span
|
|
12
|
+
v-if="config.isHtml"
|
|
13
|
+
class="popover__inner"
|
|
14
|
+
v-html="config.content"
|
|
15
|
+
>
|
|
16
|
+
</span>
|
|
17
|
+
<span v-else class="popover__inner">
|
|
18
|
+
{{ config.content }}
|
|
19
|
+
</span>
|
|
20
|
+
<span
|
|
21
|
+
v-if="config.arrow"
|
|
22
|
+
class="popover__arrow"
|
|
23
|
+
ref="contentArrow"
|
|
24
|
+
:style="arrowStyles"
|
|
25
|
+
></span>
|
|
26
|
+
</span>
|
|
27
|
+
</template>
|
|
28
|
+
|
|
29
|
+
<script setup>
|
|
30
|
+
import { ref, toRef, computed } from "vue";
|
|
31
|
+
import {
|
|
32
|
+
useFloating,
|
|
33
|
+
autoUpdate,
|
|
34
|
+
offset,
|
|
35
|
+
inline,
|
|
36
|
+
flip,
|
|
37
|
+
shift,
|
|
38
|
+
arrow,
|
|
39
|
+
} from "@floating-ui/vue";
|
|
40
|
+
|
|
41
|
+
const { config } = defineProps({
|
|
42
|
+
config: Object
|
|
43
|
+
});
|
|
44
|
+
const trigger = toRef(config.trigger);
|
|
45
|
+
const content = ref(null);
|
|
46
|
+
const contentArrow = ref(null);
|
|
47
|
+
const middleware = [
|
|
48
|
+
...(config.inline ? [ inline() ] : []),
|
|
49
|
+
...(config.offset ? [ offset(config.offset) ] : []),
|
|
50
|
+
flip(),
|
|
51
|
+
shift(),
|
|
52
|
+
...(config.arrow ? [ arrow({ element: contentArrow }) ] : []),
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
const options = {
|
|
56
|
+
placement: config.placement,
|
|
57
|
+
whileElementsMounted: autoUpdate,
|
|
58
|
+
middleware
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const {
|
|
62
|
+
floatingStyles,
|
|
63
|
+
placement,
|
|
64
|
+
middlewareData,
|
|
65
|
+
update,
|
|
66
|
+
isPositioned,
|
|
67
|
+
} = useFloating(trigger, content, options);
|
|
68
|
+
|
|
69
|
+
const arrowStyles = computed(() => {
|
|
70
|
+
const pos = middlewareData.value?.arrow;
|
|
71
|
+
if (!pos) return null;
|
|
72
|
+
return {
|
|
73
|
+
position: "absolute",
|
|
74
|
+
left: pos?.x != null ? `${ pos.x }px` : "",
|
|
75
|
+
top: pos?.y != null ? `${ pos.y }px` : "",
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (config.onReady) {
|
|
80
|
+
config.onReady({ update, isPositioned });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
</script>
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
/**
|
|
3
|
+
* Default Plugin Options
|
|
4
|
+
* @type {Object}
|
|
5
|
+
*/
|
|
6
|
+
plugin: {
|
|
7
|
+
/**
|
|
8
|
+
* Whether to install all components globally in Vue and add the directive for
|
|
9
|
+
* tooltips globally in Vue
|
|
10
|
+
*/
|
|
11
|
+
global: true,
|
|
12
|
+
/**
|
|
13
|
+
* The directive name to use (default 'tooltip' = <el v-ulu-tooltip="'hello world'">)
|
|
14
|
+
* @type {String}
|
|
15
|
+
*/
|
|
16
|
+
directiveName: "ulu-tooltip",
|
|
17
|
+
/**
|
|
18
|
+
* The element that the tooltip should be rendered within
|
|
19
|
+
* - Default bottom of the body (on top of everything)
|
|
20
|
+
* - Doesn't need to be inline for accessibility since tooltips are just an enhancement
|
|
21
|
+
* content displayed within them should be hidden for assistive devices,
|
|
22
|
+
* they are not visible to assistive devices
|
|
23
|
+
* @type {String}
|
|
24
|
+
*/
|
|
25
|
+
tooltipTeleportTo: "body",
|
|
26
|
+
},
|
|
27
|
+
/**
|
|
28
|
+
* Default Popover Options
|
|
29
|
+
*/
|
|
30
|
+
popover: {
|
|
31
|
+
/**
|
|
32
|
+
* Include the floating-ui inline middleware (for inline elements that wrap)
|
|
33
|
+
* @type {Boolean}
|
|
34
|
+
*/
|
|
35
|
+
inline: true,
|
|
36
|
+
/**
|
|
37
|
+
* Delay when using the directive
|
|
38
|
+
* @type {Number}
|
|
39
|
+
*/
|
|
40
|
+
delay: 500,
|
|
41
|
+
/**
|
|
42
|
+
* Placement for floating-ui)
|
|
43
|
+
* @type {String}
|
|
44
|
+
*/
|
|
45
|
+
placement: "bottom",
|
|
46
|
+
/**
|
|
47
|
+
* Strategy for floating-ui (strategy)
|
|
48
|
+
* @type {String}
|
|
49
|
+
*/
|
|
50
|
+
strategy: "absolute",
|
|
51
|
+
/**
|
|
52
|
+
* Include the floating-ui offset middleware,
|
|
53
|
+
* @type {Number}
|
|
54
|
+
*/
|
|
55
|
+
offset: 16,
|
|
56
|
+
/**
|
|
57
|
+
* Include the floating-ui arrow middleware
|
|
58
|
+
* @type {Boolean}
|
|
59
|
+
*/
|
|
60
|
+
arrow: true,
|
|
61
|
+
},
|
|
62
|
+
/**
|
|
63
|
+
* Default Tooltip Options
|
|
64
|
+
* @type {Object}
|
|
65
|
+
*/
|
|
66
|
+
tooltip: {
|
|
67
|
+
/**
|
|
68
|
+
* Optional class binding for tooltip element
|
|
69
|
+
* @type {String|Object|Array}
|
|
70
|
+
*/
|
|
71
|
+
class: null,
|
|
72
|
+
/**
|
|
73
|
+
* Events to show tooltip on
|
|
74
|
+
* @type {Array.<String>}
|
|
75
|
+
*/
|
|
76
|
+
showEvents: ["pointerenter", "focus"],
|
|
77
|
+
/**
|
|
78
|
+
* Events to hide tooltip on
|
|
79
|
+
* @type {Array.<String>}
|
|
80
|
+
*/
|
|
81
|
+
hideEvents: ["pointerleave", "blur"],
|
|
82
|
+
/**
|
|
83
|
+
* Content should be output as plain HTML (ie v-html)
|
|
84
|
+
* - Note don't include interactive elements in tooltips!
|
|
85
|
+
* @type {Boolean}
|
|
86
|
+
*/
|
|
87
|
+
isHtml: false,
|
|
88
|
+
/**
|
|
89
|
+
* Element for floating ui to use as reference (can be virtual) or vue ref to element
|
|
90
|
+
* @type {Node|Object}
|
|
91
|
+
*/
|
|
92
|
+
trigger: null,
|
|
93
|
+
/**
|
|
94
|
+
* The content of the tooltip (String, Reactive ref or HTML [see isHtml option])
|
|
95
|
+
* @type {String|Object}
|
|
96
|
+
*/
|
|
97
|
+
content: null,
|
|
98
|
+
/**
|
|
99
|
+
* Delay when using the directive
|
|
100
|
+
* @type {Number}
|
|
101
|
+
*/
|
|
102
|
+
delay: 500,
|
|
103
|
+
/**
|
|
104
|
+
* Callback that is passed { update, isPositioned } for manual things
|
|
105
|
+
*/
|
|
106
|
+
onReady: null,
|
|
107
|
+
},
|
|
108
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { show, hide, createConfig } from "./manager.js";
|
|
2
|
+
/**
|
|
3
|
+
* Using weak map for listener to element relationship
|
|
4
|
+
* - As an extra fallback to remove listeners without affecting garabage collection
|
|
5
|
+
* - The unmount should be called for every element the directive is bound to but
|
|
6
|
+
*/
|
|
7
|
+
const cache = new WeakMap();
|
|
8
|
+
/**
|
|
9
|
+
* - We should consider allowing components to be passed for markup
|
|
10
|
+
* - don't want to use a component as then everything that uses this will have complicated conditionals
|
|
11
|
+
* ie. menu component could just have an option to bind the directive but if component needs conditional when not being used
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Directive Object
|
|
15
|
+
*/
|
|
16
|
+
export default {
|
|
17
|
+
mounted(trigger, binding) {
|
|
18
|
+
setup(trigger, binding);
|
|
19
|
+
},
|
|
20
|
+
beforeUpdate(trigger) {
|
|
21
|
+
removeListeners(trigger);
|
|
22
|
+
},
|
|
23
|
+
updated(trigger, binding) {
|
|
24
|
+
setup(trigger, binding);
|
|
25
|
+
},
|
|
26
|
+
umounted(trigger) {
|
|
27
|
+
removeListeners(trigger);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Resolves the users local config and attaches handlers
|
|
32
|
+
*/
|
|
33
|
+
function setup(trigger, binding) {
|
|
34
|
+
const config = resolveConfig(trigger, binding);
|
|
35
|
+
if (!config) return;
|
|
36
|
+
let tid = null;
|
|
37
|
+
|
|
38
|
+
const onShow = () => {
|
|
39
|
+
if (tid) return;
|
|
40
|
+
tid = setTimeout(() => {
|
|
41
|
+
show(config);
|
|
42
|
+
clearTimeout(tid);
|
|
43
|
+
}, config.delay);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const onHide = () => {
|
|
47
|
+
if (tid) {
|
|
48
|
+
clearTimeout(tid);
|
|
49
|
+
tid = null;
|
|
50
|
+
}
|
|
51
|
+
hide(config);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
config.showEvents.forEach(eventName => {
|
|
55
|
+
trigger.addEventListener(eventName, onShow);
|
|
56
|
+
});
|
|
57
|
+
config.hideEvents.forEach(eventName => {
|
|
58
|
+
trigger.addEventListener(eventName, onHide);
|
|
59
|
+
});
|
|
60
|
+
cache.set(trigger, { onShow, onHide, config });
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Removes an elements listeners by checking agains local cache map
|
|
64
|
+
*/
|
|
65
|
+
function removeListeners(trigger) {
|
|
66
|
+
if (!cache.has(trigger)) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const { config, onShow, onHide } = cache.get(trigger);
|
|
70
|
+
config.showEvents.forEach(eventName => {
|
|
71
|
+
trigger.removeEventListener(eventName, onShow);
|
|
72
|
+
});
|
|
73
|
+
config.hideEvents.forEach(eventName => {
|
|
74
|
+
trigger.removeEventListener(eventName, onHide);
|
|
75
|
+
});
|
|
76
|
+
cache.delete(trigger);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Allow user to provide a simple string for the content of the toolkit
|
|
80
|
+
* or the extended object syntax. This resolves to the object syntax
|
|
81
|
+
*/
|
|
82
|
+
function resolveConfig(trigger, binding) {
|
|
83
|
+
const { value } = binding;
|
|
84
|
+
let config;
|
|
85
|
+
if (value === false || value === null) {
|
|
86
|
+
return; // Disabled
|
|
87
|
+
} else if (typeof value === "object") {
|
|
88
|
+
config = value;
|
|
89
|
+
// String/Number, etc (displayed as is)
|
|
90
|
+
} else {
|
|
91
|
+
config = { content: value };
|
|
92
|
+
}
|
|
93
|
+
// Using assign so users can override trigger if needed
|
|
94
|
+
return createConfig(Object.assign({}, { trigger }, config));
|
|
95
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { init } from "./manager.js";
|
|
2
|
+
import directive from "./directive.js";
|
|
3
|
+
import UluPopover from "./UluPopover.vue";
|
|
4
|
+
import UluTooltipDisplay from "./UluTooltipDisplay.vue";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Install plugin
|
|
8
|
+
* - Set user options
|
|
9
|
+
* - Add the global directive for the user to trigger tooltips
|
|
10
|
+
*/
|
|
11
|
+
export default function install(app, userOptions = {}) {
|
|
12
|
+
const options = init(userOptions);
|
|
13
|
+
if (options.plugin.global) {
|
|
14
|
+
app.directive(options.plugin.directiveName, directive);
|
|
15
|
+
app.component("UluTooltipDisplay", UluTooltipDisplay);
|
|
16
|
+
app.component("UluPopover", UluPopover);
|
|
17
|
+
}
|
|
18
|
+
}
|