@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
|
@@ -16,8 +16,22 @@
|
|
|
16
16
|
class="octopus-multiselect-field"
|
|
17
17
|
:class="{ disabled: isDisabled, open: isOpen, noBorder }"
|
|
18
18
|
@click="openDropdown"
|
|
19
|
+
@mouseenter="isHovered = true"
|
|
20
|
+
@mouseleave="isHovered = false"
|
|
19
21
|
>
|
|
22
|
+
<div
|
|
23
|
+
v-show="hasSelected && !isOpen"
|
|
24
|
+
ref="selectionRef"
|
|
25
|
+
class="octopus-multiselect-selection"
|
|
26
|
+
>
|
|
27
|
+
<span class="octopus-multiselect-selection-text">{{ selectionLabels }}</span>
|
|
28
|
+
<span
|
|
29
|
+
v-if="overflowCount > 0"
|
|
30
|
+
class="octopus-multiselect-selection-count"
|
|
31
|
+
>(+{{ overflowCount }})</span>
|
|
32
|
+
</div>
|
|
20
33
|
<input
|
|
34
|
+
v-show="!hasSelected || isOpen"
|
|
21
35
|
:id="computedId"
|
|
22
36
|
ref="inputRef"
|
|
23
37
|
v-model="searchQuery"
|
|
@@ -37,6 +51,13 @@
|
|
|
37
51
|
</button>
|
|
38
52
|
</div>
|
|
39
53
|
|
|
54
|
+
<div
|
|
55
|
+
v-if="expandOnHover && isHovered && !isOpen && overflowCount > 0"
|
|
56
|
+
class="octopus-multiselect-hover-tooltip"
|
|
57
|
+
>
|
|
58
|
+
{{ allLabelsText }}
|
|
59
|
+
</div>
|
|
60
|
+
|
|
40
61
|
<div v-if="isOpen" class="octopus-multiselect-dropdown">
|
|
41
62
|
<ClassicCheckbox
|
|
42
63
|
:text-init="allSelected"
|
|
@@ -63,11 +84,11 @@
|
|
|
63
84
|
</template>
|
|
64
85
|
|
|
65
86
|
<script setup lang="ts" generic="T">
|
|
66
|
-
import { computed,
|
|
67
|
-
import { onClickOutside } from '@vueuse/core';
|
|
87
|
+
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
|
68
88
|
import { useI18n } from 'vue-i18n';
|
|
69
89
|
import ChevronDownIcon from 'vue-material-design-icons/ChevronDown.vue';
|
|
70
90
|
import ClassicCheckbox from './ClassicCheckbox.vue';
|
|
91
|
+
import { useOctopusDropdown } from '../composable/form/useOctopusDropdown';
|
|
71
92
|
|
|
72
93
|
const props = defineProps<{
|
|
73
94
|
/** Optional label displayed above the field. */
|
|
@@ -76,6 +97,8 @@ const props = defineProps<{
|
|
|
76
97
|
selected?: T[];
|
|
77
98
|
/** Full list of options to display or filter. */
|
|
78
99
|
options: T[];
|
|
100
|
+
/** Key of each option object to use as the ID */
|
|
101
|
+
optionKey?: keyof T;
|
|
79
102
|
/** Key of each option object to use as the display label. */
|
|
80
103
|
optionLabel: keyof T & string;
|
|
81
104
|
/** Disables the field and all checkboxes when true. */
|
|
@@ -84,47 +107,39 @@ const props = defineProps<{
|
|
|
84
107
|
placeholder?: string;
|
|
85
108
|
/** Label for the "select all" checkbox. Defaults to the translated "All" string. */
|
|
86
109
|
selectAllText?: string;
|
|
87
|
-
/** If provided, called on every input change; its return value replaces the displayed options. */
|
|
88
|
-
onSearch?: (query: string) => T[] | Promise<T[]>;
|
|
89
110
|
/** Disable the border around the input */
|
|
90
111
|
noBorder?: boolean;
|
|
112
|
+
/** When true, hovering the closed field with overflow shows a tooltip listing all selected items. */
|
|
113
|
+
expandOnHover?: boolean;
|
|
91
114
|
}>();
|
|
92
115
|
|
|
93
116
|
const emit = defineEmits<{
|
|
94
117
|
/** Emitted when the selection changes. */
|
|
95
118
|
(e: 'update:selected', value: T[]): void;
|
|
119
|
+
/** Emitted on every input change. When listened to, the parent is responsible for
|
|
120
|
+
* updating `options`; otherwise the component filters `options` client-side. */
|
|
121
|
+
(e: 'search', query: string): void;
|
|
96
122
|
}>();
|
|
97
123
|
|
|
98
124
|
const { t } = useI18n();
|
|
99
125
|
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
if (props.onSearch) {
|
|
118
|
-
return internalOptions.value;
|
|
119
|
-
}
|
|
120
|
-
const query = searchQuery.value.toLowerCase();
|
|
121
|
-
if (!query) {
|
|
122
|
-
return props.options;
|
|
123
|
-
}
|
|
124
|
-
return props.options.filter((option) =>
|
|
125
|
-
getLabel(option).toLowerCase().includes(query)
|
|
126
|
-
);
|
|
127
|
-
});
|
|
126
|
+
const {
|
|
127
|
+
searchQuery,
|
|
128
|
+
isOpen,
|
|
129
|
+
isHovered,
|
|
130
|
+
containerRef,
|
|
131
|
+
inputRef,
|
|
132
|
+
computedId,
|
|
133
|
+
displayedOptions,
|
|
134
|
+
inputPlaceholder,
|
|
135
|
+
getLabel,
|
|
136
|
+
openDropdown,
|
|
137
|
+
toggleDropdown,
|
|
138
|
+
handleInput,
|
|
139
|
+
} = useOctopusDropdown(props, (query) => emit('search', query), 'multiselect');
|
|
140
|
+
|
|
141
|
+
const selectionRef = ref<HTMLElement | null>(null);
|
|
142
|
+
const visibleCount = ref(2);
|
|
128
143
|
|
|
129
144
|
const allSelected = computed(() => {
|
|
130
145
|
if (displayedOptions.value.length === 0) {
|
|
@@ -133,30 +148,36 @@ const allSelected = computed(() => {
|
|
|
133
148
|
return displayedOptions.value.every((option) => isSelected(option));
|
|
134
149
|
});
|
|
135
150
|
|
|
136
|
-
const
|
|
137
|
-
const selected = props.selected ?? [];
|
|
138
|
-
if (selected.length === 0) {
|
|
139
|
-
return props.placeholder ?? t('Search');
|
|
140
|
-
}
|
|
141
|
-
const labels = selected.map(getLabel);
|
|
142
|
-
if (labels.length <= 2) {
|
|
143
|
-
return labels.join(', ');
|
|
144
|
-
}
|
|
145
|
-
return `${labels.slice(0, 2).join(', ')} (+${labels.length - 2})`;
|
|
146
|
-
});
|
|
151
|
+
const hasSelected = computed(() => (props.selected?.length ?? 0) > 0);
|
|
147
152
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
153
|
+
const selectionLabels = computed(() =>
|
|
154
|
+
(props.selected ?? []).slice(0, visibleCount.value).map(getLabel).join(', ')
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const overflowCount = computed(() =>
|
|
158
|
+
Math.max(0, (props.selected?.length ?? 0) - visibleCount.value)
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const allLabelsText = computed(() =>
|
|
162
|
+
(props.selected ?? []).map(getLabel).join(', ')
|
|
163
|
+
);
|
|
151
164
|
|
|
152
165
|
function isSelected(option: T): boolean {
|
|
153
|
-
|
|
166
|
+
if (props.optionKey) {
|
|
167
|
+
return props.selected?.find(s => s[props.optionKey] === option[props.optionKey]) !== undefined;
|
|
168
|
+
} else {
|
|
169
|
+
return props.selected?.includes(option) ?? false;
|
|
170
|
+
}
|
|
154
171
|
}
|
|
155
172
|
|
|
156
173
|
function toggleOption(option: T): void {
|
|
157
174
|
const current = props.selected ?? [];
|
|
158
175
|
if (isSelected(option)) {
|
|
159
|
-
|
|
176
|
+
const key = props.optionKey;
|
|
177
|
+
emit('update:selected', key
|
|
178
|
+
? current.filter((item) => item[key] !== option[key])
|
|
179
|
+
: current.filter((item) => item !== option)
|
|
180
|
+
);
|
|
160
181
|
} else {
|
|
161
182
|
emit('update:selected', [...current, option]);
|
|
162
183
|
}
|
|
@@ -168,45 +189,80 @@ function toggleAll(val: boolean): void {
|
|
|
168
189
|
const toAdd = displayedOptions.value.filter((option: T) => !isSelected(option));
|
|
169
190
|
emit('update:selected', [...current, ...toAdd]);
|
|
170
191
|
} else {
|
|
171
|
-
|
|
192
|
+
const key = props.optionKey;
|
|
193
|
+
emit('update:selected', current.filter((item: T) => key
|
|
194
|
+
? !displayedOptions.value.some((opt) => opt[key] === item[key])
|
|
195
|
+
: !displayedOptions.value.includes(item)
|
|
196
|
+
));
|
|
172
197
|
}
|
|
173
198
|
}
|
|
174
199
|
|
|
175
|
-
function
|
|
176
|
-
|
|
200
|
+
function updateVisibleCount(): void {
|
|
201
|
+
const container = selectionRef.value;
|
|
202
|
+
const selected = props.selected ?? [];
|
|
203
|
+
if (!container || selected.length < 2) {
|
|
204
|
+
visibleCount.value = selected.length;
|
|
177
205
|
return;
|
|
178
206
|
}
|
|
179
|
-
isOpen.value = true;
|
|
180
|
-
nextTick(() => {
|
|
181
|
-
inputRef.value?.focus();
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function closeDropdown(): void {
|
|
186
|
-
isOpen.value = false;
|
|
187
|
-
searchQuery.value = '';
|
|
188
|
-
}
|
|
189
207
|
|
|
190
|
-
|
|
191
|
-
if (
|
|
192
|
-
|
|
193
|
-
} else {
|
|
194
|
-
openDropdown();
|
|
208
|
+
const availableWidth = container.offsetWidth;
|
|
209
|
+
if (availableWidth === 0) {
|
|
210
|
+
return;
|
|
195
211
|
}
|
|
196
|
-
}
|
|
197
212
|
|
|
198
|
-
|
|
199
|
-
|
|
213
|
+
const labels = selected.map(getLabel);
|
|
214
|
+
const measurer = document.createElement('span');
|
|
215
|
+
measurer.style.cssText = 'position:absolute;visibility:hidden;white-space:nowrap;pointer-events:none;';
|
|
216
|
+
container.appendChild(measurer);
|
|
217
|
+
|
|
218
|
+
measurer.textContent = labels.join(', ');
|
|
219
|
+
if (measurer.offsetWidth <= availableWidth) {
|
|
220
|
+
visibleCount.value = labels.length;
|
|
221
|
+
container.removeChild(measurer);
|
|
200
222
|
return;
|
|
201
223
|
}
|
|
202
|
-
|
|
203
|
-
|
|
224
|
+
|
|
225
|
+
measurer.textContent = `(+${labels.length})`;
|
|
226
|
+
const badgeWidth = measurer.offsetWidth + 4;
|
|
227
|
+
const textAvailable = availableWidth - badgeWidth;
|
|
228
|
+
|
|
229
|
+
let count = 0;
|
|
230
|
+
for (let i = 0; i < labels.length; i++) {
|
|
231
|
+
measurer.textContent = labels.slice(0, i + 1).join(', ');
|
|
232
|
+
if (measurer.offsetWidth > textAvailable) {
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
count = i + 1;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
container.removeChild(measurer);
|
|
239
|
+
visibleCount.value = Math.max(1, count);
|
|
204
240
|
}
|
|
205
241
|
|
|
206
|
-
|
|
242
|
+
let resizeObserver: ResizeObserver | null = null;
|
|
243
|
+
|
|
244
|
+
onMounted(() => {
|
|
245
|
+
if (selectionRef.value) {
|
|
246
|
+
resizeObserver = new ResizeObserver(updateVisibleCount);
|
|
247
|
+
resizeObserver.observe(selectionRef.value);
|
|
248
|
+
}
|
|
249
|
+
updateVisibleCount();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
onUnmounted(() => {
|
|
253
|
+
resizeObserver?.disconnect();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
watch(() => props.selected, updateVisibleCount);
|
|
257
|
+
|
|
258
|
+
watch(isOpen, (val) => {
|
|
259
|
+
if (!val) {
|
|
260
|
+
nextTick(updateVisibleCount);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
207
263
|
</script>
|
|
208
264
|
|
|
209
|
-
<style lang="scss">
|
|
265
|
+
<style scoped lang="scss">
|
|
210
266
|
.octopus-multiselect {
|
|
211
267
|
position: relative;
|
|
212
268
|
|
|
@@ -232,17 +288,57 @@ onClickOutside(containerRef, closeDropdown);
|
|
|
232
288
|
}
|
|
233
289
|
}
|
|
234
290
|
|
|
291
|
+
.octopus-multiselect-selection {
|
|
292
|
+
display: flex;
|
|
293
|
+
align-items: center;
|
|
294
|
+
flex: 1;
|
|
295
|
+
min-width: 0;
|
|
296
|
+
padding: 0.4rem 0.5rem;
|
|
297
|
+
height: 2rem;
|
|
298
|
+
gap: 0.25rem;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.octopus-multiselect-selection-text {
|
|
302
|
+
flex: 1;
|
|
303
|
+
min-width: 0;
|
|
304
|
+
overflow: hidden;
|
|
305
|
+
text-overflow: ellipsis;
|
|
306
|
+
white-space: nowrap;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.octopus-multiselect-selection-count {
|
|
310
|
+
flex-shrink: 0;
|
|
311
|
+
white-space: nowrap;
|
|
312
|
+
color: var(--octopus-primary);
|
|
313
|
+
}
|
|
314
|
+
|
|
235
315
|
.octopus-multiselect-input {
|
|
236
316
|
flex: 1;
|
|
237
317
|
border: none;
|
|
238
318
|
background: transparent;
|
|
239
319
|
padding: 0.4rem 0.5rem;
|
|
320
|
+
padding-right: 0;
|
|
240
321
|
height: 2rem;
|
|
241
322
|
outline: none;
|
|
242
323
|
cursor: inherit;
|
|
243
324
|
min-width: 0;
|
|
244
325
|
}
|
|
245
326
|
|
|
327
|
+
.octopus-multiselect-hover-tooltip {
|
|
328
|
+
position: absolute;
|
|
329
|
+
top: calc(100% + 2px);
|
|
330
|
+
left: 0;
|
|
331
|
+
right: 0;
|
|
332
|
+
z-index: 101;
|
|
333
|
+
background: white;
|
|
334
|
+
border: 1px solid var(--octopus-border-default);
|
|
335
|
+
border-radius: var(--octopus-border-radius);
|
|
336
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
337
|
+
padding: 0.5rem;
|
|
338
|
+
word-break: break-word;
|
|
339
|
+
pointer-events: none;
|
|
340
|
+
}
|
|
341
|
+
|
|
246
342
|
.octopus-multiselect-chevron {
|
|
247
343
|
padding: 0.25rem 0.5rem;
|
|
248
344
|
display: flex;
|
|
@@ -0,0 +1,237 @@
|
|
|
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
|
+
<div v-if="isOpen" class="octopus-select-dropdown">
|
|
46
|
+
<div class="octopus-select-options">
|
|
47
|
+
<button
|
|
48
|
+
v-for="(option, index) in displayedOptions"
|
|
49
|
+
:key="index"
|
|
50
|
+
class="octopus-select-option"
|
|
51
|
+
:class="{ selected: isSelected(option) }"
|
|
52
|
+
@click="selectOption(option)"
|
|
53
|
+
>
|
|
54
|
+
{{ getLabel(option) }}
|
|
55
|
+
</button>
|
|
56
|
+
<span v-if="displayedOptions.length === 0" class="text-indic px-2">
|
|
57
|
+
{{ t('No elements found. Consider changing the search query.') }}
|
|
58
|
+
</span>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</template>
|
|
63
|
+
|
|
64
|
+
<script setup lang="ts" generic="T">
|
|
65
|
+
import { computed, toRaw } from 'vue';
|
|
66
|
+
import { useI18n } from 'vue-i18n';
|
|
67
|
+
import ChevronDownIcon from 'vue-material-design-icons/ChevronDown.vue';
|
|
68
|
+
import { useOctopusDropdown } from '../composable/form/useOctopusDropdown';
|
|
69
|
+
|
|
70
|
+
const props = withDefaults(defineProps<{
|
|
71
|
+
/** Optional label displayed above the field. */
|
|
72
|
+
label?: string;
|
|
73
|
+
/** Currently selected item. Bind with `v-model:value`. */
|
|
74
|
+
value?: T;
|
|
75
|
+
/** Full list of options to display or filter. */
|
|
76
|
+
options: T[];
|
|
77
|
+
/** Key of each option object to use as the ID. */
|
|
78
|
+
optionKey?: keyof T;
|
|
79
|
+
/** Key of each option object to use as the display label. */
|
|
80
|
+
optionLabel: keyof T & string;
|
|
81
|
+
/** Disables the field when true. */
|
|
82
|
+
isDisabled?: boolean;
|
|
83
|
+
/** Placeholder shown in the input when no item is selected. Defaults to the translated "Search" string. */
|
|
84
|
+
placeholder?: string;
|
|
85
|
+
/** Disable the border around the input. */
|
|
86
|
+
noBorder?: boolean;
|
|
87
|
+
/** When true (default), clicking the already-selected option clears the selection. */
|
|
88
|
+
allowDeselect?: boolean;
|
|
89
|
+
}>(), {
|
|
90
|
+
label: undefined,
|
|
91
|
+
value: undefined,
|
|
92
|
+
optionKey: undefined,
|
|
93
|
+
placeholder: undefined,
|
|
94
|
+
allowDeselect: true,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const emit = defineEmits<{
|
|
98
|
+
/** Emitted when the selection changes. */
|
|
99
|
+
(e: 'update:value', value: T | undefined): void;
|
|
100
|
+
/** Emitted on every input change. When listened to, the parent is responsible for
|
|
101
|
+
* updating `options`; otherwise the component filters `options` client-side. */
|
|
102
|
+
(e: 'search', query: string): void;
|
|
103
|
+
}>();
|
|
104
|
+
|
|
105
|
+
const { t } = useI18n();
|
|
106
|
+
|
|
107
|
+
const {
|
|
108
|
+
searchQuery,
|
|
109
|
+
isOpen,
|
|
110
|
+
containerRef,
|
|
111
|
+
inputRef,
|
|
112
|
+
computedId,
|
|
113
|
+
displayedOptions,
|
|
114
|
+
inputPlaceholder,
|
|
115
|
+
getLabel,
|
|
116
|
+
openDropdown,
|
|
117
|
+
closeDropdown,
|
|
118
|
+
toggleDropdown,
|
|
119
|
+
handleInput,
|
|
120
|
+
} = useOctopusDropdown(props, (query) => emit('search', query), 'select');
|
|
121
|
+
|
|
122
|
+
const selectedLabel = computed(() =>
|
|
123
|
+
props.value !== undefined ? getLabel(props.value) : undefined
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
function isSelected(option: T): boolean {
|
|
127
|
+
if (props.value === undefined) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
if (props.optionKey) {
|
|
131
|
+
return props.value[props.optionKey] === option[props.optionKey];
|
|
132
|
+
}
|
|
133
|
+
return toRaw(props.value as object) === toRaw(option as object);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function selectOption(option: T): void {
|
|
137
|
+
if (isSelected(option) && props.allowDeselect) {
|
|
138
|
+
emit('update:value', undefined);
|
|
139
|
+
} else {
|
|
140
|
+
emit('update:value', option);
|
|
141
|
+
}
|
|
142
|
+
closeDropdown();
|
|
143
|
+
}
|
|
144
|
+
</script>
|
|
145
|
+
|
|
146
|
+
<style scoped lang="scss">
|
|
147
|
+
.octopus-select {
|
|
148
|
+
position: relative;
|
|
149
|
+
|
|
150
|
+
.octopus-select-field {
|
|
151
|
+
display: flex;
|
|
152
|
+
align-items: center;
|
|
153
|
+
border: 1px solid var(--octopus-border-default);
|
|
154
|
+
border-radius: var(--octopus-border-radius);
|
|
155
|
+
background: white;
|
|
156
|
+
cursor: pointer;
|
|
157
|
+
|
|
158
|
+
&.open {
|
|
159
|
+
border-color: var(--octopus-primary);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
&.disabled {
|
|
163
|
+
background: var(--octopus-secondary-lighter);
|
|
164
|
+
cursor: default;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
&.noBorder {
|
|
168
|
+
border: none;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.octopus-select-value {
|
|
173
|
+
flex: 1;
|
|
174
|
+
min-width: 0;
|
|
175
|
+
overflow: hidden;
|
|
176
|
+
text-overflow: ellipsis;
|
|
177
|
+
white-space: nowrap;
|
|
178
|
+
padding: 0.4rem 0.5rem;
|
|
179
|
+
padding-right: 0;
|
|
180
|
+
height: 2rem;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.octopus-select-input {
|
|
184
|
+
flex: 1;
|
|
185
|
+
border: none;
|
|
186
|
+
background: transparent;
|
|
187
|
+
padding: 0.4rem 0.5rem;
|
|
188
|
+
height: 2rem;
|
|
189
|
+
outline: none;
|
|
190
|
+
cursor: inherit;
|
|
191
|
+
min-width: 0;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.octopus-select-chevron {
|
|
195
|
+
padding: 0.25rem 0.5rem;
|
|
196
|
+
display: flex;
|
|
197
|
+
align-items: center;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.octopus-select-dropdown {
|
|
201
|
+
position: absolute;
|
|
202
|
+
top: calc(100% + 2px);
|
|
203
|
+
left: 0;
|
|
204
|
+
right: 0;
|
|
205
|
+
z-index: 100;
|
|
206
|
+
background: white;
|
|
207
|
+
border: 1px solid var(--octopus-border-default);
|
|
208
|
+
border-radius: var(--octopus-border-radius);
|
|
209
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
210
|
+
padding: 0.25rem 0;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.octopus-select-options {
|
|
214
|
+
max-height: 14rem;
|
|
215
|
+
overflow-y: auto;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.octopus-select-option {
|
|
219
|
+
display: block;
|
|
220
|
+
width: 100%;
|
|
221
|
+
text-align: left;
|
|
222
|
+
padding: 0.25rem 0.5rem;
|
|
223
|
+
border: none;
|
|
224
|
+
background: transparent;
|
|
225
|
+
cursor: pointer;
|
|
226
|
+
|
|
227
|
+
&:hover {
|
|
228
|
+
background: var(--octopus-secondary-lighter);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
&.selected {
|
|
232
|
+
font-weight: 600;
|
|
233
|
+
color: var(--octopus-primary);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
</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
|
|
|
@@ -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
|
}
|