@ulu/frontend-vue 0.1.3-beta.8 → 0.2.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/dist/frontend-vue.css +1 -1
- package/dist/frontend-vue.js +10105 -3567
- package/dist/types/components/collapsible/UluAccordionGroup.vue.d.ts +20 -0
- package/dist/types/components/collapsible/UluAccordionGroup.vue.d.ts.map +1 -1
- package/dist/types/components/index.d.ts +2 -0
- package/dist/types/components/layout/UluDataGrid.vue.d.ts +16 -1
- package/dist/types/components/layout/UluDataGrid.vue.d.ts.map +1 -1
- package/dist/types/components/layout/UluWhenBreakpoint.vue.d.ts.map +1 -1
- package/dist/types/components/systems/index.d.ts +4 -0
- package/dist/types/components/systems/scroll-anchors/UluScrollAnchors.vue.d.ts +20 -58
- package/dist/types/components/systems/scroll-anchors/UluScrollAnchors.vue.d.ts.map +1 -1
- package/dist/types/components/systems/scroll-anchors/UluScrollAnchorsHeadlessSection.vue.d.ts +27 -0
- package/dist/types/components/systems/scroll-anchors/UluScrollAnchorsHeadlessSection.vue.d.ts.map +1 -0
- package/dist/types/components/systems/scroll-anchors/UluScrollAnchorsNav.vue.d.ts +17 -13
- package/dist/types/components/systems/scroll-anchors/UluScrollAnchorsNav.vue.d.ts.map +1 -1
- package/dist/types/components/systems/scroll-anchors/UluScrollAnchorsNavAnimated.vue.d.ts +27 -30
- package/dist/types/components/systems/scroll-anchors/UluScrollAnchorsNavAnimated.vue.d.ts.map +1 -1
- package/dist/types/components/systems/scroll-anchors/UluScrollAnchorsSection.vue.d.ts +27 -45
- package/dist/types/components/systems/scroll-anchors/UluScrollAnchorsSection.vue.d.ts.map +1 -1
- package/dist/types/components/systems/scroll-anchors/useScrollAnchorSection.d.ts +9 -0
- package/dist/types/components/systems/scroll-anchors/useScrollAnchorSection.d.ts.map +1 -0
- package/dist/types/components/systems/scroll-anchors/useScrollAnchorSections.d.ts +8 -0
- package/dist/types/components/systems/scroll-anchors/useScrollAnchorSections.d.ts.map +1 -0
- package/dist/types/components/systems/scroll-anchors/useScrollAnchors.d.ts +14 -0
- package/dist/types/components/systems/scroll-anchors/useScrollAnchors.d.ts.map +1 -0
- package/dist/types/composables/useBreakpointManager.d.ts +2 -2
- package/dist/types/composables/useBreakpointManager.d.ts.map +1 -1
- package/lib/components/_index.scss +1 -0
- package/lib/components/collapsible/UluAccordionGroup.vue +39 -5
- package/lib/components/collapsible/UluModal.vue +2 -2
- package/lib/components/index.js +2 -0
- package/lib/components/layout/UluDataGrid.vue +55 -15
- package/lib/components/layout/UluWhenBreakpoint.vue +11 -4
- package/lib/components/navigation/UluSkipLink.vue +1 -1
- package/lib/components/systems/index.js +4 -0
- package/lib/components/systems/scroll-anchors/UluScrollAnchors.vue +46 -145
- package/lib/components/systems/scroll-anchors/UluScrollAnchorsHeadlessSection.vue +50 -0
- package/lib/components/systems/scroll-anchors/UluScrollAnchorsNav.vue +18 -16
- package/lib/components/systems/scroll-anchors/UluScrollAnchorsNavAnimated.vue +100 -89
- package/lib/components/systems/scroll-anchors/UluScrollAnchorsSection.vue +65 -51
- package/lib/components/systems/scroll-anchors/_scroll-anchors-nav-animated.scss +67 -0
- package/lib/components/systems/scroll-anchors/useScrollAnchorSection.js +60 -0
- package/lib/components/systems/scroll-anchors/useScrollAnchorSections.js +15 -0
- package/lib/components/systems/scroll-anchors/useScrollAnchors.js +158 -0
- package/lib/composables/useBreakpointManager.js +2 -2
- package/package.json +5 -5
- package/dist/types/components/systems/scroll-anchors/symbols.d.ts +0 -7
- package/dist/types/components/systems/scroll-anchors/symbols.d.ts.map +0 -1
- package/lib/components/systems/scroll-anchors/symbols.js +0 -6
|
@@ -16,26 +16,66 @@
|
|
|
16
16
|
-->
|
|
17
17
|
|
|
18
18
|
<template>
|
|
19
|
-
<
|
|
19
|
+
<component
|
|
20
|
+
:is="element"
|
|
21
|
+
:data-grid-init="initialized"
|
|
22
|
+
ref="rootElement"
|
|
23
|
+
>
|
|
20
24
|
<slot />
|
|
21
|
-
</
|
|
25
|
+
</component>
|
|
22
26
|
</template>
|
|
23
27
|
|
|
24
|
-
<script>
|
|
25
|
-
import {
|
|
28
|
+
<script setup>
|
|
29
|
+
import { ref, onMounted, onBeforeUnmount, watch } from "vue";
|
|
26
30
|
import { debounce } from "@ulu/utils/performance.js";
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
import { setPositionClasses } from "@ulu/frontend";
|
|
32
|
+
|
|
33
|
+
const props = defineProps({
|
|
34
|
+
/**
|
|
35
|
+
* The element to use on data-grid container
|
|
36
|
+
*/
|
|
37
|
+
element: {
|
|
38
|
+
type: String,
|
|
39
|
+
default: "div"
|
|
34
40
|
},
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Tell the component when this grid is actually in a hidden container
|
|
43
|
+
* - When value changes the component will properly update position classes
|
|
44
|
+
*/
|
|
45
|
+
hidden: Boolean // New prop from SSR version
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const rootElement = ref(null); // Ref for the template root element
|
|
49
|
+
const initialized = ref(null);
|
|
50
|
+
let setThisPositionClasses = null; // To store the setPositionClasses function
|
|
51
|
+
let resizeHandler = null; // To store the debounced resize handler
|
|
52
|
+
|
|
53
|
+
onMounted(async () => {
|
|
54
|
+
setThisPositionClasses = () => {
|
|
55
|
+
if (rootElement.value) {
|
|
56
|
+
setPositionClasses(rootElement.value);
|
|
38
57
|
}
|
|
58
|
+
};
|
|
59
|
+
setThisPositionClasses(); // Initial call
|
|
60
|
+
initialized.value = true;
|
|
61
|
+
resizeHandler = debounce(setThisPositionClasses, 200, false); // `this` context is not needed in setup
|
|
62
|
+
window.addEventListener("resize", resizeHandler);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
onBeforeUnmount(() => {
|
|
66
|
+
if (resizeHandler) {
|
|
67
|
+
resizeHandler.cancel();
|
|
68
|
+
window.removeEventListener("resize", resizeHandler);
|
|
69
|
+
resizeHandler = null;
|
|
70
|
+
setThisPositionClasses = null; // Clear the function reference
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Watcher for the hidden prop
|
|
75
|
+
watch(() => props.hidden, (newVal, oldVal) => {
|
|
76
|
+
// Only run setClasses if it was hidden and now it's not, and the function exists
|
|
77
|
+
if (oldVal && !newVal && setThisPositionClasses) {
|
|
78
|
+
setThisPositionClasses();
|
|
39
79
|
}
|
|
40
|
-
};
|
|
80
|
+
});
|
|
41
81
|
</script>
|
|
@@ -58,9 +58,16 @@
|
|
|
58
58
|
};
|
|
59
59
|
|
|
60
60
|
const tearDownHandlers = () => {
|
|
61
|
-
if (uluBreakpointManager) {
|
|
61
|
+
if (uluBreakpointManager.value) {
|
|
62
62
|
handlers.value.forEach(({ name, direction, handler }) => {
|
|
63
|
-
uluBreakpointManager.at(name)
|
|
63
|
+
const breakpoint = uluBreakpointManager.value.at(name);
|
|
64
|
+
if (breakpoint) {
|
|
65
|
+
try {
|
|
66
|
+
breakpoint.remove(handler, direction);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error(error);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
64
71
|
});
|
|
65
72
|
}
|
|
66
73
|
handlers.value = [];
|
|
@@ -77,9 +84,9 @@
|
|
|
77
84
|
// Watch all the props and update if they change
|
|
78
85
|
// - Using array syntax to avoid "deep" flag
|
|
79
86
|
watch([() => props.max, () => props.min, () => props.only], () => {
|
|
80
|
-
if (uluBreakpointManager && handlersSetup.value) {
|
|
87
|
+
if (uluBreakpointManager.value && handlersSetup.value) {
|
|
81
88
|
tearDownHandlers();
|
|
82
|
-
setupHandlers(uluBreakpointManager);
|
|
89
|
+
setupHandlers(uluBreakpointManager.value);
|
|
83
90
|
}
|
|
84
91
|
});
|
|
85
92
|
|
|
@@ -11,10 +11,14 @@ export { default as UluFacetsSidebarLayout } from './facets/UluFacetsSidebarLayo
|
|
|
11
11
|
export { default as UluFacetsSort } from './facets/UluFacetsSort.vue';
|
|
12
12
|
export { default as UluFacetsList } from './facets/UluFacetsList.vue';
|
|
13
13
|
|
|
14
|
+
export { useScrollAnchors } from './scroll-anchors/useScrollAnchors.js';
|
|
15
|
+
export { useScrollAnchorSection } from './scroll-anchors/useScrollAnchorSection.js';
|
|
16
|
+
export { useScrollAnchorSections } from './scroll-anchors/useScrollAnchorSections.js';
|
|
14
17
|
export { default as UluScrollAnchors } from './scroll-anchors/UluScrollAnchors.vue';
|
|
15
18
|
export { default as UluScrollAnchorsNav } from './scroll-anchors/UluScrollAnchorsNav.vue';
|
|
16
19
|
export { default as UluScrollAnchorsNavAnimated } from './scroll-anchors/UluScrollAnchorsNavAnimated.vue';
|
|
17
20
|
export { default as UluScrollAnchorsSection } from './scroll-anchors/UluScrollAnchorsSection.vue';
|
|
21
|
+
export { default as UluScrollAnchorsHeadlessSection } from './scroll-anchors/UluScrollAnchorsHeadlessSection.vue';
|
|
18
22
|
|
|
19
23
|
export { default as UluShowSkeleton } from './skeleton/UluShowSkeleton.vue';
|
|
20
24
|
export { default as UluSkeletonContent } from './skeleton/UluSkeletonContent.vue';
|
|
@@ -1,153 +1,54 @@
|
|
|
1
|
-
<!-- Version: 0.0.2? (NEED to diff, unsure of changes) -->
|
|
2
1
|
<template>
|
|
3
|
-
<div class="scroll-anchors">
|
|
2
|
+
<div class="scroll-anchors" ref="componentEl">
|
|
4
3
|
<slot/>
|
|
5
4
|
</div>
|
|
6
5
|
</template>
|
|
7
6
|
|
|
8
|
-
<script>
|
|
9
|
-
import { computed } from "vue";
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
emits: ["section-change"],
|
|
14
|
-
props: {
|
|
15
|
-
firstItemActive: Boolean,
|
|
16
|
-
/**
|
|
17
|
-
* Observe
|
|
18
|
-
*/
|
|
19
|
-
observerOptions: {
|
|
20
|
-
type: Object,
|
|
21
|
-
default: () => ({
|
|
22
|
-
root: null,
|
|
23
|
-
threshhold: [0,1],
|
|
24
|
-
rootMargin: "-25% 0px -55% 0px"
|
|
25
|
-
// root: null,
|
|
26
|
-
// threshhold: [0,1],
|
|
27
|
-
// rootMargin: "25% 0px 75% 0px"
|
|
28
|
-
})
|
|
29
|
-
}
|
|
30
|
-
},
|
|
31
|
-
data() {
|
|
32
|
-
return {
|
|
33
|
-
isMounted: false,
|
|
34
|
-
sections: [], // Child components will section themselves
|
|
35
|
-
};
|
|
36
|
-
},
|
|
7
|
+
<script setup>
|
|
8
|
+
import { ref, computed, provide } from "vue";
|
|
9
|
+
import { useScrollAnchors } from "./useScrollAnchors.js";
|
|
10
|
+
|
|
11
|
+
const props = defineProps({
|
|
37
12
|
/**
|
|
38
|
-
*
|
|
39
|
-
* - Uses symbols
|
|
13
|
+
* Make the first item active by default on load
|
|
40
14
|
*/
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
this.update();
|
|
55
|
-
},
|
|
56
|
-
[UNREGISTER]: (instance) => {
|
|
57
|
-
const sections = this.sections;
|
|
58
|
-
const index = sections.findIndex(r => r.instance === instance);
|
|
59
|
-
if (index > -1) {
|
|
60
|
-
sections.splice(index, 1);
|
|
61
|
-
}
|
|
62
|
-
this.update();
|
|
63
|
-
},
|
|
64
|
-
};
|
|
65
|
-
},
|
|
66
|
-
methods: {
|
|
67
|
-
update() {
|
|
68
|
-
if (this.isMounted) {
|
|
69
|
-
this.observeItems();
|
|
70
|
-
}
|
|
71
|
-
},
|
|
72
|
-
getSectionIndex(el) {
|
|
73
|
-
return this.sections.findIndex(({ element }) => el === element);
|
|
74
|
-
},
|
|
75
|
-
/**
|
|
76
|
-
* Sets up a new observer to watch the section visibility
|
|
77
|
-
*/
|
|
78
|
-
createObserver() {
|
|
79
|
-
const { observerOptions, sections, removeActive, firstItemActive } = this;
|
|
80
|
-
let lastY = 0;
|
|
81
|
-
// Observer callback, basically just sets active state for a given slide
|
|
82
|
-
// - isIntersecting will change when the element enters and leaves
|
|
83
|
-
const onObserve = (entries) => {
|
|
84
|
-
entries.forEach(({ target, isIntersecting }) => {
|
|
85
|
-
const index = this.getSectionIndex(target);
|
|
86
|
-
const y = target.offsetTop;
|
|
87
|
-
const section = sections[index];
|
|
88
|
-
const firstExiting = index === 0 && lastY > y;
|
|
89
|
-
const lastExiting = index === sections.length - 1 && lastY < y;
|
|
90
|
-
if (section) {
|
|
91
|
-
this.$nextTick(() => {
|
|
92
|
-
if (isIntersecting) {
|
|
93
|
-
removeActive(section);
|
|
94
|
-
section.active = true;
|
|
95
|
-
// Only allow first and last to
|
|
96
|
-
} else if (firstExiting && !firstItemActive) {
|
|
97
|
-
removeActive();
|
|
98
|
-
} else if (lastExiting && section.active) {
|
|
99
|
-
removeActive();
|
|
100
|
-
}
|
|
101
|
-
this.$emit("section-change", {
|
|
102
|
-
section,
|
|
103
|
-
sections,
|
|
104
|
-
active: isIntersecting
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
};
|
|
110
|
-
// Add non-reactive prop for removal and changes to targets
|
|
111
|
-
this.observer = new IntersectionObserver(onObserve, observerOptions);
|
|
112
|
-
},
|
|
113
|
-
/**
|
|
114
|
-
* Add all slide elements as targets in observer
|
|
115
|
-
*/
|
|
116
|
-
observeItems() {
|
|
117
|
-
const { observer, sections } = this;
|
|
118
|
-
observer.disconnect();
|
|
119
|
-
sections.forEach(({ element }) => {
|
|
120
|
-
if (element) {
|
|
121
|
-
observer.observe(element);
|
|
122
|
-
}
|
|
123
|
-
});
|
|
124
|
-
},
|
|
125
|
-
removeActive(except = null) {
|
|
126
|
-
this.sections.forEach(s => {
|
|
127
|
-
if (s !== except) {
|
|
128
|
-
s.active = false;
|
|
129
|
-
}
|
|
130
|
-
});
|
|
131
|
-
},
|
|
132
|
-
/**
|
|
133
|
-
* Remove observer and it's internal DOM references (GC)
|
|
134
|
-
*/
|
|
135
|
-
destroyObserver() {
|
|
136
|
-
this.observer.disconnect();
|
|
137
|
-
this.observer = null;
|
|
138
|
-
},
|
|
139
|
-
},
|
|
140
|
-
mounted() {
|
|
141
|
-
const first = this.sections[0];
|
|
142
|
-
if (this.firstItemActive && first) {
|
|
143
|
-
first.active = true;
|
|
144
|
-
}
|
|
145
|
-
this.createObserver();
|
|
146
|
-
this.observeItems();
|
|
147
|
-
this.isMounted = true;
|
|
148
|
-
},
|
|
149
|
-
unmounted() {
|
|
150
|
-
this.destroyObserver();
|
|
15
|
+
firstItemActive: Boolean,
|
|
16
|
+
/**
|
|
17
|
+
* IntersectionObserver options
|
|
18
|
+
* - Defaults: { root: null, threshold: 0, rootMargin: "-25% 0px -55% 0px" }
|
|
19
|
+
* See: https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/IntersectionObserver
|
|
20
|
+
*/
|
|
21
|
+
observerOptions: {
|
|
22
|
+
type: Object,
|
|
23
|
+
default: () => ({
|
|
24
|
+
root: null,
|
|
25
|
+
threshold: 0,
|
|
26
|
+
rootMargin: "-25% 0px -55% 0px"
|
|
27
|
+
})
|
|
151
28
|
},
|
|
152
|
-
|
|
153
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Enable debug logging for the IntersectionObserver
|
|
31
|
+
*/
|
|
32
|
+
debug: Boolean
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const emit = defineEmits(["section-change"]);
|
|
36
|
+
|
|
37
|
+
const sections = ref([]);
|
|
38
|
+
const componentEl = ref(null);
|
|
39
|
+
|
|
40
|
+
useScrollAnchors({ sections, props, emit, componentElRef: componentEl });
|
|
41
|
+
|
|
42
|
+
provide('uluScrollAnchorsSections', computed(() => sections.value));
|
|
43
|
+
|
|
44
|
+
provide('uluScrollAnchorsRegister', (section) => {
|
|
45
|
+
sections.value.push(section);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
provide('uluScrollAnchorsUnregister', (sectionId) => {
|
|
49
|
+
const index = sections.value.findIndex(r => r.id === sectionId);
|
|
50
|
+
if (index > -1) {
|
|
51
|
+
sections.value.splice(index, 1);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
</script>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<component
|
|
3
|
+
:is="element"
|
|
4
|
+
:class="[
|
|
5
|
+
'scroll-anchors__section',
|
|
6
|
+
{ 'is-active': isActive }
|
|
7
|
+
]"
|
|
8
|
+
:data-scrollpoint-state="sectionState"
|
|
9
|
+
ref="sectionRef"
|
|
10
|
+
>
|
|
11
|
+
<slot :isActive="isActive" :titleId="titleId" :section="section" :inactiveFrom="inactiveFrom" :activeFrom="activeFrom" :sectionState="sectionState" />
|
|
12
|
+
</component>
|
|
13
|
+
</template>
|
|
14
|
+
|
|
15
|
+
<script setup>
|
|
16
|
+
import { computed } from "vue";
|
|
17
|
+
import { useScrollAnchorSection } from "./useScrollAnchorSection";
|
|
18
|
+
|
|
19
|
+
const props = defineProps({
|
|
20
|
+
/**
|
|
21
|
+
* The title of the section, used for navigation and generating a default ID
|
|
22
|
+
*/
|
|
23
|
+
title: {
|
|
24
|
+
type: String,
|
|
25
|
+
required: true
|
|
26
|
+
},
|
|
27
|
+
/**
|
|
28
|
+
* A custom ID to use for the section anchor, overriding the auto-generated one
|
|
29
|
+
*/
|
|
30
|
+
customTitleId: String,
|
|
31
|
+
/**
|
|
32
|
+
* Element to use
|
|
33
|
+
*/
|
|
34
|
+
element: {
|
|
35
|
+
type: String,
|
|
36
|
+
default: "div"
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const { sectionRef, titleId, isActive, inactiveFrom, activeFrom, section } = useScrollAnchorSection(props);
|
|
41
|
+
|
|
42
|
+
const sectionState = computed(() => {
|
|
43
|
+
if (isActive.value) {
|
|
44
|
+
if (activeFrom.value) return `enter-${activeFrom.value}`;
|
|
45
|
+
} else {
|
|
46
|
+
if (inactiveFrom.value) return `exit-${inactiveFrom.value}`;
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
});
|
|
50
|
+
</script>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<component
|
|
3
|
-
v-if="sections.length"
|
|
3
|
+
v-if="sections && sections.length"
|
|
4
4
|
:is="element"
|
|
5
5
|
class="scroll-anchors__nav"
|
|
6
6
|
>
|
|
@@ -13,25 +13,27 @@
|
|
|
13
13
|
:class="{ 'is-active' : item.active }"
|
|
14
14
|
:href="`#${ item.titleId }`"
|
|
15
15
|
>
|
|
16
|
-
|
|
16
|
+
<slot :item="item" :index="index">
|
|
17
|
+
{{ item.title }}
|
|
18
|
+
</slot>
|
|
17
19
|
</a>
|
|
18
20
|
</li>
|
|
19
21
|
</ul>
|
|
20
22
|
</component>
|
|
21
23
|
</template>
|
|
22
24
|
|
|
23
|
-
<script>
|
|
24
|
-
import {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
default: "nav"
|
|
34
|
-
}
|
|
25
|
+
<script setup>
|
|
26
|
+
import { useScrollAnchorSections } from "./useScrollAnchorSections.js";
|
|
27
|
+
|
|
28
|
+
defineProps({
|
|
29
|
+
/**
|
|
30
|
+
* The HTML element to use for the navigation root
|
|
31
|
+
*/
|
|
32
|
+
element: {
|
|
33
|
+
type: String,
|
|
34
|
+
default: "nav"
|
|
35
35
|
}
|
|
36
|
-
};
|
|
37
|
-
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const sections = useScrollAnchorSections();
|
|
39
|
+
</script>
|
|
@@ -1,16 +1,11 @@
|
|
|
1
|
-
<!--
|
|
2
|
-
Version: 0.0.2
|
|
3
|
-
Changes:
|
|
4
|
-
- 0.0.2 | Added transition initial state/class so the indicator
|
|
5
|
-
doesn't transition at first
|
|
6
|
-
-->
|
|
7
1
|
<template>
|
|
8
2
|
<component
|
|
9
|
-
v-if="sections.length"
|
|
3
|
+
v-if="sections && sections.length"
|
|
10
4
|
:is="element"
|
|
11
|
-
class="scroll-anchors__nav scroll-anchors__nav--animated"
|
|
5
|
+
class="scroll-anchors__nav scroll-anchors__nav--animated scroll-anchors-nav-animated"
|
|
6
|
+
:style="{ '--ulu-sa-nav-rail-width': `${ railWidth }px` }"
|
|
12
7
|
>
|
|
13
|
-
<ul class="scroll-
|
|
8
|
+
<ul class="scroll-anchors-nav-animated__rail">
|
|
14
9
|
<li
|
|
15
10
|
v-for="(item, index) in sections" :key="index"
|
|
16
11
|
:class="{ 'is-active' : item.active }"
|
|
@@ -20,105 +15,121 @@ Changes:
|
|
|
20
15
|
:ref="(el) => addLinkRef(index, el)"
|
|
21
16
|
:href="`#${ item.titleId }`"
|
|
22
17
|
>
|
|
23
|
-
|
|
18
|
+
<slot :item="item" :index="index">
|
|
19
|
+
{{ item.title }}
|
|
20
|
+
</slot>
|
|
24
21
|
</a>
|
|
25
22
|
</li>
|
|
26
23
|
</ul>
|
|
27
24
|
<div
|
|
28
|
-
class="scroll-
|
|
25
|
+
class="scroll-anchors-nav-animated__indicator"
|
|
29
26
|
:class="{
|
|
30
|
-
'scroll-
|
|
27
|
+
'scroll-anchors-nav-animated__indicator--can-transition' : indicatorAnimReady
|
|
31
28
|
}"
|
|
32
29
|
ref="indicator"
|
|
33
|
-
:style="{
|
|
30
|
+
:style="{
|
|
34
31
|
opacity: indicatorStyles ? '1' : '0',
|
|
35
|
-
transform: `translateY(${ indicatorStyles.y }px)`,
|
|
36
|
-
height: `${ indicatorStyles.height }px`,
|
|
32
|
+
transform: `translateY(${ indicatorStyles ? indicatorStyles.y : 0 }px)`,
|
|
33
|
+
height: `${ indicatorStyles ? indicatorStyles.height : 0 }px`,
|
|
34
|
+
width: `${ indicatorStyles ? indicatorStyles.width : 0 }px`
|
|
37
35
|
}"
|
|
38
36
|
></div>
|
|
39
37
|
</component>
|
|
40
38
|
</template>
|
|
41
39
|
|
|
42
|
-
<script>
|
|
40
|
+
<script setup>
|
|
41
|
+
import { ref, computed, watch } from 'vue';
|
|
43
42
|
import { runAfterFramePaint } from "@ulu/utils/browser/performance.js";
|
|
44
|
-
import {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
import { useScrollAnchorSections } from './useScrollAnchorSections.js';
|
|
44
|
+
|
|
45
|
+
const props = defineProps({
|
|
46
|
+
/**
|
|
47
|
+
* The HTML element to use for the navigation root
|
|
48
|
+
*/
|
|
49
|
+
element: {
|
|
50
|
+
type: String,
|
|
51
|
+
default: "nav"
|
|
49
52
|
},
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
default: "nav"
|
|
57
|
-
},
|
|
53
|
+
/**
|
|
54
|
+
* The width of the navigation rail
|
|
55
|
+
*/
|
|
56
|
+
railWidth: {
|
|
57
|
+
type: Number,
|
|
58
|
+
default: 3
|
|
58
59
|
},
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
/**
|
|
61
|
+
* The width of the indicator, defaults to railWidth
|
|
62
|
+
*/
|
|
63
|
+
indicatorWidth: {
|
|
64
|
+
type: Number,
|
|
65
|
+
default: null
|
|
64
66
|
},
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (!sections || !linkCount) {
|
|
72
|
-
return false;
|
|
73
|
-
}
|
|
74
|
-
const activeIndex = sections.findIndex(s => s.active);
|
|
75
|
-
if (activeIndex === -1) {
|
|
76
|
-
return false;
|
|
77
|
-
}
|
|
78
|
-
const link = this.linkRefs[activeIndex];
|
|
79
|
-
const { offsetTop, offsetHeight } = link;
|
|
80
|
-
return {
|
|
81
|
-
y: offsetTop,
|
|
82
|
-
height: offsetHeight,
|
|
83
|
-
initial: this.inidica
|
|
84
|
-
};
|
|
85
|
-
}
|
|
67
|
+
/**
|
|
68
|
+
* If set, creates a static height, centered indicator
|
|
69
|
+
*/
|
|
70
|
+
indicatorHeight: {
|
|
71
|
+
type: Number,
|
|
72
|
+
default: null
|
|
86
73
|
},
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
}
|
|
74
|
+
/**
|
|
75
|
+
* Vertical alignment of the indicator relative to the link
|
|
76
|
+
*/
|
|
77
|
+
indicatorAlignment: {
|
|
78
|
+
type: String,
|
|
79
|
+
default: 'center' // options: center, top
|
|
95
80
|
},
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
81
|
+
/**
|
|
82
|
+
* Pixel offset for the indicator's vertical alignment
|
|
83
|
+
*/
|
|
84
|
+
indicatorAlignmentOffset: {
|
|
85
|
+
type: Number,
|
|
86
|
+
default: 0
|
|
100
87
|
}
|
|
101
|
-
};
|
|
102
|
-
</script>
|
|
88
|
+
});
|
|
103
89
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
90
|
+
const sections = useScrollAnchorSections();
|
|
91
|
+
|
|
92
|
+
const linkRefs = ref({});
|
|
93
|
+
const indicatorAnimReady = ref(false);
|
|
94
|
+
const indicator = ref(null);
|
|
95
|
+
|
|
96
|
+
const indicatorStyles = computed(() => {
|
|
97
|
+
if (!sections || !sections.value || !sections.value.length) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
const activeIndex = sections.value.findIndex(s => s.active);
|
|
101
|
+
if (activeIndex === -1) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
const link = linkRefs.value[activeIndex];
|
|
105
|
+
if (!link) return false; // Link might not be rendered yet
|
|
106
|
+
|
|
107
|
+
const { offsetTop, offsetHeight } = link;
|
|
108
|
+
const isStatic = props.indicatorHeight != null;
|
|
109
|
+
const width = props.indicatorWidth ?? props.railWidth;
|
|
110
|
+
const height = isStatic ? props.indicatorHeight : offsetHeight;
|
|
111
|
+
|
|
112
|
+
let y = offsetTop; // Default to 'top' alignment
|
|
113
|
+
if (props.indicatorAlignment === 'center') {
|
|
114
|
+
y = offsetTop + (offsetHeight / 2) - (height / 2);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
y += props.indicatorAlignmentOffset;
|
|
118
|
+
|
|
119
|
+
return { y, height, width };
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
watch(indicatorStyles, (val) => {
|
|
123
|
+
if (val && !indicatorAnimReady.value) {
|
|
124
|
+
runAfterFramePaint(() => {
|
|
125
|
+
indicatorAnimReady.value = true;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
function addLinkRef(index, el) {
|
|
131
|
+
if (el) {
|
|
132
|
+
linkRefs.value[index] = el;
|
|
133
|
+
}
|
|
123
134
|
}
|
|
124
|
-
</
|
|
135
|
+
</script>
|