@ouestfrance/sipa-bms-ui 8.15.0 → 8.16.0

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.
@@ -0,0 +1,290 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ computed,
4
+ onBeforeUnmount,
5
+ onMounted,
6
+ ref,
7
+ useTemplateRef,
8
+ watch,
9
+ } from 'vue';
10
+
11
+ const DEFAULT_MIN = 0;
12
+ const DEFAULT_MAX = 100;
13
+
14
+ interface Props {
15
+ splitOrientation?: 'horizontal' | 'vertical';
16
+ min?: number;
17
+ max?: number;
18
+ primary?: 'first' | 'second';
19
+ collapsed?: boolean;
20
+ ariaLabel?: string;
21
+ }
22
+
23
+ const props = withDefaults(defineProps<Props>(), {
24
+ splitOrientation: 'vertical',
25
+ primary: 'first',
26
+ min: DEFAULT_MIN,
27
+ max: DEFAULT_MAX,
28
+ collapsable: false,
29
+ });
30
+
31
+ const emit = defineEmits(['update:collapsed']);
32
+
33
+ const split = defineModel<number>({
34
+ default: 50,
35
+ });
36
+
37
+ const container = useTemplateRef('split-window');
38
+ const primaryId = ref<string>(crypto.randomUUID());
39
+
40
+ //State Machine
41
+ const isDragging = ref<boolean>(false);
42
+ const startSplit = ref<number | null>(null);
43
+ const startPosition = ref<number | null>(null);
44
+
45
+ const min = computed(() =>
46
+ clamp(props.min ?? DEFAULT_MIN, DEFAULT_MIN, DEFAULT_MAX),
47
+ );
48
+ const max = computed(() =>
49
+ clamp(props.max ?? DEFAULT_MAX, DEFAULT_MIN, DEFAULT_MAX),
50
+ );
51
+
52
+ // Correction: état local pour collapsed
53
+ const collapsedLocal = ref(props.collapsed ?? false);
54
+
55
+ watch(
56
+ () => props.collapsed,
57
+ (val) => {
58
+ collapsedLocal.value = val ?? false;
59
+ },
60
+ );
61
+
62
+ function setCollapsed(val: boolean) {
63
+ collapsedLocal.value = val;
64
+ emit('update:collapsed', val);
65
+ }
66
+
67
+ const clampSplit = computed(() => {
68
+ if (collapsedLocal.value) {
69
+ return props.primary === 'first' ? min.value : max.value;
70
+ } else {
71
+ return clamp(split.value, min.value, max.value);
72
+ }
73
+ });
74
+
75
+ const size = computed(() => {
76
+ return `${clampSplit.value}fr auto ${100 - clampSplit.value}fr`;
77
+ });
78
+
79
+ const gridStyle = computed(() => {
80
+ return props.splitOrientation === 'horizontal'
81
+ ? {
82
+ gridTemplateRows: size.value,
83
+ gridTemplateColumns: 'none',
84
+ }
85
+ : {
86
+ gridTemplateColumns: size.value,
87
+ gridTemplateRows: 'none',
88
+ };
89
+ });
90
+
91
+ const HANDLED_KEYS = [
92
+ 'ArrowLeft',
93
+ 'ArrowRight',
94
+ 'ArrowUp',
95
+ 'ArrowDown',
96
+ 'Enter',
97
+ 'Home',
98
+ 'End',
99
+ ];
100
+
101
+ onMounted(() => {
102
+ window.addEventListener('pointermove', onPointerMove);
103
+ window.addEventListener('pointerup', onPointerUp);
104
+ });
105
+
106
+ onBeforeUnmount(() => {
107
+ window.removeEventListener('pointermove', onPointerMove);
108
+ window.removeEventListener('pointerup', onPointerUp);
109
+ });
110
+
111
+ function onPointerDown(evt: PointerEvent) {
112
+ isDragging.value = true;
113
+ setCollapsed(false);
114
+ startSplit.value = clampSplit.value;
115
+ startPosition.value =
116
+ props.splitOrientation === 'vertical' ? evt.clientX : evt.clientY;
117
+ }
118
+
119
+ function onPointerMove(evt: PointerEvent) {
120
+ if (!isDragging.value) return;
121
+ _move(evt);
122
+ }
123
+
124
+ function onPointerUp(evt: PointerEvent) {
125
+ if (!isDragging.value) return;
126
+ _move(evt);
127
+ isDragging.value = false;
128
+ startPosition.value = null;
129
+ startSplit.value = null;
130
+ }
131
+
132
+ function onKeyDown(evt: KeyboardEvent) {
133
+ if (
134
+ !HANDLED_KEYS.includes(evt.key) ||
135
+ (props.splitOrientation === 'horizontal' &&
136
+ (evt.key === 'ArrowLeft' || evt.key === 'ArrowRight')) ||
137
+ (props.splitOrientation === 'vertical' &&
138
+ (evt.key === 'ArrowUp' || evt.key === 'ArrowDown'))
139
+ ) {
140
+ return;
141
+ }
142
+
143
+ evt.preventDefault();
144
+ evt.stopPropagation();
145
+
146
+ switch (evt.key) {
147
+ case 'ArrowLeft':
148
+ case 'ArrowUp':
149
+ if (collapsedLocal.value === true) {
150
+ setCollapsed(false);
151
+ }
152
+ split.value = Math.max(min.value, clampSplit.value - 1);
153
+ break;
154
+ case 'ArrowRight':
155
+ case 'ArrowDown':
156
+ if (collapsedLocal.value === true) {
157
+ setCollapsed(false);
158
+ }
159
+ split.value = Math.min(max.value, clampSplit.value + 1);
160
+ break;
161
+ case 'Enter':
162
+ setCollapsed(!collapsedLocal.value);
163
+ break;
164
+ case 'Home':
165
+ if (collapsedLocal.value === true) {
166
+ setCollapsed(false);
167
+ }
168
+ split.value = props.primary === 'first' ? min.value : max.value;
169
+ break;
170
+ case 'End':
171
+ if (collapsedLocal.value === true) {
172
+ setCollapsed(false);
173
+ }
174
+ split.value = props.primary === 'first' ? max.value : min.value;
175
+ break;
176
+ }
177
+ }
178
+
179
+ function _move(evt: PointerEvent) {
180
+ if (startPosition.value === null || startSplit.value === null) return;
181
+
182
+ const currentPosition =
183
+ props.splitOrientation === 'vertical' ? evt.clientX : evt.clientY;
184
+
185
+ const delta = currentPosition - startPosition.value;
186
+
187
+ const containerSize =
188
+ props.splitOrientation === 'vertical'
189
+ ? container.value?.getBoundingClientRect().width || 1
190
+ : container.value?.getBoundingClientRect().height || 1;
191
+
192
+ const deltaPercent = (delta / containerSize) * 100;
193
+ split.value = startSplit.value + deltaPercent;
194
+ }
195
+
196
+ function clamp(value: number, minValue: number, maxValue: number) {
197
+ return Math.min(Math.max(value, minValue), maxValue);
198
+ }
199
+ </script>
200
+
201
+ <template>
202
+ <div
203
+ ref="split-window"
204
+ class="split-window"
205
+ :class="`split-window--${splitOrientation}`"
206
+ :style="gridStyle"
207
+ >
208
+ <div
209
+ class="split-window__first-pane"
210
+ :id="primary === 'first' ? primaryId : undefined"
211
+ >
212
+ <slot name="first" />
213
+ </div>
214
+ <div
215
+ class="split-window__separator"
216
+ role="separator"
217
+ tabindex="0"
218
+ :aria-label="props.ariaLabel || 'Séparateur de volet'"
219
+ :aria-valuemin="min"
220
+ :aria-valuemax="max"
221
+ :aria-valuenow="clampSplit"
222
+ :aria-orientation="splitOrientation"
223
+ :aria-controls="primaryId"
224
+ @pointerdown.prevent.stop="onPointerDown"
225
+ @keydown="onKeyDown"
226
+ />
227
+ <div
228
+ class="split-window__second-pane"
229
+ :id="primary === 'second' ? primaryId : undefined"
230
+ >
231
+ <slot name="second" />
232
+ </div>
233
+ </div>
234
+ </template>
235
+
236
+ <style scoped lang="scss">
237
+ .split-window {
238
+ display: grid;
239
+ width: 100%;
240
+ height: 100%;
241
+
242
+ &__separator {
243
+ position: relative;
244
+ z-index: 2;
245
+
246
+ &:before {
247
+ content: '';
248
+ position: absolute;
249
+ top: 0;
250
+ left: 0;
251
+ }
252
+
253
+ &:focus-within {
254
+ &:before {
255
+ outline: 2px solid black;
256
+ }
257
+ }
258
+ }
259
+
260
+ &--vertical {
261
+ .split-window__separator {
262
+ height: 100%;
263
+ width: 0;
264
+
265
+ &:before {
266
+ content: '';
267
+ width: 8px;
268
+ height: 100%;
269
+ transform: translate(-50%, 0);
270
+ cursor: col-resize;
271
+ }
272
+ }
273
+ }
274
+
275
+ &--horizontal {
276
+ .split-window__separator {
277
+ height: 0;
278
+ width: 100%;
279
+
280
+ &:before {
281
+ content: '';
282
+ width: 100%;
283
+ height: 8px;
284
+ transform: translate(0, -50%);
285
+ cursor: row-resize;
286
+ }
287
+ }
288
+ }
289
+ }
290
+ </style>
@@ -9,13 +9,16 @@ describe('problem helper', () => {
9
9
  test('should return false if object has no type', () => {
10
10
  expect(isProblem({ toto: 'coucou' })).toEqual(false);
11
11
  });
12
- test('should return false if object type does not include correct origin', () => {
13
- expect(isProblem({ type: 'http://host.com' })).toEqual(false);
12
+ test('should return false if object type does not include correct protocol', () => {
13
+ expect(isProblem({ type: 'http://' })).toEqual(false);
14
+ });
15
+ test('should return false if object type does not include correct protocol', () => {
16
+ expect(isProblem({ type: 'https://my-url.fr' })).toEqual(true);
14
17
  });
15
18
  test('should return true if object is a problem', () => {
16
- expect(
17
- isProblem({ type: 'https://problems.bms.live/toto' }),
18
- ).toEqual(true);
19
+ expect(isProblem({ type: 'https://problems.bms.live/toto' })).toEqual(
20
+ true,
21
+ );
19
22
  });
20
23
  });
21
24
  });
