@saooti/octopus-sdk 41.11.1 → 41.12.0-beta
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/CHANGELOG.md +28 -0
- package/eslint-config.mjs +10 -2
- package/index.ts +5 -2
- package/package.json +4 -1
- package/src/api/radioApi.ts +14 -1
- package/src/components/buttons/ClassicButton.vue +18 -1
- package/src/components/composable/form/useOctopusDropdown.ts +99 -0
- package/src/components/composable/useSticky.ts +45 -0
- package/src/components/display/emission/EmissionGroupChooser.vue +22 -24
- package/src/components/form/ClassicButtonGroup.vue +15 -13
- package/src/components/form/ClassicSelect.vue +11 -2
- package/src/components/form/OctopusMultiselect.vue +169 -73
- package/src/components/form/OctopusSelect.vue +237 -0
- package/src/components/misc/ClassicPopover.vue +9 -2
- package/src/components/misc/modal/ClassicModal.vue +1 -0
- package/src/components/pages/PlaylistsPage.vue +1 -0
- package/src/helper/colorFromString.ts +12 -5
- package/src/helper/equals.ts +21 -9
- package/src/stores/class/general/podcast.ts +8 -0
- package/src/style/_utilities.scss +1 -0
- package/src/style/bootstrap.scss +7 -0
- package/tests/components/form/ClassicButtonGroup.spec.ts +10 -10
- package/tests/components/form/OctopusMultiselect.spec.ts +27 -6
- package/tests/components/form/OctopusSelect.spec.ts +143 -0
- package/tsconfig.json +3 -7
- package/tsconfig.test.json +13 -0
- package/vitest.config.js +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 41.12.0 (En cours)
|
|
4
|
+
|
|
5
|
+
**Features**
|
|
6
|
+
|
|
7
|
+
- Export de `colorFromString` pour usage hors du sdk
|
|
8
|
+
- Ajout d'un composable `useSticky` pour déterminer quand un élément sticky se
|
|
9
|
+
déclenche
|
|
10
|
+
- Ajout de `@stylistic/eslint-plugin` et mise à jour des règles eslint
|
|
11
|
+
- Ajout d'un prop dans `ClassicPopover` pour modifier le z-index
|
|
12
|
+
- Ajout d'un slot `option` pour `ClassicSelect`
|
|
13
|
+
- Composants `Octopus`
|
|
14
|
+
- Ajout de `OctopusSelect`, une version moderne de `ClassicSelect`
|
|
15
|
+
- Mise en commun de code entre `OctopusSelect` & `OctopusMultiselect`
|
|
16
|
+
- Ajustement de `OctopusMultiselect` pour fonctionner correctement avec des
|
|
17
|
+
objets identiques avec références différentes
|
|
18
|
+
- Ajout de nouvelles classes utilitaires dans le css
|
|
19
|
+
- Ajout de nouvelles options d'apparence pour `ClassicButton`
|
|
20
|
+
- Changement propriété principale de `ClassicButtonGroup` pour plus de cohérence
|
|
21
|
+
|
|
22
|
+
**Fixes**
|
|
23
|
+
|
|
24
|
+
- Correction d'une exception dans `deepEquals` si la comparaison est faite entre
|
|
25
|
+
une variable primitive et un objet
|
|
26
|
+
|
|
27
|
+
**Misc**
|
|
28
|
+
|
|
29
|
+
- Mise à jour des règles eslint
|
|
30
|
+
|
|
3
31
|
## 41.11.1 (17/06/2026)
|
|
4
32
|
|
|
5
33
|
**Fixes**
|
package/eslint-config.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import eslint from '@eslint/js';
|
|
|
2
2
|
import eslintPluginVue from 'eslint-plugin-vue';
|
|
3
3
|
import globals from 'globals';
|
|
4
4
|
import typescriptEslint from 'typescript-eslint';
|
|
5
|
+
import stylistic from '@stylistic/eslint-plugin';
|
|
5
6
|
|
|
6
7
|
export default typescriptEslint.config(
|
|
7
8
|
{ ignores: ['*.d.ts', '**/coverage', '**/dist'] },
|
|
@@ -11,6 +12,9 @@ export default typescriptEslint.config(
|
|
|
11
12
|
...typescriptEslint.configs.recommended,
|
|
12
13
|
...eslintPluginVue.configs['flat/recommended'],
|
|
13
14
|
],
|
|
15
|
+
plugins: {
|
|
16
|
+
'@stylistic': stylistic,
|
|
17
|
+
},
|
|
14
18
|
files: ['**/*.{ts,vue}'],
|
|
15
19
|
languageOptions: {
|
|
16
20
|
ecmaVersion: 'latest',
|
|
@@ -33,11 +37,15 @@ export default typescriptEslint.config(
|
|
|
33
37
|
"vue/no-ref-as-operand": ['error'],
|
|
34
38
|
|
|
35
39
|
// Indentation
|
|
40
|
+
"@stylistic/indent": ['warn', 4, { "SwitchCase": 0 }],
|
|
36
41
|
"vue/html-indent": ['warn', 4],
|
|
37
42
|
"vue/script-indent": ['warn', 4],
|
|
43
|
+
"@stylistic/no-mixed-spaces-and-tabs": ["error", "smart-tabs"],
|
|
38
44
|
|
|
39
45
|
// Number of attributes per line (increase because sometimes two is not a lot)
|
|
40
|
-
"vue/max-attributes-per-line": ['warn', { singleline: 2 } ]
|
|
41
|
-
|
|
46
|
+
"vue/max-attributes-per-line": ['warn', { singleline: 2 } ],
|
|
47
|
+
|
|
48
|
+
"@typescript-eslint/no-unused-vars": "warn"
|
|
49
|
+
}
|
|
42
50
|
}
|
|
43
51
|
);
|
package/index.ts
CHANGED
|
@@ -59,7 +59,8 @@ export * from "./src/components/buttons/";
|
|
|
59
59
|
// Form
|
|
60
60
|
import ClassicButtonGroup from "./src/components/form/ClassicButtonGroup.vue";
|
|
61
61
|
import OctopusMultiselect from "./src/components/form/OctopusMultiselect.vue";
|
|
62
|
-
|
|
62
|
+
import OctopusSelect from "./src/components/form/OctopusSelect.vue";
|
|
63
|
+
export { ClassicButtonGroup, OctopusMultiselect, OctopusSelect };
|
|
63
64
|
export type { ButtonGroupOption } from "./src/components/form/ClassicButtonGroup.vue";
|
|
64
65
|
|
|
65
66
|
//Display
|
|
@@ -144,6 +145,7 @@ export { useSeoTitleUrl } from "./src/components/composable/route/useSeoTitleUrl
|
|
|
144
145
|
export { useSeasonsManagement } from "./src/components/composable/useSeasonsManagement.ts";
|
|
145
146
|
export { useTranslation } from "./src/components/composable/useTranslation.ts";
|
|
146
147
|
export { useDayjs } from "./src/components/composable/useDayjs.ts";
|
|
148
|
+
export { useSticky } from "./src/components/composable/useSticky.ts";
|
|
147
149
|
|
|
148
150
|
//helper
|
|
149
151
|
import domHelper from "./src/helper/domHelper.ts";
|
|
@@ -155,6 +157,7 @@ import downloadHelper from "./src/helper/downloadHelper.ts";
|
|
|
155
157
|
import displayHelper from "./src/helper/displayHelper.ts";
|
|
156
158
|
import debounce from "./src/helper/debounceHelper.ts";
|
|
157
159
|
import { deepEqual } from "./src/helper/equals.ts";
|
|
160
|
+
export { colorFromString } from './src/helper/colorFromString';
|
|
158
161
|
|
|
159
162
|
//stores
|
|
160
163
|
import {useVastStore} from "./src/stores/VastStore.ts";
|
|
@@ -175,7 +178,7 @@ export * from "./src/api";
|
|
|
175
178
|
// Types
|
|
176
179
|
export { type Emission, SeasonMode, emptyEmissionData } from "./src/stores/class/general/emission.ts";
|
|
177
180
|
export { type Organisation, type OrganisationAttributes, emptyOrganisationData, emptyOrgaData } from "./src/stores/class/general/organisation.ts";
|
|
178
|
-
export { type Podcast, type PodcastAvailability, PodcastType } from "./src/stores/class/general/podcast.ts";
|
|
181
|
+
export { type Podcast, type SimplifiedPodcast, type PodcastAvailability, PodcastType, podcastToSimplified } from "./src/stores/class/general/podcast.ts";
|
|
179
182
|
export { type Playlist, type PlaylistRule } from "./src/stores/class/general/playlist.ts";
|
|
180
183
|
export { type Annotations } from "./src/stores/class/general";
|
|
181
184
|
export {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saooti/octopus-sdk",
|
|
3
|
-
"version": "41.
|
|
3
|
+
"version": "41.12.0-beta",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Javascript SDK for using octopus",
|
|
6
6
|
"author": "Saooti",
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@pinia/testing": "^1.0.3",
|
|
64
|
+
"@stylistic/eslint-plugin": "*",
|
|
64
65
|
"@types/sockjs-client": "^1.5.4",
|
|
65
66
|
"@types/webpack-env": "^1.18.8",
|
|
66
67
|
"@vitejs/plugin-vue": "^5.2.4",
|
|
@@ -90,9 +91,11 @@
|
|
|
90
91
|
"url": "git+https://github.com/saooti/octopus-sdk.git"
|
|
91
92
|
},
|
|
92
93
|
"peerDependencies": {
|
|
94
|
+
"@stylistic/eslint-plugin": "^5.10.0",
|
|
93
95
|
"dayjs": "^1.11.0",
|
|
94
96
|
"eslint-plugin-vue": "^10.9.1",
|
|
95
97
|
"pinia": ">=2.3.0",
|
|
98
|
+
"typescript-eslint": "^8.47.0",
|
|
96
99
|
"vue": "^3.5.0",
|
|
97
100
|
"vue-i18n": "^11.1.0",
|
|
98
101
|
"vue-router": "^4.6.0"
|
package/src/api/radioApi.ts
CHANGED
|
@@ -14,6 +14,19 @@ async function get(radioId: number): Promise<Canal> {
|
|
|
14
14
|
});
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Retrieve all radio canal by organisation
|
|
19
|
+
* @param organisationId Id of the organisation
|
|
20
|
+
* @returns The list of canal data
|
|
21
|
+
*/
|
|
22
|
+
async function getAll(organisationId: string): Promise<Array<Canal>> {
|
|
23
|
+
return await classicApi.fetchData<Array<Canal>>({
|
|
24
|
+
api: ModuleApi.RADIO,
|
|
25
|
+
path: "canal/orga/" + organisationId + "/"
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
17
29
|
export const radioApi = {
|
|
18
|
-
get
|
|
30
|
+
get,
|
|
31
|
+
getAll
|
|
19
32
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<!-- A simple button with accessibility in mind -->
|
|
2
2
|
<template>
|
|
3
3
|
<button
|
|
4
|
-
class="
|
|
4
|
+
:class="classes"
|
|
5
5
|
:aria-disabled="disabled"
|
|
6
6
|
:title="title"
|
|
7
7
|
:aria-label="title"
|
|
@@ -12,11 +12,17 @@
|
|
|
12
12
|
</template>
|
|
13
13
|
|
|
14
14
|
<script setup lang="ts">
|
|
15
|
+
import { computed } from 'vue';
|
|
16
|
+
|
|
15
17
|
const props = defineProps<{
|
|
16
18
|
/** Disable the button */
|
|
17
19
|
disabled?: boolean;
|
|
18
20
|
/** Title of the button */
|
|
19
21
|
title?: string;
|
|
22
|
+
/** Primary style for button */
|
|
23
|
+
primary?: boolean;
|
|
24
|
+
/** Icon style for button */
|
|
25
|
+
icon?: boolean;
|
|
20
26
|
}>();
|
|
21
27
|
|
|
22
28
|
const emit = defineEmits<{
|
|
@@ -24,6 +30,17 @@ const emit = defineEmits<{
|
|
|
24
30
|
(e: 'click'): void;
|
|
25
31
|
}>();
|
|
26
32
|
|
|
33
|
+
const classes = computed((): Array<string> => {
|
|
34
|
+
const ary: Array<string> = ['btn', 'd-flex'];
|
|
35
|
+
if (props.primary) {
|
|
36
|
+
ary.push('btn-primary');
|
|
37
|
+
}
|
|
38
|
+
if (props.icon) {
|
|
39
|
+
ary.push('btn-icon');
|
|
40
|
+
}
|
|
41
|
+
return ary;
|
|
42
|
+
});
|
|
43
|
+
|
|
27
44
|
function onClick(): void {
|
|
28
45
|
if (!props.disabled) {
|
|
29
46
|
emit('click');
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { computed, getCurrentInstance, nextTick, ref } from 'vue';
|
|
2
|
+
import { onClickOutside } from '@vueuse/core';
|
|
3
|
+
import { useI18n } from 'vue-i18n';
|
|
4
|
+
|
|
5
|
+
export interface OctopusDropdownProps<T> {
|
|
6
|
+
options: T[];
|
|
7
|
+
optionLabel: keyof T & string;
|
|
8
|
+
optionKey?: keyof T;
|
|
9
|
+
isDisabled?: boolean;
|
|
10
|
+
placeholder?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function useOctopusDropdown<T>(
|
|
14
|
+
props: OctopusDropdownProps<T>,
|
|
15
|
+
emitSearch: (query: string) => void,
|
|
16
|
+
idPrefix: string = 'octopus-dropdown'
|
|
17
|
+
) {
|
|
18
|
+
const { t } = useI18n();
|
|
19
|
+
const instance = getCurrentInstance();
|
|
20
|
+
|
|
21
|
+
const searchQuery = ref('');
|
|
22
|
+
const isOpen = ref(false);
|
|
23
|
+
const isHovered = ref(false);
|
|
24
|
+
const containerRef = ref<HTMLElement | null>(null);
|
|
25
|
+
const inputRef = ref<HTMLInputElement | null>(null);
|
|
26
|
+
|
|
27
|
+
const computedId = computed(() => `${idPrefix}-${instance?.uid}`);
|
|
28
|
+
|
|
29
|
+
// vnode.props contains the raw parent-provided props before defineEmits strips them
|
|
30
|
+
// from attrs, so onSearch is present whenever the parent writes @search="...".
|
|
31
|
+
const hasSearchListener = computed(() => !!instance?.vnode.props?.onSearch);
|
|
32
|
+
|
|
33
|
+
const displayedOptions = computed((): T[] => {
|
|
34
|
+
if (hasSearchListener.value) {
|
|
35
|
+
return props.options;
|
|
36
|
+
}
|
|
37
|
+
const query = searchQuery.value.toLowerCase();
|
|
38
|
+
if (!query) {
|
|
39
|
+
return props.options;
|
|
40
|
+
}
|
|
41
|
+
return props.options.filter((option) =>
|
|
42
|
+
getLabel(option).toLowerCase().includes(query)
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const inputPlaceholder = computed(() => props.placeholder ?? t('Search'));
|
|
47
|
+
|
|
48
|
+
function getLabel(option: T): string {
|
|
49
|
+
return option[props.optionLabel] as string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function openDropdown(): void {
|
|
53
|
+
if (props.isDisabled) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
isOpen.value = true;
|
|
57
|
+
nextTick(() => {
|
|
58
|
+
inputRef.value?.focus();
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function closeDropdown(): void {
|
|
63
|
+
isOpen.value = false;
|
|
64
|
+
searchQuery.value = '';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function toggleDropdown(): void {
|
|
68
|
+
if (isOpen.value) {
|
|
69
|
+
closeDropdown();
|
|
70
|
+
} else {
|
|
71
|
+
openDropdown();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function handleInput(): void {
|
|
76
|
+
if (hasSearchListener.value) {
|
|
77
|
+
emitSearch(searchQuery.value);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
onClickOutside(containerRef, closeDropdown);
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
searchQuery,
|
|
85
|
+
isOpen,
|
|
86
|
+
isHovered,
|
|
87
|
+
containerRef,
|
|
88
|
+
inputRef,
|
|
89
|
+
computedId,
|
|
90
|
+
hasSearchListener,
|
|
91
|
+
displayedOptions,
|
|
92
|
+
inputPlaceholder,
|
|
93
|
+
getLabel,
|
|
94
|
+
openDropdown,
|
|
95
|
+
closeDropdown,
|
|
96
|
+
toggleDropdown,
|
|
97
|
+
handleInput,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { ref, toValue, onMounted, onBeforeUnmount, type Ref, type MaybeRef } from 'vue';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Detects whether a sticky element is currently stuck (fixed at its `top` offset)
|
|
5
|
+
* or in its natural DOM position.
|
|
6
|
+
*
|
|
7
|
+
* A 1px invisible sentinel element is inserted immediately before the sticky element.
|
|
8
|
+
* When that sentinel scrolls out of the IntersectionObserver's root viewport (adjusted
|
|
9
|
+
* by `topOffset`), the element is considered stuck.
|
|
10
|
+
*
|
|
11
|
+
* `topOffset` must match the element's CSS `top` value in pixels. It is read once at
|
|
12
|
+
* mount time. Pass a `Ref<number>` if the value is computed asynchronously (e.g. from
|
|
13
|
+
* a CSS variable set by JavaScript after first render).
|
|
14
|
+
*/
|
|
15
|
+
export function useSticky(elementRef: Ref<HTMLElement | null>, topOffset: MaybeRef<number> = 0) {
|
|
16
|
+
const isStuck = ref(false);
|
|
17
|
+
let sentinel: HTMLElement | null = null;
|
|
18
|
+
let observer: IntersectionObserver | null = null;
|
|
19
|
+
|
|
20
|
+
onMounted(() => {
|
|
21
|
+
const el = elementRef.value;
|
|
22
|
+
if (!el) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const offset = toValue(topOffset);
|
|
27
|
+
|
|
28
|
+
sentinel = document.createElement('div');
|
|
29
|
+
sentinel.style.cssText = 'height:1px;pointer-events:none;';
|
|
30
|
+
el.parentNode!.insertBefore(sentinel, el);
|
|
31
|
+
|
|
32
|
+
observer = new IntersectionObserver(
|
|
33
|
+
([entry]) => { isStuck.value = !entry.isIntersecting; },
|
|
34
|
+
{ rootMargin: `-${offset}px 0px 0px 0px` }
|
|
35
|
+
);
|
|
36
|
+
observer.observe(sentinel);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
onBeforeUnmount(() => {
|
|
40
|
+
observer?.disconnect();
|
|
41
|
+
sentinel?.remove();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return { isStuck };
|
|
45
|
+
}
|
|
@@ -1,23 +1,19 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
<OctopusMultiselect
|
|
3
|
+
:placeholder="placeholder ?? $t('Search - Emission groups placeholder')"
|
|
4
|
+
:selected="groups"
|
|
5
|
+
:options="allGroups"
|
|
6
|
+
:no-border="noBorder"
|
|
5
7
|
option-label="name"
|
|
6
|
-
|
|
7
|
-
:
|
|
8
|
-
width="400px"
|
|
9
|
-
in-modal
|
|
10
|
-
:option-chosen="groups"
|
|
11
|
-
multiple
|
|
12
|
-
@on-search="onSearch"
|
|
13
|
-
@selected="emitSelected"
|
|
8
|
+
option-key="groupId"
|
|
9
|
+
@update:selected="emitSelected"
|
|
14
10
|
/>
|
|
15
11
|
</template>
|
|
16
12
|
|
|
17
13
|
<script setup lang="ts">
|
|
18
|
-
import {
|
|
14
|
+
import { onMounted, ref } from "vue";
|
|
19
15
|
|
|
20
|
-
import
|
|
16
|
+
import OctopusMultiselect from "../../form/OctopusMultiselect.vue";
|
|
21
17
|
import { groupsApi, EmissionGroup } from "../../../api/groupsApi";
|
|
22
18
|
|
|
23
19
|
//Props
|
|
@@ -26,29 +22,31 @@ const props = defineProps<{
|
|
|
26
22
|
organisationId?: string|Array<string>;
|
|
27
23
|
/** Currently selected groups */
|
|
28
24
|
groups: Array<EmissionGroup>;
|
|
25
|
+
/** Change default placeholder */
|
|
26
|
+
placeholder?: string;
|
|
27
|
+
/** Disable borders */
|
|
28
|
+
noBorder?: boolean;
|
|
29
29
|
}>();
|
|
30
30
|
|
|
31
31
|
//Emits
|
|
32
|
-
const emit = defineEmits
|
|
32
|
+
const emit = defineEmits<{
|
|
33
|
+
(e: "update:groups", groups: Array<EmissionGroup>): void;
|
|
34
|
+
}>();
|
|
33
35
|
|
|
34
36
|
//Data
|
|
35
|
-
const maxElement =
|
|
36
|
-
const
|
|
37
|
+
const maxElement = 200;
|
|
38
|
+
const allGroups = ref<Array<EmissionGroup>>([]);
|
|
37
39
|
|
|
38
|
-
|
|
39
|
-
async function onSearch(query?: string): Promise<void> {
|
|
40
|
+
onMounted(async() => {
|
|
40
41
|
const response = await groupsApi.search({
|
|
41
42
|
first: 0,
|
|
42
43
|
size: maxElement,
|
|
43
|
-
search: query,
|
|
44
44
|
organisationIds: [props.organisationId].flat(),
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
);
|
|
51
|
-
}
|
|
47
|
+
// Only groups with emissions are available
|
|
48
|
+
allGroups.value = response.result.filter(g => g.emissionIds?.length ?? 0 > 0);
|
|
49
|
+
});
|
|
52
50
|
|
|
53
51
|
function emitSelected(option: Array<EmissionGroup>) {
|
|
54
52
|
emit("update:groups", option);
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
:key="option.value"
|
|
6
6
|
type="button"
|
|
7
7
|
class="btn"
|
|
8
|
-
:class="{ active:
|
|
8
|
+
:class="{ active: value.includes(option.value) }"
|
|
9
9
|
@click="toggle(option.value)"
|
|
10
10
|
>
|
|
11
11
|
{{ option.label }}
|
|
@@ -13,28 +13,30 @@
|
|
|
13
13
|
</div>
|
|
14
14
|
</template>
|
|
15
15
|
|
|
16
|
-
<script setup lang="ts">
|
|
17
|
-
export interface ButtonGroupOption {
|
|
16
|
+
<script setup lang="ts" generic="T">
|
|
17
|
+
export interface ButtonGroupOption<T> {
|
|
18
18
|
label: string;
|
|
19
|
-
value:
|
|
19
|
+
value: T;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
const props = defineProps<{
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
/** Currently selected values */
|
|
24
|
+
value: T[];
|
|
25
|
+
/** Possible values */
|
|
26
|
+
options: ButtonGroupOption<T>[];
|
|
25
27
|
}>();
|
|
26
28
|
|
|
27
29
|
const emit = defineEmits<{
|
|
28
|
-
'update:
|
|
30
|
+
(e: 'update:value', values: T[]): void;
|
|
29
31
|
}>();
|
|
30
32
|
|
|
31
|
-
function toggle(value:
|
|
32
|
-
const isSelected = props.
|
|
33
|
-
if (isSelected && props.
|
|
33
|
+
function toggle(value: T): void {
|
|
34
|
+
const isSelected = props.value.includes(value);
|
|
35
|
+
if (isSelected && props.value.length <= 1) { return; }
|
|
34
36
|
const newValues = isSelected
|
|
35
|
-
? props.
|
|
36
|
-
: [...props.
|
|
37
|
-
emit('update:
|
|
37
|
+
? props.value.filter(v => v !== value)
|
|
38
|
+
: [...props.value, value];
|
|
39
|
+
emit('update:value', newValues);
|
|
38
40
|
}
|
|
39
41
|
</script>
|
|
40
42
|
|
|
@@ -17,7 +17,14 @@
|
|
|
17
17
|
:required="displayRequired"
|
|
18
18
|
@change="onChange($event.target.value)"
|
|
19
19
|
>
|
|
20
|
-
<option
|
|
20
|
+
<option
|
|
21
|
+
v-if="placeholder"
|
|
22
|
+
value=""
|
|
23
|
+
disabled
|
|
24
|
+
selected
|
|
25
|
+
>
|
|
26
|
+
{{ placeholder }}
|
|
27
|
+
</option>
|
|
21
28
|
<option
|
|
22
29
|
v-for="option in optionsOrder"
|
|
23
30
|
:key="option.title"
|
|
@@ -25,7 +32,9 @@
|
|
|
25
32
|
:data-selenium="'select-option-' + option.value"
|
|
26
33
|
:style="option.fontFamily ? 'font-family:' + option.fontFamily : ''"
|
|
27
34
|
>
|
|
28
|
-
|
|
35
|
+
<slot name="option" :option="option">
|
|
36
|
+
{{ option.title }}
|
|
37
|
+
</slot>
|
|
29
38
|
</option>
|
|
30
39
|
</select>
|
|
31
40
|
</div>
|