@dative-gpi/foundation-shared-components 1.0.179-translate → 1.0.179-utils-composable
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/components/FSDialogMultiFormBody.vue +69 -3
- package/components/FSEditImage.vue +4 -3
- package/components/FSTab.vue +3 -1
- package/components/fields/FSTranslateField.vue +3 -5
- package/components/fields/FSTranslateTextArea.vue +3 -5
- package/composables/useAccessibilityPreferences.ts +7 -0
- package/composables/useCountUp.ts +101 -0
- package/composables/useElementVisibility.ts +39 -0
- package/models/index.ts +2 -1
- package/models/translations.ts +4 -0
- package/package.json +4 -4
|
@@ -3,10 +3,45 @@
|
|
|
3
3
|
gap="24px"
|
|
4
4
|
>
|
|
5
5
|
<FSPagination
|
|
6
|
+
v-if="$props.mode === 'pagination'"
|
|
6
7
|
width="calc(100% - 16px)"
|
|
7
8
|
:pages="$props.steps"
|
|
8
9
|
:modelValue="currentStep - 1"
|
|
9
10
|
/>
|
|
11
|
+
<FSTabs
|
|
12
|
+
v-else-if="$props.mode === 'tabs'"
|
|
13
|
+
:tab="currentStep - 1"
|
|
14
|
+
:color="$props.tabsColor"
|
|
15
|
+
@update:tab="(val) => currentStep = val + 1"
|
|
16
|
+
>
|
|
17
|
+
<FSTab
|
|
18
|
+
v-for="(step, index) in $props.steps"
|
|
19
|
+
:key="index"
|
|
20
|
+
>
|
|
21
|
+
<slot
|
|
22
|
+
:name="`tab-${index + 1}`"
|
|
23
|
+
>
|
|
24
|
+
<FSRow>
|
|
25
|
+
<FSIcon
|
|
26
|
+
v-if="tabIconSlots[`tab-${index + 1}-icon`]"
|
|
27
|
+
>
|
|
28
|
+
<slot
|
|
29
|
+
:name="`tab-${index + 1}-icon`"
|
|
30
|
+
/>
|
|
31
|
+
</FSIcon>
|
|
32
|
+
<FSSpan
|
|
33
|
+
:font="index + 1 === currentStep ? 'text-button' : 'text-body'"
|
|
34
|
+
>
|
|
35
|
+
<slot
|
|
36
|
+
:name="`tab-${index + 1}-label`"
|
|
37
|
+
>
|
|
38
|
+
{{ $tr('ui.tabs.step.default', 'Step {0}', step) }}
|
|
39
|
+
</slot>
|
|
40
|
+
</FSSpan>
|
|
41
|
+
</FSRow>
|
|
42
|
+
</slot>
|
|
43
|
+
</FSTab>
|
|
44
|
+
</FSTabs>
|
|
10
45
|
<FSWindow
|
|
11
46
|
width="100%"
|
|
12
47
|
:modelValue="currentStep - 1"
|
|
@@ -78,6 +113,11 @@ import FSButton from "./FSButton.vue";
|
|
|
78
113
|
import FSForm from "./FSForm.vue";
|
|
79
114
|
import FSCol from "./FSCol.vue";
|
|
80
115
|
import FSRow from "./FSRow.vue";
|
|
116
|
+
import FSSpan from "./FSSpan.vue";
|
|
117
|
+
import FSTabs from "./FSTabs.vue";
|
|
118
|
+
import FSTab from "./FSTab.vue";
|
|
119
|
+
import FSIcon from "./FSIcon.vue";
|
|
120
|
+
import FSWindow from "./FSWindow.vue";
|
|
81
121
|
|
|
82
122
|
export default defineComponent({
|
|
83
123
|
name: "FSDialogMultiFormBody",
|
|
@@ -87,7 +127,12 @@ export default defineComponent({
|
|
|
87
127
|
FSButton,
|
|
88
128
|
FSForm,
|
|
89
129
|
FSCol,
|
|
90
|
-
FSRow
|
|
130
|
+
FSRow,
|
|
131
|
+
FSSpan,
|
|
132
|
+
FSTabs,
|
|
133
|
+
FSTab,
|
|
134
|
+
FSIcon,
|
|
135
|
+
FSWindow
|
|
91
136
|
},
|
|
92
137
|
props: {
|
|
93
138
|
subtitle: {
|
|
@@ -178,10 +223,20 @@ export default defineComponent({
|
|
|
178
223
|
type: Boolean,
|
|
179
224
|
required: false,
|
|
180
225
|
default: false
|
|
181
|
-
}
|
|
226
|
+
},
|
|
227
|
+
mode: {
|
|
228
|
+
type: String as PropType<"pagination" | "tabs">,
|
|
229
|
+
required: false,
|
|
230
|
+
default: "pagination"
|
|
231
|
+
},
|
|
232
|
+
tabsColor: {
|
|
233
|
+
type: String as PropType<ColorBase>,
|
|
234
|
+
required: false,
|
|
235
|
+
default: ColorEnum.Primary
|
|
236
|
+
},
|
|
182
237
|
},
|
|
183
238
|
emits: ["click:cancelButton", "click:submitButton"],
|
|
184
|
-
setup(props, { emit }) {
|
|
239
|
+
setup(props, { emit, slots }) {
|
|
185
240
|
const { isMobileSized } = useBreakpoints();
|
|
186
241
|
const { $tr } = useTranslationsProvider();
|
|
187
242
|
|
|
@@ -189,6 +244,16 @@ export default defineComponent({
|
|
|
189
244
|
const valid = ref(false);
|
|
190
245
|
const valids = ref(Array.from({ length: props.steps }, () => false));
|
|
191
246
|
|
|
247
|
+
const hasSlot = (name: string) => !!slots[name];
|
|
248
|
+
|
|
249
|
+
const tabIconSlots = computed(() => {
|
|
250
|
+
const result: Record<string, boolean> = {};
|
|
251
|
+
for (let i = 1; i <= props.steps; i++) {
|
|
252
|
+
result[`tab-${i}-icon`] = hasSlot(`tab-${i}-icon`);
|
|
253
|
+
}
|
|
254
|
+
return result;
|
|
255
|
+
});
|
|
256
|
+
|
|
192
257
|
const maxHeight = computed(() => {
|
|
193
258
|
const other = 24 + 24 // Paddings
|
|
194
259
|
+ (isMobileSized.value ? 24 : 32) + 24 // Title
|
|
@@ -246,6 +311,7 @@ export default defineComponent({
|
|
|
246
311
|
maxHeight,
|
|
247
312
|
valids,
|
|
248
313
|
valid,
|
|
314
|
+
tabIconSlots,
|
|
249
315
|
onPrevious,
|
|
250
316
|
onSubmit
|
|
251
317
|
};
|
|
@@ -13,7 +13,7 @@ import { computed, defineComponent, type PropType } from "vue";
|
|
|
13
13
|
|
|
14
14
|
import { IMAGE_RAW_URL } from "@dative-gpi/foundation-shared-services/config";
|
|
15
15
|
|
|
16
|
-
import { useImage } from "@dative-gpi/foundation-shared-services/composables";
|
|
16
|
+
import { useImage, useAppAuthToken } from "@dative-gpi/foundation-shared-services/composables";
|
|
17
17
|
|
|
18
18
|
import FSEditImageUI from "./FSEditImageUI.vue";
|
|
19
19
|
|
|
@@ -32,10 +32,11 @@ export default defineComponent({
|
|
|
32
32
|
emits: ["update:imageId"],
|
|
33
33
|
setup(props) {
|
|
34
34
|
const { get: getImage, entity: image } = useImage();
|
|
35
|
+
const { authToken } = useAppAuthToken();
|
|
35
36
|
|
|
36
37
|
const source = computed(() => {
|
|
37
|
-
return props.imageId ? IMAGE_RAW_URL(props.imageId) : null;
|
|
38
|
-
})
|
|
38
|
+
return props.imageId ? IMAGE_RAW_URL(props.imageId, authToken.value) : null;
|
|
39
|
+
});
|
|
39
40
|
|
|
40
41
|
const onError = (): void => {
|
|
41
42
|
if (props.imageId) {
|
package/components/FSTab.vue
CHANGED
|
@@ -97,6 +97,7 @@ import FSButton from "../FSButton.vue";
|
|
|
97
97
|
import FSIcon from "../FSIcon.vue";
|
|
98
98
|
import FSSpan from "../FSSpan.vue";
|
|
99
99
|
import FSRow from "../FSRow.vue";
|
|
100
|
+
import type { Translation } from "@dative-gpi/foundation-shared-components/models";
|
|
100
101
|
|
|
101
102
|
export default defineComponent({
|
|
102
103
|
name: "FSTranslateField",
|
|
@@ -140,7 +141,7 @@ export default defineComponent({
|
|
|
140
141
|
default: "label"
|
|
141
142
|
},
|
|
142
143
|
translations: {
|
|
143
|
-
type: Array as PropType<
|
|
144
|
+
type: Array as PropType<Translation[]>,
|
|
144
145
|
required: false,
|
|
145
146
|
default: () => []
|
|
146
147
|
},
|
|
@@ -165,10 +166,7 @@ export default defineComponent({
|
|
|
165
166
|
|
|
166
167
|
const dialog = ref(false);
|
|
167
168
|
|
|
168
|
-
const innerTranslations = ref<
|
|
169
|
-
languageCode: string;
|
|
170
|
-
[key: string]: string | null;
|
|
171
|
-
}[]>([]);
|
|
169
|
+
const innerTranslations = ref<Translation[]>(props.translations);
|
|
172
170
|
|
|
173
171
|
const lights = getColors(ColorEnum.Light);
|
|
174
172
|
const darks = getColors(ColorEnum.Dark);
|
|
@@ -90,6 +90,7 @@ import { computed, defineComponent, type PropType, ref, type StyleValue, watch }
|
|
|
90
90
|
|
|
91
91
|
import { type ColorBase, ColorEnum } from "@dative-gpi/foundation-shared-components/models";
|
|
92
92
|
import { useAppLanguages } from "@dative-gpi/foundation-shared-services/composables";
|
|
93
|
+
import type { Translation } from "@dative-gpi/foundation-shared-components/models";
|
|
93
94
|
|
|
94
95
|
import { useColors } from "../../composables";
|
|
95
96
|
|
|
@@ -142,7 +143,7 @@ export default defineComponent({
|
|
|
142
143
|
default: "label"
|
|
143
144
|
},
|
|
144
145
|
translations: {
|
|
145
|
-
type: Array as PropType<
|
|
146
|
+
type: Array as PropType<Translation[]>,
|
|
146
147
|
required: false,
|
|
147
148
|
default: () => []
|
|
148
149
|
},
|
|
@@ -164,10 +165,7 @@ export default defineComponent({
|
|
|
164
165
|
|
|
165
166
|
const dialog = ref(false);
|
|
166
167
|
|
|
167
|
-
const innerTranslations = ref<
|
|
168
|
-
languageCode: string;
|
|
169
|
-
[key: string]: string | null;
|
|
170
|
-
}[]>([]);
|
|
168
|
+
const innerTranslations = ref<Translation[]>(props.translations);
|
|
171
169
|
|
|
172
170
|
const lights = getColors(ColorEnum.Light);
|
|
173
171
|
const darks = getColors(ColorEnum.Dark);
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { ref, computed, onBeforeUnmount, watch } from 'vue';
|
|
2
|
+
import { useAccessibilityPreferences } from './useAccessibilityPreferences';
|
|
3
|
+
|
|
4
|
+
export function useCountUp(options: {
|
|
5
|
+
value: number | string,
|
|
6
|
+
duration?: number,
|
|
7
|
+
countUp?: boolean,
|
|
8
|
+
pad?: number,
|
|
9
|
+
startOnVisible?: boolean,
|
|
10
|
+
easing?: (t: number) => number
|
|
11
|
+
}) {
|
|
12
|
+
const {
|
|
13
|
+
value,
|
|
14
|
+
duration = 800,
|
|
15
|
+
countUp = true,
|
|
16
|
+
pad = 2,
|
|
17
|
+
startOnVisible = true,
|
|
18
|
+
easing = easeOutCubic
|
|
19
|
+
} = options;
|
|
20
|
+
|
|
21
|
+
const { prefersReducedMotion } = useAccessibilityPreferences();
|
|
22
|
+
const current = ref(0);
|
|
23
|
+
const rafId = ref(0);
|
|
24
|
+
const hasAnimated = ref(false);
|
|
25
|
+
|
|
26
|
+
// Easing function
|
|
27
|
+
function easeOutCubic(t: number) {
|
|
28
|
+
return 1 - Math.pow(1 - t, 3);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Target value computation
|
|
32
|
+
const target = computed(() => {
|
|
33
|
+
const n = Number(value);
|
|
34
|
+
return Number.isFinite(n) ? Math.trunc(n) : 0;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Formatted display value
|
|
38
|
+
const displayText = computed(() => {
|
|
39
|
+
const s = String(countUp ? current.value : target.value);
|
|
40
|
+
return pad > 0 ? s.padStart(pad, "0") : s;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Animation function
|
|
44
|
+
function animate(from: number, to: number, animDuration: number) {
|
|
45
|
+
cancelAnimationFrame(rafId.value);
|
|
46
|
+
const start = performance.now();
|
|
47
|
+
|
|
48
|
+
const step = (now: number) => {
|
|
49
|
+
const t = Math.min(1, (now - start) / animDuration);
|
|
50
|
+
const v = from + (to - from) * easing(t);
|
|
51
|
+
current.value = Math.round(v);
|
|
52
|
+
if (t < 1) {
|
|
53
|
+
rafId.value = requestAnimationFrame(step);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
rafId.value = requestAnimationFrame(step);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Start animation
|
|
61
|
+
function start() {
|
|
62
|
+
if (!countUp) {
|
|
63
|
+
current.value = target.value;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (prefersReducedMotion) {
|
|
68
|
+
current.value = target.value;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
animate(current.value, target.value, duration);
|
|
73
|
+
hasAnimated.value = true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Clean up
|
|
77
|
+
onBeforeUnmount(() => {
|
|
78
|
+
cancelAnimationFrame(rafId.value);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// The restart method can be useful when the value changes.
|
|
82
|
+
function restart() {
|
|
83
|
+
if (hasAnimated.value || !startOnVisible) {
|
|
84
|
+
start();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Monitor changes in value
|
|
89
|
+
watch(() => value, () => {
|
|
90
|
+
restart();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
current,
|
|
95
|
+
target,
|
|
96
|
+
displayText,
|
|
97
|
+
start,
|
|
98
|
+
restart,
|
|
99
|
+
hasAnimated
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ref, onMounted, onBeforeUnmount } from 'vue';
|
|
2
|
+
|
|
3
|
+
export function useElementVisibility(element: HTMLElement | null, options: {
|
|
4
|
+
threshold?: number,
|
|
5
|
+
onVisible?: () => void
|
|
6
|
+
}) {
|
|
7
|
+
const {
|
|
8
|
+
threshold = 0.3,
|
|
9
|
+
onVisible
|
|
10
|
+
} = options;
|
|
11
|
+
|
|
12
|
+
const isVisible = ref(false);
|
|
13
|
+
const observer = ref<IntersectionObserver | null>(null);
|
|
14
|
+
|
|
15
|
+
onMounted(() => {
|
|
16
|
+
if (!element) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
observer.value = new IntersectionObserver((entries) => {
|
|
21
|
+
entries.forEach((entry) => {
|
|
22
|
+
isVisible.value = entry.isIntersecting;
|
|
23
|
+
if (entry.isIntersecting && onVisible) {
|
|
24
|
+
onVisible();
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}, { threshold });
|
|
28
|
+
|
|
29
|
+
observer.value.observe(element);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
onBeforeUnmount(() => {
|
|
33
|
+
observer.value?.disconnect();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
isVisible
|
|
38
|
+
};
|
|
39
|
+
}
|
package/models/index.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dative-gpi/foundation-shared-components",
|
|
3
3
|
"sideEffects": false,
|
|
4
|
-
"version": "1.0.179-
|
|
4
|
+
"version": "1.0.179-utils-composable",
|
|
5
5
|
"description": "",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
"author": "",
|
|
11
11
|
"license": "ISC",
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@dative-gpi/foundation-shared-domain": "1.0.179-
|
|
14
|
-
"@dative-gpi/foundation-shared-services": "1.0.179-
|
|
13
|
+
"@dative-gpi/foundation-shared-domain": "1.0.179-utils-composable",
|
|
14
|
+
"@dative-gpi/foundation-shared-services": "1.0.179-utils-composable"
|
|
15
15
|
},
|
|
16
16
|
"peerDependencies": {
|
|
17
17
|
"@dative-gpi/bones-ui": "^1.0.0",
|
|
@@ -35,5 +35,5 @@
|
|
|
35
35
|
"sass": "1.71.1",
|
|
36
36
|
"sass-loader": "13.3.2"
|
|
37
37
|
},
|
|
38
|
-
"gitHead": "
|
|
38
|
+
"gitHead": "c2d2ca5e8ea8a151a41f26e08e75f5369cd876e2"
|
|
39
39
|
}
|