@@ -1,4 +1,4 @@
1
1
  export const isProblem = (object: any): boolean =>
2
2
  !!object?.type &&
3
3
  typeof object.type === 'string' &&
4
- object.type.includes('https://problems.bms.live');
4
+ object.type.includes('https://');
package/src/index.ts CHANGED
@@ -37,12 +37,14 @@ import BmsTextArea from './components/form/BmsTextArea.vue';
37
37
 
38
38
  import BmsContentPageLayout from './components/layout/BmsContentPageLayout.vue';
39
39
  import BmsCard from './components/layout/BmsCard.vue';
40
+ import BmsFloatingWindow from './components/layout/BmsFloatingWindow.vue';
40
41
  import BmsForm from './components/layout/BmsForm.vue';
41
42
  import BmsHeader from './components/layout/BmsHeader.vue';
42
43
  import BmsHeaderTitle from './components/layout/BmsHeaderTitle.vue';
43
44
  import BmsModal from './components/layout/BmsModal.vue';
44
45
  import BmsOverlay from './components/layout/BmsOverlay.vue';
45
46
  import BmsSection from './components/layout/BmsSection.vue';
47
+ import BmsSplitWindow from './components/layout/BmsSplitWindow.vue';
46
48
  import BmsStep from './components/layout/BmsStep.vue';
