@saooti/octopus-sdk 41.11.1 → 41.12.0-beta2
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 +29 -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 +104 -0
- package/src/components/composable/useSticky.ts +45 -0
- package/src/components/display/emission/EmissionGroupChooser.vue +22 -24
- package/src/components/display/podcasts/PodcastPlayButton.vue +3 -3
- package/src/components/form/ClassicButtonGroup.vue +15 -13
- package/src/components/form/ClassicSelect.vue +11 -2
- package/src/components/form/OctopusMultiselect.vue +236 -109
- package/src/components/form/OctopusSelect.vue +278 -0
- package/src/components/misc/ClassicPopover.vue +9 -2
- package/src/components/misc/modal/ClassicModal.vue +3 -0
- package/src/components/misc/player/elements/PlayerPlayButton.vue +22 -18
- 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/ParamSdkStore.ts +1 -0
- package/src/stores/class/general/organisation.ts +6 -0
- package/src/stores/class/general/podcast.ts +8 -0
- package/src/stores/class/rubrique/rubrique.ts +1 -1
- package/src/style/_utilities.scss +1 -0
- package/src/style/_variables.scss +11 -0
- package/src/style/bootstrap.scss +9 -2
- package/tests/components/form/ClassicButtonGroup.spec.ts +10 -10
- package/tests/components/form/OctopusMultiselect.spec.ts +84 -39
- package/tests/components/form/OctopusSelect.spec.ts +168 -0
- package/tsconfig.json +3 -7
- package/tsconfig.test.json +13 -0
- package/vitest.config.js +2 -1
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
ref="containerRef"
|
|
4
|
+
class="octopus-select"
|
|
5
|
+
:class="{ 'form-margin': label }"
|
|
6
|
+
>
|
|
7
|
+
<label
|
|
8
|
+
v-if="label"
|
|
9
|
+
:for="computedId"
|
|
10
|
+
class="form-label"
|
|
11
|
+
>
|
|
12
|
+
{{ label }}
|
|
13
|
+
</label>
|
|
14
|
+
|
|
15
|
+
<div
|
|
16
|
+
class="octopus-select-field"
|
|
17
|
+
:class="{ disabled: isDisabled, open: isOpen, noBorder }"
|
|
18
|
+
@click="openDropdown"
|
|
19
|
+
>
|
|
20
|
+
<span
|
|
21
|
+
v-show="selectedLabel && !isOpen"
|
|
22
|
+
class="octopus-select-value"
|
|
23
|
+
>{{ selectedLabel }}</span>
|
|
24
|
+
<input
|
|
25
|
+
v-show="!selectedLabel || isOpen"
|
|
26
|
+
:id="computedId"
|
|
27
|
+
ref="inputRef"
|
|
28
|
+
v-model="searchQuery"
|
|
29
|
+
type="text"
|
|
30
|
+
class="octopus-select-input"
|
|
31
|
+
:placeholder="inputPlaceholder"
|
|
32
|
+
:disabled="isDisabled"
|
|
33
|
+
@focus="openDropdown"
|
|
34
|
+
@input="handleInput"
|
|
35
|
+
>
|
|
36
|
+
<button
|
|
37
|
+
class="btn-transparent octopus-select-chevron"
|
|
38
|
+
:disabled="isDisabled"
|
|
39
|
+
@click.stop="toggleDropdown"
|
|
40
|
+
>
|
|
41
|
+
<ChevronDownIcon />
|
|
42
|
+
</button>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<Teleport to=".octopus-app">
|
|
46
|
+
<div
|
|
47
|
+
v-if="isOpen"
|
|
48
|
+
ref="dropdownRef"
|
|
49
|
+
class="octopus-select-dropdown"
|
|
50
|
+
:style="dropdownStyle"
|
|
51
|
+
>
|
|
52
|
+
<div class="octopus-select-options">
|
|
53
|
+
<button
|
|
54
|
+
v-for="(option, index) in displayedOptions"
|
|
55
|
+
:key="index"
|
|
56
|
+
class="octopus-select-option"
|
|
57
|
+
:class="{ selected: isSelected(option) }"
|
|
58
|
+
@click="selectOption(option)"
|
|
59
|
+
>
|
|
60
|
+
{{ getLabel(option) }}
|
|
61
|
+
</button>
|
|
62
|
+
<span v-if="displayedOptions.length === 0" class="text-indic px-2">
|
|
63
|
+
{{ t('No elements found. Consider changing the search query.') }}
|
|
64
|
+
</span>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</Teleport>
|
|
68
|
+
</div>
|
|
69
|
+
</template>
|
|
70
|
+
|
|
71
|
+
<script setup lang="ts" generic="T">
|
|
72
|
+
import { type CSSProperties, computed, nextTick, onMounted, onUnmounted, ref, toRaw, watch } from 'vue';
|
|
73
|
+
import { useI18n } from 'vue-i18n';
|
|
74
|
+
import ChevronDownIcon from 'vue-material-design-icons/ChevronDown.vue';
|
|
75
|
+
import { useOctopusDropdown } from '../composable/form/useOctopusDropdown';
|
|
76
|
+
|
|
77
|
+
const props = withDefaults(defineProps<{
|
|
78
|
+
/** Optional label displayed above the field. */
|
|
79
|
+
label?: string;
|
|
80
|
+
/** Currently selected item. Bind with `v-model:value`. */
|
|
81
|
+
value?: T;
|
|
82
|
+
/** Full list of options to display or filter. */
|
|
83
|
+
options: T[];
|
|
84
|
+
/** Key of each option object to use as the ID. */
|
|
85
|
+
optionKey?: keyof T;
|
|
86
|
+
/** Key of each option object to use as the display label. */
|
|
87
|
+
optionLabel: keyof T & string;
|
|
88
|
+
/** Disables the field when true. */
|
|
89
|
+
isDisabled?: boolean;
|
|
90
|
+
/** Placeholder shown in the input when no item is selected. Defaults to the translated "Search" string. */
|
|
91
|
+
placeholder?: string;
|
|
92
|
+
/** Disable the border around the input. */
|
|
93
|
+
noBorder?: boolean;
|
|
94
|
+
/** When true (default), clicking the already-selected option clears the selection. */
|
|
95
|
+
allowDeselect?: boolean;
|
|
96
|
+
}>(), {
|
|
97
|
+
label: undefined,
|
|
98
|
+
value: undefined,
|
|
99
|
+
optionKey: undefined,
|
|
100
|
+
placeholder: undefined,
|
|
101
|
+
allowDeselect: true,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const emit = defineEmits<{
|
|
105
|
+
/** Emitted when the selection changes. */
|
|
106
|
+
(e: 'update:value', value: T | undefined): void;
|
|
107
|
+
/** Emitted on every input change. When listened to, the parent is responsible for
|
|
108
|
+
* updating `options`; otherwise the component filters `options` client-side. */
|
|
109
|
+
(e: 'search', query: string): void;
|
|
110
|
+
}>();
|
|
111
|
+
|
|
112
|
+
const { t } = useI18n();
|
|
113
|
+
|
|
114
|
+
// Ref on the teleported dropdown div — passed as ignored element to useOctopusDropdown
|
|
115
|
+
// so clicks inside the dropdown don't trigger the click-outside handler.
|
|
116
|
+
const dropdownRef = ref<HTMLElement | null>(null);
|
|
117
|
+
|
|
118
|
+
const {
|
|
119
|
+
searchQuery,
|
|
120
|
+
isOpen,
|
|
121
|
+
containerRef,
|
|
122
|
+
inputRef,
|
|
123
|
+
computedId,
|
|
124
|
+
displayedOptions,
|
|
125
|
+
inputPlaceholder,
|
|
126
|
+
getLabel,
|
|
127
|
+
openDropdown,
|
|
128
|
+
closeDropdown,
|
|
129
|
+
toggleDropdown,
|
|
130
|
+
handleInput,
|
|
131
|
+
} = useOctopusDropdown(props, (query) => emit('search', query), 'select', [dropdownRef]);
|
|
132
|
+
|
|
133
|
+
// Position of the teleported dropdown (position: fixed, anchored below the trigger field)
|
|
134
|
+
const dropdownStyle = ref<CSSProperties>({});
|
|
135
|
+
|
|
136
|
+
function updateDropdownPosition(): void {
|
|
137
|
+
if (!containerRef.value) { return; }
|
|
138
|
+
const rect = containerRef.value.getBoundingClientRect();
|
|
139
|
+
dropdownStyle.value = {
|
|
140
|
+
position: 'fixed',
|
|
141
|
+
top: `${rect.bottom + 2}px`,
|
|
142
|
+
left: `${rect.left}px`,
|
|
143
|
+
width: `${rect.width}px`,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
watch(isOpen, (val) => {
|
|
148
|
+
if (val) {
|
|
149
|
+
nextTick(updateDropdownPosition);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
onMounted(() => {
|
|
154
|
+
// Keep the teleported dropdown aligned when the page scrolls or the viewport resizes
|
|
155
|
+
window.addEventListener('scroll', updateDropdownPosition, true);
|
|
156
|
+
window.addEventListener('resize', updateDropdownPosition);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
onUnmounted(() => {
|
|
160
|
+
window.removeEventListener('scroll', updateDropdownPosition, true);
|
|
161
|
+
window.removeEventListener('resize', updateDropdownPosition);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const selectedLabel = computed(() =>
|
|
165
|
+
props.value !== undefined ? getLabel(props.value) : undefined
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
function isSelected(option: T): boolean {
|
|
169
|
+
if (props.value === undefined) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
if (props.optionKey) {
|
|
173
|
+
return props.value[props.optionKey] === option[props.optionKey];
|
|
174
|
+
}
|
|
175
|
+
return toRaw(props.value as object) === toRaw(option as object);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function selectOption(option: T): void {
|
|
179
|
+
if (isSelected(option) && props.allowDeselect) {
|
|
180
|
+
emit('update:value', undefined);
|
|
181
|
+
} else {
|
|
182
|
+
emit('update:value', option);
|
|
183
|
+
}
|
|
184
|
+
closeDropdown();
|
|
185
|
+
}
|
|
186
|
+
</script>
|
|
187
|
+
|
|
188
|
+
<style scoped lang="scss">
|
|
189
|
+
.octopus-select {
|
|
190
|
+
position: relative;
|
|
191
|
+
|
|
192
|
+
.octopus-select-field {
|
|
193
|
+
display: flex;
|
|
194
|
+
align-items: center;
|
|
195
|
+
border: 1px solid var(--octopus-border-default);
|
|
196
|
+
border-radius: var(--octopus-border-radius);
|
|
197
|
+
background: white;
|
|
198
|
+
cursor: pointer;
|
|
199
|
+
|
|
200
|
+
&.open {
|
|
201
|
+
border-color: var(--octopus-primary);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
&.disabled {
|
|
205
|
+
background: var(--octopus-secondary-lighter);
|
|
206
|
+
cursor: default;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
&.noBorder {
|
|
210
|
+
border: none;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.octopus-select-value {
|
|
215
|
+
flex: 1;
|
|
216
|
+
min-width: 0;
|
|
217
|
+
overflow: hidden;
|
|
218
|
+
text-overflow: ellipsis;
|
|
219
|
+
white-space: nowrap;
|
|
220
|
+
padding: 0.4rem 0.5rem;
|
|
221
|
+
padding-right: 0;
|
|
222
|
+
height: 2rem;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.octopus-select-input {
|
|
226
|
+
flex: 1;
|
|
227
|
+
border: none;
|
|
228
|
+
background: transparent;
|
|
229
|
+
padding: 0.4rem 0.5rem;
|
|
230
|
+
height: 2rem;
|
|
231
|
+
outline: none;
|
|
232
|
+
cursor: inherit;
|
|
233
|
+
min-width: 0;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.octopus-select-chevron {
|
|
237
|
+
padding: 0.25rem 0.5rem;
|
|
238
|
+
display: flex;
|
|
239
|
+
align-items: center;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Dropdown is teleported to body — scoped rules must be top-level so that [data-v-xxxx]
|
|
245
|
+
// is matched directly on the element rather than via a descendant-of-.octopus-select selector.
|
|
246
|
+
.octopus-select-dropdown {
|
|
247
|
+
z-index: 100;
|
|
248
|
+
background: white;
|
|
249
|
+
border: 1px solid var(--octopus-border-default);
|
|
250
|
+
border-radius: var(--octopus-border-radius);
|
|
251
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
252
|
+
padding: 0.25rem 0;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.octopus-select-options {
|
|
256
|
+
max-height: 14rem;
|
|
257
|
+
overflow-y: auto;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.octopus-select-option {
|
|
261
|
+
display: block;
|
|
262
|
+
width: 100%;
|
|
263
|
+
text-align: left;
|
|
264
|
+
padding: 0.25rem 0.5rem;
|
|
265
|
+
border: none;
|
|
266
|
+
background: transparent;
|
|
267
|
+
cursor: pointer;
|
|
268
|
+
|
|
269
|
+
&:hover {
|
|
270
|
+
background: var(--octopus-secondary-lighter);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
&.selected {
|
|
274
|
+
font-weight: 600;
|
|
275
|
+
color: var(--octopus-primary);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
</style>
|
|
@@ -48,7 +48,9 @@ const props = defineProps({
|
|
|
48
48
|
popoverClass: { type: String, default: undefined },
|
|
49
49
|
isTopLayer: { type: Boolean, default: false },
|
|
50
50
|
/** @deprecated No longer needed. If set to true, max height of popover will not overflow from parent */
|
|
51
|
-
constrainHeight: { type: Boolean, default: true }
|
|
51
|
+
constrainHeight: { type: Boolean, default: true },
|
|
52
|
+
/** Force z-index */
|
|
53
|
+
zIndex: { type: Number, default: undefined }
|
|
52
54
|
})
|
|
53
55
|
|
|
54
56
|
//Emits
|
|
@@ -74,7 +76,12 @@ const router = useRouter();
|
|
|
74
76
|
|
|
75
77
|
//Computed
|
|
76
78
|
const popoverId = computed(() => "popover" + props.target);
|
|
77
|
-
const positionInlineStyle = computed(() =>
|
|
79
|
+
const positionInlineStyle = computed(() => ({
|
|
80
|
+
left: `${posX.value}px`,
|
|
81
|
+
top: `${posY.value}px`,
|
|
82
|
+
'max-height': maxHeight.value,
|
|
83
|
+
'z-index': props.zIndex ?? 10
|
|
84
|
+
}));
|
|
78
85
|
const displayPopover = computed(() => show.value && !props.disable);
|
|
79
86
|
const isTopLayerPopover = computed(() => (props.isTopLayer || "octopus-modal"===props.relativeClass) && Object.hasOwn(HTMLElement.prototype, "popover"));
|
|
80
87
|
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
</div>
|
|
32
32
|
</div>
|
|
33
33
|
<div v-show="!onlyHeader" class="octopus-modal-body">
|
|
34
|
+
<slot />
|
|
34
35
|
<slot name="body" />
|
|
35
36
|
</div>
|
|
36
37
|
<div v-show="!onlyHeader" class="octopus-modal-footer">
|
|
@@ -109,6 +110,8 @@ function closePopup(): void {
|
|
|
109
110
|
|
|
110
111
|
@media (width <= 500px) {
|
|
111
112
|
width: 95vw;
|
|
113
|
+
margin-left: 2.5vw;
|
|
114
|
+
margin-right: 2.5vw;
|
|
112
115
|
}
|
|
113
116
|
|
|
114
117
|
.octopus-modal-body {
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
'play-button-box': !isBigButton,
|
|
8
8
|
'play-big-button-box': isBigButton,
|
|
9
9
|
}"
|
|
10
|
-
class="btn text-light
|
|
10
|
+
class="btn text-light"
|
|
11
11
|
@click="switchPausePlay"
|
|
12
12
|
>
|
|
13
13
|
<PlayIcon v-if="displayIsPaused" :size="isBigButton ? 60 : 30" />
|
|
@@ -107,23 +107,27 @@ function switchPausePlay(): void {
|
|
|
107
107
|
}
|
|
108
108
|
</script>
|
|
109
109
|
|
|
110
|
-
<style lang="scss">
|
|
110
|
+
<style scoped lang="scss">
|
|
111
111
|
@use "../../../../style/playButton";
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
112
|
+
|
|
113
|
+
.btn {
|
|
114
|
+
color: var(--octopus-player-btn-fg);
|
|
115
|
+
background-color: var(--octopus-player-btn-bg);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.play-button-box:not(.small-font) {
|
|
119
|
+
font-size: 1rem !important;
|
|
120
|
+
}
|
|
121
|
+
.play-big-button-box {
|
|
122
|
+
height: 5rem;
|
|
123
|
+
width: 5rem;
|
|
124
|
+
display: flex;
|
|
125
|
+
align-items: center;
|
|
126
|
+
justify-content: center;
|
|
127
|
+
margin: 0 0.5rem;
|
|
128
|
+
border-radius: 50% !important;
|
|
129
|
+
font-size: 2.5rem !important;
|
|
130
|
+
flex-shrink: 0;
|
|
131
|
+
cursor: pointer;
|
|
128
132
|
}
|
|
129
133
|
</style>
|
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Get a color from a string.
|
|
3
3
|
* Using the same string, the color will always be the same.
|
|
4
|
+
*
|
|
5
|
+
* Uses FNV-1a hashing (XOR-then-multiply with the FNV prime 16777619) for strong
|
|
6
|
+
* bit avalanche: a single character difference propagates through all hash bits,
|
|
7
|
+
* so similar strings produce very different hues.
|
|
8
|
+
*
|
|
4
9
|
* @param str The string used for the color
|
|
5
10
|
* @param saturation Saturation of the color
|
|
6
11
|
* @param lightness Lightness of the color
|
|
7
12
|
* @returns A color
|
|
8
13
|
*/
|
|
9
14
|
export function colorFromString(str: string, saturation = 80, lightness = 70) {
|
|
10
|
-
let hash =
|
|
11
|
-
|
|
12
|
-
hash
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
let hash = 2166136261;
|
|
16
|
+
for (const char of str) {
|
|
17
|
+
hash ^= char.charCodeAt(0);
|
|
18
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
19
|
+
}
|
|
20
|
+
const hue = Math.round((hash / 0xFFFFFFFF) * 360);
|
|
21
|
+
return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
|
|
15
22
|
}
|
package/src/helper/equals.ts
CHANGED
|
@@ -1,20 +1,32 @@
|
|
|
1
1
|
export function deepEqual(obj1: any, obj2: any) {
|
|
2
|
+
// it's just the same object. No need to compare.
|
|
3
|
+
if(obj1 === obj2) {
|
|
4
|
+
return true;
|
|
5
|
+
}
|
|
2
6
|
|
|
3
|
-
|
|
4
|
-
|
|
7
|
+
// compare primitives
|
|
8
|
+
if(isPrimitive(obj1) && isPrimitive(obj2)) {
|
|
9
|
+
return obj1 === obj2;
|
|
10
|
+
}
|
|
5
11
|
|
|
6
|
-
|
|
7
|
-
|
|
12
|
+
// one is a primitive but not the other (for example undefined vs array)
|
|
13
|
+
if (isPrimitive(obj1) || isPrimitive(obj2)) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
8
16
|
|
|
9
17
|
if(Object.keys(obj1).length !== Object.keys(obj2).length) {
|
|
10
18
|
return false;
|
|
11
19
|
}
|
|
12
20
|
|
|
13
21
|
// compare objects with same number of keys
|
|
14
|
-
for(const key in obj1)
|
|
15
|
-
|
|
16
|
-
if(!(key in obj2)) {
|
|
17
|
-
|
|
22
|
+
for(const key in obj1) {
|
|
23
|
+
//other object doesn't have this prop
|
|
24
|
+
if(!(key in obj2)) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
if(!deepEqual(obj1[key], obj2[key])) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
18
30
|
}
|
|
19
31
|
|
|
20
32
|
return true;
|
|
@@ -23,4 +35,4 @@ export function deepEqual(obj1: any, obj2: any) {
|
|
|
23
35
|
//check if value is primitive
|
|
24
36
|
function isPrimitive(obj: unknown) {
|
|
25
37
|
return (obj !== Object(obj));
|
|
26
|
-
}
|
|
38
|
+
}
|
|
@@ -48,6 +48,7 @@ export interface ParamStore {
|
|
|
48
48
|
/** Show time with the dates on podcasts */
|
|
49
49
|
showTimeWithDates?: boolean;
|
|
50
50
|
buttonPlus?: boolean;
|
|
51
|
+
/** Show the "Radio & Live" tab in the navigation; driven by additionalConfiguration from the backend */
|
|
51
52
|
isLiveTab?: boolean;
|
|
52
53
|
isCaptchaTest?: boolean;
|
|
53
54
|
podcastItem?: number;
|
|
@@ -24,6 +24,12 @@ export type OrganisationAttributes = {
|
|
|
24
24
|
PRIVATE?: string;
|
|
25
25
|
/** Live recordings enabled */
|
|
26
26
|
'live.active'?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* List of rubriquage **names**.
|
|
29
|
+
* The rubriques from these rubriquage are stored in stats and can be queried
|
|
30
|
+
* upon.
|
|
31
|
+
*/
|
|
32
|
+
rubriquage4MMIdentifier?: string;
|
|
27
33
|
};
|
|
28
34
|
|
|
29
35
|
export interface Organisation {
|
|
@@ -129,6 +129,14 @@ export function simplifiedToFull(simplified: SimplifiedPodcast, organisation: Or
|
|
|
129
129
|
};
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
export function podcastToSimplified(full: Podcast): SimplifiedPodcast {
|
|
133
|
+
return {
|
|
134
|
+
...full,
|
|
135
|
+
emissionId: full.emission.emissionId,
|
|
136
|
+
organisationId: full.organisation.id
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
132
140
|
export function emptyPodcastData(): Podcast {
|
|
133
141
|
return {
|
|
134
142
|
podcastId: 0,
|
|
@@ -43,6 +43,13 @@
|
|
|
43
43
|
// ClassicDataTable
|
|
44
44
|
--table-line-height: 48px;
|
|
45
45
|
|
|
46
|
+
// Buttons
|
|
47
|
+
--octopus-btn-primary-bg: var(--octopus-primary);
|
|
48
|
+
--octopus-btn-primary-fg: var(--octopus-color-on-primary);
|
|
49
|
+
--octopus-btn-play-bg: var(--octopus-primary-less-transparent);
|
|
50
|
+
--octopus-btn-play-fg: white;
|
|
51
|
+
--octopus-btn-play-radius: var(--octopus-border-radius);
|
|
52
|
+
|
|
46
53
|
// Player
|
|
47
54
|
// Color for the transcript background
|
|
48
55
|
--octopus-player-transcript-bg-color: oklch(from var(--octopus-player-color) calc(l + 0.1) c h);
|
|
@@ -52,6 +59,10 @@
|
|
|
52
59
|
--octopus-player-progress-color-current: var(--octopus-primary);
|
|
53
60
|
// Color for the progress bar background
|
|
54
61
|
--octopus-player-progress-background-color: var(--octopus-secondary-lighter);
|
|
62
|
+
// Color of the player's button
|
|
63
|
+
--octopus-player-btn-bg: var(--octopus-btn-primary-bg);
|
|
64
|
+
// Color of the player's button
|
|
65
|
+
--octopus-player-btn-fg: var(--octopus-btn-primary-fg);
|
|
55
66
|
|
|
56
67
|
// Smartlink
|
|
57
68
|
--octopus-smartlink-title-color: var(--octopus-gray-text);
|
package/src/style/bootstrap.scss
CHANGED
|
@@ -153,10 +153,10 @@ input:not([class^="vs__"]), button:not([class^="vs__"]), select:not([class^="vs_
|
|
|
153
153
|
display: flex;
|
|
154
154
|
align-items: center;
|
|
155
155
|
justify-content: center;
|
|
156
|
-
background: var(--octopus-primary);
|
|
156
|
+
background: var(--octopus-btn-primary-bg);
|
|
157
157
|
border: 1px solid var(--octopus-primary);
|
|
158
158
|
border-radius: var(--octopus-border-radius) !important;
|
|
159
|
-
color: var(--octopus-
|
|
159
|
+
color: var(--octopus-btn-primary-fg) !important;
|
|
160
160
|
font-weight: 500;
|
|
161
161
|
|
|
162
162
|
&:not(.btn-on-dark):is(:focus, :hover, :active, .active){
|
|
@@ -219,6 +219,13 @@ input:not([class^="vs__"]), button:not([class^="vs__"]), select:not([class^="vs_
|
|
|
219
219
|
border: 0;
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
+
.btn-icon {
|
|
223
|
+
background: transparent;
|
|
224
|
+
border: 0;
|
|
225
|
+
border-radius: 100%;
|
|
226
|
+
height: fit-content;
|
|
227
|
+
}
|
|
228
|
+
|
|
222
229
|
.share-btn {
|
|
223
230
|
display: inline-flex;
|
|
224
231
|
align-items: center;
|
|
@@ -11,14 +11,14 @@ const options = [
|
|
|
11
11
|
describe('ClassicButtonGroup', () => {
|
|
12
12
|
it('renders one button per option', async () => {
|
|
13
13
|
const wrapper = await mount(ClassicButtonGroup, {
|
|
14
|
-
props: { options,
|
|
14
|
+
props: { options, value: ['PODCAST'] },
|
|
15
15
|
});
|
|
16
16
|
expect(wrapper.findAll('button')).toHaveLength(3);
|
|
17
17
|
});
|
|
18
18
|
|
|
19
19
|
it('applies active class to selected options only', async () => {
|
|
20
20
|
const wrapper = await mount(ClassicButtonGroup, {
|
|
21
|
-
props: { options,
|
|
21
|
+
props: { options, value: ['PODCAST', 'LIVE'] },
|
|
22
22
|
});
|
|
23
23
|
const buttons = wrapper.findAll('button');
|
|
24
24
|
expect(buttons[0].classes()).toContain('active');
|
|
@@ -26,27 +26,27 @@ describe('ClassicButtonGroup', () => {
|
|
|
26
26
|
expect(buttons[2].classes()).toContain('active');
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
-
it('emits update:
|
|
29
|
+
it('emits update:value with added value when clicking inactive button', async () => {
|
|
30
30
|
const wrapper = await mount(ClassicButtonGroup, {
|
|
31
|
-
props: { options,
|
|
31
|
+
props: { options, value: ['PODCAST'] },
|
|
32
32
|
});
|
|
33
33
|
await wrapper.findAll('button')[1].trigger('click');
|
|
34
|
-
expect(wrapper.emitted('update:
|
|
34
|
+
expect(wrapper.emitted('update:value')?.[0]).toEqual([['PODCAST', 'VIDEO']]);
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
-
it('emits update:
|
|
37
|
+
it('emits update:value without removed value when clicking active button (not last)', async () => {
|
|
38
38
|
const wrapper = await mount(ClassicButtonGroup, {
|
|
39
|
-
props: { options,
|
|
39
|
+
props: { options, value: ['PODCAST', 'VIDEO'] },
|
|
40
40
|
});
|
|
41
41
|
await wrapper.findAll('button')[0].trigger('click');
|
|
42
|
-
expect(wrapper.emitted('update:
|
|
42
|
+
expect(wrapper.emitted('update:value')?.[0]).toEqual([['VIDEO']]);
|
|
43
43
|
});
|
|
44
44
|
|
|
45
45
|
it('does not emit when clicking the only active button', async () => {
|
|
46
46
|
const wrapper = await mount(ClassicButtonGroup, {
|
|
47
|
-
props: { options,
|
|
47
|
+
props: { options, value: ['PODCAST'] },
|
|
48
48
|
});
|
|
49
49
|
await wrapper.findAll('button')[0].trigger('click');
|
|
50
|
-
expect(wrapper.emitted('update:
|
|
50
|
+
expect(wrapper.emitted('update:value')).toBeUndefined();
|
|
51
51
|
});
|
|
52
52
|
});
|