47
49
  import BmsStepper from './components/layout/BmsStepper.vue';
48
50
 
@@ -108,12 +110,14 @@ export const createBmsUi = () => ({
108
110
 
109
111
  app.component('BmsContentPageLayout', BmsContentPageLayout);
110
112
  app.component('BmsCard', BmsCard);
113
+ app.component('BmsFloatingWindow', BmsFloatingWindow);
111
114
  app.component('BmsForm', BmsForm);
112
115
  app.component('BmsHeader', BmsHeader);
113
116
  app.component('BmsHeaderTitle', BmsHeaderTitle);
114
117
  app.component('BmsModal', BmsModal);
115
118
  app.component('BmsOverlay', BmsOverlay);
116
119
  app.component('BmsSection', BmsSection);
120
+ app.component('BmsSplitWindow', BmsSplitWindow);
117
121
  app.component('BmsStep', BmsStep);
118
122
  app.component('BmsStepper', BmsStepper);
119
123
 
@@ -185,12 +189,14 @@ export {
185
189
  BmsTextArea,
186
190
  BmsContentPageLayout,
187
191
  BmsCard,
192
+ BmsFloatingWindow,
188
193
  BmsForm,
189
194
  BmsHeader,
190
195
  BmsHeaderTitle,
191
196
  BmsModal,
192
197
  BmsOverlay,
193
198
  BmsSection,
199
+ BmsSplitWindow,
194
200
  BmsStep,
195
201
  BmsStepper,
196
202
  BmsBackButton,
@@ -99,10 +99,10 @@ const onAddNewOption = (newOption: string) => {
99
99
  };
100
100
 
101
101
  const autocompleteRequest = (
102
- abortController: AbortController,
102
+ _abortController: AbortController,
103
103
  searchString?: string,
104
104
  ) =>
105
105
  fetch(`http://localhost:3000/pokemon-types?search=${searchString}`).then(
106
- (res) => res.json(),
106
+ (res) => res.json().then((res) => ({ data: res })),
107
107
  );
108
108
  </script>
@@ -56,9 +56,7 @@ const pokemonTypeAutocompleteRequest = async (
56
56
  signal: abortController.signal,
57
57
  });
58
58
 
59
- return {
60
- data,
61
- };
59
+ return { data };
62
60
  };
63
61
 
64
62
  const filters: Filter[] = [
@@ -9,17 +9,25 @@ app.use(cors());
9
9
  const port = 3000;
10
10
 
11
11
  app.get('/pokemon-types', (req, res) => {
12
- let data = pokemons.map((pokemon) => ({
13
- label: pokemon.name,
14
- value: pokemon.name,
15
- }));
16
- if (req.query.search) {
17
- data = data.filter((pokemon) =>
18
- pokemon.label.toLowerCase().includes(req.query.search.toLowerCase()),
19
- );
20
- }
21
-
22
- res.send({ data });
12
+ res.send([
13
+ { label: 'Grass', value: 'Grass' },
14
+ { label: 'Poison', value: 'Poison' },
15
+ { label: 'Fire', value: 'Fire' },
16
+ { label: 'Flying', value: 'Flying' },
17
+ { label: 'Water', value: 'Water' },
18
+ { label: 'Bug', value: 'Bug' },
19
+ { label: 'Normal', value: 'Normal' },
20
+ { label: 'Electric', value: 'Electric' },
21
+ { label: 'Ground', value: 'Ground' },
22
+ { label: 'Fairy', value: 'Fairy' },
23
+ { label: 'Fighting', value: 'Fighting' },
24
+ { label: 'Psychic', value: 'Psychic' },
25
+ { label: 'Rock', value: 'Rock' },
26
+ { label: 'Steel', value: 'Steel' },
27
+ { label: 'Ice', value: 'Ice' },
28
+ { label: 'Ghost', value: 'Ghost' },
29
+ { label: 'Dragon', value: 'Dragon' },
30
+ ]);
23
31
  });
24
32
 
25
33
  app.get('/pokemons', (req, res) => {